Conversation History Is Not State: Designing Agents for Evolving User Intent
August 16, 2026 | 13 minutesIn This Post
- Why Conversation History Fails as Operational State
- Model Evolving User Intent as Explicit Application State
- Ask the LLM to Resolve Intent Deltas, Not Rebuild State
- Keep Intent State Transitions Deterministic
- Use XState to Replan When User Intent Changes Mid-Execution
- Map Different Intent Changes to Different Control-Flow Policies
- Let Current Intent Decide What Context the Agent Sees
- Turn Evolving-Intent Research into Repeatable Agent Evaluations
- Where Explicit Intent State Still Falls Short
- Treat Intent as a First-Class Runtime Primitive
- Run the Evolving-Intent Agent Example
- References
Most agent loops have an uncomfortable amount of faith in the message array. A typical flow looks like this:
- A user says what they want.
- The agent acts.
- The user adds a constraint.
- The agent acts again.
- Then the user changes their mind, switches to a related task, comes back to the original task, and corrects an assumption they made five turns ago.
We usually hand the model the conversation and hope it can reconstruct the current task from the transcript, which works surprisingly well until it doesn't. ðŸ«
A recent paper from Microsoft Research, LLMs Get Lost in Evolving User Intent, gives us a useful way to think about why. Jihoon Tack, Philippe Laban, and Jennifer Neville evaluated models in conversations where user intent changes over time instead of being fully specified in one prompt. As part of their research, they modeled three kinds of transitions:
- Argument reveal: the user adds information that was previously missing.
- Argument revision: the user changes a previously supplied value.
- Function switch: the user changes the task while potentially carrying useful context forward.
The result is relevant to anyone building long-running agents: strong single-turn performance does not reliably transfer to conversations with evolving intent. I've definitely seen this happen in agentic writing workflows. The agent tends to perform well if the tasks follow a path where the user asks for something, the agent completes it, and then the user moves onto something else. It gets easily confused, though, whenever tasks are co-mingled or changed.
In the paper's experiments, function switches are particularly difficult. In one turn-wise intent-tracking analysis, GPT 5.1 tracked intent at 99% after one argument reveal and 98% after two, but only 89% after one function switch and 82% after two.
The paper is an evaluation paper, not an agent architecture proposal. But I think it points toward a useful engineering principle:
Conversation history is evidence about what the user wants, but it should not be the only representation of what the user wants now.
In this post, I'm going to turn that principle into a small TypeScript agent built with XState. The agent maintains an explicit intent state, asks an LLM to detect what changed on each user turn, deterministically applies that change, and replans when the new state invalidates its current work.
The full example project is linked at the end.
Why Conversation History Fails as Operational State
Consider a coding agent conversation:
User: Add authentication to the app. User: We're using Next.js and Postgres. User: Actually, let's use magic links instead of passwords. User: Before implementing that, make the database migration. User: Great. Now implement the login flow. User: Don't use Prisma though. This repo uses Drizzle.
By the final turn, the relevant task is not "whatever the last message says." It is something closer to:
1{ 2 goal: "implement_login_flow", 3 constraints: { 4 framework: "nextjs", 5 database: "postgres", 6 authMethod: "magic_link", 7 orm: "drizzle" 8 }, 9 completedGoals: ["create_database_migration"] 10}
The transcript tells us how we got there, but the object tells us what is operationally true now. It gives the agent clearer direction on how to move forward.
That distinction matters once old turns become stale. If a user says "use Prisma" and later says "actually, use Drizzle," both strings remain in conversation history. A model has to infer which one is authoritative every time it reasons over the transcript.
For a short chat, that may be fine. For a collaborative agent that plans, calls tools, edits files, waits for feedback, and accepts interruptions, it's a fragile place to keep application state.
Model Evolving User Intent as Explicit Application State
The paper formalizes intent at a given turn as a function plus its arguments. In less mathematical terms:
intent = task + active parameters
For a coding agent, that can become an ordinary TypeScript type.
1// src/types.ts 2export type AgentGoal = 3 | "design_auth" 4 | "create_database_migration" 5 | "implement_login_flow" 6 | "test_auth"; 7 8export type Constraints = { 9 framework?: "nextjs"; 10 database?: "postgres"; 11 orm?: "drizzle" | "prisma"; 12 authMethod?: "password" | "magic_link"; 13}; 14 15export type IntentState = { 16 goal: AgentGoal | null; 17 constraints: Constraints; 18 completedGoals: AgentGoal[]; 19};
The important part is making that state explicit. To do this, we can separately represent the change introduced by a user turn:
1export type IntentDelta = 2 | { 3 type: "reveal"; 4 constraints: Partial<Constraints>; 5 } 6 | { 7 type: "revise"; 8 constraints: Partial<Constraints>; 9 } 10 | { 11 type: "switch"; 12 goal: AgentGoal; 13 } 14 | { 15 type: "continue"; 16 };
This mirrors the paper's three transition types, plus continue for turns that don't change operational intent.
The research paper notes that transition types can co-occur in a single turn. I'm deliberately keeping the demo to one transition object per turn so the control flow is easy to inspect, but a production version should probably return an ordered list of deltas or a single patch containing both goal and constraint changes.
Ask the LLM to Resolve Intent Deltas, Not Rebuild State
A common approach to intent detection is classification:
message -> intent label
This works for routing a one-shot request, but it becomes awkward when intent is stateful.
For example:
"Actually, use Drizzle."
That message does not fully describe the task. Its meaning depends on the current state, so I want the resolver to answer a narrower question:
What changed relative to the intent state I already have?
In the example project, the resolver interface looks like this:
1export interface IntentResolver {
2 resolve(
3 message: string,
4 current: IntentState
5 ): Promise<IntentDelta>;
6}
The LLM receives the latest message and the explicit current intent. It doesn't own the state or regenerate the entire state object. It only emits the transition.
Conceptually:
current intent + latest message | v semantic resolver | v IntentDelta
That boundary is useful because the LLM does the fuzzy part of interpreting language while ordinary application code does the deterministic part of updating state.
Keep Intent State Transitions Deterministic
Once the resolver returns a delta, updating the intent does not require another model call.
1// src/intent-reducer.ts 2export function applyIntentDelta( 3 state: IntentState, 4 delta: IntentDelta 5): IntentState { 6 switch (delta.type) { 7 case "reveal": 8 case "revise": 9 return { 10 ...state, 11 constraints: { 12 ...state.constraints, 13 ...delta.constraints 14 } 15 }; 16 17 case "switch": 18 return { 19 ...state, 20 goal: delta.goal 21 }; 22 23 case "continue": 24 return state; 25 } 26}
This makes it easy for us to overwrite the operational state as the user's goals change. If the state contains { orm: "prisma" } and the user corrects it to { type: "revise", constraints: { orm: "drizzle" }}, then the new operational state contains only: { orm: "drizzle" }.
The transcript can still contain the old Prisma turn for auditability, but the execution state does not have to pretend both values are equally current.
This leads to a separation I increasingly like for agents:
LLM -> What does this turn mean? Reducer -> What is true now? State machine -> What are we allowed to do next? Planner -> How should we accomplish it?
A single large agent prompt can technically attempt all four. Separating them gives us much better surfaces for debugging and evaluation.
Use XState to Replan When User Intent Changes Mid-Execution
The research problem is fundamentally about transitions: a user's task is revealed, changed, redirected, and resumed over time.
That makes a state machine a natural place to control the agent loop.
The example has four important runtime states:
waiting | | USER_MESSAGE v resolvingIntent | v planning | v executing
The interesting edge is from executing back to resolvingIntent.
1// src/machine.ts 2executing: { 3 on: { 4 USER_MESSAGE: { 5 target: "resolvingIntent", 6 actions: assign({ 7 message: ({ event }) => event.message 8 }) 9 } 10 } 11}
If the user interrupts the agent with: Don't use Prisma though. This repo uses Drizzle., then the agent is not allowed to blindly continue its previous plan. The new turn goes back through intent resolution first.
That produces this transition:
executing | | "Use Drizzle, not Prisma" v resolvingIntent | | revise orm -> drizzle v planning | v executing
This is a small implementation detail with a big effect: user messages become events capable of invalidating ongoing work, not just more text to append to the next prompt.
Map Different Intent Changes to Different Control-Flow Policies
The three transition types are semantically different, so the agent can react differently to them.
A reveal may be compatible with the current plan:
"We're using Postgres."
A revision may invalidate an assumption:
"Actually, use Drizzle instead of Prisma."
A function switch may replace the active task entirely:
"Before that, create the migration."
The example captures one simple policy:
1export function shouldReplan(delta: IntentDelta): boolean { 2 return delta.type === "revise" || delta.type === "switch"; 3}
A production agent could go further. A revision could trigger plan validation instead of unconditional replanning, for example. A reveal could invalidate a plan if the newly revealed constraint conflicts with a planned tool call. A function switch could suspend the current task instead of abandoning it.
The point is not that every agent needs this exact policy. The point is that once intent transitions are explicit, those policies become possible to encode and test.
Let Current Intent Decide What Context the Agent Sees
Explicit state solves only part of the problem.
One of the paper's most useful experiments compares two forms of recap. A cheap "prompt recap" asks the model to reconsider the conversation and an "oracle recap" directly provides the correct active intent. On BIRD-SQL with GPT 5.5, oracle recap substantially improves performance under function switching from 65% to 75% but still falls short of the 80% single-turn result.
That is an important result for architecture. Even if we perfectly track the current intent, stale context can still interfere with execution because the agent is still receiving the message history and the current intent.
So instead of building an executor prompt like this const prompt = entireConversation;, maybe intent should be part of context selection:
1const executionContext = { 2 currentIntent, 3 relevantHistory: selectHistory({ 4 messages, 5 currentIntent 6 }) 7};
The executor should see an authoritative recap first:
CURRENT INTENT Goal: Implement the login flow Active constraints: - Next.js - PostgreSQL - Magic-link authentication - Drizzle Completed: - Database migration
Then, it should see only history that is useful to the current task.
This is different from deleting the conversation. History still matters for reasoning, provenance, user preferences, tool results, and decisions. It simply stops being the only place where current truth lives.
I think of the two as having different jobs: Conversation history tells the agent what happened. Intent state tells it what currently applies.
Turn Evolving-Intent Research into Repeatable Agent Evaluations
The architecture is useful, but the evaluation idea may be even more powerful.
The paper starts from single-turn tasks that already have objective verifiers. It treats the original task as a final "anchor" intent, then works backward to synthesize plausible earlier turns containing reveals, revisions, and function switches. Because the conversation eventually returns to the original target, the existing verifier can still score the final action.
That is a clever way to test agent robustness without inventing an entirely new benchmark! We can borrow the same idea for our agents.
Suppose we already have a coding eval like this:
Final task: Implement magic-link auth using Drizzle. Verifier: Repository test suite.
We can construct an evolving-intent trajectory that ends at the same task:
1. Add authentication. 2. We're using Next.js and Postgres. 3. Use magic links instead of passwords. 4. Before that, create the database migration. 5. Now implement the login flow. 6. This repo uses Drizzle, not Prisma.
The final repository can still be scored by the original tests, allowing us to now compare:
Baseline conversation history -> coding agent Intent-aware conversation -> intent resolver -> explicit state -> coding agent
This means we can measure more than final success:
1type EvalResult = {
2 taskSuccess: boolean;
3 intentAccuracy: number;
4 staleConstraintViolations: number;
5 unnecessaryToolCalls: number;
6};
The example repository includes a tiny version of this idea in src/eval.ts. It starts with a known final intent, applies a trajectory of transitions, and verifies that the resulting state converges on the expected target.
The toy eval is intentionally simple. The interesting next step would be generating counterfactual trajectories automatically from an existing suite of coding tasks, just as the paper retrospectively expands single-turn benchmark examples.
Where Explicit Intent State Still Falls Short
There's a tempting conclusion here: add an intent tracker and multi-turn agent reliability is solved. The paper gives us a reason not to make that claim, though.
Even when models are handed the correct current intent through oracle recap, performance does not fully recover to the single-turn baseline. That means there are at least two failure modes:
- Tracking failure: the model no longer knows exactly what the user currently wants.
- Execution-under-conflict failure: the model knows the current intent but is still distracted or misled by prior context.
Explicit intent state attacks the first problem directly. Intent-aware context selection can help with the second, but it introduces more design questions: What should survive a function switch? Which tool results are still relevant? When is an old constraint superseded versus merely scoped to another subtask? How should suspended goals be represented if the user wants to come back to them later? Those are application-level questions and it's why I would rather expose intent as runtime state than bury all of them inside a prompt.
Treat Intent as a First-Class Runtime Primitive
The big takeaway I took from LLMs Get Lost in Evolving User Intent is not that we need a better intent classifier. It is that intent has a lifecycle. It can be partially known. It can be revised. It can become irrelevant when the user switches tasks. Parts of it can carry into the new task or the user can interrupt execution and change it again.
Once you look at intent that way, treating it as application state starts to feel less like extra architecture and more like acknowledging what the agent was already trying to track implicitly. The pattern I would start with is:
User message | v Resolve semantic delta | v Update explicit intent state | v Select relevant context | v Validate or rebuild the plan | v Execute
The LLM is still doing the part it is good at: understanding messy language and reasoning about the task. It just no longer has to reconstruct the entire current state of the collaboration from scratch every time it gets another turn. And, just as importantly, we gain something agents badly need: a state transition we can inspect, log, test, replay, and debug when the model gets it wrong.
Run the Evolving-Intent Agent Example
The companion TypeScript project contains:
- an
IntentStateandIntentDeltamodel; - deterministic state reduction;
- an optional LLM-backed intent resolver;
- an XState agent loop that handles mid-execution interruptions;
- simple replanning rules; and
- a small evolving-intent eval.
Run the no-key demo with:
1npm install 2npm run demo
Then inspect the transition trace as each user turn updates the agent's operational state. The project deliberately keeps the domain small. The architecture becomes more interesting when goals are hierarchical, task switches suspend and resume previous goals, constraints have scopes, and context retrieval uses the active intent as a filter. Those are the directions I'll explore next.
References
Tack, J., Laban, P., & Neville, J. (2026). LLMs Get Lost in Evolving User Intent. Microsoft Research. https://arxiv.org/abs/2607.20734
