Skip to content
Sneak peek — you found hosted ggui early · official launch soon

Sessions and the event model

read as .md

Every 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 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_handshakeggui_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.

Glossary → GguiSession

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.

A feedback card after a support chat. Three things happen in it, and each lands somewhere different:

// contract sketch — the two inbound specs
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: "" },
}
  • The user presses Send. That is an action. The agent must read the payload and decide what happens next, so submit lives on actionSpec and 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 contextSpec and 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.

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:

  • nextStep is advisory. An actionSpec entry 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.

ggui_consume

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_amend repaints 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_update mints 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.

ggui_amend and ggui_update

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