Live-channel envelopes
read as.mdThe 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).
ActionEnvelope
Section titled “ActionEnvelope”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. |
Fields intentionally NOT on the envelope
Section titled “Fields intentionally NOT on the envelope”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
Section titled “EventType”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.
ActionEventValue
Section titled “ActionEventValue”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.
StreamEnvelope
Section titled “StreamEnvelope”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. |
Fields intentionally NOT on the envelope
Section titled “Fields intentionally NOT on the envelope”| 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. |
PropsUpdatePayload
Section titled “PropsUpdatePayload”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.
Reserved channels
Section titled “Reserved channels”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.
Schema versioning
Section titled “Schema versioning”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.
See also
Section titled “See also”- WebSocket Protocol — live-channel message types (
subscribe,action,ack,render,data,error) that carry these envelope payloads. - MCP Protocol — MCP JSON-RPC methods (
ggui_handshake,ggui_render,ggui_consume, etc.). - Protocol overview — the three-channel topology.
- Version policy — semver semantics, breaking-change definition, deprecation timeline.
@ggui-ai/protocolsource — canonical TypeScript definitions.