Skip to content
Public preview · the console is open

Live-channel envelopes

read as .md

The live WebSocket carries three body shapes:

Shape Direction Carries
ActionEnvelope client → server A canonical user action (form submit, click, …)
StreamEnvelope server → client An outbound delivery on a named stream channel
PropsUpdatePayload server → client The full replacement props state for an already-mounted render

The first two are the envelopes an agent-declared contract validates against. PropsUpdatePayload carries no contract-declared channel — it is the state fan-out — but it rides the same socket, so it is documented here too. For the complete frame catalog (subscribe, ack, render, render_event, channel_*, …) see the WebSocket Protocol reference.

Canonical wire reference. Shapes below mirror the TypeScript in @ggui-ai/protocol verbatim: types/events.ts (ActionEnvelope), types/live-channel.ts (StreamEnvelope, PropsUpdatePayload).


Inbound live-channel envelope — the body of a type: 'action' WebSocket message. Flat, narrow, limited to fields the server actually enforces or diagnostic fields real consumers populate today.

interface ActionEnvelope<TPayload = JsonValue> {
sessionId: string; // required — render identity
type: EventType; // required — see EventType below
payload?: TPayload; // shape depends on `type`
clientSeq?: number; // client-monotonic dedup hint
schemaVersion?: string; // producer's PROTOCOL_SCHEMA_VERSION (advisory pre-launch)
}
Field Type Required Semantics
sessionId string Yes Server enforces subscriber-render binding — envelopes whose sessionId doesn’t match the render the WebSocket subscriber is bound to are rejected with SESSION_MISMATCH.
type EventType Yes Always data:submit — the only member of the EventType union (see below). The active render’s actionSpec gates which action names are accepted. (The pre-actionSpec EventSubscription / DEFAULT_SUBSCRIPTION gating shapes were deleted from @ggui-ai/protocol — there is no subscription gating on the wire.)
payload TPayload? No For type: 'data:submit' this carries an ActionEventValue shape ({action, data, tool?}) whose data field is validated against the render’s actionSpec[action].schema.
clientSeq number? No Client-monotonic sequence number for at-least-once dedup. Declared shape; no server enforcement today (no inbound dedup infrastructure yet). Clients SHOULD populate when their transport can replay (e.g., reconnect-with-backfill).
schemaVersion string? No Protocol schema version stamped by the producer. Pre-launch: advisory — consumers MUST NOT reject on mismatch. Post-launch: receiving major mismatches surface as UPGRADE_REQUIRED.

These fields appear on neighboring shapes but are excluded from ActionEnvelope by design:

Field Why omitted
appId Server resolves it from the render; client-claimed values are ignored for enforcement.
userId / user Diagnostic render metadata captured at subscribe time, not per-delivery.
deviceInfo / interfaceContext Same as above — render-level, not action-level.
componentId / contractHash Diagnostic; no enforcement consumer today.
timestamp Server uses its own clock for ordering + log emission; client-supplied timestamps aren’t authoritative.
correlationId The doctrine names this for agent-push ↔ user-action pairing; sessionId covers the narrow case today.

EventType is a single-member union — 'data:submit' is the only action type the protocol recognizes:

Type Notes
data:submit Carries ActionEventValue ({action, data, tool?}); schema-validated.

The old data:change / lifecycle:focus / lifecycle:blur / interaction:click / interaction:hover / interaction:scroll / error:validation / error:connection members were removed in draft-2026-06-12 — they had no producers.

Payload shape for data:submit:

interface ActionEventValue<TData = unknown> {
action: string; // action ID from the contract (e.g. "submit", "archive")
data: TData; // action payload (e.g. form data)
tool?: string; // MCP tool name mirrored from actionSpec[action].nextStep (when declared)
}

Derivation. tool is derived server-side from actionSpec[action].nextStep at event-build time — clients do not populate it. When the action has no declared nextStep, tool is absent — the agent decides the next tool freely from broader context. The hint is advisory either way; the agent owns the call decision.


Outbound live-channel envelope — the body of a type: 'data' WebSocket message. Carries a single delivery on a named stream channel.

interface StreamEnvelope {
sessionId: string; // required
channel: string; // required — keys into streamSpec
mode: StreamChannelMode; // required — 'append' | 'replace'
payload: JsonValue; // required — channel-specific shape
complete?: boolean; // terminal marker for completable channels
seq?: number; // server-assigned monotonic outbound sequence
schemaVersion?: string; // producer's PROTOCOL_SCHEMA_VERSION
}
Field Type Required Semantics
sessionId string Yes Render this delivery belongs to.
channel string Yes Channel name. Keys into the agent’s declared streamSpec. Names starting with _ggui: are server-owned (see Reserved channels).
mode StreamChannelMode Yes State-folding mode. 'append' (default for narrative streams — message log, telemetry) accumulates deliveries; 'replace' (default for snapshot streams — task progress, session state) replaces the current value. Senders declare; receivers honor. Typically equals the channel’s declared mode on the spec, but the envelope is the authoritative per-delivery signal.
payload JsonValue Yes Validated against streamSpec[channel].schema. Shape is channel-specific; consumers typecheck via contract inference when they use defineContract + useStream.
complete boolean? No Terminal completion marker — truthy on the last delivery for a completable channel (one declared with complete: true on the spec). Consumers use this to transition subscribers into a “channel closed” state. Absent on non-terminal deliveries.
seq number? No Render-scoped monotonic outbound sequence. Server-assigned; clients MUST NOT populate it on producer-side inputs. Gap-free within a single render, starting at 1. Used by the client to track lastSeenSeq, the highest seq it has applied, for reconnect (pass it back as SubscribePayload.fromSeq), and to dedupe deliveries against it (at-least-once semantics), never against the ack’s streamSeq (SPEC §12.2.1). OPTIONAL because stamping is best-effort: both OSS @ggui-ai/mcp-server and hosted cloud stamp seq on every successfully buffered delivery, but a delivery whose buffer record failed ships unstamped rather than blocking the emit. A future slice promotes this to required once buffering is guaranteed non-lossy.
schemaVersion string? No Same advisory-pre-launch / UPGRADE_REQUIRED-post-launch semantics as on ActionEnvelope.
Field Why omitted
replay Per-channel policy declared on streamSpec[channel].replay. A per-delivery field would imply replay can vary message-to-message, which it can’t.
timestamp Replay correctness needs seq only; timestamp is a future optional addition driven by a concrete client-UX need.

Server → client. The body of a type: 'props_update' message — the ggui_update / ggui_amend fan-out. Replaces props on an already-rendered component without re-generation.

interface PropsUpdatePayload {
sessionId: string;
props: JsonObject;
epoch?: number;
}
Field Type Required Description
sessionId string Yes The render being updated.
props JsonObject Yes Full replacement state, not a patch — post-merge for kind: "merge" updates. Persistence is the source of truth; this frame is the latency optimization.
epoch number? No The commit-time history epoch. ggui_update stamps its just-advanced epoch; ggui_amend stamps the unchanged head.

epoch is the freeze-latch signal. A mount whose own epoch is lower than the frame’s has been superseded and MUST freeze rather than apply — that is what keeps an old history card from silently repainting with state belonging to a newer one.

Absent epoch reads as 0: pre-epoch producers and amends against an epoch-0 head omit it. Consumers MUST tolerate the absence rather than reject the frame.


Channel names starting with _ggui: are reserved for the server. Agent-authored streamSpec MUST NOT declare any name in this namespace; structural validation rejects every reserved-prefix entry with the message Stream channel '<name>' is in the reserved '_ggui:' namespace — server-owned channels cannot be declared in agent streamSpec.

Two channel names are recognized today:

Channel Owner Payload shape Purpose
_ggui:preview Server (A2UI) A2UI ServerMessage — see @ggui-ai/preview-a2ui Provisional A2UI assembly stream emitted by the server during fresh-gen ggui_render flows. The renderer subscribes implicitly and dispatches the payload through its preview surface.
_ggui:lifecycle Server GguiLifecyclePayload Generation-progress lifecycle kinds (handshake_started, handshake_completed, render_started, consume_polling) the renderer surfaces as a progress affordance.

A typo inside the reserved namespace (e.g. _ggui:preveiw) is NOT recognized — delivery falls through to the normal “unknown channel” rejection rather than silently passing as a reserved channel.

The _ggui:contract-error channel was a reserved channel in earlier drafts but was removed entirely in draft-2026-06-11 — the channel, its payload shape, and its validator are gone. Contract violations now surface as a typed CONTRACT_VIOLATION error frame on the live channel on the call that caused them; nothing lands on the consume buffer.


The schemaVersion field is present on both envelopes. It carries the producer’s PROTOCOL_SCHEMA_VERSION constant.

Pre-launch (draft- versions): advisory. Consumers MUST NOT reject on mismatch; missing schemaVersion fields are normal.

Post-launch: receiving an envelope whose major version diverges from the consumer’s known major surfaces as a live-channel error with code: UPGRADE_REQUIRED. The handshake resolution flow is documented in the WebSocket Protocol reference.

See Version policy for the full mapping of what counts as a major / minor / patch bump.