The three buckets
All three live in the client’s in-memory store, rebuilt from the stream on reload. They are render state; the agent’s conversation memory lives on the server: a different layer.
The reaction contract
A component’s state changes exactly two ways: both the same write into its own slice:- Arrival: it streams in already in a state (the manifest’s
defaultState, 03). - Interaction: the user clicks it into a state:
setValue { defaultState: "focused" }, now, or ten turns later.
inline, surfaces empty, panels retract, the template derives its base state. Nothing is “reset”; a component with an inline face returns to it in its turn’s history, while a surface-only component (no inline/default face + a surfaced arrival in its manifest: e.g. a rail card) simply retires: visible only while surfaced, invisible after. The explicit opt-out: "lifetime": "conversation" in the component’s manifest, marks a durable, conversation-scoped surface (a cart, an itinerary, a composed page): the platform keys its instance by the conversation instead of the turn (every re-call hydrates the same slice, a repeated arrival merges, never re-places) and the new-turn reset skips it, so it stays on screen until it’s replaced, closes itself, or a new template loads, the template swap is the hard refresh boundary: every surface (durable ones included) retires when the shell changes. Default is "turn" (everything above).
A template is in exactly ONE state at a time. The latest surfaced view IS the template’s active state: a card entering product puts the template in product mode; nothing surfaced = the conversation/welcome base. Exactly one reaction surface renders: the active state’s. Other states’ data is untouched (a rail’s cards persist in the store while an apply panel is open) and re-presents the moment its state is active again. Surfaces never stack; combinations of panels cannot exist.
Layouts sync by name (05). The same derivation goes one step further: a template is a set of layouts (full arrangements), and the active one is the layout named after the latest surfaced view, a card entering product selects the template’s product layout; nothing (or nothing matching) surfaced selects the default. Component faces and template layouts are the same mechanism at two tiers, connected purely by the view name: no writes, no wiring, and the component never knows templates exist. The template’s own LOCAL states (welcome, conversation, call phases) branch inside whichever layout is showing: component causes it = layout; template knows it = state.
Templates react via selectors: never by component type, never by id:
- A component enters a state the template has a surface for → the surface presents it.
- A state the template doesn’t know → nothing happens; the component stays inline. No error, no registry of valid state names.
- “Which component?” is intrinsic: the one that changed state is the one selected. Conflicts: most recent wins (
limit: 1). - One instance → one placeholder. A component instance renders in exactly one place. While its view matches a surface, it renders there: it lifts out of the flow into the surface; with no matching surface it renders in the flow (the
inlineplaceholder). It never paints in two places at once: you never hide a flow copy yourself (nohideBelow, no overlay-to-cover; the SDK keeps a claimed instance out of the flow). Its data stays in the conversation history throughout. Close = the instance switches its view back (its expanded face carries its own ✕ →setValue { defaultState: "inline" }) → the surface releases it → it’s back in the flow. - State is local; the view is the interface. A component’s internal state (
step,phase, …) is private: the template never reads it. The only thing that crosses to the template is the active view (defaultState); the template reacts to that (select.where { defaultState }), never to a component’s internal keys. - Many instances are fine. A source can create many instances of a component (three products → three cards); the rule is per instance. The template decides how a placeholder lays its instances out: a flow list, one focus (
limit: 1), a rail/grid, via itsselect. - Template-focus is derived, not stored: “is anything focused?” = “does any component match my focus selector?”. Nothing writes a focus flag anywhere. For template chrome (not surfaces), the same fact is projected into scope as
surfacedView: the name of the most recently surfaced view with an occupant,""when everything is inline, so e.g. a header button reacts by name:visibleWhen { "field": "surfacedView", "in": ["", "products"] }.
templateswaps the shell (the one reserved name): the whole surface re-renders; conversation, components, and data stay in the store, and the new template reacts through its own selectors.- Inline is the universal default. No state, or a state no selector matches → the component renders inline in the flow. Always.
State names are open: the template decides what they mean
focused isn’t hardcoded anywhere. A component can arrive in any named state: focused, product, pip, and a template reacts to exactly the names it defines surfaces for: a where: { eq: "product" } rail frames product cards; a template without one renders them inline. New names ship with zero protocol change. (Convention: keep names consistent per org, focused in one component and focus in another silently fragments the vocabulary templates select on.)
The two writes
setValue→ the component’s own slice: its answers, itsstep, itsdefaultState. This is the only thing a component ever writes.setTemplateValue→ template state: only for what is genuinely the template’s own (a disclosure panel, the composer draft). ❌ A component chainingsetTemplateValueto open a surface is the deprecated bridge: the linter flags it; use awhereselector instead.
tools/call; answering a waiting wizard is an elicitation. You never build transport.
The four moves: one condition vocabulary
All reactivity iseq / ne / in / truthy applied four ways:
✅ Mutually exclusive views belong in one
Switch; a case never re-guards its own discriminant. Name one field per axis (defaultState, step, callState): never boolean soup.
Named states are first-class: enumerated, viewable, served
Each layer lives as a file (states/, faces in layouts/), so three consumers read them automatically: Studio shows a pill per state (+ the Inline/Focused master toggle), the served manifest lists them for any MCP caller, and stateOrder fixes the order. Add a state = add a file.
Locked state: managed FOR you, read-only to you
- Conversation & lifecycle: written only by the stream. “Is it thinking” is the derived
isStreaming/isEmptyflags; project them, never simulate them (a stuck flag is a stream-delivery bug to report, not to patch in a definition). - Voice: the SDK’s voice service owns the audio natively and, as a producer, projects
callState(idle · active · agentSpeaking · userSpeaking) into the template’s scope; a voice template branches its phases on that one field. The transcript rides the conversation. A template binds the service by declaringservice: "voice"in its manifest. You never wire audio. - Native host chrome: ephemeral toggles that belong to the embedding host live in the host’s own state, passed as
props; never in the store, never a new primitive.
The decision table
Next: 05. Templates (MCP Apps).

