### Shell
```
npm install @ably/ai-transport ably openai next react react-dom
```
AI Transport does not depend on Next.js. This guide uses it because the agent route has to keep streaming after the HTTP response returns, which Next.js provides through `after()`, and any server that can do the same works unchanged.
## Set up authentication
Create an auth endpoint at `/api/auth/token` that returns an Ably JWT to the client. The endpoint validates the user and signs a token with their client ID and the channel capabilities they need. See [Set up authentication](https://ably.com/docs/ai-transport/getting-started/authentication.md) for the full setup.
The client below uses `authUrl: '/api/auth/token'` to fetch tokens from this endpoint.
## Configure the channel rule
AI Transport streams each response by appending tokens to a single channel message. That requires the **Message annotations, updates, deletes, and appends** channel rule (`mutableMessages`) on the namespace your conversations live on.
In your Ably dashboard, enable **Message annotations, updates, deletes, and appends** on the `conversations` namespace. See [Configure the channel rule](https://ably.com/docs/ai-transport/getting-started/channel-rules.md) for the dashboard, Control API, and CLI steps.
## Create the agent route
Create `app/api/chat/route.ts`. The agent receives an [invocation](https://ably.com/docs/ai-transport/concepts/runs.md#invocations), creates an [agent session](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md) bound to the `ResponsesCodec`, starts a [run](https://ably.com/docs/ai-transport/concepts/runs.md), rebuilds the conversation, opens a Responses stream, pipes it, and ends the run:
### Javascript
```
import { after } from 'next/server';
import OpenAI from 'openai';
import * as Ably from 'ably';
import { createAgentSession, Invocation } from '@ably/ai-transport';
import { ResponsesCodec, toResponsesInput } from '@ably/ai-transport/openai';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
const openai = new OpenAI();
export async function POST(req) {
const invocation = Invocation.fromJSON(await req.json());
const session = createAgentSession({
client: ably,
channelName: invocation.sessionName,
codec: ResponsesCodec,
});
await session.connect();
// No identity is pinned: this run is not retried, so a generated run id and
// invocation id are correct.
const run = session.createRun(invocation, {}, { signal: req.signal });
// Load the full conversation from history before starting the run.
// run.start() needs to have seen this run's triggering input; draining it
// from history means run.start() resolves at once instead of waiting for
// that input to arrive live on the channel.
while (run.view.hasOlder()) {
await run.view.loadOlder();
}
// Start before returning the response. A continuation re-keys run.runId from
// the triggering input's headers, so the id is only final once start resolves.
await run.start();
// Flatten the conversation into the Responses `input` array. Each stored
// message already holds valid model input, so no conversion is needed.
const input = toResponsesInput(run.view.getMessages().map(({ message }) => message));
after(async () => {
try {
const stream = await openai.responses.create(
{ model: 'gpt-5.5', input, stream: true },
{ signal: run.abortSignal },
);
// The Responses stream is an async iterable, and each raw event is valid
// codec output, so pipe it straight through. run.pipe watches
// run.abortSignal, so a cancel ends the run with no extra handling here.
const { reason } = await run.pipe(stream);
await run.end({ reason });
} catch (err) {
await run.end({ reason: 'error' });
throw err;
} finally {
await session.end();
}
});
return Response.json({ runId: run.runId, invocationId: run.invocationId });
}
```
This route handles a single model turn. To configure server-side tools and run the agentic loop (model turn, run tools, continue), see [OpenAI Responses](https://ably.com/docs/ai-transport/frameworks/openai.md).
## Create the chat component
Create `app/chat.tsx`. The component reads the conversation with [`useView`](https://ably.com/docs/ai-transport/api/react/core/use-view.md) and sends a user turn with `ResponsesCodec.createUserMessage`. Because the core SDK never sends HTTP itself, the component POSTs to the agent endpoint after `view.send` resolves.
An OpenAI message holds a list of `items`, so rendering flattens each message's text content parts:
### Javascript
```
'use client';
import { useState } from 'react';
import { useClientSession, useView } from '@ably/ai-transport/react';
import { ResponsesCodec } from '@ably/ai-transport/openai';
async function wakeAgent(run) {
await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(run.toInvocation().toJSON()),
});
}
// Flatten an OpenAI message's items into rendered text.
function messageText(message) {
let text = '';
for (const item of message.items) {
if (item.type !== 'message') continue;
for (const part of item.content) {
if (part.type === 'output_text' || part.type === 'input_text') text += part.text;
}
}
return text;
}
export function Chat() {
const [input, setInput] = useState('');
const { session } = useClientSession();
const view = useView({ limit: 30 });
const { messages, runOf } = view;
// The Stop button shows only while the latest message's Run is streaming.
const latestRun = runOf(messages.at(-1)?.codecMessageId ?? '');
const isStreaming = latestRun?.status === 'active';
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
const text = input;
setInput('');
const run = await view.send(
ResponsesCodec.createUserMessage({
role: 'user',
items: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text }] }],
}),
);
await wakeAgent(run);
};
const stop = () => {
if (latestRun) void session.cancel(latestRun.runId);
};
return (
{messages.map(({ codecMessageId, message }) => (
{message.role}: {messageText(message)}
))}
);
}
```
## Wire it together
Create `app/page.tsx`. `Providers` sets up an authenticated Ably client. [`ClientSessionProvider`](https://ably.com/docs/ai-transport/api/react/core/providers.md) binds the channel and the `ResponsesCodec` into AI Transport. `ResponsesCodec` is a ready-made codec, so you pass it directly with no factory call.
Update `channelName` to match a namespace with the AIT [channel rule](https://ably.com/docs/ai-transport/getting-started/channel-rules.md) configured:
### Javascript
```
'use client';
import { useEffect, useState } from 'react';
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { ClientSessionProvider } from '@ably/ai-transport/react';
import { ResponsesCodec } from '@ably/ai-transport/openai';
import { Chat } from './chat';
function Providers({ children }) {
const [client, setClient] = useState(null);
useEffect(() => {
const ably = new Ably.Realtime({ authUrl: '/api/auth/token', clientId: 'user' });
setClient(ably);
return () => ably.close();
}, []);
if (!client) return null;
return {children} ;
}
export default function Page() {
const channelName = 'conversations:my-chat-session';
return (
);
}
```
Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the same URL; both tabs share the same durable session.
## What happens when you send a message
1. The user types a message. `view.send(...)` publishes the message on the channel and returns a `ClientRun`. The SDK does not POST to your agent endpoint itself.
2. Your client code calls `clientRun.toInvocation().toJSON()` and POSTs the resulting [`InvocationData`](https://ably.com/docs/ai-transport/concepts/runs.md#invocations) to your agent endpoint. The body identifies the session and the input event the agent should respond to.
3. The agent endpoint creates an `AgentSession` bound to the `ResponsesCodec` and starts a Run. `run.start()` waits until it has seen the triggering input on the channel, whether loaded from history or arriving live.
4. The agent reads the full conversation from `run.view` and calls `toResponsesInput` to build the Responses `input` array. Each stored message is already valid Responses input.
5. The agent opens a streaming Responses API call and pipes the event stream through `run.pipe()`, which encodes each Responses event onto the channel.
6. Every client subscribed to the channel decodes the streamed events in realtime. `useView` re-renders as the visible Run accumulates OpenAI items.
7. If a client disconnects mid-stream, Ably resumes the subscription from the last serial on reconnect; the SDK rehydrates the view without losing tokens.
## Understand the architecture
The OpenAI SDK handles the model call and the typed event stream. AI Transport handles the durable session between agent and devices, encoding the Responses stream onto an Ably channel through the `ResponsesCodec`. See [OpenAI Responses](https://ably.com/docs/ai-transport/frameworks/openai.md) for how the codec works and how to run server-side tools.
## Explore next
- [OpenAI Responses](https://ably.com/docs/ai-transport/frameworks/openai.md): the codec, `toResponsesInput`, and the server-side tool loop.
- [OpenAI demo app](https://github.com/ably/ably-ai-transport-js/tree/main/demo/openai/react/use-client-session): the runnable version of this app, with client-side tools and approval gates added.
- [ResponsesCodec reference](https://ably.com/docs/ai-transport/api/javascript/openai/codec.md): the codec's methods, tool payloads, and types.
- [Conversation helpers reference](https://ably.com/docs/ai-transport/api/javascript/openai/conversation-helpers.md): `toResponsesInput` and the correlation readers.
- [Cancellation](https://ably.com/docs/ai-transport/features/cancellation.md): the stop button pattern and the agent-side `onCancel` authorisation hook.
- [Multi-device sessions](https://ably.com/docs/ai-transport/features/multi-device.md): open another tab to see realtime sync.
- [Branching, edit, and regenerate](https://ably.com/docs/ai-transport/features/branching.md): fork the conversation and navigate alternative branches.
- [Codec architecture](https://ably.com/docs/ai-transport/internals/codec-architecture.md): how a codec translates a framework's events into Ably messages.
## Related Topics
- [Core SDK](https://ably.com/docs/ai-transport/getting-started/core-sdk.md): Build a streaming AI chat app using AI Transport's core React hooks. Full access to the conversation tree, branching, and pagination.
- [Vercel AI SDK](https://ably.com/docs/ai-transport/getting-started/vercel-ai-sdk.md): Build a streaming AI chat app with Vercel AI SDK and Ably AI Transport in a few minutes. Durable sessions, multi-device sync, and cancellation out of the box.
- [Vercel WDK](https://ably.com/docs/ai-transport/getting-started/vercel-wdk.md): Build a streaming AI chat app whose agent side runs as a Vercel Workflow inside your Next.js app. Each model call and tool is its own retryable WDK step; a retry supersedes the failed attempt on the session, and the user's stream never breaks.
- [Temporal](https://ably.com/docs/ai-transport/getting-started/temporal.md): Build a streaming AI chat app whose agent side runs inside a Temporal workflow. Retryable steps supersede failed attempts on the session, and the user's stream never breaks.
## 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.