Sessions and the event model
read as.mdEvery ggui exchange runs on two ideas: a GguiSession — one rendered UI with a stable id — and a placement rule that decides where each thing the user does shows up on the wire. Get both right and the loop is small: render, consume, react, repaint.
This page is conceptual. The wire shapes live in MCP Protocol and Envelopes; nothing is duplicated here.
A GguiSession is one rendered UI
Section titled “A GguiSession is one rendered UI”A GguiSession is minted server-side inside ggui_render — one per UI emission — and keyed by a stable sessionId. It carries the compiled component, the props the agent rendered, and the contract that pins the wire surface. The server is the authority on its state, not the agent.
There is no conversation-level session object. Each ggui_handshake → ggui_render pair mints a fresh sessionId; sibling cards inside the same host chat are grouped through the _meta["ai.ggui/host-session"] slice, captured once at creation. Your agent never threads a conversation id.
Two inbound specs, one test
Section titled “Two inbound specs, one test”A contract has four specs. Two of them are inbound — they carry what the UI tells the agent’s side:
| Spec | Direction | Cardinality | Drives an agent turn |
|---|---|---|---|
actionSpec |
UI → agent | discrete events | Yes, every entry |
contextSpec |
UI → server (mirrored) | state, last-write-wins | No, never |
The placement test is one question:
Does this thing need the agent’s next-turn reasoning?
Yes → actionSpec. No → contextSpec. There is no third inbound category, and no flag that softens one into the other. If you reach for terminal: false on an action to stop it waking the agent, the thing you are describing is state.
Worked example
Section titled “Worked example”A feedback card after a support chat. Three things happen in it, and each lands somewhere different:
// contract sketch — the two inbound specsactionSpec: { 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: "" },}- The user presses Send. That is an action. The agent must read the payload and decide what happens next, so
submitlives onactionSpecand reaches the consume pipe as its own event. - The user switches the category filter. That is context. The agent does not react per switch, but when it next does work it wants to know what the user was looking at. It lives on
contextSpecand is mirrored continuously. - The card scrolls the newest comment into view. That is neither. Nothing about it crosses the wire, so it is not on the contract at all — it is behavior, written into the generated component code.
The third bullet is the split the placement rule sits inside: the contract describes data flow; the component code describes behavior. Scroll, focus, debounce, toast, animation, clipboard writes and local drafts are component decisions. Put them on the contract and every renderer has to honor them identically, and two contracts for the same logical UI stop hashing the same — which is how blueprint reuse fragments.
If the agent genuinely must know that a behavior happened, the signal becomes an action or a context slot; the mechanic stays in component code.
The loop is agent-driven
Section titled “The loop is agent-driven”Actions do not call your tools. The server appends each gesture to a render-scoped pipe, and your agent drains it by calling ggui_consume({ sessionId }) — a long-poll that returns on the first event or at timeout, with consume-once semantics.
Each row carries the action key that fired, its validated payload, and uiContext — the contextSpec snapshot taken at the moment the user acted, not whatever the state had drifted to by the time consume returned. That is why context needs no event channel of its own: it rides along with the action that woke you.
Two properties follow from the loop being yours:
nextStepis advisory. AnactionSpecentry may name a tool to run next. Implementations must treat it as a hint; the agent owns the call decision.- There is one routing model. A deployment with no agent attached queues events on the same pipe until one attaches and drains them. The server never invokes a tool on the user’s behalf.
Exit the loop when you have the events you need — that is the real terminal condition. status: "expired" is the other exit and only fires when a render’s TTL actually elapses.
Amend repaints, update writes history
Section titled “Amend repaints, update writes history”After you react to an event you usually want the card to reflect it. Two tools, and the choice is about the transcript, not the mechanics:
ggui_amendrepaints the already-mounted card in place. No new card appears, the history number does not advance, and scroll position, focus and uncommitted input survive. This is the default move in the gesture loop: consume → domain tool → amend → consume again. Skipping the amend is the most common wire-compliance bug.ggui_updatemints the updated state as a new card. The epoch advances by one and the previous card freezes as a history record, so the transcript stays an honest account of what the user actually saw. Use it for milestones worth showing.
Neither changes the contract. When the surface itself must change shape, handshake and render a fresh session.
How long a session lives
Section titled “How long a session lives”There is no close ceremony, because there is nothing to close. When an exchange is over, your agent stops addressing that sessionId.
On hosted ggui the default retention is effectively indefinite, so reopening an old card is the expected path rather than a lucky one. A render still accepts an optional per-render ttlSeconds when you want a bounded lifetime, and self-hosted deployments set their own default.
Because the record is durable, a host that reads the same ui:// locator later gets the view back. If the live row has been evicted, the read re-mints it — from the stored identity record when the deployment keeps one, or from the blueprint named in the locator, which returns the original card with its default props rather than a typed failure. Old cards rehydrate; they do not go stale.
→ Render locator grammar for the URI shape, including the epoch pin
See also
Section titled “See also”- How ggui works — the same lifecycle as a narrative walkthrough
- MCP Protocol — every tool’s input and output
- Envelopes — live-channel wire reference
- Glossary — term lookup