# 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.