There is no third category

Every field on a generative UI wire is a promise that every renderer will honor it and every agent will reason about it. That is the price, and it is why the wire in front of an agent should carry two kinds of inbound thing and refuse a third. In GGUI the two are actionSpec, for events that wake the agent, and contextSpec, for state the agent reads when it next does work. Everything a user can do to a surface lands in one of them, and the test that decides which is one question. This essay is about that question. It covers the two failures the question prevents and the flag we refused to add, and it ends on the two things the rule deliberately leaves open.

Two inbound specs, one question

A GGUI contract has four specs. propsSpec pushes initial values into the surface once at render, and streamSpec carries the agent’s later deliveries outward. The other two point the other way, and they are the whole of what a surface can tell the agent’s side. The placement test between them reads, in full:

Does this thing need the agent’s next-turn reasoning?

Yes, and the entry belongs on actionSpec. The server appends the event to a per-render pipe, and the agent drains it with ggui_consume, a long-poll that returns on the first event or at timeout, consume-once. No, and the entry belongs on contextSpec. The surface mirrors the value to the server continuously. Last write wins, and nobody is woken.

The examples sort themselves once the question is asked. A submit click, a cancel, a wizard’s next, a chat message sent, a choice picked from a list: each one needs a decision from the agent, so each is an action. Draft text as it is typed, a slider mid-drag, the current tab, a typing indicator, an autosave timer firing: none of these deserves a turn, so all of them are context. The one case that looks ambiguous resolves the same way. A slider being dragged is context; the slider being released at a final value is the commit point, and the commit point is an action.

If two contract authors disagree about where a gesture goes, they are disagreeing about whether the agent should react to it. That is a product conversation, and the placement rule is deliberately silent on it. The rule only guarantees that once the product decision is made, the wire has exactly one place to put it.

What goes wrong in each direction

The two mistakes are mirror images, and each breaks something you can watch break. Here is the contract sketch our own docs use to show the split, a feedback card after a support chat:

actionSpec: {
  submit: {
    label: "Send feedback",
    schema: {
      type: "object",
      properties: { rating: { type: "number" }, comments: { type: "string" } },
    },
  },
},
contextSpec: {
  category: { schema: { type: "string" }, default: "all" },
  draft: { schema: { type: "string" }, default: "" },
}

Move draft onto actionSpec, on the theory that the agent might like to see the text as it forms, and the loop degrades at once. Consume returns on the first event in the pipe, and now there is an event per keystroke. The agent wakes and reads a partial sentence. It has nothing to do with it, so it calls consume again. Every turn the model spends on that is a turn it does not spend on the submission that eventually arrives. The turn budget drains on noise, and nothing in the protocol is broken. The contract simply asked for it. The mistake is common enough that the protocol ships a validator for it. A name in actionSpec matching save, draft, change, typing, scroll, hover, focus or input draws a warning that says, in effect, this might be state; consider contextSpec.

Move submit onto contextSpec instead as a boolean the surface flips. The failure is quieter and worse. Context is mirrored state, and mirrored state overwrites. The server sees submit: true, then the surface resets the flag and the server sees submit: false, and at no point did anything land on the consume pipe. The agent never wakes. The user pressed a button that did nothing, and no error was raised anywhere, because nothing violated the contract. Discrete events reach the agent in order only if they are declared as events. A state slot has no notion of “fired”. The same validator warns in this direction too, on a contextSpec name like submit, send, confirm, cancel or done. Both checks are deliberately loose. They surface candidates and leave the decision to the author, which is the right posture for a rule whose answer depends on the product.

Both failures are placement errors and neither is a bug in either side’s implementation. That is the point of having a test that fits in one line. The cost of getting it wrong is paid in agent turns or in lost gestures, and the cost of getting it right is one question at authoring time.

The flag we refused to add

Every contract author eventually reaches for the same escape hatch. An action that should not end the consume. A terminal: false on the entry, meaning “deliver this, but keep listening”. We considered it, along with a consumeSpec field that would describe how the agent should listen, and an interaction mode enum that would label the surface as collecting or conversing. The public contract type has none of them, and the docs say why in a sentence: if you reach for terminal: false on an action to stop it waking the agent, the thing you are describing is state.

Each rejected field fails the same way. It exists to soften an action into something that does not drive a turn, and the placement rule already has a name for a thing that does not drive a turn. A terminal flag on an action is contextSpec with extra steps. A consumeSpec is redundant with the presence of actionSpec, which is the only thing consume needs to know; a render whose contract has an empty actionSpec gets no nextStep at all, because there is nothing to wait for. An interaction mode tried to label in one word what the four specs already describe by their presence, and the type’s own comment refuses it on exactly that ground. When the placement rule holds, every one of these becomes metadata about a decision the specs already made.

The reason to refuse them is not tidiness. Each one would have been a field every renderer must interpret identically and every agent must author correctly, on the wire forever, to solve a problem that a one-line test already solves at authoring time.

Why the cache cares which spec you chose

There is a second reason the split has to be clean, and it is economic. GGUI reuses generated components. The reuse key is the canonical hash of the contract plus its design variance, and the same pair reuses one component. Two contracts with the same wire-observable shape produce the same canonical bytes, so they share a blueprint; two that differ hash differently and each pays for its own generation.

Now imagine actionSpec carrying state-mirroring entries that vary from one author to the next. One feedback card declares draftChanged as an action and another does not, though both surfaces do exactly the same thing. The hashes diverge and the cache fragments. A render that should have been a lookup becomes a generation. The placement rule keeps actionSpec tight to genuine events and lets contextSpec absorb the variable surface, and the hash stays stable across authors who made the same product decision. A cached blueprint is only worth having if two people describing the same UI produce the same key.

The split this rule sits inside

The placement question is the inner half of a larger one. The published rule is an assertion, that the contract describes data flow and the component code describes behavior. Asked as a question, it is whether the agent can observe the thing from the wire at all. If yes, it is data and goes on one of the four specs. If no, it is behavior and lives in the generated component code. Scroll position, focus rules, debounce cadence, toasts, animations, local drafts and clipboard writes are all behavior. None of them crosses the wire, so none of them is the contract’s business.

That is what keeps a contract renderer-neutral. A React renderer, a native one and a hand-written harness can read the same contract and choose their own scroll and focus rules. The moment a scrollToBottom command or a toast channel appears on the wire, every renderer must implement it identically, and the agent is dictating presentation it cannot even see. The feedback card above scrolls its newest comment into view, and that scroll is nowhere in the contract.

The two tests compose. If the agent must know that a behavior happened, the signal becomes data and the mechanic stays in code. Suppose a user presses copy. If the agent needs to react, declare a copied action and keep the clipboard write in the component. The wire carries the fact; the component carries the doing.

What the rule does not decide

A test that fits in one line has to leave things out, and two of them are worth stating plainly.

It does not decide whether the agent should react. A wizard author who wants the agent to see every field change and one who wants only the final submit are both right for different products. The placement rule will file either decision correctly. What it will not do is make the decision.

It also does not give the agent a live view of context between turns. Each consume event carries uiContext, the contextSpec snapshot taken at the moment the user acted, which is why context needs no event channel of its own. Between actions, an agent that wants the current state reads it with ggui_get_session, and there is a real limit there. Its contextSnapshot is present only on a component mount; an MCP Apps mount never carries it. On a host that mounts the surface as an MCP Apps resource, the agent’s between-turn view of context is whatever the last action brought with it. We have not closed that gap, and a contract author designing for an MCP Apps host should know it is there.

The smell, named

When you read a generative UI wire, GGUI’s or anyone’s, look for the fields that describe how the agent should listen or what the surface should do. A terminal flag, a consumeSpec, a listening mode, a commandSpec full of scrollToBottom and focusInput, a behaviors block tucked into metadata. Each one is a sign that the placement decision was skipped and a field was added to paper over it, and each one will be honored by every renderer and reasoned about by every agent for the life of the protocol.

The alternative costs one question per entry. GGUI’s public contract type has six optional members, the four specs and two capability catalogs, and no field of that kind.