Live-channel envelopes
read as.md The live WebSocket carries two envelope shapes:
| Envelope | Direction | Carries |
|---|---|---|
ActionEnvelope | client → server | A canonical user action (form submit, click, …) |
StreamEnvelope | server → client | An outbound delivery on a named stream channel |
Canonical wire reference. Shapes below mirror the TypeScript in @ggui-ai/protocol verbatim: types/events.ts (ActionEnvelope), types/live-channel.ts (StreamEnvelope).
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 for reconnect (pass it back as SubscribePayload.fromSeq) and to dedupe deliveries (at-least-once semantics). OPTIONAL today because hosted cloud doesn’t yet stamp seq; OSS @ggui-ai/mcp-server always populates it. A future slice promotes this to required. |
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. |
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 (code -32020) 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.