# 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, routing, and the stream lifecycle, and `extras.ai.codec` for the payload identity a codec declares. The tiering isolates the two concerns, so a codec cannot stamp transport routing headers and the transport never has to understand a codec's payload. A typical run produces the following sequence on the channel: ```text 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, stream=false, discrete=true } extras.ai.codec: { kind=user-message, partType=text } 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 step-start name: ai-step-start extras.ai.transport: { run-id=R1, invocation-id=I1, step-id=SP1, step-client-id=agent-1 } 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, step-id=SP1, step-start-serial=, stream=true, stream-id=S1, status=streaming } extras.ai.codec: { kind=text } Agent appends text deltas to M2 channel.appendMessage on the assistant message's serial extras.ai.transport: { stream=true, stream-id=S1, status=streaming, ...every other persistent header repeated } extras.ai.codec: { kind=text } Agent closes the stream name: ai-output (channel.appendMessage with status=complete) extras.ai.transport: { stream=true, stream-id=S1, status=complete, ...every other persistent header repeated } extras.ai.codec: { kind=text } Agent publishes step-end name: ai-step-end extras.ai.transport: { run-id=R1, invocation-id=I1, step-id=SP1, step-reason=complete } Agent publishes run-end name: ai-run-end extras.ai.transport: { run-id=R1, invocation-id=I1, run-reason=complete } ``` 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 and stream lifecycle 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 `clientRun.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. The SDK stamps `user` on client inputs and `assistant` on agent output and on a reconstructed assistant turn such as a tool-result fork. The field is an open string, and the Vercel codec's message role also allows `system`. The tree only distinguishes `user` from everything else. | | `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`. | | `supersedes` | The `run-id` of a run whose output the run this message opens replaces. A client answering a tool call sets it to the suspended run it forked away from, and the tree excludes that run from branch selection, so one client's single response renders as one linear reply while concurrent forks from several clients still show as sibling branches. `fork-of` and `msg-regenerate` differ in that they create a navigable sibling and keep both visible. | | `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. | | `step-id` | Step correlation. Stable across retry attempts of the same [step](https://ably.com/docs/ai-transport/concepts/runs.md#steps): a retry publishes `ai-step-start` under the same `step-id` and the retry's output supersedes the failed attempt. Set on `ai-step-start`, `ai-step-end`, and every `ai-output` published inside the step. | | `step-client-id` | The Ably `clientId` attributed to a step. Sticky across steps in a run by default; a mid-run steering message that incorporates a fresh input overrides it. | | `step-start-serial` | The Ably serial of the step's own `ai-step-start`. Stamped on every `ai-output` inside the step so the tree can elect the latest attempt when two attempts share a `step-id`. | | `step-reason` | The reason a step ended. Present on `ai-step-end`. One of `complete`, `failed`, `cancelled`. | | `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`. | | `input-codec-message-id` | The `codec-message-id` of the input event that drove the current invocation. Lets the transport route a cancel or a continuation to the run an input opened before the `run-id` is known. | | `steer-codec-message-ids` | The `codec-message-id` list a mid-run steering message incorporated. Stamped after the message is built rather than by the header builders. | | `stream` | `'true'` if the message uses streaming (appends), `'false'` for a discrete publish. Stamped on content messages only, so `ai-input` and `ai-output` carry it. The run and step lifecycle events and `ai-cancel` omit it. | | `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 on the parts of an exploded batch. | Ably replaces the whole `extras` object when a message is appended or updated, so the encoder repeats every persistent header on each append, on the closing append, and on a recovery update. It captures the persistent set once when the stream starts. ### Codec headers Codec headers carry the payload identity the codec's own descriptors declare. They are stamped under `extras.ai.codec`, and the tier is omitted entirely when a message has no codec headers. | Header | Description | | --- | --- | | `kind` | The descriptor key the decoder dispatches on. Stamped by the SDK on every codec message, and reserved, so a codec cannot bind a field to it. | | `partType` | The sub-discriminator for one part of an exploded batch input. Reserved in the same way as `kind`. | Every other codec header comes from the `fields` a codec declares on an individual descriptor, so the set varies by codec. The [codec architecture](https://ably.com/docs/ai-transport/internals/codec-architecture.md) page covers declaring them. 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`, `supersedes`, `steer-codec-message-ids`, `step-id`, `step-client-id`, `step-start-serial`, `step-reason`) are SDK internals and are not exported as constants. The SDK reads and writes them through the `getTransportHeaders` and `getCodecHeaders` utilities. ## Event names AI Transport uses nine Ably message names. The message `name` selects the path: `ai-input` and `ai-output` carry content, and the other seven are lifecycle events the decoder handles from their transport headers alone. On the content path, dispatch runs in three stages. The Ably action and the `stream` transport header choose between the streamed and discrete paths, the message `name` fixes the direction as either an input or an output, and the `kind` codec header selects which of the codec's descriptors decodes the payload. The decoder never dispatches on the shape of the payload. | 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-step-start` | Agent | [Step](https://ably.com/docs/ai-transport/concepts/runs.md#steps) lifecycle. Opens a bracket for one re-attemptable unit of output. A fresh `ai-step-start` under an existing `step-id` supersedes the prior attempt. | | `ai-step-end` | Agent | Step lifecycle. Closes the step with `step-reason: complete`, `step-reason: failed`, or `step-reason: cancelled`. A step terminal is not a run terminal; the run stays active for the next step or for its own `ai-run-end` / `ai-run-suspend`. | | `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 is published on `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, published by the client: #### Javascript ``` 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: 1. `channel.publish('ai-output', ...)` with `stream: 'true'` and `status: 'streaming'`. The publish returns a serial that the encoder captures. 2. `channel.appendMessage(...)` for each token, using the captured serial. The append carries the codec payload and the `stream-id`. 3. A final `channel.appendMessage(...)` with `status: '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 rather than 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. Exposed on the returned `ClientRun` as `inputCodecMessageId` so 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 `clientRun.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`](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md)) calls `clientRun.toInvocation().toJSON()` to obtain an [`InvocationData`](https://ably.com/docs/ai-transport/concepts/runs.md#invocations) 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. `AgentRun.start()` awaits `run.located`: the run's input-event watcher matches the trigger `event-id` carried in the invocation body, whether it is already folded into the Tree (from channel history the agent paged in) or arrives live. There is no lookup timeout. 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 `clientRun.runId` is populated once `ai-run-start` lands (await `clientRun.started`). The application returns `run.runId` and `run.invocationId` on the HTTP response so the caller can observe the agent-minted ids directly. This mechanism 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](https://ably.com/docs/ai-transport/internals/codec-architecture.md): how the codec uses the wire protocol to encode and decode messages. - [Conversation tree](https://ably.com/docs/ai-transport/internals/conversation-tree.md): how `parent`, `fork-of`, and `msg-regenerate` build the tree. - [Transport patterns](https://ably.com/docs/ai-transport/internals/transport-patterns.md): the stream router, input-event lookup, and cancel routing. - [Errors](https://ably.com/docs/ai-transport/api/errors.md): error codes the wire layer surfaces. ## Related Topics - [Overview](https://ably.com/docs/ai-transport/internals.md): Under the hood of Ably AI Transport. Wire protocol, codec architecture, conversation tree, and transport patterns. - [Codec architecture](https://ably.com/docs/ai-transport/internals/codec-architecture.md): How the AI Transport codec bridges TInput and TOutput events to Ably messages. Encoder, decoder, and writing a custom codec with defineCodec. - [Conversation tree](https://ably.com/docs/ai-transport/internals/conversation-tree.md): How AI Transport maintains a branching conversation tree. The two-node turn model, sibling resolution, and message-anchored branch selection. - [Transport patterns](https://ably.com/docs/ai-transport/internals/transport-patterns.md): Internal transport components in AI Transport. Stream router, input-event lookup, and cancel routing. ## Documentation Index To discover additional Ably documentation: 1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages. 2. Identify relevant URLs from that index. 3. Fetch target pages as needed. Avoid using assumed or outdated documentation paths.