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

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

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

treeTree
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.
viewView
The default paginated, branch-aware view for rendering. Events scope to the visible messages.
presenceAbly.RealtimePresence
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().
objectRealtimeObject
The Ably LiveObjects 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; without both, the underlying SDK throws.

Create a client session

function createClientSession<TInput, TOutput, TProjection, TMessage>(options: ClientSessionOptions<TInput, TOutput, TProjection, TMessage>): ClientSession<TInput, TOutput, TProjection, TMessage>

Construct a ClientSession bound to an Ably channel. The session does not attach to the channel until connect() resolves.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

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

clientrequiredAbly.Realtime
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.
channelNamerequiredString
The channel to subscribe to and publish cancel signals on. The session owns this channel; do not also resolve it elsewhere with conflicting options.
codecrequiredCodec<TInput, TOutput, TProjection, TMessage>
The codec used to encode and decode events and messages.
channelModesoptionalAbly.ChannelMode[]
Extra channel modes to request on top of the modes AI Transport always needs. Pass OBJECT_MODES to use Ably LiveObjects via object. Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on.
historyPageSizeoptionalNumber
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.
loggeroptionalLogger
Logger instance for diagnostic output.

Returns

ClientSession<TInput, TOutput, TProjection, TMessage>. The session instance. Call connect() to attach before sending or cancelling.

Connect the session

connect(): Promise<void>

Subscribe to the channel and implicitly attach. Idempotent: subsequent calls return the same promise.

All write methods on view and cancel throw InvalidArgument until connect() resolves.

JavaScript

1

await session.connect();

Returns

Promise<void>. Resolves when the channel is attached and the session is ready for writes.

Create an additional view

createView(): ClientView<TInput, TMessage>

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

1

2

3

4

5

const secondaryView = session.createView();
secondaryView.branchSelection(messageId).select(1);

// the default view is unaffected
session.view.branchSelection(messageId).index; // 0

Returns

ClientView<TInput, TMessage>. A new view with its own pagination window and branch selection state.

Cancel a run

cancel(runId: string): Promise<void>

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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

runIdrequiredString
The run to cancel. Typically obtained from ClientRun.runId (after awaiting clientRun.started), or from a RunInfo.runId in view.runs().

Returns

Promise<void>. 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, 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 verb.

Properties

runIdString
The run's unique identifier, minted by the agent. Empty until the agent's ai-run-start is observed; await started before reading it.
statusRunStatus
The run's lifecycle status, read live off the tree.
errorAbly.ErrorInfo or Undefined
The terminal error, present exactly when status is 'error'.
messagesTMessage[]
This run's whole turn: its originating input followed by the run's output, deduplicated by codecMessageId.
startedPromise<void>
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.
inputCodecMessageIdString
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.
inputEventIdString
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.

Steer the run

steer(input: TInput): SteerResult

Publish a codec input event that targets this run. The steering message 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 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

1

2

3

4

5

6

7

8

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

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

Returns

SteerResult. Two promises: published for the channel-publish acknowledgement, and outcome for the consumed determination resolved at the run's next terminal event.

Cancel the run

cancel(): Promise<void>

Cancel this specific run. Keyed by inputCodecMessageId, 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.

Build the invocation

toInvocation(): Invocation

Build the 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

1

2

3

4

5

6

const unsubscribe = session.on('error', (error) => {
  console.error('Session error:', error.code, error.message);
});

// later, when the listener is no longer needed
unsubscribe();

Parameters

eventrequired'error'
The event to subscribe to. Currently only 'error'.
handlerrequiredFunction
Called with an ErrorInfo for every non-fatal error.

Returns

() => void. An unsubscribe function. Call it to remove the listener.

Close the session

close(): Promise<void>

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 for each before close().

JavaScript

1

2

3

4

5

6

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<void>. Resolves once the channel has been released.

Example

End-to-end usage covering construction, connect, send, and teardown.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

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();