# ClientSession
The `ClientSession` subscribes to an Ably channel, decodes incoming messages through a codec, and builds a conversation tree. It owns the channel attach and the cancel-publish path, exposes a default branch-aware `View` for rendering, and lets you derive additional views over the same tree.
Construct one with `createClientSession` from the core entry point. For Vercel `UIMessage` sessions, use the pre-bound factory from [`@ably/ai-transport/vercel`](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md) instead.
#### Javascript
```
import * as Ably from 'ably';
import { createClientSession } from '@ably/ai-transport';
import { createUIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/api/auth/token' });
const session = createClientSession({
client: ably,
channelName: 'conversation-42',
codec: createUIMessageCodec(),
});
await session.connect();
```
## Properties
| Property | Description | Type |
| --- | --- | --- |
| tree | The complete conversation tree. Holds every known run node and emits events on any change. Use `view` in most cases; use `tree` for low-level inspection. | |
| view | The default paginated, branch-aware view for rendering. Events scope to the visible messages. | |
| presence | The Ably presence object for the session's channel. Use it to see which clients are connected (`enter`, `leave`, `get`, `subscribe`). The session adds no semantics of its own (it is the same instance the channel exposes), and presence operations implicitly attach, so they work without first awaiting [`connect()`](#connect). | `Ably.RealtimePresence` |
| object | The Ably [LiveObjects](https://ably.com/docs/ai-transport/features/liveobjects.md) API for the session's channel. Use it to read and write shared `LiveMap` / `LiveCounter` state on the channel the session already uses; call `get()` to resolve the object. The session adds no semantics; it is the same instance the channel exposes. Operating on it requires the client to be constructed with the `LiveObjects` plugin from `ably/liveobjects` and the object modes to be requested via [`channelModes`](#constructor-params); without both, the underlying SDK throws. | `RealtimeObject` |
| Property | Description | Type |
| --- | --- | --- |
| getRunNode | `(runId: string) => RunNode \| undefined`. Get a run by id. | Function |
| getNodeByCodecMessageId | `(codecMessageId: string) => ConversationNode \| undefined`. Get the input or run node that owns a given codec-message-id. Narrow on `kind` (`'input'` or `'run'`) before reading kind-specific fields. | Function |
| getSiblingNodes | `(key: string) => ConversationNode[]`. The sibling group for a node key: edit versions for an input node, regenerate siblings for a reply run. Ordered oldest-first by serial; single-element when there are no siblings; empty when the key is unknown. | Function |
| on | `(event: 'update' \| 'ably-message' \| 'run' \| 'output', handler) => () => void`. Subscribe to tree changes. Returns an unsubscribe function. | Function |
| Property | Description | Type |
| --- | --- | --- |
| getMessages | `() => CodecMessage[]`. Visible messages along the selected branch, each paired with its `codecMessageId`. Read the domain object from each entry's `message` field; correlate back to the transport (run lookups, branch navigation, continuation routing) via `codecMessageId`. | Function |
| runs | `() => RunInfo[]`. Visible runs along the selected branch. | Function |
| runOf | `(codecMessageId: string) => RunInfo \| undefined`. Run that owns the message. | Function |
| run | `(runId: string) => RunInfo \| undefined`. Direct run lookup by id. | Function |
| branchSelection | `(codecMessageId: string) => BranchHandle`. The branch siblings at the anchor, plus a `select(index)` verb to switch between them. | Function |
| hasOlder | `() => boolean`. Whether older messages can be revealed. | Function |
| loadOlder | `(limit?: number) => Promise[]>`. Reveal older messages, resolving to the revealed page (oldest-first); `[]` when nothing older was revealed. | Function |
| loadUntil | `(predicate, signal?) => Promise[]>`. Page older history back until `predicate` matches a message (the seam), then resolve to the messages newer than it. Drives database-backed hydration. | Function |
| send | `(events, options?) => Promise>`. Publish an input on the session. The core session is HTTP-free, so wake the agent by POSTing `run.toInvocation().toJSON()` to your agent endpoint. At most one new message per send (it mints a fresh run); the array form carries only wire-only inputs (tool results, approvals) for a continuation. Wrap a domain message via `codec.createUserMessage` to send a fresh user message. | Function |
| regenerate | `(messageId, options?) => Promise>`. Regenerate an assistant message. | Function |
| edit | `(messageId, inputs, options?) => Promise>`. Edit a user message. | Function |
| on | `(event: 'update' \| 'ably-message' \| 'run', handler) => () => void`. Subscribe to view updates, raw Ably messages for visible nodes, or run lifecycle events. Returns an unsubscribe function. | Function |
| close | `() => void`. Tear down the view. | Function |
## Create a client session
`function createClientSession(options: ClientSessionOptions): ClientSession`
Construct a `ClientSession` bound to an Ably channel. The session does not attach to the channel until [`connect()`](#connect) resolves.
### Javascript
```
import * as Ably from 'ably';
import { createClientSession } from '@ably/ai-transport';
import { createUIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/api/auth/token' });
const session = createClientSession({
client: ably,
channelName: 'conversation-42',
codec: createUIMessageCodec(),
});
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| client | required | The Ably Realtime client. The caller owns its lifecycle; `session.close()` does not close the client. The session's identity is read from this client's `auth.clientId` at publish time, stamped on the wire as the run-owner / input-owner id so other clients can attribute messages. A connection with no concrete clientId (anonymous, or a wildcard `*` token) publishes without one. | `Ably.Realtime` |
| channelName | required | The channel to subscribe to and publish cancel signals on. The session owns this channel; do not also resolve it elsewhere with conflicting options. | String |
| codec | required | The codec used to encode and decode events and messages. | `Codec` |
| channelModes | optional | Extra channel modes to request on top of the modes AI Transport always needs. Pass `OBJECT_MODES` to use Ably [LiveObjects](https://ably.com/docs/ai-transport/features/liveobjects.md) via [`object`](#properties). Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on. | `Ably.ChannelMode[]` |
| historyPageSize | optional | Wire-message limit fetched per channel-history round trip when paging older history through `view.loadOlder()`, shared by every view on the session. Independent of `loadOlder`'s reveal `limit`: it tunes fetch cost rather than reveal granularity. Defaults to 100. | Number |
| logger | optional | Logger instance for diagnostic output. | `Logger` |
### Returns
`ClientSession`. The session instance. Call [`connect()`](#connect) to attach before sending or cancelling.
## Connect the session
`connect(): Promise`
Subscribe to the channel and implicitly attach. Idempotent: subsequent calls return the same promise.
All write methods on [`view`](https://ably.com/docs/ai-transport/api/javascript/core/client-session.md#properties) and [`cancel`](#cancel) throw `InvalidArgument` until `connect()` resolves.
### Javascript
```
await session.connect();
```
### Returns
`Promise`. Resolves when the channel is attached and the session is ready for writes.
## Create an additional view
`createView(): ClientView`
Create an additional view over the same conversation tree. Each view has independent branch selections and pagination state.
The caller owns the returned view's lifecycle: call its `close()` when it is no longer needed, or `session.close()` closes it.
### Javascript
```
const secondaryView = session.createView();
secondaryView.branchSelection(messageId).select(1);
// the default view is unaffected
session.view.branchSelection(messageId).index; // 0
```
### Returns
`ClientView`. A new view with its own pagination window and branch selection state.
## Cancel a run
`cancel(runId: string): Promise`
Publish a cancel signal for the specified run. The agent receives the cancel through its own channel subscription and ends the run with reason `'cancelled'`.
### Javascript
```
const clientRun = await session.view.send({
kind: 'user-message',
message: {
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Tell me a story' }],
},
});
// later, cancel from the UI.
// clientRun.cancel() works immediately, even before the agent's
// run-start has been observed (so clientRun.runId is still empty).
await clientRun.cancel();
// Or, when you already have a resolved runId (for example from a
// RunInfo in view.runs()):
// await session.cancel(someRunInfo.runId);
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| runId | required | The run to cancel. Typically obtained from `ClientRun.runId` (after awaiting `clientRun.started`), or from a `RunInfo.runId` in `view.runs()`. | String |
### Returns
`Promise`. Resolves once the cancel message has been published. The cancel is best-effort: if the agent has already ended the run, the cancel is a no-op.
## ClientRun
The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the client's control methods, including the run-scoped [`steer`](#steer) verb.
### Properties
| Property | Description | Type |
| --- | --- | --- |
| runId | The run's unique identifier, minted by the agent. Empty until the agent's `ai-run-start` is observed; await [`started`](#client-run-properties) before reading it. | String |
| status | The run's lifecycle status, read live off the tree. | `RunStatus` |
| error | The terminal error, present exactly when `status` is `'error'`. | `Ably.ErrorInfo` or Undefined |
| messages | This run's whole turn: its originating input followed by the run's output, deduplicated by `codecMessageId`. | `TMessage[]` |
| started | Resolves when the agent's `ai-run-start` (or `ai-run-resume`) is observed, the point at which `runId` is populated. No built-in deadline; race it against your own timeout. | `Promise` |
| inputCodecMessageId | The triggering input's codec-message-id, owned by the client the moment it publishes. Stream routing and cancel key on this, so it is known synchronously, unlike `runId`. | String |
| inputEventId | The input event's unique identifier, stamped on the published input event and forwarded in the POST body so the agent can locate the trigger. | String |
### Steer the run
`steer(input: TInput): SteerResult`
Publish a codec input event that targets this run. The [steering message](https://ably.com/docs/ai-transport/features/interruption-and-steering.md) carries this run's `run-id` so the agent folds it into the active run rather than starting a new run. Pass the same shape [`view.send`](#properties) accepts, typically `codec.createUserMessage(...)`. The SDK awaits `runId` internally, so this is safe to call as soon as the handle is returned. Once an `ai-run-end` has folded for this run the handle is dead and further `steer()` calls return immediately-rejected promises.
#### Javascript
```
const { published, outcome } = activeRun.steer(createUIMessageCodec().createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Also include vegan options.' }],
}));
const { serial } = await published;
const { consumed, runTerminalReason } = await outcome;
```
#### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| input | required | The codec input event to publish, in the codec's input shape. Wrap a domain message via `codec.createUserMessage` for a follow-up user message. | `TInput` |
#### Returns
`SteerResult`. Two promises: `published` for the channel-publish acknowledgement, and `outcome` for the consumed determination resolved at the run's next terminal event.
| Property | Description | Type |
| --- | --- | --- |
| published | Resolves when the steering message is published to the channel, carrying the Ably-assigned `serial`. Rejects if the publish (or the internal `runId` await) fails. | `Promise<{ serial: string \| undefined }>` |
| outcome | A `Promise` that resolves once a terminal lifecycle event (`ai-run-end`, or `ai-run-suspend` for a consumed steering message) folds for the run. Rejects if the handle dies first (for example the session closes). | |
| Property | Description | Type |
| --- | --- | --- |
| consumed | `true` when the steering message's codec-message-id appears in the union of `steer-codec-message-ids` stamps on the run's responses (the agent's loop had it visible when it produced that response), `false` when it never appeared before `ai-run-end`. For an `ai-run-suspend` a not-consumed steering message stays pending. | Boolean |
| runTerminalReason | How the run ended, present when the outcome was determined by an `ai-run-end`. Absent when determined by an `ai-run-suspend`, since the run has not ended. | `RunEndReason` |
### Cancel the run
`cancel(): Promise`
Cancel this specific run. Keyed by [`inputCodecMessageId`](#client-run-properties), which the client owns synchronously, so a cancel issued before the agent mints the `runId` is still honoured (the agent buffers it and fires it once its input-event watcher matches the trigger). Resolves once the cancel is published; it does not wait for [`started`](#client-run-properties).
### Build the invocation
`toInvocation(): Invocation`
Build the [`Invocation`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#invocation) pointer for this run, carrying only `inputEventId` and the session's channel name. POST `run.toInvocation().toJSON()` to your agent endpoint to wake the agent; run identity lives on the channel rather than in the invocation body.
## Subscribe to session errors
`on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void`
Subscribe to non-fatal session errors. These indicate something went wrong but the session is still operational; examples are subscription callback failures and channel continuity loss.
### Javascript
```
const unsubscribe = session.on('error', (error) => {
console.error('Session error:', error.code, error.message);
});
// later, when the listener is no longer needed
unsubscribe();
```
### Parameters
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| event | required | The event to subscribe to. Currently only `'error'`. | `'error'` |
| handler | required | Called with an [`ErrorInfo`](https://ably.com/docs/ai-transport/api/errors.md#errorinfo) for every non-fatal error. | Function |
### Returns
`() => void`. An unsubscribe function. Call it to remove the listener.
## Close the session
`close(): Promise`
Tear down the session. Unsubscribe from the channel, close active streams, clear handlers, and prevent further operations.
`close()` is local-state-only. The server keeps streaming until its runs end on their own. To stop in-progress runs, call [`cancel`](#cancel) for each before `close()`.
### Javascript
```
const runIds = session.view.runs()
.filter((run) => run.status === 'active')
.map((run) => run.runId);
await Promise.all(runIds.map((runId) => session.cancel(runId)));
await session.close();
```
### Returns
`Promise`. Resolves once the channel has been released.
## Example
End-to-end usage covering construction, connect, send, and teardown.
### Javascript
```
import * as Ably from 'ably';
import { createClientSession } from '@ably/ai-transport';
import { createUIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ authUrl: '/api/auth/token' });
const session = createClientSession({
client: ably,
channelName: 'conversation-42',
codec: createUIMessageCodec(),
});
await session.connect();
session.view.on('update', () => {
render(session.view.getMessages().map(({ message }) => message));
});
const clientRun = await session.view.send(createUIMessageCodec().createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }],
}));
// The SDK doesn't POST. The application wakes the agent itself.
await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(clientRun.toInvocation().toJSON()),
});
// The agent mints the runId on the server, so clientRun.runId is empty
// until run-start is observed. Await `started`, then read it.
await clientRun.started;
const runId = clientRun.runId;
await session.close();
ably.close();
```
## Related Topics
- [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.
- [Codec](https://ably.com/docs/ai-transport/api/javascript/core/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.