Wire protocol
Wire-format detail for AI Transport over Ably channels. Header tiers, lifecycle events, content event names, and the Run sequence.
AI Transport communicates through Ably channel messages. Every message carries headers under two tiers in the message's extras.ai object: extras.ai.transport for run identity and routing, and extras.ai.codec for stream and status metadata. The tiering isolates the two concerns so the codec layer cannot stamp transport routing headers and the transport layer cannot leak into codec stream state.
A typical Run produces the following sequence on the channel:
Client publishes input event (no run-id on a fresh send) name: ai-input extras.ai.transport: { event-id=E1, codec-message-id=M1, role=user, parent=M0 } extras.ai.codec: { stream=false } Agent publishes run-start (run-id and invocation-id minted by createRun) name: ai-run-start extras.ai.transport: { run-id=R1, invocation-id=I1, run-client-id=user-abc, input-client-id=user-abc, input-codec-message-id=M1 } Agent publishes assistant stream create name: ai-output extras.ai.transport: { run-id=R1, invocation-id=I1, codec-message-id=M2, role=assistant, parent=M1, input-codec-message-id=M1 } extras.ai.codec: { stream=true, stream-id=S1, status=streaming } Agent appends text deltas to M2 channel.appendMessage on the assistant message's serial extras.ai.codec: { stream-id=S1, status=streaming } Agent closes the stream name: ai-output (channel.appendMessage with status=complete) extras.ai.codec: { stream-id=S1, status=complete } Agent publishes run-end name: ai-run-end extras.ai.transport: { run-id=R1, invocation-id=I1, run-reason=complete }CopyCopied!
A cancel inserts an ai-cancel message keyed by the triggering input's codec-message-id (which the client owns from send time, before the agent has minted the run-id). The agent's per-Run AbortSignal fires once the input lookup resolves the input to the run, the stream closes with status=cancelled, and the agent publishes ai-run-end with run-reason=cancelled.
A continuation (a tool result follow-up, a regenerate, a suspend resume) is different in one wire detail: the client knows the existing run-id already, so it stamps it on the continuation input event under extras.ai.transport.run-id. The agent reads that header from the input event in its lookup and reuses it as the Run's id; on a fresh send the input event omits run-id and the agent mints one in createRun.
Header tiers
Headers live under extras.ai.transport and extras.ai.codec. The tier prefix isolates transport-level routing from codec-defined payload metadata.
Transport headers
Transport headers carry the routing and identity that the transport layer reads. They are stamped under extras.ai.transport.
| Header | Description |
|---|---|
run-id | Run correlation. Minted by the agent inside session.createRun(invocation) for a fresh run, and stamped on every event the agent publishes. Continuation client publishes also carry it (the client knows the existing run-id from ActiveRun.runId and stamps it on the continuation input event); the agent reads it from the input event's wire headers to reuse the existing Run rather than minting a fresh one. Absent on the first ai-input of a fresh send. |
invocation-id | Per-HTTP-request identifier. Minted by the agent inside session.createRun(invocation) (one per HTTP request) and stamped on every event the agent publishes for that invocation (lifecycle + outputs). Not present on client ai-input events; the application returns it on the HTTP response so the caller can observe it. |
event-id | Per-event identifier on client publishes. The invocation body carries the triggering event-id so the agent's Run.start() knows which event to wait for. Distinct from codec-message-id so edits and retries that reuse the same codec-message-id still carry a fresh per-send identity. |
codec-message-id | Message identity in the conversation tree. Used for branch anchoring (edit forks at the user's codec-message-id, regenerate forks at the assistant's), for optimistic reconciliation, and for stream append targeting. |
run-client-id | The Ably clientId of the client that started the Run. Set on agent-side stream messages. |
input-client-id | The Ably clientId of the input event that drove the current invocation. May differ from run-client-id on continuation invocations driven by a non-owner (for example, a tool result from a different device). |
role | Message role: user, assistant, system, or tool. |
parent | The codec-message-id of the immediately preceding message in this branch. |
fork-of | The codec-message-id this message replaces. Present on edits. |
msg-regenerate | The codec-message-id of the assistant message this Run regenerates. Stamped on the regenerate input and echoed on ai-run-start. |
run-reason | The reason a Run ended. Present on ai-run-end. One of complete, cancelled, error. A suspended Run uses the ai-run-suspend event instead and is not terminal. |
error-code | Numeric error code on ai-run-end with run-reason: error. |
error-message | Human-readable error message on ai-run-end with run-reason: error. |
Codec headers
Codec headers carry the stream lifecycle the codec encoder and decoder read. They are stamped under extras.ai.codec.
| Header | Description |
|---|---|
stream | 'true' if the message uses streaming (appends), 'false' for a discrete publish. |
stream-id | Stream identity. Set on every message that participates in a stream so the decoder can correlate appends. |
status | Lifecycle status of a streamed message. One of streaming, complete, cancelled. Set only when stream is 'true'. |
discrete | Marks a message as a discrete part. Set by publishDiscreteBatch; not set on lifecycle events from publishDiscrete. |
The following header constants are exported from @ably/ai-transport as HEADER_*:
| Constant | Value |
|---|---|
HEADER_RUN_ID | 'run-id' |
HEADER_CODEC_MESSAGE_ID | 'codec-message-id' |
HEADER_RUN_CLIENT_ID | 'run-client-id' |
HEADER_INPUT_CLIENT_ID | 'input-client-id' |
HEADER_ROLE | 'role' |
HEADER_PARENT | 'parent' |
HEADER_FORK_OF | 'fork-of' |
HEADER_MSG_REGENERATE | 'msg-regenerate' |
HEADER_RUN_REASON | 'run-reason' |
HEADER_ERROR_CODE | 'error-code' |
HEADER_ERROR_MESSAGE | 'error-message' |
HEADER_STREAM | 'stream' |
HEADER_STREAM_ID | 'stream-id' |
HEADER_STATUS | 'status' |
The remaining wire headers (invocation-id, event-id, input-codec-message-id, discrete) are SDK internals and not part of the exported constant surface. The SDK reads and writes them through the getTransportHeaders and getCodecHeaders utilities.
Event names
AI Transport uses seven Ably message names. The decoder dispatches on the Ably message name (and the stream header to distinguish discrete from streamed content), not on a separate codec-type header.
| Event | Direction | Description |
|---|---|---|
ai-input | Client | Every client-published codec event (user-message parts, tool-approval responses, regenerate signals, tool results, edits). |
ai-output | Agent | Every agent-published codec event (text deltas, reasoning, tool calls, file or source parts, data chunks). |
ai-run-start | Agent | Run lifecycle. The agent publishes this once, for the first start of a Run, before any ai-output. |
ai-run-suspend | Agent | Run lifecycle. The agent pauses the Run pending external input (a tool approval, a human-in-the-loop response). The Run is not terminal; a continuation Invocation resumes it. |
ai-run-resume | Agent | Run lifecycle. The agent publishes this when a continuation Invocation re-activates a Run (tool-result follow-up, suspended-Run resume), instead of a second ai-run-start. The agent detects the continuation by reading the existing run-id off the triggering input event's wire headers; a fresh input event omits run-id and produces ai-run-start. |
ai-run-end | Agent | Run lifecycle. Closes the Run terminally with one of complete, cancelled, error. |
ai-cancel | Client | Cancel intent, targeting a specific run-id. The agent matches against its registered Runs and fires the matching AbortSignal. |
Content messages
Content rides ai-input or ai-output depending on the publisher. The codec layer is responsible for the payload; the wire layer cares only about whether the message is discrete or streamed.
Discrete messages
A discrete message is published as a single Ably message. The entire content is in one channel.publish call. User messages are typically discrete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
channel.publish('ai-input', {
data: { role: 'user', content: 'What is the weather?' },
extras: {
ai: {
transport: {
// No run-id on a fresh send (the agent mints it). Continuations stamp the
// existing run-id here so the agent reuses it instead of minting fresh.
'event-id': 'E1',
'codec-message-id': 'M1',
role: 'user',
},
codec: { stream: 'false' },
},
},
});Streamed messages
A streamed message is published incrementally. It uses three Ably operations:
channel.publish('ai-output', ...)withstream: 'true'andstatus: 'streaming'. The publish returns a serial that the encoder captures.channel.appendMessage(...)for each token, using the captured serial. The append carries the codec payload and thestream-id.- A final
channel.appendMessage(...)withstatus: 'complete'(or'cancelled') closes the stream.channel.updateMessage(...)is only used as a recovery path when an intermediate append failed and the encoder needs to flush the accumulated payload.
A subscriber that joins mid-stream sees the latest accumulated state of the message on attach (Ably stores the rollup) and then live appends after that. The decoder treats the on-attach state as the current full state and accumulates from there.
Message identity
The codec-message-id header is the primary identifier for a message in the conversation tree. It is distinct from the Ably channel serial: the serial is assigned by Ably and totally orders messages on the channel; the codec-message-id is assigned by the publisher and identifies the message across edits, retries, and optimistic reconciliation.
Edit and regenerate forks are message-anchored: the new Run's fork-of (for an edit) or msg-regenerate (for a regenerate) points at the codec-message-id being forked, not at the previous Run.
Input-event lookup
When a client sends, the SDK mints two identifiers on the publishing side:
inputEventId: one per published input event. The last event in the send becomes the trigger the agent waits on.codecMessageId: identity for the new message in the conversation tree. Surfaced on the returnedActiveRunasinputCodecMessageIdso the client can route the agent's outputs back to the input it owned at send time.
For continuations only, the client also stamps the existing run-id on the input event's wire headers (the client knows it from the previous ActiveRun.runId). Fresh sends omit run-id and leave the agent to mint it.
The SDK does not POST to the agent endpoint itself; the developer (or the bundled Vercel ChatTransport) calls activeRun.toInvocation().toJSON() to obtain an InvocationData body ({ inputEventId, sessionName }) and POSTs it to the agent.
On the agent side, session.createRun(invocation) mints the invocationId (one per HTTP request) and either mints a fresh runId (no run-id on the input event) or reads the existing runId off the triggering input event's wire headers (continuation). Both are stamped on every event the agent publishes for this invocation. Run.start() attaches the channel with rewind and waits for the trigger event-id carried in the invocation body to arrive (live or via rewind). The rewind window is configured by rewindWindow on AgentSessionOptions; the lookup timeout is inputEventLookupTimeoutMs. If the lookup lapses, Run.start() rejects with InputEventNotFound.
The agent's outputs carry input-codec-message-id (the codec-message-id of the triggering input) under extras.ai.transport. The client uses this to correlate outputs back to the input it owned at send time, so ActiveRun.runId resolves once ai-run-start lands. The application returns run.runId and run.invocationId on the HTTP response so the caller can observe the agent-minted ids directly.
This is the mechanism that lets a serverless agent publish a Run reliably even when the channel publish and the HTTP POST race: the agent waits on the channel until the input event lands, then proceeds.
Related pages
- Codec architecture: how the codec uses the wire protocol to encode and decode messages.
- Conversation tree: how
parent,fork-of, andmsg-regeneratebuild the tree. - Transport patterns: the stream router, input-event lookup, and cancel routing.
- Errors: error codes the wire layer surfaces.