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.