Where state lives
A generative interface is a negotiated view of five stores of state that change at different speeds and belong to different parties. This module is about keeping them honest — snapshots and deltas, shared state and conflicts, streaming, interrupts, checkpoints and persistence.
- 01Name the five state stores and the four planes, and diagnose a bug by which two were confused
- 02Choose an ownership model for a feature and write its authority map
- 03Explain snapshots, JSON Patch deltas and guarded patches, and when each is used
- 04Apply the settled-field rule and discriminant-first schemas to a streaming component
- 05Describe how interrupts, resumes and checkpoints work, and the side-effect traps they hide
The model owns nothing
Start with the claim this whole module rests on: the model is a proposer, not a renderer. It emits text, tool arguments, component specs and state patches. It has authority over none of them. Something else decides whether a proposal becomes something the person sees, and something else again decides whether it becomes an effect in the world.
That makes every generative interface a negotiation between five stores of state:
| Store | Owner | How fast it changes | Durable? | Does the model see it? |
|---|---|---|---|---|
| Model context | the model call | per turn | no | it is the model's view |
| Runtime state | the agent runtime | 100 ms – 10 s | checkpointed | through prompt assembly |
| Client state | the browser | under 16 ms | no | only if you export it |
| World state | systems of record | varies | yes | only through tools |
| Human intent | the person | continuously | no | only through their actions |
And four planes cut across them. Proposal (what the model suggested), projection (what's on screen), commitment (what actually happened in the world) and context (what the model has been told). Most bugs in this field are a value crossing from one plane to another without anyone deciding it should: a predicted "✓ Sent" shown before the tool returned, a payment that fired before its dialog painted, an answer based on a filter the person already cleared. The essay The model only proposes makes the full case. This module is the engineering.
Four ways to own state
Every architecture you'll meet is one of four ownership models, or a mix.
Client-owned. The interface owns the state. The agent can change it only through frontend tools the browser executes — "add this row", "select this tab". Validation happens where the data lives. The cost: the agent can reach only what you gave it tools for, and it can't keep working when the tab is closed.
Agent-owned. The runtime owns the state; the interface is a read-only view of it. The person changes things by asking ("set the budget to 2,000"). Simple and auditable, but every edit is a round trip through the model, so it feels slow.
Shared, bidirectional. Both sides read and write one structured object. This is what AG-UI's shared state and CopilotKit's agent-state hooks provide: the agent streams changes down, the client writes changes back, and both see one document. It is the most powerful model and the one with the most ways to go wrong (see conflicts, below).
Data-model binding. Structure and data travel on separate channels, and components bind to paths in the data. In A2UI a property is either a literal or a binding to a JSON Pointer path in the surface's data model, and data updates arrive separately from component updates. json-render has the same idea with state paths. The payoff: the agent can stream new rows into a table without regenerating the table, and restyle a form without touching what's been typed into it.
Choose per feature, not per product. A trip planner might use client-owned state for the map viewport, shared state for the itinerary, and agent-owned state for the agent's plan.
Snapshots and deltas
Shared state needs a sync mechanism, and the industry has converged on two message types.
- A snapshot replaces the whole state. Use it to establish a baseline, after a reconnect, and whenever the client might have drifted.
- A delta changes part of it, as a list of JSON Patch operations (RFC 6902):
add,remove,replace,move,copyandtest, each addressed by a JSON Pointer path like/trip/items/0/status.
[
{ "op": "replace", "path": "/trip/city", "value": "Lisbon" },
{ "op": "add", "path": "/trip/items/-", "value": { "name": "Casa do Rio", "cost": 5400 } }
]
Two operational rules matter more than the format. Apply each delta atomically — all operations or none, against a copy — and on failure, ask for a fresh snapshot rather than guessing. AG-UI's reference client behaves this way. Lab 02 streams a real-shaped sequence of both; watch the state pane flash the paths each delta touches.
When the person edits while the agent writes
This is the defining conflict of shared state. The agent is mid-run, holding a picture of state from when the run began. Meanwhile the person changes a field. The agent's next patch was computed against a world that no longer exists. What happens?
| Strategy | How it works | Use it for |
|---|---|---|
| Last-writer-wins | apply whatever arrives last | nothing that matters — it silently destroys edits |
| Guarded patch | agent patches start with test operations asserting the values they assumed; any failed test rejects the whole patch and the agent re-plans |
forms, structured records |
| Field lease | the agent takes a time-boxed, breakable lock on a region; the UI shows "agent is editing" | long agent edits to one area |
| Suggestion layer | agent writes become suggestions the person accepts or rejects | documents, code, anything reviewable |
| CRDT merge | the agent is a peer in a Yjs or Liveblocks document; concurrent edits merge | canvases, rich text, multiplayer |
| Rebase intent | store the agent's intent, not its patch, and recompute on conflict | planners, agentic forms |
The guarded patch is the cheapest big win, because JSON Patch already has the operation you need:
// the agent assumed days = 3 when it planned this itinerary
const delta = [
{ op: "test", path: "/trip/days", value: 3 },
{ op: "replace", path: "/trip/itinerary", value: threeDayPlan },
];
// if the person changed days to 5, the test fails, nothing is applied,
// and the agent gets a fresh snapshot instead of overwriting them
One warning about CRDTs: a CRDT merges characters, not meaning. An agent rewriting a paragraph while a person fixes a typo in it can merge cleanly and still produce nonsense. That's why collaborative editors with AI still add suggestion layers on top. Lab 04 lets you switch strategies and watch who wins.
Write the outcome down as an authority map: one small table, ideally in code, giving every field a writer, a conflict rule, and whether the model may see it. Payment details, for instance, should be writable only by the system and never enter the model's context at all.
Streaming: everything on screen is provisional
The moment you render while the model is still emitting, every visible value is a proposal until proven otherwise. A modern agent turn interleaves several streams — text, reasoning, tool arguments, UI structure, state patches, progress activity — and each needs its own rule. Four rules cover most of it.
The settled-field rule. A field is settled when the parser has moved past it: the next key has started, or its container has closed. Unsettled strings can render as provisional text. Unsettled numbers, enums and booleans should render as placeholders, never as values — an amount that streams as 1, 12, 1200 is a trust bug. And nothing unsettled may trigger an action.
Discriminant first. Models usually emit keys in the order your schema presents them. So schema order is a UX decision: put type before props so the renderer picks the right shell immediately; id early so references resolve; small structural fields before long text; arrays of self-contained items so rows can appear one at a time. A2UI takes this further: its flat list of components with ID references lets children be referenced before they arrive, and nothing renders until the root component exists, so you never flash orphaned fragments.
Act on end. Never execute a tool on partial arguments. AG-UI's guidance is to treat arguments as incomplete until the tool call ends; providers warn that eagerly streamed arguments may never become valid JSON at all.
Cancellation is not undo. Stopping a stream stops proposals. It doesn't reverse tools that already finished, and tools in flight may or may not complete. After a cancel, an honest interface shows three lists — committed, abandoned, unknown — not one "Stopped" label.
Pair these with a latency budget built on the classic response-time limits: acknowledge input within 100 ms (locally, no model needed), show what kind of answer is coming within a second (first tokens, or a skeleton shaped by the schema once the discriminant settles), keep content arriving within ten seconds, and past that, turn the request into a task with progress, cancel and permission to leave. Open-ended generation (rung 5) often blows the ten-second budget, so it needs task-style UX, not chat-style UX. Lab 03 shows the repair, the progressive render and the catalog guardrail.
The tool-call lifecycle
A tool call is a little state machine, and the interface should render every state:
input-streaming → input-available → (approval-requested) → executing → output-available
↓ ↓
output-denied output-error / unknown
The Vercel AI SDK names most of these on its tool parts; recent versions add approval states. Notice executing. Most SDKs leave it implicit, so the interface can't tell "waiting to run" from "running for forty seconds" from "the server died". For any tool with a real-world effect, render executing explicitly, with elapsed time and a cancel button that's honest about whether cancelling is possible.
Tools also differ by where they run:
| Frontend tool | Backend tool | |
|---|---|---|
| Runs in | the browser | the agent runtime or server |
| Good for | manipulating the UI, reading local state, asking the person | data, external APIs, side effects |
| Approval lives in | the component itself | a policy plus an interrupt |
| Main danger | the agent drives the UI into states the person didn't choose | effects the UI was never told about |
Interrupts: pause, decide, resume
Human-in-the-loop has converged on one shape across protocols: the run pauses with a typed question and resumes with a typed answer, matched by ID.
- In AG-UI, a run can finish with an interrupt outcome listing what it needs — a confirmation, some input, approval of a tool call — and the client starts a new run on the same thread with answers to every open interrupt. Approving with edits replaces the arguments rather than merging them.
- In LangGraph, a node calls
interrupt(value)to pause and the client resumes with a command carrying the answer. - Agent Inbox-style interfaces declare which responses each interrupt allows — accept, edit, respond, ignore — so the UI can render exactly the right controls.
Two traps catch almost everyone:
- Resuming re-runs the node. In LangGraph the runtime restarts the paused node from the beginning on resume. Anything before the
interrupt()call runs twice. If that "anything" sent an email, you've sent two. Make everything before an interrupt idempotent. - Approvals must bind to arguments. "Approve" must authorise these arguments, not "something like this". If the arguments can change between the render and the execution, sign or hash them and check on execute.
Checkpoints and time travel
There are two families of persistence, and they produce different interfaces.
Checkpointed state (LangGraph's model). Every step writes a checkpoint. You can replay from an earlier checkpoint, or fork: change something in an earlier state and run forward from there, creating a branch. History becomes a tree, not a line, so the interface needs a branch view, not an undo stack.
Event-sourced threads (AG-UI's model). The event stream is the truth and the interface is a fold over it. Snapshots compact the log. The payoff is that replaying a logged stream reproduces the interface exactly — the basis of every lab in this guide, and of regression-testing generative UI.
The rule that keeps both honest: an undo boundary is only as wide as the effects you instrumented. Rewinding the thread to before "send the email" doesn't unsend the email. A time-travel interface that implies it can rewind the world, when it can only rewind the conversation, is the fake-"Sent"-tick bug at the scale of a whole product. Lab 06 lets you fork from a checkpoint and see which effects come along.
Persisting generated UI
Generated interfaces live in message history, and history outlives the code that drew it. Three rules:
- Store the spec, not the pixels — and the catalog version with it. A2UI pins a surface to one catalog for its lifetime for exactly this reason.
- Expect rehydration drift. Rendering an old spec with today's components produces a different screen from the one the person saw — possibly the one they approved. For anything consequential, store a render hash or screenshot alongside the approval.
- What the person saw is not what the model knows. Progress UI that the person watched may never be sent back to the agent. If they say "the third step looked wrong", the model has no idea what they mean unless you route it back deliberately.
Check yourself
- A copilot shows "Refund issued ✓" while the refund tool is still running. Which two planes were confused, and what should the interface show instead?
- Write an authority map for a shared "event planning" state with fields
venue,guestList,notesandpaymentMethod. Justify each conflict rule. - Your agent's patch replaces
/order/itemsbut the person just removed an item. Rewrite the patch so it can't destroy their change. - A streaming product card flickers from "$1" to "$1,299". Name the rule that was broken and two ways to fix it.
- Why must code before a LangGraph
interrupt()be idempotent? - A person rewinds a conversation to before the agent booked a flight. What does your interface owe them?
Go deeper
- Labs: 02 · watch an agent run, 04 · two writers, one state, 06 · graphs, checkpoints, time travel
- Patterns: S1 Authority Map, S3 Guarded Patch, ST2 Settled-Field Rendering, C7 Idempotent Resume
- Docs: AG-UI state, AG-UI interrupts, A2UI specification, LangGraph interrupts, LangGraph time travel, RFC 6902
- Next: 04 · Building it for real