The model only proposes
The fake "sent" tick, the payment that fires before the dialog paints, the agent reasoning about a filter you already cleared — they're the same bug. A four-plane map for building generative UI that doesn't lie.
Here are three bugs we have seen in generated interfaces this year.
One. A support copilot drafts a refund email and shows "✓ Sent to customer" in the chat. The send tool hasn't returned yet. Thirty seconds later it fails on a rate limit. The person has already moved on, certain the customer was told.
Two. An agent-built settings page asks "Delete your account? This cannot be undone." The dialog looks right. Every screenshot of the flow looks right. But the page fires the DELETE request on the first click and shows the dialog afterwards. The confirmation is decoration.
Three. A person filters a dashboard to "last 30 days", asks a question, clears the filter, and asks a follow-up. The agent answers the follow-up using the 30-day numbers. Its picture of the screen is ten seconds stale.
They look unrelated: a streaming bug, a consent bug, a context bug. They are the same bug. In each case the system treated one kind of state as if it were another. Once you can name the kinds, you can design them apart.
The model is not a renderer
We say "the model generates UI" as if the model drew the screen. It doesn't. A model emits tokens: text, tool arguments, component specs, state patches. It has authority over none of them. Every one of those outputs is a proposal that something else must accept, render, or act on.
Authority lives in four other places:
- The agent runtime owns durable task state: the plan, the checkpoints, what step we're on.
- The client owns fast local state: focus, scroll, the half-typed value in a field.
- The world owns effects: rows in a database, emails in an inbox, money on a rail.
- The person owns intent — which no system can observe directly, only infer from what they do.
So a generative interface isn't "the model's UI". It's a negotiated view across five stores of state — model context, runtime, client, world and human intent — that change at different speeds and belong to different parties. The engineering job is to keep them from being mistaken for each other.
Four planes
The most useful cut we've found across those stores is four planes. Every value in a generative interface lives on one of them, and moving a value from one plane to another should be a deliberate, visible act.
| Plane | What lives there | Cost to change | Who decides |
|---|---|---|---|
| Proposal | streamed text, partial tool arguments, draft specs, state deltas | free, revocable | the model |
| Projection | what the person can see and touch right now | cheap | the client and host |
| Commitment | effects in the world: rows written, emails sent, money moved | expensive, sometimes permanent | tools, gated by consent |
| Context | what the model has been told about the screen and the world | silent, easy to let drift | whatever feeds the prompt |
Now re-read the three bugs.
- The fake "✓ Sent" is a proposal rendered as a commitment. The model predicted success; the UI displayed the prediction as a fact.
- The dialog-after-delete is a commitment without projection. An irreversible effect left the browser before the person could see what they were agreeing to.
- The stale filter is a context plane lagging the projection. The screen changed; what the model was told did not.
The pattern generalises. A pre-ticked insurance box in a generated checkout is a projection designed to manufacture a commitment nobody intended. A prompt injection hidden in a web page the agent read is the context plane written by an untrusted party — then promoted into proposals with authority it never earned. Different symptoms, same disease: a value crossed planes without anyone deciding it should.
Rule one: authority is explicit, field by field
For every value on screen you should be able to answer three questions. Who may write it? Who must be told when it changes? What happens when two writers disagree? If the answer is "whoever wrote last", you have a bug that will eventually eat someone's work.
The classic case is shared state. The agent is updating a trip plan; the person is typing a note into the same plan. With last-writer-wins, the agent's patch lands a second after the person's keystroke and silently erases it. Nobody sees an error. The person just loses trust.
Three fixes, in increasing order of rigour:
- Ownership by focus. While a person is editing a field, agent writes to that field become suggestions they can accept, not overwrites.
- Guarded patches. The agent's patch carries a precondition — JSON Patch has a
testoperation for exactly this — asserting the value it last saw. If the person changed it, the patch fails and the agent re-plans instead of clobbering. - An authority map. A declared table of which party owns which field, enforced in one place rather than remembered in many.
The protocols are already moving this way, which tells you the problem is real. AG-UI separates full state snapshots from incremental deltas, and separates activity the person sees from messages the agent receives. A2UI separates a surface's component structure from its data model, and two-way-bound inputs update local data without telling the agent until an action fires. MCP Apps lets a server mark tools as callable by the app's UI but invisible to the model. Each of those is an answer to "who may write this?". Lab 04 lets you watch last-writer-wins lose an edit, then fix it.
Rule two: provisional is a different type from committed
Streaming changed what anything on screen can be trusted to mean. Once you render while the model is still emitting, every visible value is provisional until proven otherwise. Good systems make that distinction impossible to miss — and impossible to act on.
That gives three concrete practices:
- The settled-field rule. Render or act on a streamed field only once it can no longer change. An amount that is still streaming reads
1,12,124,1240. Showing it is fine. Enabling "Approve" next to it is not. - Act on end. Never execute anything on partial tool arguments. Wait for the end of the call. AG-UI's own guidance is to treat tool arguments as incomplete until the tool call ends; model providers warn that eagerly streamed arguments aren't even guaranteed to be valid JSON.
- Different visuals for different planes. "Drafting…", "Sending…" and "Sent ✓" are three states, not one. Only the tool's result may produce the last one. Assistive technology should hear the difference too.
Schema design matters more than people expect here. If the field that decides what kind of component this is arrives last, the interface reflows as it streams. Put the discriminant first. Lab 03 shows a spec rendering before its closing brace arrives — and refusing a component it was never allowed to render.
Rule three: consent scales with consequence, and binds to the effect
A confirmation dialog spends human attention. Spend it where effects are hard to undo, and nowhere else. The scale we use is reversibility:
- Reversible by the person — they can press undo. These want an undo window, not a dialog.
- Reversible by an operator — support can put it back. A clear inline confirmation is enough.
- Irreversible but bounded — permanent, with a known ceiling, like a $20 charge. An explicit confirmation showing the exact amount and recipient.
- Irreversible and unbounded — a mass email, a deletion, a wire transfer. A confirmation the person can see, plus approval from a human outside the run.
Two details turn this from a policy into a guarantee. First, the confirmation has to be visible before the effect leaves the browser, not merely present in the DOM at some point. Second, the approval has to be bound to the exact arguments that execute. "Approve" on a $1,240 refund must not authorise a $12,400 one because the arguments changed between render and execution. Some SDKs now sign approvals over the tool name, the call and its input for exactly this reason.
There's a third detail people miss: the classification belongs to the call, not the tool. sendEmail to one colleague and sendEmail to 40,000 customers are different classes. Compute the class from the arguments.
This is the bug from the opening, the dialog-after-delete, and it's why we built RUI: it opens the generated page in a real browser, pauses each outgoing request at the network layer, reads what was on screen at that instant, and grades the effect against the consent its reversibility class requires. Two pixel-identical dialogs can earn opposite verdicts, because one guards a soft delete and the other a hard one. You can't see that in a screenshot. Lab 05 walks through it.
Rule four: keep the model's picture honest
The context plane is the quiet one, which is why it fails silently. Whatever the model believes about the screen came from somewhere: readable app state, the selection, an accessibility tree, a tool result, a page it browsed. Three habits keep it honest.
- Version the context. Stamp what you send the model with the projection it describes. When the person changes the screen, the next turn should say so — or the model should be able to ask.
- Selection is pointing. When someone says "make this bigger", the selection is the referent. Send it explicitly; don't make the model guess from a screenshot.
- Taint what you didn't write. Text from web pages, emails, tool outputs and generated UI didn't come from your user. Mark it, keep it out of instruction position, and never let it trigger a consequential tool without the same consent a person would need.
The reverse direction matters too: agents increasingly operate interfaces built for people. They perceive them through lossy projections — screenshots, accessibility trees, declared tools. An unlabelled div with a click handler is invisible to a screen reader and to an agent alike. Accessibility debt is now agent debt. Lab 08 shows a page the way an agent reads it.
A checklist for your next design review
Before a generated surface ships, walk every value and every action through these ten questions:
- Which plane is this value on right now, and does its appearance say so?
- Who may write it, and what happens when two writers disagree?
- Can anything on screen claim an outcome before a tool has returned it?
- Is any action enabled while a field it depends on is still streaming?
- What is the reversibility class of each effect — computed from its arguments?
- Is the required confirmation visible before the request leaves?
- Is the approval bound to the exact arguments that execute?
- Who renders the confirmation: trusted host chrome, or the generated UI itself?
- What does the model currently believe about this screen, and how stale can that be?
- Which text in the model's context came from someone other than the user?
If you can answer all ten for every surface, you're in the top few percent of teams shipping this. If you can prove the answers with evidence from a real browser, you're ahead of nearly everyone.
The short version
The model proposes. The interface projects. Tools commit. Context informs. Keep the four apart, move values between them on purpose, make every crossing visible, and price every commitment in consent. Most of what goes wrong in generative UI is a crossing nobody decided to make.
This framework comes out of our lab work on RUI and Agentable UI. The full pattern language — 45 named patterns across generation, state, streaming, consent, visibility, the reverse channel and safety — is in the Patterns section.