AgentSession

The AgentSession is the server-side counterpart to ClientSession. It subscribes to the channel for cancel signals and creates AgentRun instances that publish lifecycle events, user messages, and streamed assistant output.

Construct one with createAgentSession from the core entry point. For Vercel UIMessage channels, 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

14

15

16

17

import * as Ably from 'ably';
import { createAgentSession, Invocation } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';

const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });

const invocation = Invocation.fromJSON(await req.json());

const session = createAgentSession({
  client: ably,
  channelName: invocation.sessionName,
  codec: UIMessageCodec,
});

await session.connect();
const run = session.createRun(invocation, { signal: req.signal });
await run.start();

Properties

presenceAbly.RealtimePresence
The Ably presence object for the session's channel. Use it to see which clients are connected, for example to detect whether the requesting user is still online (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. See LiveObjects State.

Create an agent session

function createAgentSession<TInput, TOutput, TProjection, TMessage>(options: AgentSessionOptions<TInput, TOutput, TProjection, TMessage>): AgentSession<TOutput, TProjection, TMessage>

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

JavaScript

1

2

3

4

5

6

7

8

9

10

11

import * as Ably from 'ably';
import { createAgentSession } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';

const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });

const session = createAgentSession({
  client: ably,
  channelName: 'conversation-42',
  codec: UIMessageCodec,
});

Parameters

clientrequiredAbly.Realtime
The Ably Realtime client. The caller owns its lifecycle; session.close() does not close the client.
channelNamerequiredString
The channel to publish to. The session owns this channel; do not also resolve it elsewhere with conflicting options.
codecrequiredCodec<TInput, TOutput, TProjection, TMessage>
The codec used to encode 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. See LiveObjects State.
historyPageSizeoptionalNumber
Wire-message limit fetched per channel-history round trip, used by every run.view pagination on this session. Independent of loadOlder's reveal limit: it tunes fetch cost, not reveal granularity. Defaults to 100.
loggeroptionalLogger
Logger instance for diagnostic output.

Subscribe to non-fatal session errors with on('error') rather than a constructor option.

Returns

AgentSession<TOutput, TProjection, TMessage>. The session instance. Call connect() before createRun.

Connect the session

connect(): Promise<void>

Attaches and subscribes to the channel backing the session. Idempotent: subsequent calls return the same promise. All AgentRun methods (start, pipe, suspend, end) throw InvalidArgument until connect() has been called.

JavaScript

1

await session.connect();

Returns

Promise<void>. Resolves when the channel is attached and the session is ready to create runs.

Create a run

createRun(invocation: Invocation, runtime?: RunRuntime<TOutput>): AgentRun<TOutput, TProjection, TMessage>

Create a new AgentRun for the input event named in the Invocation. Returns synchronously and publishes nothing to the channel until AgentRun.start is called. The run is registered for cancel routing immediately so early cancels fire the abortSignal.

JavaScript

1

2

const invocation = Invocation.fromJSON(await req.json());
const run = session.createRun(invocation, { signal: req.signal });

Parameters

invocationrequiredInvocation
The Invocation carrying run identity and conversation context.
runtimeoptionalRunRuntime
Per-run hooks and an external abort signal.

Returns

An AgentRun<TOutput, TProjection, TMessage> handle for publishing lifecycle events, user messages, and streamed output. See AgentRun interface below.

AgentRun

The handle returned by createRun. It extends the shared BaseRun read-model (runId, status, error, messages) with the agent's lifecycle surface.

Properties

runIdString
The Run's unique identifier. Known synchronously on the agent (it mints the id for a fresh run, or reads it off the triggering input event for a continuation).
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[]
All of this Run's messages: its triggering input followed by its streamed output (across any suspend and resume), deduplicated by codecMessageId. The unit to persist. See Hydrate the conversation.
invocationIdString
The invocation id minted by the agent for this createRun call (one per HTTP request). Readable synchronously; the application returns it on the HTTP response. The agent stamps it on every event it publishes for this invocation.
abortSignalAbortSignal
AbortSignal scoped to this Run. Fires when a cancel event arrives.
viewView<TMessage>
A read-only View<TMessage> of the conversation branch this Run belongs to, from its triggering input back to the conversation root. Use it to reconstruct the conversation to feed the model. See Hydrate the conversation.
locatedPromise<void>
Resolves once the Run's triggering input (named in its invocation) has been observed on the channel, whether through the live subscription or by paging history with view.loadOlder(). start awaits it internally; await it directly only to read the trigger before deciding how to start.

Start the run

start(): Promise<void>

Wait until the Run's triggering input has been observed on the channel (see located), then publish the opening lifecycle event (ai-run-start, or ai-run-resume for a continuation). Must be called before pipe, suspend, or end.

There is no built-in deadline: start() does not time out waiting for the trigger. It rejects only if the run is cancelled or the session is closed before the trigger is observed. Race it against your own timeout if you need one.

Pipe the response stream

pipe(stream: ReadableStream<TOutput>, options?: PipeOptions<TOutput>): Promise<StreamResult>

Pipe a ReadableStream of outputs through the encoder to the channel. Returns when the stream completes, is cancelled, or errors. Does NOT call end(); the caller must call end() after pipe() returns.

Parameters

streamrequiredReadableStream<TOutput>
The output stream from your LLM call.
optionsoptionalPipeOptions
Branching and per-output hooks.

Returns

Promise<StreamResult>. Resolves when the stream ends. Pass result.reason to Run.end.

Suspend the run

suspend(): Promise<void>

Publish the ai-run-suspend event to the channel, pausing the Run pending external input (a tool approval, a human-in-the-loop response). The Run is not terminal: RunInfo.status becomes 'suspended', and a continuation Invocation resumes it via ai-run-resume.

Use suspend instead of end when you want the run to come back. Use end only for terminal outcomes.

End the run

end(params: RunEndParams): Promise<void>

Publish the ai-run-end event to the channel terminally and clean up. params is a RunEndParams object carrying the terminal reason and, when reason is 'error', an optional error. To pause a Run instead of ending it, use suspend.

Parameters

RunEndParams is discriminated on reason:

  • { reason: 'complete' | 'cancelled' }: a non-error terminal reason that carries no error.
  • { reason: 'error', error? }: the run ended in error. error is an optional Ably.ErrorInfo to surface to clients. Omit it to end in error without detail.
reasonrequiredRunEndReason
The terminal reason.
erroroptionalAbly.ErrorInfo
The terminal error to surface to clients. Allowed only when reason is 'error'.

Subscribe to session errors

on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void

Subscribe to non-fatal session-level errors not scoped to any run: channel continuity loss (a re-attach with resumed: false, or FAILED / SUSPENDED / DETACHED), cancel-listener or attach failures, and any run-scoped error whose run supplied no onError. Returns an unsubscribe function. Once the session is closed this is a no-op.

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 session error.

Returns

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

Close the session

close(): Promise<void>

Cancel active runs, detach the channel the session attached, and clear handlers. Local-state-only for run lifecycle; it does not end in-progress Runs on the wire. End each Run explicitly before closing the session.

Invocation

A value object wrapping the JSON body a client sends to the agent's HTTP endpoint to start a Run.

Build from JSON

Invocation.fromJSON(data: InvocationData): Invocation

The entry point used by agent handlers: parse the request body and pass it to Invocation.fromJSON, then hand the result to createRun.

JavaScript

1

2

3

4

import { Invocation } from '@ably/ai-transport';

const data = await req.json();
const invocation = Invocation.fromJSON(data);

Parameters

datarequiredInvocationData
The parsed JSON request body matching the InvocationData wire shape.

Hydrate the conversation

Two accessors expose conversation content, at different scopes:

  • run.messages is all of this Run's own messages: its triggering input plus its streamed output (across any suspend and resume). This is the unit to persist, not the value to feed the model in a multi-turn conversation.
  • run.view is a read-only, leaf-pinned View of this Run's full branch, from its triggering input back to the conversation root. This is the value to feed the model.

run.view includes an ancestor turn only once its run has completed. An ancestor that is still active, suspended, cancelled, or errored is omitted, along with the input it replied to, so a dangling tool call from a concurrent or interrupted turn can't invalidate the prompt. The current run is always included, and an omitted ancestor reappears once it completes.

To rebuild the prior conversation for the model, drain run.view with loadOlder() for as much ancestor context as you want, then read getMessages():

JavaScript

1

2

3

4

5

6

7

8

// Rebuild the conversation from run.view before run.start(): draining pages in
// this run's triggering input (otherwise run.start() awaits it arriving live).
while (run.view.hasOlder()) {
  await run.view.loadOlder();
}
const conversation = run.view.getMessages().map(({ message }) => message);

await run.start();

For database-backed hydration, page run.view back only to the newest stored message with loadUntil instead of draining to the root.

RunEndReason

'complete' | 'cancelled' | 'error'. The terminal-reason discriminant: it is the reason field of the RunEndParams you pass to Run.end, the reason on StreamResult, and the value reflected on RunInfo.status once the Run terminates.

A Run that pauses for external input (tool approval, human-in-the-loop) uses Run.suspend instead of end, which publishes ai-run-suspend and leaves the Run alive at RunInfo.status === 'suspended'. A continuation Invocation resumes it via ai-run-resume.

Example

An HTTP handler that sets up the session, creates a Run, rebuilds the conversation from run.view, pipes the LLM stream, and ends the Run.

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

39

40

41

import * as Ably from 'ably';
import { createAgentSession, Invocation } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';

const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });

export async function POST(req: Request) {
  const invocation = Invocation.fromJSON(await req.json());

  const session = createAgentSession({
    client: ably,
    channelName: invocation.sessionName,
    codec: UIMessageCodec,
  });

  await session.connect();

  const run = session.createRun(invocation, { signal: req.signal });

  try {
    // Rebuild the conversation from run.view before run.start(): draining pages
    // in this run's triggering input (otherwise run.start() awaits it live).
    while (run.view.hasOlder()) {
      await run.view.loadOlder();
    }
    const conversation = run.view.getMessages().map(({ message }) => message);

    await run.start();

    const llmStream = await callMyLLM(conversation);
    const result = await run.pipe(llmStream);
    await run.end({ reason: result.reason });
  } catch (err) {
    await run.end({ reason: 'error' });
    throw err;
  } finally {
    await session.close();
  }

  return Response.json({ runId: run.runId, invocationId: run.invocationId });
}