# Decoder A `Decoder` reads each message your client receives from an [Ably channel](https://ably.com/docs/channels.md), live or from history, and returns the events of one [codec](https://ably.com/docs/ai-transport/api/javascript/codec.md), such as a user's prompt, the text of a response, or a tool call. Pass it every live message with `channel.subscribe(decoder.receive)`, and every history message with `decoder.decode`. Each message becomes at least one `Delivery`, which holds the decoded event and the Ably message it came from. A message that is not the codec's arrives as `{ event: undefined, message }`, so your client sees every message on the channel, including the messages of another codec and your application's messages, such as an [ephemeral message](https://ably.com/docs/ai-transport/awareness/ephemeral-messages.md). The decoder keeps state for each stream that is still open. A stream is one message that the agent builds with [appends](https://ably.com/docs/messages/updates-deletes.md#append), such as the text of a response. The decoder remembers the text of the stream it has passed on so far, and the latest version of the message it has decoded. A message that live delivery and history both carry decodes once while its stream is open. When the channel delivers an update that carries the full text of a stream, the decoder passes on only the text it has not passed on yet. The decoder forgets a stream when the message that ends it arrives. A discrete event, such as a prompt that [`send`](https://ably.com/docs/ai-transport/api/javascript/agent-adaptor.md#send) publishes, is a single message, so the decoder keeps no state for it and passes it on each time it arrives. The following client code creates a decoder and subscribes it to the channel: #### Javascript ``` import { createClient } from '@ably/pubsub-device'; import { createDecoder } from '@ably/ai-transport'; import { codec } from './codec.js'; const client = createClient({ authUrl: '/auth' }); const channel = client.channels.get('conversations:42'); const decoder = createDecoder(codec); decoder.onEvent(({ event, message }) => render(event, message)); await channel.subscribe(decoder.receive); ``` ## Create a decoder `createDecoder(codec: Codec, options?: DecoderOptions): Decoder` Create a decoder over a `codec` that you built with [`defineCodec`](https://ably.com/docs/ai-transport/api/javascript/codec.md). For a codec that AI Transport ships, use its [provider factory](https://ably.com/docs/ai-transport/api.md#entry-points), which returns the same `Decoder` interface and takes the same `options`: - [`vercelDecoder`](https://ably.com/docs/ai-transport/api/javascript/vercel.md) - [`openaiDecoder`](https://ably.com/docs/ai-transport/api/javascript/openai.md) - [`agUiDecoder`](https://ably.com/docs/ai-transport/api/javascript/ag-ui.md) Use one decoder for each codec on each channel, and pass it both live messages and history messages. If one decoder gets the live messages and another gets history, each decoder decodes a message that both carry. Create a new decoder each time you subscribe again, so that the decoder starts with no state from the earlier subscription. After the channel [loses continuity](https://ably.com/docs/ai-transport/conversation/reconnection-and-recovery.md#detect), create a new decoder for the [catch-up](https://ably.com/docs/ai-transport/conversation/history.md#catch-up). The following client code creates a decoder with a logger: ### Javascript ``` import { createDecoder, makeLogger, LogLevel } from '@ably/ai-transport'; const decoder = createDecoder(codec, { logger: makeLogger({ logLevel: LogLevel.Debug }), }); ``` ### Parameters The `createDecoder()` function takes the following parameters: | Parameter | Required | Description | Type | | --- | --- | --- | --- | | codec | Required | The codec the agent published through. | [`Codec`](https://ably.com/docs/ai-transport/api/javascript/codec.md) | | options | Optional | A logger. |
|
| Property | Required | Description | Type | | --- | --- | --- | --- | | logger | Optional | The logger for the decoder's diagnostics, such as failed decodes and handlers that throw. Create one with [`makeLogger`](https://ably.com/docs/ai-transport/api.md#logger). The decoder does not log when you leave it out. | `Logger` |
### Returns `Decoder`. Returns the decoder. The function does not throw. ## Receive a channel message `decoder.receive(message: Ably.InboundMessage): void` Decode one message and pass each delivery to the `onEvent` handlers, in order. `receive` is bound to the decoder, so you can pass it to `channel.subscribe` as it is, and later to `channel.unsubscribe`. `receive` does not throw. When the codec cannot decode a message, the decoder reports the error on `onError`, and passes the message to the `onEvent` handlers with no event. Subscribe the decoder before the channel attaches, so it receives the messages that a [rewind](https://ably.com/docs/ai-transport/conversation/history.md#catch-up) delivers as the channel attaches. `channel.subscribe` adds the listener and then attaches the channel. With `ChannelProvider` from `@ably/pubsub-device/react`, subscribing does not attach the channel, so call `channel.attach()` after you subscribe. The following client code subscribes the decoder, and later unsubscribes it: ### Javascript ``` await channel.subscribe(decoder.receive); // Later, when the client stops reading the channel. channel.unsubscribe(decoder.receive); ``` ### Parameters The `receive()` method takes the following parameters: | Parameter | Required | Description | Type | | --- | --- | --- | --- | | message | Required | The message as the channel delivered it. | [`InboundMessage`](https://ably.com/docs/api/realtime-sdk/messages.md) |
## Decode a message `decoder.decode(message: Ably.InboundMessage): Delivery[]` Decode one message and return its deliveries, without passing them to the `onEvent` handlers. Use it to decode the messages of a history page, or in a listener you write yourself. `decode` does not throw, and it never returns an empty array. When the codec cannot decode a message, the decoder reports the error on `onError`, and returns one delivery for the message with no event. History pages hold their messages newest first, and the decoder does not reorder them. The decoder returns deliveries in the order you pass it messages. Your client is responsible for ordering messages across pages, by serial or by collecting the pages first. Reading history with `untilAttach: true` needs an attached channel and the `history` [capability](https://ably.com/docs/ai-transport/setup/authentication.md#capabilities). The decoder drops a repeated message only while its stream is open. It has three limits: - When an append fails after later appends reached the client, the agent repairs the stream with a full-content update. The decoder delivers the whole content again, as a replacement. - When your client decodes live messages as they arrive but applies history first, a stream that is still open can come out of order. If history holds the stream at a later version than the first live update, the client applies the end of the text from history before the start of the text from that update. - The decoder forgets a stream that it decoded live up to the message that ends it, so history read after that point returns the whole stream again. You can avoid these limits by reading history before you decode live messages, or by [rewinding the channel](https://ably.com/docs/ai-transport/conversation/history.md#catch-up) to a serial so one subscription delivers history and live messages in order. Your client can also skip a message whose serial it has already shown. The following client code reads history one page at a time, and decodes each page oldest first. `renderOlder` is your function that adds the deliveries above the messages already in your UI: ### Javascript ``` // The live subscription has attached the channel. let page = await channel.history({ untilAttach: true, limit: 50 }); while (page) { renderOlder(page.items.slice().reverse().flatMap((message) => decoder.decode(message))); // next() resolves with null when no older page exists. page = page.hasNext() ? await page.next() : null; } ``` ### Parameters The `decode()` method takes the following parameters: | Parameter | Required | Description | Type | | --- | --- | --- | --- | | message | Required | The message, live or from history. | [`InboundMessage`](https://ably.com/docs/api/realtime-sdk/messages.md) |
### Returns `Delivery[]`. Returns the deliveries for the message, in order: one for each event the codec decodes from it, or one with `event` set to `undefined`. | Property | Description | Type | | --- | --- | --- | | event | The decoded event, or `undefined` when the decoder did not produce an event for the message. | `E` or `undefined` | | message | The Ably message as delivered, with its `serial`, `clientId`, `timestamp`, `action`, `data` and `extras`. Read the headers of the `send` or `pipe` call from `message.extras.headers`. | [`InboundMessage`](https://ably.com/docs/api/realtime-sdk/messages.md) |
A delivery has `event` set to `undefined` in these cases: - The message is not the codec's. A message is the codec's when its name is the codec's `name` and it carries the codec's `id` under `extras.ai.codec`. Your application's own messages and the messages of another codec on the same channel arrive this way. - The decoder has already decoded this version of the message while its stream is open. - The message is an update that does not add new text. - The message is a `message.delete`. - The codec did not produce an event for the message. - The codec could not decode the message. The decoder also reports the error on `onError`. ## Listen for decoded events `decoder.onEvent(handler: (delivery: Delivery) => void): () => void` Register a handler that the decoder calls with each delivery that `receive` passes on. `decode` does not call the handlers. When a handler throws, the decoder logs the error, and the other handlers still run. The following client code passes each decoded event to your UI, and ignores deliveries with no event: ### Javascript ``` const off = decoder.onEvent(({ event, message }) => { if (event === undefined) return; apply(event, message.serial); }); // Later, to remove the handler. off(); ``` ### Parameters The `onEvent()` method takes the following parameters: | Parameter | Required | Description | Type | | --- | --- | --- | --- | | handler | Required | Called with each delivery, in the order `receive` decodes them, with the same `Delivery` shape that `decode` returns. | `(delivery: Delivery) => void` |
### Returns `() => void`. Returns a function that removes the handler. ## Listen for decode errors `decoder.onError(handler: (error: Ably.ErrorInfo) => void): () => void` Register a handler that the decoder calls when the codec throws on a message in `receive` or `decode`. Each error is an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object with the [error code](https://ably.com/docs/ai-transport/api/errors.md#reference) `SessionMessageProcessingFailed` (104009), and has the error the codec threw as its `cause`. The decoder still delivers the message with no event, and carries on with the next message. The following client code logs each decode error: ### Javascript ``` decoder.onError((error) => { console.error('decode failed', error.code, error.cause); }); ``` ### Parameters The `onError()` method takes the following parameters: | Parameter | Required | Description | Type | | --- | --- | --- | --- | | handler | Required | Called with each decode error. | `(error: ErrorInfo) => void` |
### Returns `() => void`. Returns a function that removes the handler. ## Example The following client code loads the stored conversation, rewinds the channel to the stored serial, and decodes every message after it. When the channel [loses continuity](https://ably.com/docs/ai-transport/conversation/reconnection-and-recovery.md#detect), the client reads your database again, rewinds from the newer serial, and subscribes a new decoder. `loadConversation` reads your database, `render` replaces the messages in your UI, and `apply` adds each decoded event to your UI: ### Javascript ``` import { createClient } from '@ably/pubsub-device'; import { createDecoder } from '@ably/ai-transport'; import { codec } from './codec.js'; import { apply, loadConversation, render } from './app.js'; const channelName = 'conversations:42'; const client = createClient({ authUrl: '/auth' }); const stored = await loadConversation(channelName); render(stored.messages); const channel = client.channels.get( channelName, stored.serial ? { params: { rewind: stored.serial } } : undefined, ); let decoder; async function subscribe() { decoder = createDecoder(codec); decoder.onEvent(({ event, message }) => { if (event !== undefined) apply(event, message); }); decoder.onError((error) => console.error('decode failed', error)); await channel.subscribe(decoder.receive); // Listen after the first attach, which also has `resumed: false`. channel.on(['attached', 'update', 'failed'], onStateChange); } function onStateChange(change) { if (!change.resumed) recover().catch((error) => console.error('recovery failed', error)); } async function recover() { channel.off(onStateChange); channel.unsubscribe(decoder.receive); // A FAILED channel rejects detach(), and attaching again recovers it. await channel.detach().catch(() => {}); const latest = await loadConversation(channelName); render(latest.messages); if (latest.serial) await channel.setOptions({ params: { rewind: latest.serial } }); await subscribe(); } await subscribe(); ``` ## Related Topics - [Agent adaptor](https://ably.com/docs/ai-transport/api/javascript/agent-adaptor.md): API reference for the AI Transport agent adaptor: createAgentAdaptor, send, pipe, and pipeEach, and the options, results, and errors of each. - [Codec](https://ably.com/docs/ai-transport/api/javascript/codec.md): API reference for the AI Transport Codec: the encode and decode contract an adaptor and a decoder use, EncodedMessage, and how defineCodec builds a codec from a table of event rows. - [Vercel](https://ably.com/docs/ai-transport/api/javascript/vercel.md): API reference for the AI Transport codec for the Vercel AI SDK: vercelAdaptor, vercelDecoder, and the VercelEvent union of UIMessageChunk events and the user message. - [OpenAI](https://ably.com/docs/ai-transport/api/javascript/openai.md): API reference for the AI Transport codec for the OpenAI Responses API: openaiAdaptor, openaiDecoder, and the OpenAIEvent union of Responses stream events and client input. - [AG-UI](https://ably.com/docs/ai-transport/api/javascript/ag-ui.md): API reference for the AI Transport codec for AG-UI: agUiAdaptor, agUiDecoder, and the AGUIEvent union of AG-UI events and the run-input a client sends. ## 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.