Durable sessions

A drop-in durable session layer for AI applications. AI Transport holds the conversation and the message state your UI renders, including branching, and your components render from its React hooks.

A durable session provides the complete, persistent state of a conversation, and it exists independently of anything that connects to it. The conversation history can be fully hydrated from the Ably channel, including the messages, their order, the branches a user creates by editing or regenerating, and the state your components render. Your UI can read all of it through a hook that re-renders your components when the conversation changes. Choose a durable session when you want the SDK to hold the conversation state, the branches a user creates by editing or regenerating, and the React hooks that render them. Choose streaming when your application owns the message format and its own store.

Understand the model

Streaming delivers an agent's messages and responses to every subscribed client. A durable session goes further, so edits, regenerating, branching, and tool calls come with the session rather than being things you model yourself.

A new client can reconstruct the whole conversation from the channel. A phone picking up a conversation that started on a laptop can read it back from the session.

The conversation tree holds one node per user prompt and one per agent reply, each with a parent and siblings. Editing or regenerating adds a sibling and leaves the original where it is, so the whole history stays addressable. A view reads that tree and selects one path through its branches, exposing it as an ordered list your UI can render, so switching branch is a change of selection rather than a refetch. The React hooks bind a view to your UI: useView returns the messages, the run statuses, and the write operations, and re-renders your component when the tree changes underneath it.

Diagram showing two clients and a phone attached to one session, the conversation tree of runs inside it, the Ably channel as the append-only log underneath, and the invocation the client posts to the agent

Client and agent example

The agent publishes the stream into a run on the session, and the client subscribes to the channel to read the conversation.

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

// Agent-side, in place of `return result.toUIMessageStreamResponse()`:
import { after } from 'next/server';
import * as Ably from 'ably';
import { streamText, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { Invocation } from '@ably/ai-transport';
import { createAgentSession, vercelRunOutcome } from '@ably/ai-transport/vercel';

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

export async function POST(req) {
  const invocation = Invocation.fromJSON(await req.json());
  const session = createAgentSession({ client: ably, channelName: invocation.sessionName });
  await session.connect();

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

  after(async () => {
    try {
      // Drain the view's history pages before run.start(), which otherwise waits
      // for this run's triggering input to arrive live.
      while (run.view.hasOlder()) await run.view.loadOlder();
      const conversation = run.view.getMessages().map(({ message }) => message);

      await run.start();
      const result = streamText({
        model: anthropic('claude-sonnet-4-20250514'),
        messages: await convertToModelMessages(conversation),
        abortSignal: run.abortSignal,
      });
      const pipeResult = await run.pipe(result.toUIMessageStream());
      await run.end(await vercelRunOutcome(pipeResult, result.finishReason));
    } finally {
      session.end();
    }
  });

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

The route returns the run's identifiers and the client reads the response from the channel.

In the browser, a client attaches to that same channel by name:

JavaScript

1

2

3

4

5

6

7

8

// Client-side.
import * as Ably from 'ably';
import { createClientSession } from '@ably/ai-transport/vercel';

const ably = new Ably.Realtime({ authUrl: '/api/auth/token' });
const session = createClientSession({ client: ably, channelName: 'conversations:42' });

await session.connect();

The SDK is JavaScript and TypeScript, with React hooks for the client. The roadmap covers other languages.

Know who implements what

A durable session leaves one row for your application to write. Everything else comes from the SDK or from the channel underneath it:

Part of the systemWho implements it
Token streaming to the clientSDK
Messages from the client to the agentSDK
Cancelling a run in flightSDK
Run and step lifecycleSDK
Steering a run while it streamsSDK
Finding the message that woke a restarted agentSDK
Reading message history back from the channelSDK
Hydrating the client's view of the conversationSDK
Assembling context for the agent's model callSDK
Merging decoded codec events into the conversation's messagesSDK
Branching, editing, and regeneratingSDK
The message state your UI rendersSDK
Where the conversation livesAbly channel
Fan-out to several clientsAbly channel
Ordering, storage, and replay of messagesAbly channel
Resume after a disconnectAbly channel
Presence and a shared state storeAbly channel
Waking the agentYour application

What you still own

You are responsible for waking an agent, typically using an HTTP request that carries an invocation. The client publishes the input to the session, then calls your own endpoint to run the agent.

The channel holds the conversation for as long as your retention window covers it. For history beyond that window, keep your own store of completed runs and let database hydration join it to the live conversation.

How to get started

To get started, see one of the quickstarts with the React hooks or Vercel useChat.

Both quickstarts build the same application. The React hooks read the conversation tree directly, with branch navigation and pagination, and useChat keeps Vercel as the message manager, reading its messages from the session.