# Transport
A `Transport` publishes to and receives from one [Ably channel](https://ably.com/docs/channels.md), through one codec. The agent and the client use the same transport: the agent pipes the model's output to the channel, and the client sends its own messages and subscribes to everything on the channel. Each inbound Ably message reaches a subscriber as a delivery that holds the decoded event and the message it came from. The transport does not hold a message list, so your application merges the deliveries into what it renders.
Create one with `createTransport` from the core entry point, and pass it a codec from one of the framework entry points:
#### OpenAI Javascript
```
import * as Ably from 'ably';
import { createTransport } from '@ably/ai-transport';
import { openai } from '@ably/ai-transport/openai';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const transport = createTransport({ client: ably, channelName: 'conversations:42', codec: openai });
```
#### Vercel AI SDK Javascript
```
import * as Ably from 'ably';
import { createTransport } from '@ably/ai-transport';
import { vercel } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const transport = createTransport({ client: ably, channelName: 'conversations:42', codec: vercel });
```
## Create a transport
`createTransport(options: TransportOptions): Transport`
Create a transport for one channel and one codec. The transport gets the channel from the client by name and sets its options, so it owns the channel. Your application owns the client: [`close()`](#close) never closes it.
Creating a transport does not attach the channel. The first [`subscribe()`](#subscribe) or [`history()`](#history) call attaches it, and [`send()`](#send) and [`pipe()`](#pipe) publish without waiting for an attach.
To use the same channel elsewhere, for example for [presence](https://ably.com/docs/presence-occupancy/presence.md), get it from the client by name with no options: `ably.channels.get(channelName)`. That returns the channel the transport configured.
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| options | required | The client, the channel name and the codec. | |
| Property | Description | Type |
| --- | --- | --- |
| client | The Ably Realtime client to get the channel from. Your application owns it, and `close()` never closes it. | `Ably.Realtime` |
| channelName | The name of the channel to publish to and receive from. | String |
| codec | The codec that turns events into Ably messages and back, for example `vercel` from `@ably/ai-transport/vercel`, `openai` from `@ably/ai-transport/openai` or `agui` from `@ably/ai-transport/ag-ui`. | `Codec` |
| channelModes | Optional. Channel modes to request on top of the [default channel modes](https://ably.com/docs/channels/options.md#modes), for example `OBJECT_MODES`. Two transports that share a client and a channel name share the channel, so they must ask for the same modes. | `Ably.ChannelMode[]` |
| echoMessages | Optional. Whether the channel delivers this connection's own publishes back to it. Defaults to `true`, so a client receives the messages it sent as ordinary deliveries. Set `false` to render what you sent from the event itself and match it up on the serial that `send()` returns. | Boolean |
| logger | Optional. A logger for diagnostics. The transport logs nothing when you omit it. | `Logger` |
### Returns
`Transport`. The transport, with its channel not yet attached.
## Send an event
`send(event: E, options?: SendOptions): Promise`
Encode one event and publish the messages the codec encodes it to, in order. A client uses `send()` to publish the user's message, so every subscribed client receives it, and then wakes the agent with [its own HTTP request](https://ably.com/docs/ai-transport/streaming.md#prompt).
Publishing stops at the first failure. The messages published before the failure stay on the channel.
### OpenAI Javascript
```
// Client code.
const items = [{ role: 'user', content: 'What is the weather?' }];
const { serial } = await transport.send({ type: 'input', items }, { headers: { requestId: crypto.randomUUID() } });
```
### Vercel AI SDK Javascript
```
// Client code.
const message = { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'What is the weather?' }] };
const { serial } = await transport.send({ type: 'user-message', message }, { headers: { requestId: message.id } });
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| event | required | The event to publish, of the codec's event type. | `E` |
| options | optional | The headers to add to every message the call publishes. | |
| Property | Description | Type |
| --- | --- | --- |
| headers | Optional. Headers for every message the call publishes, under `extras.headers`. Where the codec sets the same key, the codec's value is used. A subscriber reads them from `delivery.message.extras.headers`. An `undefined` value is dropped. | |
### Returns
`Promise`. Returns a promise. The promise is fulfilled with the serial of the last message published, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object. It rejects with `InvalidArgument` when a header value is not a string, number, boolean or `null`, or when the codec encodes the event as an append or an update, with `InsufficientCapability` when the token cannot publish to the channel, with `SessionSendFailed` for any other publish failure, and with `SessionClosed` after `close()`.
| Property | Description | Type |
| --- | --- | --- |
| serial | The serial of the last message published for the event, or `undefined` when the codec published nothing for it. | String or `undefined` |
## Pipe a stream of events
`pipe(source: PipeSource, options?: PipeOptions): Promise`
Read a stream of events and write each one to the channel as the codec directs. An agent uses `pipe()` to publish the model's output. The codec decides how each event is written. With the Vercel codec, each text stream or tool-input stream becomes one message that grows by [appends](https://ably.com/docs/messages/updates-deletes.md#append), and every other event is its own publish.
The pipe resolves when the source ends. When `signal` fires or the transport closes, the pipe stops reading, finishes the messages it has written, and rejects with `OperationCancelled`. A cancelled pipe leaves whole messages on the channel.
If an append fails, the pipe carries on. When that stream ends, the pipe repairs the message with an update that carries the full content. The pipe rejects only if the repair fails too.
### OpenAI Javascript
```
// Agent code.
import OpenAI from 'openai';
const stream = await new OpenAI().responses.create({ model: 'gpt-5.5', input, stream: true }, { signal: req.signal });
const { serial } = await transport.pipe(stream, {
signal: req.signal,
headers: { requestId },
});
```
### Vercel AI SDK Javascript
```
// Agent code.
import { streamText, toUIMessageStream } from 'ai';
const result = streamText({ model, messages });
const { serial } = await transport.pipe(toUIMessageStream({ stream: result.fullStream }), {
signal: req.signal,
headers: { requestId },
});
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| source | required | The events to write. A provider SDK stream that is async-iterable pipes in directly. The pipe releases the source when it ends, is cancelled, or fails. | `ReadableStream` or `AsyncIterable` |
| options | optional | The abort signal and the headers. | |
| Property | Description | Type |
| --- | --- | --- |
| signal | Optional. Cancels the pipe when it fires. The pipe stops at the next event, finishes what it has written, and rejects with `OperationCancelled`. | `AbortSignal` |
| headers | Optional. Headers for every message the pipe writes, including each append and each update. Ably keeps the headers of the last write on an appended message, so the pipe writes them every time. They travel with every append, so keep them small. | |
### Returns
`Promise`. Returns a promise. The promise is fulfilled once the source has ended and everything the pipe wrote is on the channel, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object. It rejects with `OperationCancelled` when the signal fires or the transport closes, and with `PipeFailed` when the source throws, the codec cannot encode an event, a publish or update fails, or the repair of a stream fails. The underlying failure is the `cause`. It also rejects with `InvalidArgument` for a header value Ably cannot store, before it reads the source, and with `SessionClosed` after `close()`.
| Property | Description | Type |
| --- | --- | --- |
| serial | The serial of the last message the pipe published, or `undefined` when it published nothing. Store it with the response, so a client can [replay the channel from it](#from-serial). | String or `undefined` |
## Subscribe to deliveries
`subscribe(handler: (delivery: Delivery) => void, options?: SubscribeOptions): Subscription`
Call `handler` for every message on the channel, in channel order. A message reaches the handler as one delivery for each event the codec decodes from it. When the codec does not decode an event from a message, the handler receives one delivery with `event` set to `undefined`. The handler receives such a delivery for a message another publisher wrote, a streamed message the transport has already decoded at that version, a deleted message, and a message the codec failed to decode.
The first call attaches the channel. If the attach fails, the transport reports it on [`on('error')`](#on) and the next call tries again. Any number of handlers can subscribe. A handler that throws is logged, and the other handlers still receive the delivery.
### Javascript
```
// Client code.
const unsubscribe = transport.subscribe(({ event, message }) => {
if (event === undefined) return;
render(event, message.extras?.headers?.requestId);
});
// Later, stop delivering to this handler.
unsubscribe();
```
### Replay history before live deliveries
Pass `history` to replay messages from the channel's history to the handler before its live deliveries. A client that holds part of the conversation, in its own store or from before a disconnection, uses it to receive the rest in channel order.
With `history` set, the transport:
1. Buffers this handler's live deliveries.
2. Reads history backwards from the point where the channel attached, and gives the newest page to `history.replay`.
3. Sorts the deliveries that `replay` returns into channel order, and delivers them to the handler.
4. Delivers the buffered live deliveries, in the order they arrived.
From then on the handler receives live deliveries. The transport buffers only this handler, and runs one replay at a time, so a second `subscribe()` with `history` waits for the first replay to finish. Unsubscribing during a replay stops it and drops the buffered deliveries.
#### Javascript
```
// Client code.
import { fromSerial } from '@ably/ai-transport';
const subscription = transport.subscribe(handle, {
history: { replay: fromSerial(stored.serial), pageSize: 100 },
});
const { replayed, found } = await subscription.replayed;
```
The replay reads history from the point where the channel attached. Only the first subscription with `history` on a transport has no gap between its history and its live deliveries. A later subscription with `history`, after the channel has attached, misses the messages that arrived between the attach and that subscription.
The replay and live buffers have no size limit. History reaches back as far as the channel's [retention period](https://ably.com/docs/storage-history/storage.md). The appends rule that AI Transport requires persists messages, so the period depends on your package.
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| handler | required | Called with each delivery, in channel order. | `(delivery: Delivery) => void` |
| options | optional | The history to replay before live deliveries. Omit it to receive live deliveries only. | |
| Property | Description | Type |
| --- | --- | --- |
| event | The decoded event, or `undefined` when the codec produced no event for the message. | `E` or `undefined` |
| message | The Ably message as delivered, with its `serial`, `version`, `clientId`, `timestamp`, `action`, `data` and `extras`. Read the headers from `message.extras.headers`. | `Ably.InboundMessage` |
| Property | Description | Type |
| --- | --- | --- |
| history | Optional. The history to replay to the handler before its live deliveries. | |
| Property | Description | Type |
| --- | --- | --- |
| replay | Reads history and returns the deliveries to replay. [`fromSerial`](#from-serial) and [`untilEvent`](#until-event) build the usual ones. | |
| pageSize | Optional. Messages per page of history. Defaults to `100`. | Number |
| Property | Description | Type |
| --- | --- | --- |
| (page) | A function the transport calls once, with the newest page of history. It reads older pages with `page.next()` as far as it needs, and returns the deliveries to replay in any order. It returns an array of deliveries, or a `Replay`, directly or through a promise. If it throws, the replay ends, the handler receives live deliveries with a gap, and `replayed` rejects. | Function that takes a `HistoryPage` and returns `Delivery[]` or , directly or through a promise |
| Property | Description | Type |
| --- | --- | --- |
| deliveries | The deliveries to replay, in any order. The transport sorts them into channel order. | `Delivery[]` |
| found | Optional. Whether the function reached the point it was looking for. The transport reports it on `ReplayResult.found`. | Boolean |
### Returns
`Subscription`. A function that stops delivering to this handler. The transport keeps its own channel listener for the other handlers until [`close()`](#close). The function also carries a `replayed` property.
| Property | Description | Type |
| --- | --- | --- |
| replayed | A promise that resolves once the transport has replayed history and delivered the buffered deliveries, or immediately when the subscription has no `history`. It rejects with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object when the replay fails: `SessionHistoryFetchFailed` when a page cannot be fetched, the replay function throws a value that is not an `ErrorInfo`, or a builder set to `onExhausted: 'error'` runs out of pages, and `OperationCancelled` when the handler unsubscribes or the transport closes during the replay. After a failure the handler still receives live deliveries, with a gap where the replay would have been. The transport logs the failure. A failed replay does not raise an unhandled rejection when your code does not await `replayed`. | `Promise` of |
| Property | Description | Type |
| --- | --- | --- |
| replayed | The number of deliveries from history that reached the handler before its live deliveries. `0` for a subscription with no `history`. | Number |
| found | Optional. The replay function's verdict, when it returned one. `fromSerial` sets it when it reached its serial, and `untilEvent` when a delivery matched. | Boolean |
## Replay from a serial
`fromSerial(serial: string, options?: FromSerialOptions): ReplayFn`
Build a replay function that reads history back to a serial your application holds, and returns the deliveries after it. Store the serial that [`pipe()`](#pipe) or [`send()`](#send) returns with each message in your database, and subscribe from it when the client loads the conversation. After a [discontinuity](#on), subscribe again from the serial of the last message the handler received.
`fromSerial` compares serials as strings. It compares each message's `serial`, the one its creation allocated, even if the message was appended to later, so the replay leaves out a stream that started at or before the serial. The replay stops at the first page that holds the serial or an older message.
### Javascript
```
// Client code.
import { fromSerial } from '@ably/ai-transport';
transport.subscribe(handle, { history: { replay: fromSerial(stored.serial) } });
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| serial | required | The serial to replay from, such as the serial a publish returned. | String |
| options | optional | The page cap, what to do when history runs out, and whether to replay the message at the serial. | |
| Property | Description | Type |
| --- | --- | --- |
| maxPages | Optional. The most pages of history to read. Defaults to no limit. | Number |
| onExhausted | Optional. What to do when history runs out, or `maxPages` is reached, before the serial is found. `'deliver'` replays what was read and reports `found: false`. `'error'` fails the replay with `SessionHistoryFetchFailed`. Defaults to `'deliver'`. | `'deliver'` or `'error'` |
| inclusive | Optional. Whether to replay the message at the serial too. Defaults to `false`, for a serial that is the last message you applied. | Boolean |
### Returns
`ReplayFn`. The replay function to pass as `history.replay`. `fromSerial` throws an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) with `InvalidArgument` when `serial` is empty or `maxPages` is less than one.
## Replay until an event
`untilEvent(predicate: (delivery: Delivery) => boolean, options?: UntilEventOptions): ReplayFn`
Build a replay function that reads history back to the newest delivery that `predicate` accepts, and returns that delivery and every delivery after it. Use it when the client does not hold a serial, for example to replay from the start of the last response, or from the user's last input. The predicate receives each decoded delivery, so it can match on the event, on a header, or on the Ably message.
### OpenAI Javascript
```
// Client code.
import { untilEvent } from '@ably/ai-transport';
transport.subscribe(handle, {
history: { replay: untilEvent(({ event }) => event?.type === 'input') },
});
```
### Vercel AI SDK Javascript
```
// Client code.
import { untilEvent } from '@ably/ai-transport';
transport.subscribe(handle, {
history: { replay: untilEvent(({ event }) => event?.type === 'start') },
});
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| predicate | required | Returns `true` for the delivery to replay from. | `(delivery: Delivery) => boolean` |
| options | optional | The page cap, what to do when history runs out, and whether to replay the matching delivery. | |
| Property | Description | Type |
| --- | --- | --- |
| maxPages | Optional. The most pages of history to read. Defaults to no limit. | Number |
| onExhausted | Optional. What to do when history runs out, or `maxPages` is reached, before a delivery matches. `'deliver'` replays what was read and reports `found: false`. `'error'` fails the replay with `SessionHistoryFetchFailed`. Defaults to `'deliver'`. | `'deliver'` or `'error'` |
| inclusive | Optional. Whether to replay the matching delivery too. Defaults to `true`. | Boolean |
### Returns
`ReplayFn`. The replay function to pass as `history.replay`. `untilEvent` throws an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) with `InvalidArgument` when `maxPages` is less than one.
## Read channel history
`history(options: HistoryOptions): Promise>`
Read the channel's history backwards from the point where the channel attached, decoded through the codec. Each call starts again from the current attach point and resolves with the newest page. Older pages follow through the page's [`next()`](#HistoryPage-next). The call attaches the channel if nothing has attached it yet.
A message that both history and a live delivery carry decodes once, so a message a subscriber has already received comes back with `event` set to `undefined`. A replay function receives the same page that `history()` returns.
### Javascript
```
// Client code.
let page = await transport.history({ limit: 50 });
const older = [...page.items];
while (page.hasNext && older.length < 200) {
page = await page.next();
older.unshift(...page.items);
}
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| options | required | The page size. | |
| Property | Description | Type |
| --- | --- | --- |
| limit | Messages per Ably page, for every page of the read. | Number |
### Returns
`Promise>`. Returns a promise. The promise is fulfilled with the newest page, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object: `SessionHistoryFetchFailed` when the page cannot be fetched after retries, or `SessionClosed` after `close()`.
#### HistoryPage
| Property | Description | Type |
| --- | --- | --- |
| items | The page's deliveries, oldest first: one for each event the codec decodes from a message, and one with `event` set to `undefined` for a message it has nothing for. | |
| hasNext | Whether an older page exists. | Boolean |
##### Read the next page
`next(): Promise>`
Read the next older page. Past the end of history it resolves with an empty page whose `hasNext` is `false`. It rejects with `SessionHistoryFetchFailed` when the page cannot be fetched after retries.
## Listen for discontinuities and errors
`on(event: 'discontinuity', handler: () => void): () => void`
`on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void`
Listen for a discontinuity, or for an error that has no caller to reject.
A discontinuity is a channel state change after which messages may have been missed: `FAILED`, `SUSPENDED`, `DETACHED`, or `ATTACHED` with `resumed` set to `false`. A stream that was cut by the gap repairs itself, because its next append arrives as an update with the full content. To recover the other messages, unsubscribe and subscribe again with `fromSerial`, from the serial of the last message the handler received.
The `error` event reports a codec that threw while decoding a message, a failed attach, and a history page that could not be decoded.
### Javascript
```
// Client code.
let unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(stored.serial) } });
transport.on('discontinuity', () => {
unsubscribe();
unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(lastSeen) } });
});
transport.on('error', (error) => console.error(error));
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| event | required | The event to listen for. | `'discontinuity'` or `'error'` |
| handler | required | Called on each discontinuity with no arguments, or with each error. | `() => void` or `(error: ErrorInfo) => void` |
### Returns
`() => void`. A function that removes the listener.
## Close the transport
`close(): Promise`
Stop every pipe in flight, remove the transport's channel listener, and detach the channel if `subscribe()` or `history()` attached it. Each pipe finishes the messages it has written before it rejects with `OperationCancelled`. A transport that shares the channel name on the same client shares the channel, so it sees the channel detach too. The client stays connected, because your application owns it.
`close()` never rejects, and calling it again has no effect. `send()`, `pipe()` and `history()` reject with `SessionClosed` after `close()`, and `subscribe()` throws it.
### Javascript
```
// Agent code.
try {
await transport.pipe(stream, { signal: req.signal });
} finally {
await transport.close();
}
```
### Returns
`Promise`. Returns a promise that resolves once the pipes have settled and the transport has tried to detach the channel.
## Example
An agent route pipes the model's output to the channel and stores the response with its serial. A client subscribes from the stored serial, so it receives the rest of the conversation in order before its live deliveries.
### OpenAI Javascript
```
// Agent code, in your route handler.
import * as Ably from 'ably';
import OpenAI from 'openai';
import { createTransport } from '@ably/ai-transport';
import { openai } from '@ably/ai-transport/openai';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
const client = new OpenAI();
export async function POST(req) {
const { channelName, items, requestId } = await req.json();
await recordItems(channelName, items);
const history = await loadConversation(channelName);
const transport = createTransport({ client: ably, channelName, codec: openai });
try {
const stream = await client.responses.create({ model: 'gpt-5.5', input: history, stream: true }, { signal: req.signal });
const { serial } = await transport.pipe(stream, {
signal: req.signal,
headers: { requestId },
});
await recordSerial(channelName, serial);
return new Response(null, { status: 204 });
} finally {
await transport.close();
}
}
```
### Vercel AI SDK Javascript
```
// Agent code, in your route handler.
import * as Ably from 'ably';
import { createTransport } from '@ably/ai-transport';
import { vercel } from '@ably/ai-transport/vercel';
import { convertToModelMessages, streamText, toUIMessageStream } from 'ai';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
export async function POST(req) {
const { channelName, message } = await req.json();
await recordMessage(channelName, message);
const messages = await loadConversation(channelName);
const transport = createTransport({ client: ably, channelName, codec: vercel });
try {
const result = streamText({ model, messages: await convertToModelMessages(messages), abortSignal: req.signal });
const { serial } = await transport.pipe(toUIMessageStream({ stream: result.fullStream }), {
signal: req.signal,
headers: { requestId: message.id },
});
await recordSerial(channelName, serial);
return new Response(null, { status: 204 });
} finally {
await transport.close();
}
}
```
### OpenAI Javascript
```
// Client code.
import * as Ably from 'ably';
import { createTransport, fromSerial } from '@ably/ai-transport';
import { openai } from '@ably/ai-transport/openai';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const transport = createTransport({ client: ably, channelName: 'conversations:42', codec: openai });
// The conversation your server stored, with the serial of its last message.
const stored = await (await fetch('/api/messages?channel=conversations:42')).json();
let lastSeen = stored.serial;
const apply = (delivery) => {
render(delivery);
if (delivery.message.serial > lastSeen) lastSeen = delivery.message.serial;
};
let unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(lastSeen) } });
transport.on('discontinuity', () => {
unsubscribe();
unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(lastSeen) } });
});
```
### Vercel AI SDK Javascript
```
// Client code.
import * as Ably from 'ably';
import { createTransport, fromSerial } from '@ably/ai-transport';
import { vercel } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const transport = createTransport({ client: ably, channelName: 'conversations:42', codec: vercel });
// The conversation your server stored, with the serial of its last message.
const stored = await (await fetch('/api/messages?channel=conversations:42')).json();
let lastSeen = stored.serial;
const apply = (delivery) => {
render(delivery);
if (delivery.message.serial > lastSeen) lastSeen = delivery.message.serial;
};
let unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(lastSeen) } });
transport.on('discontinuity', () => {
unsubscribe();
unsubscribe = transport.subscribe(apply, { history: { replay: fromSerial(lastSeen) } });
});
```
## Related Topics
- [Codec](https://ably.com/docs/ai-transport/api/javascript/transport/codec.md): API reference for the AI Transport Codec: the encode and decode contract a transport uses, EncodedMessage, and how defineCodec builds a codec from a table of event rows.
## 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.