# ClientTransport
The `ClientTransport` subscribes to an Ably channel, decodes incoming messages through a codec, and builds a conversation tree. It exposes a default branch-aware `View` for rendering, plus methods for cancellation, turn tracking, and lifecycle management.
Construct one with `createClientTransport` from the core entry point, or use the Vercel-pre-bound factory from `@ably/ai-transport/vercel` when the channel carries Vercel `UIMessage` content.
#### Javascript
```
import { createClientTransport } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';
const transport = createClientTransport({
channel,
codec: UIMessageCodec,
api: '/api/chat',
});
```
## Properties
The `ClientTransport` instance has the following properties:
| Property | Description | Type |
| --- | --- | --- |
| tree | The complete conversation tree. Holds every known node, emits events on any change. Use the view in most cases; reach for the tree for low-level inspection. | |
| view | The default paginated, branch-aware view for rendering. Events are scoped to the visible messages. | `View` |
| Property | Description | Type |
| --- | --- | --- |
| getNode | `(msgId: string) => MessageNode \| undefined`. Get a node by message ID. | Function |
| getHeaders | `(msgId: string) => Record \| undefined`. Get the stored transport headers for a node. | Function |
| getSiblings | `(msgId: string) => TMessage[]`. Get all sibling messages at a fork point. | Function |
| hasSiblings | `(msgId: string) => boolean`. Whether a message has sibling alternatives. | Function |
| upsert | `(msgId: string, message: TMessage, headers: Record, serial?: string) => void`. Insert or update a message in the tree. | Function |
| delete | `(msgId: string) => void`. Remove a message from the tree. | Function |
| getActiveTurnIds | `() => Map>`. Active turn IDs grouped by `clientId` across the whole tree. | Function |
| on | `(event: 'update' \| 'ably-message' \| 'turn', handler) => () => void`. Subscribe to tree structure changes, raw Ably messages, or turn lifecycle events. Returns an unsubscribe function. | Function |
## Create a client transport
`function createClientTransport(options: ClientTransportOptions): ClientTransport`
Construct a `ClientTransport` bound to an Ably channel. The transport attaches to the channel lazily on first use.
### Javascript
```
import * as Ably from 'ably';
import { createClientTransport } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const channel = ably.channels.get('conversation-42');
const transport = createClientTransport({
channel,
codec: UIMessageCodec,
api: '/api/chat',
});
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| options | Required | Configuration for the client transport. | |
| Property | Description | Type |
| --- | --- | --- |
| channel | The Ably channel to receive responses on and publish cancel signals to. | `Ably.RealtimeChannel` |
| codec | The codec to use for encoding and decoding messages. See [Codec](https://ably.com/docs/ai-transport/api/javascript/codec.md). | `Codec` |
| api | Server endpoint URL for the HTTP POST that triggers a new turn. The Vercel-pre-bound factory defaults this to `/api/chat`; the core factory requires it. | String |
| clientId | The client's identity. Sent to the server in the POST body. | String or Undefined |
| headers | Headers for the HTTP POST. Function form for dynamic values such as auth tokens. | `Record` or `() => Record` or Undefined |
| body | Additional body fields merged into the HTTP POST. Function form for dynamic values. | `Record` or `() => Record` or Undefined |
| credentials | Fetch credentials mode for the HTTP POST. | `RequestCredentials` or Undefined |
| fetch | Custom fetch implementation. Defaults to `globalThis.fetch`. | `typeof fetch` or Undefined |
| messages | Initial messages to seed the conversation tree with. Forms a linear chain. | `TMessage[]` or Undefined |
| logger | Logger instance for diagnostic output. | `Logger` or Undefined |
### Returns
`ClientTransport`
A new client transport. Lifecycle is managed by the caller; call `close()` to release resources.
## Create an additional view
`createView(): View`
Create an additional `View` over the same conversation tree. Each view has independent branch selections and pagination state. The caller is responsible for closing the returned view when it is no longer needed, or it will be closed when the transport closes.
### Javascript
```
const sideView = transport.createView();
```
### Returns
`View`
A new view bound to the same tree as `transport.view`.
## Cancel turns
`cancel(filter?: CancelFilter): Promise`
Publish a cancel signal on the channel for every turn that matches `filter`. Defaults to `{ own: true }` (every turn started by this client).
### Javascript
```
await transport.cancel(); // cancel own turns
await transport.cancel({ turnId: 'abc' }); // cancel one specific turn
await transport.cancel({ all: true }); // cancel every turn on the channel
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| filter | Optional | Scope of the cancellation. Defaults to `{ own: true }`. | |
| Property | Description | Type |
| --- | --- | --- |
| turnId | Cancel a specific turn by ID. | String or Undefined |
| own | Cancel every turn belonging to the sender's `clientId`. | Boolean or Undefined |
| clientId | Cancel every turn belonging to a specific `clientId`. | String or Undefined |
| all | Cancel every turn on the channel. | Boolean or Undefined |
### Returns
`Promise`
Returns a promise. The promise is fulfilled when the cancel signal has been published, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
## Wait for turns to complete
`waitForTurn(filter?: CancelFilter): Promise`
Wait for every active turn matching `filter` to complete. Resolves immediately if no matching turns are active. Defaults to `{ own: true }`.
### Javascript
```
await transport.waitForTurn(); // own turns
await transport.waitForTurn({ turnId: 'abc' }); // one specific turn
await transport.waitForTurn({ all: true }); // every turn on the channel
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| filter | Optional | Scope of the wait. Defaults to `{ own: true }`. | |
### Returns
`Promise`
Returns a promise. The promise is fulfilled when every matching turn has ended, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
## Stage events on a message
`stageEvents(msgId: string, events: TEvent[]): void`
Apply events to an existing tree message locally and queue them for delivery on the next send. The events are applied via the codec's accumulator (the tree's `update` fires once with the merged message) and are flushed into the next send operation's POST body for the server to republish.
Use for cross-turn updates where the event value is produced on the client (for example, after `addToolResult` resolves a client-executed tool) and must appear in the tree immediately.
If `msgId` is not present in the tree, the call is a no-op and a warning is logged.
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| msgId | Required | The `x-ably-msg-id` of the existing message to amend. | String |
| events | Required | Events to apply locally and ship on the next send. | `TEvent[]` |
## Stage a replacement message
`stageMessage(msgId: string, message: TMessage): void`
Replace the tree's copy of an existing message with a caller-provided version, preserving headers and serial. Runs synchronously.
Use for `useChat`-style state transitions the codec cannot express as chunks. The canonical example is `addToolApprovalResponse`, which sets `state: 'approval-responded'` on a `dynamic-tool` part directly on the `UIMessage` and has no corresponding chunk variant.
Staged messages are not queued for the next send. The tree is authoritative for the POST body's history, so updating it is sufficient.
If `msgId` is not present in the tree, the call is a no-op and a warning is logged.
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| msgId | Required | The `x-ably-msg-id` of the existing message to replace. | String |
| message | Required | The patched message to store. | `TMessage` |
## Subscribe to errors
`on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void`
Subscribe to non-fatal transport errors. These indicate something went wrong but the transport is still operational. Returns an unsubscribe function.
### Javascript
```
const off = transport.on('error', (error) => {
console.error(`Transport error ${error.code}: ${error.message}`);
});
// Later, to stop listening:
off();
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| event | Required | The event name. Currently only `'error'` is supported. | `'error'` |
| handler | Required | Called with each emitted [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo). | `(error: Ably.ErrorInfo) => void` |
### Returns
`() => void`
A function that unsubscribes the handler.
## Close the transport
`close(options?: CloseOptions): Promise`
Tear down the transport: unsubscribe from the channel, close active streams, clear handlers, and prevent further operations. Pass `cancel` to publish a cancel message before closing; without it, only local state is torn down (the server keeps streaming).
### Javascript
```
await transport.close(); // local teardown only
await transport.close({ cancel: { own: true } }); // cancel own turns first
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| options | Optional | Close options. | |
| Property | Description | Type |
| --- | --- | --- |
| cancel | Cancel in-progress turns before closing. Publishes a cancel message to the channel. | or Undefined |
### Returns
`Promise`
Returns a promise. The promise is fulfilled when teardown completes, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
## View
A `View` is a paginated, branch-aware projection of the conversation tree. It tracks which branch is selected at each fork point and supports lazy loading of older messages. The transport exposes a default view as `transport.view`; create additional views with `transport.createView()`.
### Get visible messages
`getMessages(): TMessage[]`
The visible domain messages along the selected branch. Shorthand for `flattenNodes().map(n => n.message)`.
### Get visible nodes
`flattenNodes(): MessageNode[]`
Visible nodes along the selected branch, filtered by the pagination window. Each node wraps the domain message with tree metadata:
### Check for older messages
`hasOlder(): boolean`
Whether there are older messages that can be loaded or revealed.
### Load older messages
`loadOlder(limit?: number): Promise`
Reveal older messages. Loads from channel history if the tree does not have enough, then advances the window to show up to `limit` more messages. Emits `'update'` when the visible list changes. Returns a promise; rejection surfaces an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo).
### Select a sibling at a fork point
`select(msgId: string, index: number): void`
Select a sibling at a fork point by index. Updates this view's branch selection. Index is clamped to `[0, siblings.length - 1]`. Emits `'update'` when the visible output changes.
### Get the selected sibling index
`getSelectedIndex(msgId: string): number`
The index of the currently selected sibling at a fork point.
### Get sibling messages
`getSiblings(msgId: string): TMessage[]`
Every sibling message at a fork point, ordered chronologically by serial.
### Check for siblings
`hasSiblings(msgId: string): boolean`
Whether a message has sibling alternatives. Use this to decide whether to render branch-navigation arrows.
### Get a node by ID
`getNode(msgId: string): MessageNode | undefined`
The node for a given message ID, or `undefined` if the tree has no such node.
### Send one or more messages
`send(messages: TMessage | TMessage[], options?: SendOptions): Promise>`
Send one or more user messages and start a new turn. The parent is auto-computed from this view's selected branch unless overridden in `options`. Messages are inserted optimistically into the tree. The HTTP POST is fire-and-forget; the returned stream is available immediately.
#### Javascript
```
const turn = await transport.view.send([
{ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Hello' }] },
]);
for await (const chunk of turn.stream) {
// chunk is one decoded event from the agent.
}
```
#### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| messages | Required | One message or an array of messages to send. | `TMessage` or `TMessage[]` |
| options | Optional | Per-send overrides for headers, body, and branching metadata. | |
| Property | Description | Type |
| --- | --- | --- |
| body | Additional fields merged into the HTTP POST body for this send. | `Record` or Undefined |
| headers | Additional headers for the HTTP POST for this send. | `Record` or Undefined |
| forkOf | The msg-id of the message this send replaces. Set for regeneration (forkOf an assistant message) or edit (forkOf a user message). | String or Undefined |
| parent | Override the auto-computed parent msg-id for this send. | String or Undefined |
#### Returns
`Promise>`
Returns a promise. The promise is fulfilled with an [`ActiveTurn`](#ActiveTurn) carrying the decoded event `stream`, `turnId`, and a `cancel()` method.
### Regenerate an assistant message
`regenerate(messageId: string, options?: SendOptions): Promise>`
Regenerate an assistant message. Creates a new turn that forks the target message with no new user messages. `forkOf`, `parent`, and the truncated history are computed automatically from this view's branch.
#### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| messageId | Required | The msg-id of the assistant message to regenerate. | String |
| options | Optional | Per-send overrides. | |
#### Returns
`Promise>`
Returns a promise. The promise is fulfilled with an [`ActiveTurn`](#ActiveTurn), or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
### Edit a user message
`edit(messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions): Promise>`
Edit a user message and start a new turn from that point. The original message and its descendants remain in the tree as a separate branch. `forkOf`, `parent`, and history are computed automatically.
#### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| messageId | Required | The msg-id of the user message to fork. | String |
| newMessages | Required | The replacement message or messages. | `TMessage` or `TMessage[]` |
| options | Optional | Per-send overrides. | |
#### Returns
`Promise>`
Returns a promise. The promise is fulfilled with an [`ActiveTurn`](#ActiveTurn), or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
### Update an existing message
`update(msgId: string, events: TEvent[], options?: SendOptions): Promise>`
Update an existing message in place and start a continuation turn. The local tree is updated optimistically, then the events are sent to the server in the POST body. The server publishes them to the channel and streams a continuation response. Commonly used for delivering client-executed tool results.
#### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| msgId | Required | The `x-ably-msg-id` of the existing message to amend. | String |
| events | Required | Events to apply to the target message (for example, tool output). | `TEvent[]` |
| options | Optional | Per-send overrides. | |
#### Returns
`Promise>`
Returns a promise. The promise is fulfilled with an [`ActiveTurn`](#ActiveTurn) carrying the continuation response stream, or rejected with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) object.
## ActiveTurn
An `ActiveTurn` is the handle returned by `view.send()`, `view.regenerate()`, `view.edit()`, and `view.update()`. It exposes the decoded event stream, the turn's identity, and a cancel handle.
| Property | Description | Type |
| --- | --- | --- |
| stream | The decoded event stream for this turn. May error if the delivery guarantee is broken (POST failure, channel continuity loss). | `ReadableStream` |
| turnId | The turn's unique identifier. | String |
| cancel | `() => Promise`. Cancel this specific turn. Publishes a cancel message and closes the local stream. | Function |
| optimisticMsgIds | The msg-ids of optimistically inserted user messages, in order. Present when the send included user messages (`send`, `edit`); empty for `regenerate`. | `string[]` |
| Property | Description | Type |
| --- | --- | --- |
| kind | `'message'`. Distinguishes a message node from an events node. | String |
| msgId | The Ably message ID. Primary key in the tree. | String |
| message | The decoded domain message. | `TMessage` |
| parentId | The parent node's `msgId`, or `undefined` for root messages. | String or Undefined |
| forkOf | The `msgId` this node forks from (for example, regeneration or edit), or `undefined` if it is the first version. | String or Undefined |
| headers | Transport headers for tree identity, ordering, and branching. | `Record` |
| serial | The Ably serial for tree ordering. Absent on optimistically-inserted nodes until the server relay arrives. | String or Undefined |
## Example
### Javascript
```
import * as Ably from 'ably';
import { createClientTransport } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/auth' });
const channel = ably.channels.get('conversation-42');
const transport = createClientTransport({
channel,
codec: UIMessageCodec,
api: '/api/chat',
});
transport.on('error', (error) => {
console.error(`Transport error ${error.code}: ${error.message}`);
});
// Send a message and consume the response stream.
const turn = await transport.view.send([
{ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Summarise this contract.' }] },
]);
for await (const chunk of turn.stream) {
// chunk is one decoded event for this turn.
}
// Browse history and regenerate the last response.
await transport.view.loadOlder(50);
const last = transport.view.getMessages().at(-1);
if (last?.role === 'assistant') {
await transport.view.regenerate(last.id);
}
// Cancel an in-flight turn from anywhere.
await transport.cancel({ own: true });
// Wait for the current set of own turns to finish before tearing down.
await transport.waitForTurn();
await transport.close();
```
## Related Topics
- [Server transport](https://ably.com/docs/ai-transport/api/javascript/server-transport.md): API reference for the AI Transport server transport: turn lifecycle, cancel routing, abort hooks, and configuration options.
- [Vercel integration](https://ably.com/docs/ai-transport/api/javascript/vercel.md): API reference for the AI Transport Vercel AI SDK integration. UIMessageCodec, ChatTransport, and pre-bound transport factories.
- [Codec](https://ably.com/docs/ai-transport/api/javascript/codec.md): API reference for the AI Transport codec interface. Build custom codecs to integrate any AI framework with Ably channels.
## 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.