# Codec The `Codec` interface is the bridge between an AI framework and Ably channel messages. It describes the wire as a flat stream of input and output events and folds those events into a per-Run projection that the SDK extracts messages from for the conversation tree. Implement `Codec` to integrate any AI framework with AI Transport. The SDK ships [`UIMessageCodec`](https://ably.com/docs/ai-transport/api/javascript/vercel/codec.md) for the Vercel AI SDK. For other frameworks, implement the methods below. #### Typescript ``` import type { Codec } from '@ably/ai-transport'; const myCodec: Codec = { init() { /* ... */ }, fold(state, event, meta) { /* ... */ }, createEncoder(channel, options) { /* ... */ }, createDecoder() { /* ... */ }, getMessages(projection) { /* ... */ }, createUserMessage(message) { /* ... */ }, createRegenerate(target, parent) { /* ... */ }, }; ``` ## Properties `Codec` extends [`Reducer`](#reducer) and adds factories for encoders, decoders, and the message-extraction step the SDK uses to populate the tree. | Property | Description | Type | | --- | --- | --- | | init | Build an empty initial projection. Called once per Run before any events are folded. | `() => TProjection` | | fold | Fold one event into the projection and return the updated projection. May mutate `state` in place. | `(state, event, meta) => TProjection` | | createEncoder | Create a stateful encoder bound to the given channel writer. Takes optional
. | `(channel, options?) => Encoder` | | createDecoder | Create a stateful decoder that maps inbound Ably messages to typed inputs and outputs. Each call returns a
. | `() => Decoder` | | getMessages | Extract the per-message list from a projection as `{ codecMessageId, message }` pairs. The SDK uses the pairs to correlate rendered messages back to the wire id; the View concatenates them across the visible Run chain. | `(projection) => CodecMessage[]` | | createUserMessage | Wrap a `TMessage` as the codec's `UserMessage` variant for publishing on the `ai-input` wire. Returns the codec's `TInput` so the result is usable as a `View.send` argument without a cast. | `(message) => TInput` | | createRegenerate | Build a `Regenerate` input that targets an assistant message. The View calls this from `regenerate()`. | `(target, parent) => TInput` | | createToolResult | Optional. Build a `ToolResult` input addressed at the assistant codec-message containing the tool call. The codec defines the payload shape (for example the Vercel layer's `{ toolCallId, output }`). | `(codecMessageId, payload) => TInput` | | createToolResultError | Optional. Build a `ToolResultError` input. The codec defines the payload shape (for example `{ toolCallId, message }`). | `(codecMessageId, payload) => TInput` | | createToolApprovalResponse | Optional. Build a `ToolApprovalResponse` input. The codec defines the payload shape (for example `{ toolCallId, approved, reason? }`). | `(codecMessageId, payload) => TInput` |
## Fold an event into a projection `fold(state: TProjection, event: TInput | TOutput, meta: ReducerMeta): TProjection` The reducer contract: fold one event into the projection. The same `(state, event, meta)` triple must produce the same result every time. Re-folding an event whose serial has already been incorporated must be a no-op. The reducer is free to store a high-water-mark inside the projection. | Property | Description | Type | | --- | --- | --- | | serial | Ably channel serial of the message that produced this event. The reducer uses this for idempotency and deduplication. | String | | messageId | Optional `codec-message-id` from the inbound Ably message. Reducers use this to route an event to a target message within the projection. | String |
### Parameters | Parameter | Required | Description | Type | | --- | --- | --- | --- | | state | required | The current projection. May be mutated and returned. | `TProjection` | | event | required | The next input or output event to fold. | `TInput \| TOutput` | | meta | required | Transport-derived metadata stamped from the inbound Ably message. |
|
### Returns `TProjection`. The updated projection. ## Create an encoder `createEncoder(channel: ChannelWriter, options?: EncoderOptions): Encoder` Build a stateful encoder bound to a channel. The encoder owns the message-append lifecycle for both directions on a single channel. ### Parameters | Parameter | Required | Description | Type | | --- | --- | --- | --- | | channel | required | The channel writer to publish through. An `Ably.RealtimeChannel` satisfies this directly. | `ChannelWriter` | | options | optional | Per-encoder defaults (clientId, extras, messageId, onMessage hook). |
|
| Property | Description | Type | | --- | --- | --- | | clientId | Default clientId for all writes. | String | | extras | Default extras merged into every Ably message. |
| | onMessage | Hook called before each Ably message is published. Mutate the message in place to add transport-level headers under `extras.ai`. | `(message: Ably.Message) => void` | | messageId | Default `codec-message-id` for messages where the event payload doesn't supply one. | String |
| Property | Description | Type | | --- | --- | --- | | headers | Transport-tier headers to attach to the message's `extras.ai.transport` namespace. | `Record` |
### Returns [`Encoder`](#encoder). A stateful encoder bound to the supplied channel. ## Create a decoder `createDecoder(): Decoder` Build a stateful decoder for a single channel subscription. The decoder maintains stream-tracker state across messages so that mid-stream join synthesises any missing start events before deltas reach the SDK. The reducer always sees a clean `(start, delta*, end)` sequence. ### Returns [`Decoder`](#decoder). A stateful decoder for the channel. ## Encoder A stateful encoder for a single channel. Two publish methods enforce direction at the call site. `publishInput` writes to the `ai-input` wire, `publishOutput` writes to the `ai-output` wire. Stream-tracker state lives inside the encoder and is shared across both directions. ### Properties | Property | Description | Type | | --- | --- | --- | | publishInput | Encode and publish a single client input on the `ai-input` wire. Throws synchronously if the codec cannot encode the given input variant. | `(input, options?) => Promise` | | publishOutput | Encode and publish a single agent output on the `ai-output` wire. Throws synchronously if the codec cannot encode the given output variant. | `(output, options?) => Promise` | | cancel | Cancel any in-progress streams and emit a codec-specific cancel signal. Idempotent. | `(reason?: string) => Promise` | | close | Flush pending appends and release encoder resources. | `() => Promise` |
| Property | Description | Type | | --- | --- | --- | | clientId | Override the default clientId for this write. | String | | extras | Override the default extras for this write. |
| | messageId | Message identity for projection routing. Stamped as `codec-message-id`. | String |
### Publish an input `publishInput(input: TInput, options?: WriteOptions): Promise` Encode and publish a single client input on the `ai-input` wire. Per-write overrides live in [`WriteOptions`](#write-options). #### Parameters | Parameter | Required | Description | Type | | --- | --- | --- | --- | | input | required | The input event to encode and publish. | `TInput` | | options | optional | Per-write overrides (clientId, extras, messageId). |
|
### Publish an output `publishOutput(output: TOutput, options?: WriteOptions): Promise` Encode and publish a single agent output on the `ai-output` wire. #### Parameters | Parameter | Required | Description | Type | | --- | --- | --- | --- | | output | required | The output event to encode and publish. | `TOutput` | | options | optional | Per-write overrides. |
|
## Decoder A stateful decoder for a single channel subscription. Decodes one Ably inbound message into the input and output halves. ### Decode a message `decode(message: Ably.InboundMessage): DecodedMessage` Tagged result of decoding one inbound Ably message. The codec routes by the wire `name` and returns inputs and outputs separately so the SDK never has to introspect direction. | Property | Description | Type | | --- | --- | --- | | inputs | Inputs decoded from the inbound message (populated when the wire `name` is `ai-input`). | `TInput[]` | | outputs | Outputs decoded from the inbound message (populated when the wire `name` is `ai-output`). | `TOutput[]` |
### Returns `DecodedMessage`. Tagged inputs and outputs. ## Well-known input variants Codec input variants extend the `CodecInputEvent` base. The SDK reserves a handful of well-known `kind` literals so callers can publish a user message, regenerate an assistant message, or resolve a tool call without inventing their own shapes. Codec-specific variants pick any other literal. (Edits ride the user-message path with a `forkOf` routing header; there is no separate `'edit'` kind on the wire.)
| Property | Description | Type | | --- | --- | --- | | kind | Discriminator. The SDK reserves `'user-message'`, `'regenerate'`, `'tool-result'`, `'tool-result-error'`, and `'tool-approval-response'`. | String | | parent | The codec-message-id of the preceding codec-message on this branch. Auto-computed when omitted. | String | | target | Pointer to another codec-message this input references. Semantic depends on `kind`. | String | | codecMessageId | Targets an existing codec-message-id instead of minting a fresh one. Used by continuation inputs that amend an existing assistant message. | String |
The full input union is the codec author's responsibility. Use the variants below directly or extend them with extra fields. ### UserMessage | Property | Description | Type | | --- | --- | --- | | kind | Pinned to `'user-message'`. | `'user-message'` | | message | The user's message in the codec's domain representation. | `TMessage` |
A new user message. Produced by `Codec.createUserMessage` and surfaced via `View.send` (wrap a domain message via `codec.createUserMessage(message)` or build the literal directly). ### Regenerate | Property | Description | Type | | --- | --- | --- | | kind | Pinned to `'regenerate'`. | `'regenerate'` | | target | The codec-message-id of the assistant to regenerate. Required. | String | | parent | The codec-message-id of the parent user message the new assistant threads under. Required. | String |
A signal to regenerate an existing assistant codec-message. Produced by `Codec.createRegenerate` and surfaced via `View.regenerate`. ### ToolResult | Property | Description | Type | | --- | --- | --- | | kind | Pinned to `'tool-result'`. | `'tool-result'` | | codecMessageId | The assistant codec-message containing the tool call. | String | | payload | Codec-defined domain payload describing the result. The Vercel codec uses `{ toolCallId, output }`; custom codecs pick their own shape. | `TPayload` |
A client-published tool result for a successful execution. Mutates the assistant codec-message addressed by `codecMessageId`. ### ToolResultError | Property | Description | Type | | --- | --- | --- | | kind | Pinned to `'tool-result-error'`. | `'tool-result-error'` | | codecMessageId | The assistant codec-message containing the tool call. | String | | payload | Codec-defined domain payload describing the failure. The Vercel codec uses `{ toolCallId, message }`. | `TPayload` |
A client-published tool result for a failed execution. ### ToolApprovalResponse | Property | Description | Type | | --- | --- | --- | | kind | Pinned to `'tool-approval-response'`. | `'tool-approval-response'` | | codecMessageId | The assistant codec-message containing the tool call. | String | | payload | Codec-defined domain payload describing the decision. The Vercel codec uses `{ toolCallId, approved, reason? }`. | `TPayload` |
A client-published response to an agent-emitted tool-approval request. Flips the targeted tool call from pending-approval to approved or denied. ## Codec output base | Property | Description | Type | | --- | --- | --- | | type | Discriminator. Codec authors pick the literal value per variant. | String |
Every codec output variant must satisfy `CodecOutputEvent`. The SDK reads `type` so it can reliably narrow `TInput | TOutput` (inputs carry `kind`, outputs carry `type`). ## Wire payloads The encoder and decoder cores describe Ably messages with two payload shapes, used internally by `createEncoderCore` and `createDecoderCore`. Custom codec implementations that build on those cores work with these shapes directly. `MessagePayload` describes a discrete Ably message. | Property | Description | Type | | --- | --- | --- | | name | Ably message name (for example `"text"`, `"tool-input"`, `"user-message"`). | String | | data | Message data. Ably handles serialisation. | `unknown` | | codecHeaders | Codec-tier headers carried under `extras.ai.codec`. | `Record` | | transportHeaders | Transport-tier headers carried under `extras.ai.transport`. | `Record` | | ephemeral | Mark this message as ephemeral (not persisted in channel history). | Boolean |
`StreamPayload` describes a streamed message. Data must be a string because the append lifecycle uses text append semantics. Deltas are concatenated for recovery and prefix-matching on the decoder. | Property | Description | Type | | --- | --- | --- | | name | Ably message name. | String | | data | Initial or closing data for the stream. Must be a string for append/accumulate semantics. | String | | codecHeaders | Codec-tier headers carried under `extras.ai.codec`. | `Record` | | transportHeaders | Transport-tier headers carried under `extras.ai.transport`. | `Record` |
## Reducer The reducer is a pure, stateless folding contract that `Codec` extends. | Property | Description | Type | | --- | --- | --- | | init | Build an empty initial projection. | `() => TProjection` | | fold | Fold one event into the projection. | `(state, event, meta) => TProjection` |
## Example A minimal codec skeleton that folds a single input and output variant and extracts a flat message list. ### Typescript ``` import type { Codec, CodecInputEvent, CodecOutputEvent, CodecMessage, ReducerMeta, } from '@ably/ai-transport'; interface MyInput extends CodecInputEvent { kind: 'user-message'; message: MyMessage } interface MyOutput extends CodecOutputEvent { type: 'text-delta'; text: string } interface MyProjection { highWater: string; messages: CodecMessage[] } interface MyMessage { id: string; role: 'user' | 'assistant'; text: string } const myCodec: Codec = { init: () => ({ highWater: '', messages: [] }), fold: (state, event, meta: ReducerMeta) => { if (meta.serial <= state.highWater) return state; state.highWater = meta.serial; if ('kind' in event && event.kind === 'user-message') { state.messages.push({ codecMessageId: meta.messageId ?? event.message.id, message: event.message }); } else if ('type' in event && event.type === 'text-delta') { const last = state.messages.at(-1); if (last?.message.role === 'assistant') last.message.text += event.text; } return state; }, createEncoder: (channel, options) => buildEncoder(channel, options), createDecoder: () => buildDecoder(), getMessages: (projection) => projection.messages, createUserMessage: (message) => ({ kind: 'user-message', message }), createRegenerate: (target, parent) => ({ kind: 'regenerate', target, parent }), }; ``` ## Related Topics - [Client session](https://ably.com/docs/ai-transport/api/javascript/core/client-session.md): API reference for the AI Transport ClientSession: factory, properties, lifecycle methods, and the View interface returned by createView(). - [Agent session](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md): API reference for the AI Transport AgentSession: factory, lifecycle methods, the Run interface, and the Invocation value object. ## 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.