From 494286ec987af3713d2f99be0f4617e8094d5c94 Mon Sep 17 00:00:00 2001 From: zak Date: Thu, 16 Apr 2026 17:10:17 +0100 Subject: [PATCH 1/4] Restructure AI Transport docs around concepts Replace the "How it works" section with a dedicated "Concepts" section containing five pages: Sessions, Messages and conversation tree, Turns, Transport, and Authentication. These pages explain AI Transport's conceptual model (sessions, the conversation tree, turn lifecycle, client/agent transports, and the codec) as logical entities rather than code walkthroughs. Merge the "Framework guides" section into "Getting started" so all Vercel AI SDK content lives on a single page. Add a new "Core SDK" getting started page for using AI Transport's generic React hooks without a framework wrapper. Strengthen the "Why" page with clearer durable session properties (resilient delivery, continuity across surfaces, live control) and update the overview page with a Core SDK tile. Update cross-references across all 16 feature pages to point to the new concepts paths. Add redirects from all old URLs. --- src/data/nav/aitransport.ts | 27 +- .../authentication.mdx | 8 +- .../messages-and-conversation-tree.mdx | 64 +++++ .../docs/ai-transport/concepts/sessions.mdx | 77 ++++++ .../docs/ai-transport/concepts/transport.mdx | 62 +++++ .../docs/ai-transport/concepts/turns.mdx | 44 +++ .../ai-transport/features/agent-presence.mdx | 2 +- .../docs/ai-transport/features/branching.mdx | 2 +- .../ai-transport/features/cancellation.mdx | 2 +- .../features/chain-of-thought.mdx | 2 +- .../features/concurrent-turns.mdx | 2 +- .../ai-transport/features/double-texting.mdx | 2 +- .../features/edit-and-regenerate.mdx | 2 +- .../docs/ai-transport/features/history.mdx | 2 +- .../features/human-in-the-loop.mdx | 2 +- .../ai-transport/features/interruption.mdx | 2 +- .../ai-transport/features/multi-device.mdx | 2 +- .../features/optimistic-updates.mdx | 2 +- .../features/push-notifications.mdx | 2 +- .../features/reconnection-and-recovery.mdx | 2 +- .../ai-transport/features/token-streaming.mdx | 2 +- .../ai-transport/features/tool-calling.mdx | 2 +- .../framework-guides/vercel-ai-sdk.mdx | 116 -------- .../ai-transport/getting-started/core-sdk.mdx | 250 ++++++++++++++++++ .../getting-started/vercel-ai-sdk.mdx | 50 +++- .../how-it-works/sessions-and-turns.mdx | 73 ----- .../ai-transport/how-it-works/transport.mdx | 100 ------- src/pages/docs/ai-transport/index.mdx | 18 +- src/pages/docs/ai-transport/why.mdx | 25 +- 29 files changed, 595 insertions(+), 351 deletions(-) rename src/pages/docs/ai-transport/{how-it-works => concepts}/authentication.mdx (95%) create mode 100644 src/pages/docs/ai-transport/concepts/messages-and-conversation-tree.mdx create mode 100644 src/pages/docs/ai-transport/concepts/sessions.mdx create mode 100644 src/pages/docs/ai-transport/concepts/transport.mdx create mode 100644 src/pages/docs/ai-transport/concepts/turns.mdx delete mode 100644 src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx create mode 100644 src/pages/docs/ai-transport/getting-started/core-sdk.mdx delete mode 100644 src/pages/docs/ai-transport/how-it-works/sessions-and-turns.mdx delete mode 100644 src/pages/docs/ai-transport/how-it-works/transport.mdx diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts index 930c4c12fd..fcf001f514 100644 --- a/src/data/nav/aitransport.ts +++ b/src/data/nav/aitransport.ts @@ -18,19 +18,27 @@ export default { link: '/docs/ai-transport/why', }, { - name: 'How it works', + name: 'Concepts', pages: [ { - name: 'Sessions and turns', - link: '/docs/ai-transport/how-it-works/sessions-and-turns', + name: 'Sessions', + link: '/docs/ai-transport/concepts/sessions', + }, + { + name: 'Messages and conversation tree', + link: '/docs/ai-transport/concepts/messages-and-conversation-tree', + }, + { + name: 'Turns', + link: '/docs/ai-transport/concepts/turns', }, { name: 'Transport', - link: '/docs/ai-transport/how-it-works/transport', + link: '/docs/ai-transport/concepts/transport', }, { name: 'Authentication', - link: '/docs/ai-transport/how-it-works/authentication', + link: '/docs/ai-transport/concepts/authentication', }, ], }, @@ -41,14 +49,9 @@ export default { name: 'Vercel AI SDK', link: '/docs/ai-transport/getting-started/vercel-ai-sdk', }, - ], - }, - { - name: 'Framework guides', - pages: [ { - name: 'Vercel AI SDK', - link: '/docs/ai-transport/framework-guides/vercel-ai-sdk', + name: 'Core SDK', + link: '/docs/ai-transport/getting-started/core-sdk', }, ], }, diff --git a/src/pages/docs/ai-transport/how-it-works/authentication.mdx b/src/pages/docs/ai-transport/concepts/authentication.mdx similarity index 95% rename from src/pages/docs/ai-transport/how-it-works/authentication.mdx rename to src/pages/docs/ai-transport/concepts/authentication.mdx index 27793375d9..a07921f69d 100644 --- a/src/pages/docs/ai-transport/how-it-works/authentication.mdx +++ b/src/pages/docs/ai-transport/concepts/authentication.mdx @@ -3,6 +3,7 @@ title: "Authentication" meta_description: "Understand how authentication works in Ably AI Transport: Ably token auth for channel access, HTTP headers for server endpoints, and cancel authorization." meta_keywords: "AI Transport, authentication, token auth, cancel authorization, HTTP headers, credentials" redirect_from: + - /docs/ai-transport/how-it-works/authentication - /docs/ai-transport/sessions-identity/identifying-users-and-agents --- @@ -252,7 +253,8 @@ const turn = transport.newTurn({ If `onCancel` returns `false`, the cancellation is rejected. If `onCancel` is not provided, all cancel requests are accepted by default. -## What to read next + +A message is a discrete unit of content in the conversation: a user's input, an assistant's response, a tool call, a tool result. Each message has a unique identity (its message ID) and a position in the tree defined by parent and fork-of pointers. + +A message may arrive on the channel as a single publish or as a sequence of operations that share the same message ID. A streamed assistant response, for example, is initiated with a publish and built up through a series of appends (token deltas) until a final closing append marks it complete. These are not separate messages; they are the incremental delivery of a single message. The message ID is the identity; the channel operations are the mechanism. + +Each message carries transport headers (identity, parentage, role, streaming state) and a codec-encoded payload. Messages are ordered by their serial and are immutable once complete. + +## Understand the conversation tree + +The tree holds the complete branching history of a conversation. Every message that has ever been part of the [session](/docs/ai-transport/concepts/sessions), whether it is currently visible or not, is a node in the tree. + +The tree is not a linear list. When you regenerate a response, the new response is a sibling of the old one, not a replacement. When you edit a message, the edited version forks from the same parent as the original. This means the tree preserves all paths through the conversation, not just the currently visible one. No message is ever deleted from the tree by a regeneration or edit; it becomes a sibling on an alternative branch. + +### Construct the tree from transport headers + +Each message on the channel carries transport headers that define the tree structure: + +- A message ID that uniquely identifies the node. +- A parent pointer that establishes the parent-child relationship. +- An optional fork-of pointer that marks a regeneration or edit. + +The tree applies these headers to construct the graph. Messages are ordered by their serial, so the tree's construction is deterministic. The same sequence of messages always produces the same tree. + +### Use the tree as the source of truth + +The tree is the single source of truth for conversation state. It is unfiltered: it contains every node on every branch, and it emits events for every mutation regardless of which branch was affected. The tree does not know or care which branch a particular participant is looking at. That is the view's job. + +## Understand views + +A view is a linear projection over the tree. It selects one sibling at each branch point and produces the flat sequence of messages that a participant works with: the conversation as it appears in a chat UI, or the message history that an agent sends to an LLM. + +The tree contains every branch; the view chooses a path through it. When a conversation has been regenerated three times at a particular point, the tree holds all three responses as siblings. The view selects one of them and presents the conversation as if that response is the only one, while still allowing navigation to the others. + +### Maintain independent views per participant + +Each participant can have their own view with independent branch selections. Two clients looking at the same session might be viewing different branches of the same conversation. The tree is shared; the views are independent. + +### Manage pagination + +The view emits the same kinds of events as the tree (updates, turn lifecycle), but scoped to visibility. If a mutation affects a branch that the view is not currently showing, the view does not emit an event. This is the filtering that makes views practical for driving UI: a component subscribed to view events only re-renders when something it is displaying changes. + +### Interact through a view + +The view is the surface through which participants interact with the conversation. Sending a message, regenerating a response, editing a message, and updating an existing message are all operations on a view. The view knows its current position in the tree (the selected branch, the last visible message) and uses that context to construct the correct parent and fork-of pointers for new operations. + +## Read next + +A session is built on top of an Ably channel, but a session is not the same thing as a channel. + +The channel is an Ably realtime channel: a durable, ordered, append-only log of messages. Every event in the session passes through the channel in real time, and every connected participant receives it. Messages on the channel have a total order defined by their serial, assigned by the Ably service on publish. This ordering is deterministic: two participants reading the same log produce the same sequence. + +The session is the structured, navigable conversation that those events produce. The channel is the ordered log of events; the session is the result of interpreting that log. The relationship is analogous to a database transaction log and the tables it produces: the log is the authoritative sequence of operations, but the useful state is the result of applying them. + +## Materialise a session + +A session can be materialised from two sources. + +By default, it is materialised from the Ably channel, which serves as both the live delivery layer and the historical record. When the channel retains the full history, the channel alone is sufficient and no external infrastructure is required. + +Alternatively, you can supply historical messages from your own database, with the channel providing only live and in-progress activity. In both cases, the session merges historical and live data into a single consistent state. + +Materialisation is not a simple replay of the log. Certain events on the channel affect how prior events are interpreted. A clear event discards all messages that preceded it from the materialised result, even though those messages still exist on the channel. A compaction event replaces a sequence of messages with a summary. The channel retains the full, unedited log, but the materialisation process applies these events as instructions that reshape what the session contains. The session is the result of interpreting the log, not transcribing it. + +## Understand durable session properties + +In direct HTTP streaming (SSE or WebSocket), the stream is the connection. If the connection dies, the stream dies with it. A session inverts this: the session is an independently addressable resource that agents write to and clients subscribe to. Connections come and go; the session persists. + +Sessions have the following properties: + +- Independently addressable. Any participant (client, agent, observer) connects to the session by name without knowledge of the other participants. There is no requirement that the client that initiated a request is the one that receives the response. +- Persistent. Messages on the session outlive any single connection. An agent that crashes and restarts can resume publishing. A client that drops and reconnects can resume consuming. The session state is not held in memory on either end. +- Ordered and resumable. Messages have a total order. A client that disconnects mid-stream can reconnect and resume from the exact point of disconnection without replaying the entire conversation or re-invoking the agent. +- Bidirectional. Any participant can publish to the session at any time. This is what makes cancel, steer, interrupt, and multi-client interaction possible. +- Fan-out. Multiple clients subscribe to the same session simultaneously. A second tab, a phone, or a new client joining later all receive the same ordered stream of activity. +- Multiplexed. Multiple concurrent interactions (turns, agents, tool calls) coexist on the same session. An orchestrator agent and its subagents can all publish independently without routing updates through a single bottleneck. + +These properties enable three capabilities that direct HTTP streaming does not provide: + +1. Resilient delivery. Streams survive connection drops, device switches, page refreshes, and process restarts. The client resumes from a known position. The agent continues publishing regardless of client connectivity. No events are lost and no events are duplicated. +2. Continuity across surfaces. The session follows the user, not the connection. Open a second tab, switch to a phone, come back hours later. Every surface sees the same session state. +3. Live control. Any participant can communicate with any other participant through the session while work is in progress. Cancel a generation from a different device. Steer an agent mid-response. Send a follow-up before the current response finishes. + +## Share a session across participants + +The session is the unit of sharing. When a second client joins, it joins the session. When an agent hydrates context for an LLM call, it hydrates the session. When a message is published, it is published to the session via the channel. Every participant's interaction with the conversation is mediated by the session. + +No participant needs to be present for any other participant to function, and no participant's arrival or departure corrupts session state. + +Agent lifecycle does not affect the session. An agent hydrates the session, processes a turn, and may terminate. The session survives because it lives on the channel (or in an external store), not in the agent's memory. A different agent instance can handle the next turn with the same session state. + +Clients are resilient to disconnection. A client that drops its connection loses nothing. On reconnect, the client's Ably connection resumes from the last received serial, and any messages published during the disconnection are delivered. + +New participants can join at any time. A second client attaching to the channel hydrates the full session from history and receives live updates going forward. There is no handshake or coordination required between participants. + +## Support different persistence models + +The channel serves two distinct roles: live delivery and historical persistence. It always serves as the live delivery layer. Whether it also serves as the historical persistence layer depends on the configuration. + +When channel history retention covers the session's lifetime, the channel is sufficient to hydrate the full session. No external infrastructure is required. This is the zero-configuration path. + +When you store completed messages in your own database, the external store provides historical session data and the channel provides live and in-progress activity. The session is hydrated from the external store for past messages and from the channel subscription for current activity. This is appropriate when channel retention is limited, the conversation is long-lived, or you need to enrich or index conversation data in your own systems. + +In both cases, the channel carries all live activity: in-progress streams, turn lifecycle events, cancel signals, and newly published messages. + +## Read next + +The client transport is how a client application connects to a session. It is a long-lived object that subscribes to the channel, materialises the session into a [tree](/docs/ai-transport/concepts/messages-and-conversation-tree), and provides one or more [views](/docs/ai-transport/concepts/messages-and-conversation-tree#views) for the application to work with. + +The client transport owns the read path: decoding incoming channel messages, applying them to the tree, and emitting scoped events through views so the application knows when visible state has changed. It also owns the client's side of the write path: when the application sends a message, the transport publishes turn-start and the user's input messages to the channel, then sends a [poke](/docs/ai-transport/concepts/turns#poke) to the agent endpoint to trigger an execution. The channel publish and the poke are decoupled: the turn is established on the session before the agent is triggered, and the poke can be deferred or retried independently. Cancellation is a channel publish: the transport sends a cancel signal targeting a turn, and the agent receives it through its own channel subscription. + +The client transport is also responsible for connection resilience. It monitors channel continuity, detects if messages were lost during a disconnection, and ensures the session state remains consistent after reconnection. + +A client transport is not the session. It is one participant's connection to the session. Multiple clients can be connected to the same session simultaneously, each with their own views and branch selections, all sharing the same underlying conversation state on the channel. + +## Understand the agent transport + +The agent transport is how an agent connects to a session for a single execution attempt within a [turn](/docs/ai-transport/concepts/turns). It is typically short-lived: created when the agent receives a poke, used for the duration of that execution, then disposed. + +The agent transport handles the write path for the agent's contribution: piping model output through the codec encoder to the channel, publishing turn-end when the task completes, and listening for cancel signals that target the active turn. When the agent receives a poke, it uses the transport to hydrate the session from the channel (or from an external store), locate the message the poke references, and begin its work. + +Because the agent is stateless between execution attempts, the agent transport does not maintain long-lived subscriptions or accumulated state. Each execution hydrates what it needs, does its work, publishes its output, and tears down. If the turn spans multiple executions (human-in-the-loop, retry after failure), each execution creates its own agent transport against the same session. The session on the channel is what provides continuity, not the transport. + +## Understand the codec + +The codec is the translation layer between domain-specific message models and Ably's native message primitives. It defines how events in the application's domain (text deltas, tool calls, finish signals, or whatever the domain model requires) are encoded into Ably messages for transmission over the channel, and how incoming Ably messages are decoded back into domain events and accumulated into domain messages. + +The codec is an interface, not an implementation. The generic layer of the SDK knows nothing about what the events or messages look like. It operates entirely through the codec contract. A Vercel AI SDK codec translates `UIMessageChunk` events into Ably messages and assembles them into `UIMessage` objects. A different codec could do the same for a different framework, or for a custom domain model. + +The codec has three responsibilities: + +- The encoder takes domain events and produces Ably message operations: publishes for discrete messages, append sequences for streamed content. +- The decoder takes incoming Ably messages and produces domain events, handling the reconstruction of streaming state from Ably's message lifecycle (create, append, update). +- The accumulator takes a sequence of decoded events and assembles them into complete domain messages, tracking which messages are still being streamed and which are complete. + +The codec also determines what constitutes a terminal event: the event that signals a stream is finished. This is domain-specific knowledge (a `finish` chunk in the Vercel model, for example) that the generic transport layer needs in order to close streams at the right time. + +The codec is the boundary between transport concerns and application domain concerns. Everything below the codec (headers, serials, channel operations, tree structure) is generic. Everything above (the shape of events, the structure of messages, what a tool call or text delta means) is domain-specific. + +## Choose an entry point + +| Entry point | Import path | Use when | +| --- | --- | --- | +| Core | `@ably/ai-transport` | Building with a custom codec or non-React client | +| React | `@ably/ai-transport/react` | Building a React UI with any codec | +| Vercel | `@ably/ai-transport/vercel` | Server-side Vercel AI SDK integration | +| Vercel React | `@ably/ai-transport/vercel/react` | Client-side Vercel AI SDK with `useChat` | + +The Vercel AI SDK has the deepest integration. The `@ably/ai-transport/vercel/react` entry point provides `useChatTransport`, which wraps the core transport for direct use with Vercel's `useChat` hook. Other frameworks use the core entry point with a custom or framework-specific codec. + +## Read next + +A turn has a unique identifier and is bracketed by lifecycle events on the channel. The client initiates a turn and passes the prompt for that turn to the server to query the AI model. The user's input messages are part of the turn, and the turn is visible on the [session](/docs/ai-transport/concepts/sessions) from the moment the user expresses intent, not from the moment the agent begins processing. The agent publishes turn-end when the task completes, is cancelled, or fails. The turn ID correlates all activity that belongs to the same task. + +A turn progresses through a defined set of states: + +- Pending. The turn has been opened but no agent has begun working. The user's input is on the session; the agent has not yet started. +- Active. The agent is working. +- Suspended. The most recent execution has intentionally paused. The agent is waiting for external input (human-in-the-loop approval, tool output) or a durable step handoff. The turn is still open. +- Completed. The task finished successfully. +- Cancelled. The user (or another participant) stopped the turn. This can happen from any state: a pending turn can be cancelled before any agent picks it up, an active turn can be cancelled mid-stream, and a suspended turn can be cancelled while awaiting input. +- Failed. The task could not be completed after exhausting recovery options. + +Multiple turns can be active simultaneously on the same session. See [concurrent turns](/docs/ai-transport/features/concurrent-turns) for details. + +## Cancel a turn + +The turn is the unit of cancellation. When a user hits stop, they are cancelling the turn: all work associated with the current task, across all execution attempts. There is no user-facing cancel operation below the turn. + +Internal execution failures (a stream dying, a model call erroring, a serverless timeout) fail the execution attempt, not the turn. The turn remains active and can be retried. See [cancellation](/docs/ai-transport/features/cancellation) for details on cancel filters and authorization. + +## Trigger a turn + +When the client sends a message, the client transport sends an HTTP POST to your server endpoint. This request carries the user's messages, the conversation history, the turn ID, the channel name, and branching context (parent and fork-of pointers). The server uses this to create a turn, publish the user's messages to the channel, invoke the AI model, and stream the response back through the channel. + +The HTTP response returns immediately. The response stream is decoupled from the HTTP request: tokens flow through the Ably channel, not through the HTTP response. This is what allows the stream to survive independently of the client's connection. + +Because the server receives the turn ID and channel name from the client, any server process that can reach the channel can handle the request. The agent does not need to maintain state between turns. It creates a server transport, processes the turn, and tears down. + +## Read next - -Vercel AI SDK handles intelligence and UI. AI Transport handles what happens between the model and every device. - -| Vercel AI SDK | Ably AI Transport | -| --- | --- | -| Model orchestration (`streamText`, providers) | Durable sessions (Ably channels) | -| UI state (`useChat`, message management) | Multi-device fan-out | -| Tool calls and structured output | Reconnection and recovery | -| Streaming via HTTP/SSE | Active turn tracking | -| `ChatTransport` interface | Bidirectional control | -| | Ordering and persistence | - -Vercel explicitly built the `ChatTransport` interface as the extension point for this. AI Transport implements `ChatTransport`, so you swap the transport layer without changing your application code. - -## See how they fit together - -The architecture stacks four layers: - -1. Vercel AI SDK provides `useChat()`, `streamText()`, tool calls, and UI state management. -2. The `ChatTransport` interface is the plug-in point that Vercel designed for custom transports. -3. AI Transport implements `ChatTransport` and adds sessions, presence, recovery, and control. -4. Ably infrastructure provides the global edge network, multi-region routing, ordering, and persistence. - -All connected devices share the same session and receive the same stream of events. - - -```javascript -// Before: default HTTP transport -const { messages } = useChat() - -// After: Ably transport (everything else stays the same) -const transport = useChatTransport({ channel }) -const { messages } = useChat({ transport }) -``` - - -Your existing `useChat()` code, tool calls, and UI logic stay the same. One transport swap. - -## Choose an integration path - -Both paths use the same server code. The difference is client-side only. - -### Use the useChat path - -The simplest path. `useChatTransport` wraps the core transport for direct use with Vercel's `useChat` hook. `useMessageSync` pushes other clients' messages into `useChat` state. You get Vercel's message management with AI Transport's durable delivery. - -When to use: you want the standard Vercel `useChat` developer experience with durable sessions added. - -### Use the generic hooks path - -Use AI Transport's React hooks (`useView`, `useSend`, `useRegenerate`, `useEdit`) directly. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. - -When to use: you need branching UI, custom message rendering, or direct control over the conversation tree. - -## Discover what this unlocks - -### Problems that go away - -AI SDK frameworks handle model orchestration well. They are not built to solve transport-level problems. With AI Transport, these issues are resolved: - -- Streams that die on disconnect. AI Transport resumes from where you left off, automatically. -- Lost context on tab switch or device change. The session persists across any surface. -- No way to tell if the agent crashed or is still thinking. Active turn tracking gives you real-time status. -- Partial responses lost on mobile network drops. Ordering and persistence mean nothing is lost. - -### New capabilities - -Things your app can do that were not possible before: - -- Share the same conversation on phone, laptop, and tablet, all in sync. -- Regenerate, edit, and navigate conversation branches. Each branch is a fork in the conversation tree. -- Cancel, interrupt, and steer agents mid-stream through bidirectional control. -- Approval gates reach the user on any device, even after reconnecting. -- Multiple agents or requests stream simultaneously, each with independent cancel handles. - -## Read next + +- Node.js 20+ +- An [Ably account](https://ably.com/sign-up) with an API key +- An Anthropic API key (or OpenAI, adapt the model config) + +## Install dependencies + + +```shell +npm install @ably/ai-transport ably ai @ai-sdk/anthropic next react react-dom jsonwebtoken +``` + + +## Set up environment variables + +Create `.env.local`: + + +```shell +ABLY_API_KEY=your-ably-api-key +ANTHROPIC_API_KEY=your-anthropic-api-key +``` + + +## Step 1: Create an Ably token endpoint + +Create the file `app/api/auth/ably-token/route.ts`. This endpoint issues JWT tokens for client authentication: + + +```javascript +import jwt from 'jsonwebtoken' +import { NextResponse } from 'next/server' + +export async function GET(req) { + const apiKey = process.env.ABLY_API_KEY + const [keyName, keySecret] = apiKey.split(':') + + const url = new URL(req.url) + const clientId = url.searchParams.get('clientId') ?? `user-${crypto.randomUUID().slice(0, 8)}` + + const token = jwt.sign( + { + 'x-ably-clientId': clientId, + 'x-ably-capability': JSON.stringify({ '*': ['publish', 'subscribe', 'history'] }), + }, + keySecret, + { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }, + ) + + return new NextResponse(token, { + headers: { 'Content-Type': 'application/jwt' }, + }) +} +``` + + +## Step 2: Create an Ably provider + +Create the file `app/providers.tsx`: + + +```javascript +'use client' + +import { useEffect, useState } from 'react' +import * as Ably from 'ably' +import { AblyProvider } from 'ably/react' + +export function Providers({ clientId, children }) { + const [client, setClient] = useState(null) + + useEffect(() => { + const ably = new Ably.Realtime({ + authCallback: async (_tokenParams, callback) => { + try { + const response = await fetch(`/api/auth/ably-token?clientId=${encodeURIComponent(clientId ?? '')}`) + const jwt = await response.text() + callback(null, jwt) + } catch (err) { + callback(err instanceof Error ? err.message : String(err), null) + } + }, + }) + setClient(ably) + return () => ably.close() + }, [clientId]) + + if (!client) return null + return {children} +} +``` + + +## Step 3: Create the server API route + +Create the file `app/api/chat/route.ts`. The server creates a turn, publishes the user message, streams the LLM response through the Ably channel, and returns immediately: + + +```javascript +import { after } from 'next/server' +import { streamText, convertToModelMessages } from 'ai' +import { anthropic } from '@ai-sdk/anthropic' +import Ably from 'ably' +import { createServerTransport } from '@ably/ai-transport/vercel' + +const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY }) + +export async function POST(req) { + const { messages, history, id, turnId, clientId, forkOf, parent } = await req.json() + const channel = ably.channels.get(id) + + const transport = createServerTransport({ channel }) + const turn = transport.newTurn({ turnId, clientId, parent, forkOf }) + + await turn.start() + + if (messages.length > 0) { + await turn.addMessages(messages, { clientId }) + } + + const allMessages = [...(history ?? []).map((h) => h.message), ...messages.map((m) => m.message)] + + const result = streamText({ + model: anthropic('claude-sonnet-4-20250514'), + system: 'You are a helpful assistant.', + messages: await convertToModelMessages(allMessages), + abortSignal: turn.abortSignal, + }) + + after(async () => { + const { reason } = await turn.streamResponse(result.toUIMessageStream()) + await turn.end(reason) + transport.close() + }) + + return new Response(null, { status: 200 }) +} +``` + + +## Step 4: Create the chat component + +Create the file `app/chat.tsx`. This uses AI Transport's generic hooks directly rather than Vercel's `useChat`. The `useView` hook provides the visible messages, a `send` function, and pagination: + + +```javascript +'use client' + +import { useChannel } from 'ably/react' +import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react' +import { UIMessageCodec } from '@ably/ai-transport/vercel' +import { useState } from 'react' + +export function Chat({ chatId, clientId }) { + const { channel } = useChannel({ channelName: chatId }) + const [input, setInput] = useState('') + + const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) + const { messages, send, hasOlder, loadOlder } = useView(transport, { limit: 30 }) + const activeTurns = useActiveTurns(transport) + const isStreaming = activeTurns.size > 0 + + const handleSubmit = async (e) => { + e.preventDefault() + if (!input.trim()) return + const text = input + setInput('') + await send({ + id: crypto.randomUUID(), + role: 'user', + parts: [{ type: 'text', text }], + }) + } + + return ( +
+ {hasOlder && ( + + )} + {messages.map((msg) => ( +
+ {msg.role}:{' '} + {msg.parts.map((part, i) => + part.type === 'text' ? {part.text} : null + )} +
+ ))} +
+ setInput(e.target.value)} placeholder="Type a message..." /> + {isStreaming ? ( + + ) : ( + + )} +
+
+ ) +} +``` +
+ +## Step 5: Wire it together
+ +Create the file `app/page.tsx`: + + +```javascript +import { Providers } from './providers' +import { Chat } from './chat' + +export default function Page() { + const chatId = 'my-chat-session' + return ( + + + + ) +} +``` + + +Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the same URL to see both tabs share the same durable session. + +## What's happening + +1. The client sends the user message via HTTP POST to your API route. +2. The server publishes the message to the Ably channel, invokes the LLM, and streams tokens to the channel. +3. Every client subscribed to the channel receives tokens in realtime. +4. If a client disconnects, it automatically reconnects and resumes from where it left off. + +The `useView` hook subscribes to the conversation tree and returns the visible messages along the currently selected branch. Unlike `useChat`, you have direct access to the tree structure: branching, pagination, and sibling navigation. + +## Explore next @@ -252,9 +249,44 @@ Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the sam This is a [durable session](/docs/ai-transport/why#durable-sessions). The HTTP request triggers the agent, but all communication flows through the Ably channel. -## What to explore next + +Vercel AI SDK handles intelligence and UI. AI Transport handles what happens between the model and every device. + +| Vercel AI SDK | Ably AI Transport | +| --- | --- | +| Model orchestration (`streamText`, providers) | Durable sessions (Ably channels) | +| UI state (`useChat`, message management) | Multi-device fan-out | +| Tool calls and structured output | Reconnection and recovery | +| Streaming via HTTP/SSE | Active turn tracking | +| `ChatTransport` interface | Bidirectional control | +| | Ordering and persistence | + +Vercel built the `ChatTransport` interface as the extension point for custom transports. AI Transport implements `ChatTransport`, so you swap the transport layer without changing your application code: + + +```javascript +// Before: default HTTP transport +const { messages } = useChat() + +// After: Ably transport (everything else stays the same) +const transport = useChatTransport({ channel }) +const { messages } = useChat({ transport }) +``` + + +### Choose an integration path + +Both paths use the same server code. The difference is client-side only. + +The `useChat` path is the simplest. `useChatTransport` wraps the core transport for direct use with Vercel's `useChat` hook. `useMessageSync` pushes other clients' messages into `useChat` state. You get Vercel's message management with AI Transport's durable delivery. Use this path when you want the standard Vercel `useChat` developer experience with durable sessions added. This is the path the tutorial above follows. + +The generic hooks path uses AI Transport's React hooks (`useView`, `useSend`, `useRegenerate`, `useEdit`) directly. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. Use this path when you need branching UI, custom message rendering, or direct control over the conversation tree. + +## Explore next - -A session is an Ably channel. It represents the ongoing conversation between one or more agents and one or more clients, including its full history. Every participant on the channel sees every message: user prompts, agent responses, cancel signals. - -Because a session is a channel rather than a connection, it survives disconnections. If a client drops off, the agent keeps publishing tokens. When the client reconnects, Ably's connection protocol resumes from the last received message. If the client has been offline longer, it loads the conversation from channel history. - -Multiple devices can join the same session. Open a second tab, switch to your phone, hand the session to a colleague. They all see the same conversation in real time. - - -```javascript -const channel = ably.channels.get('my-session') -const transport = useClientTransport({ channel, codec: UIMessageCodec }) -``` - - -## Turns - -Each user prompt to the agent is wrapped as a turn. A turn contains the user's message and the agent's response, with clear start and end boundaries. - -Without turns, you'd have a flat stream of tokens with no structure. You couldn't cancel one response without killing the connection or distinguish between two concurrent responses. Turns group tokens into units you can track, cancel, and replay individually. - -### Server-side lifecycle - -On the server, you control the turn lifecycle explicitly: - -1. `transport.newTurn({ turnId, clientId })` creates the turn. -2. `turn.addMessages(messages)` publishes the user's input to the channel. -3. `turn.streamResponse(stream)` pipes LLM tokens through the codec to the channel. -4. `turn.end(reason)` marks the turn as complete. - -A turn ends with one of three reasons: `'complete'`, `'cancelled'`, or `'error'`. - -### Client-side lifecycle - -On the client, turns are implicit. Calling `view.send(messages)` creates a turn and returns an `ActiveTurn`: - - -```javascript -const turn = await view.send([userMessage]) -turn.stream // ReadableStream of decoded events -turn.cancel() // Cancel this turn only -``` - - -### Concurrent turns - -Multiple turns can be active at the same time, each with independent streams and cancel handles. See [concurrent turns](/docs/ai-transport/features/concurrent-turns) for details. - -### Cancellation - -Cancel signals are scoped to specific turns. Filters can target a single turn by ID, all turns from a specific client, your own turns, or all turns. The server can authorize or reject cancel requests via the `onCancel` hook. See [cancellation](/docs/ai-transport/features/cancellation) for details. - -## What to read next - -The server transport publishes to an Ably channel. It manages turns and listens for control signals (like cancel requests) from clients. - -The data flow for a single turn is: - -1. The client sends an HTTP POST with the user's message. -2. The server creates a turn and publishes the user message to the channel. -3. The server invokes the LLM and pipes the token stream through the codec to the channel. -4. The server ends the turn. - -The HTTP response is immediate (status 200, empty body). The response stream is decoupled from the HTTP request. Tokens flow through the Ably channel, not through the HTTP response. - - -```javascript -const transport = createServerTransport({ channel, codec: UIMessageCodec }) -const turn = transport.newTurn({ turnId, clientId }) - -await turn.start() -await turn.addMessages(messages, { clientId }) -const { reason } = await turn.streamResponse(llmStream) -await turn.end(reason) -``` - - -## Client transport - -The client transport subscribes to the Ably channel and builds a conversation tree from incoming messages. It provides a `View`, a paginated, branch-aware projection of the conversation. - -The client transport manages the following: - -- Subscribes to the channel before attaching so no messages are missed. -- Decodes Ably messages through the codec into domain events. -- Builds a conversation tree with branching support. -- Provides views for pagination and branch navigation. -- Tracks active turns across all clients. -- Handles optimistic insertion of user messages before server confirmation. - - -```javascript -const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) -const { nodes, send, hasOlder, loadOlder } = useView(transport) -``` - - -## The codec - -The codec is the bridge between your AI framework and Ably messages. It has four responsibilities: - -1. **Encoder.** Converts domain events (like LLM tokens) into Ably publish operations (create, append, update). -2. **Decoder.** Converts inbound Ably messages back into domain events. -3. **Accumulator.** Builds complete messages from a stream of events, used for history and multi-client sync. -4. **Terminal detection.** Identifies events that end a stream, such as finish, error, or abort. - -The SDK ships `UIMessageCodec` for the Vercel AI SDK, which maps `UIMessageChunk` events to `UIMessage` messages. - -For other frameworks, implement the `Codec` interface: - - -```javascript -interface Codec { - createEncoder(channel, options?): StreamEncoder - createDecoder(): StreamDecoder - createAccumulator(): MessageAccumulator - isTerminal(event: TEvent): boolean -} -``` - - -See the [Codec API reference](/docs/ai-transport/api-reference/codec) for the full interface. - -## Entry points - -| Entry point | Import path | Use when | -| --- | --- | --- | -| Core | `@ably/ai-transport` | Building with a custom codec or non-React client | -| React | `@ably/ai-transport/react` | Building a React UI with any codec | -| Vercel | `@ably/ai-transport/vercel` | Server-side Vercel AI SDK integration | -| Vercel React | `@ably/ai-transport/vercel/react` | Client-side Vercel AI SDK with `useChat` | - -The Vercel AI SDK has the deepest integration. The `@ably/ai-transport/vercel/react` entry point provides `useChatTransport`, which wraps the core transport for direct use with Vercel's `useChat` hook. Other frameworks use the core entry point with a custom or framework-specific codec. - -## What to read next -Token streaming is how LLMs progressively deliver responses to users, token by token, minimizing perceived latency. AI Transport makes these streams durable and persistent. They survive tab changes, page refreshes, device switches, and temporary network loss. Automatic append rollup batches high-rate token output into fewer published messages, keeping costs efficient without sacrificing realtime delivery. +Token streaming is how LLMs progressively deliver responses to users, token by token, minimizing perceived latency. AI Transport makes these streams durable and persistent. They survive tab changes, page refreshes, device switches, and temporary network loss. The Ably channel delivers each individual token to clients subscribed in realtime and automatically compacts the tokens into full LLM responses so clients do not have to re-stream the entire conversation token-by-token when they reconnect, refresh, or load history. [Read more about token streaming](/docs/ai-transport/features/token-streaming). diff --git a/src/pages/docs/ai-transport/why.mdx b/src/pages/docs/ai-transport/why.mdx index f225c767dc..f53c05ec81 100644 --- a/src/pages/docs/ai-transport/why.mdx +++ b/src/pages/docs/ai-transport/why.mdx @@ -3,10 +3,6 @@ title: "Why AI Transport" meta_description: "Learn why AI Transport is the best way to connect your AI agents to users in realtime, with built-in support for streaming, recovery, and multi-device sessions." --- - - AI Transport exists because the default pattern for interactive AI experiences limits the quality and richness of the interactions you can build, when using direct, streamed HTTP requests from clients and agents. Most AI frameworks support simple client-driven interactions, with streamed responses from the agent via server-sent events (SSE) or similar HTTP streaming. The client's request is handled by an agent instance which pipes tokens in response to the client request. This approach is simple, surprisingly effective for simple interactions, and every framework supports it. However, the simplicity of the pattern is also the source of its limitations. @@ -21,7 +17,7 @@ The operation of a response stream is tied to the health of the underlying conne This happens all the time in practice. For example, when a phone switches from Wi-Fi to cellular, a user refreshes the page, a laptop lid closes mid-response. The LLM continues to generate tokens, but there's no way to deliver them to the client, so there's nowhere for them to go. -The SSE protocol does support a mechanism at the protocol level, for a reconnecting client to specify a position in the stream to resume from. However, this is usually not supported in practice because it would require a significant increase in complexity in the backend. To support resumable streams with SSE, you would need to assign sequence numbers to token events for ordering, buffer those events in an external store, and build a resume handler. This is a big departure from a simple, stateless request handler. Even having done that, you have only addressed a part of the problem; the solution would not support continuity of streams after a page refresh because that's not supported by SSE. +SSE is the default streaming transport for most AI frameworks. The SSE protocol does support a mechanism at the protocol level, for a reconnecting client to specify a position in the stream to resume from. However, this is usually not supported in practice because it would require a significant increase in complexity in the backend. To support resumable streams with SSE, you would need to assign sequence numbers to token events for ordering, buffer those events in an external store, and build a resume handler. This is a big departure from a simple, stateless request handler. Even having done that, you have only addressed a part of the problem; the solution would not support continuity of streams after a page refresh because that's not supported by SSE. ### Sessions don't span devices @@ -47,12 +43,11 @@ These problems all stem from the coupling between client-to-agent interaction an The pattern that engineering teams are adopting to solve these problems is to break that coupling, through the idea of a durable session: a shared, persistent medium through which clients and agents interact, instead of an exclusive pipe between one client and one agent. -Using a durable session: +A durable session provides three capabilities that direct HTTP streaming does not: -- The agent writes events to the session. -- Clients independently connect to the session. -- The session persists across connection drops, device switches, and can be resumed at a later time. -- Any participant can publish to the session, enabling bidirectional control. +1. Resilient delivery. Streams survive connection drops, device switches, page refreshes, and process restarts. The client resumes from a known position. The agent continues publishing regardless of client connectivity. No events are lost and no events are duplicated. +2. Continuity across surfaces. The session follows the user, not the connection. Open a second tab, switch to a phone, come back hours later. Every surface sees the same session state. Any client with the session identifier can attach and hydrate. +3. Live control. Any participant can communicate with any other participant through the session while work is in progress. Cancel a generation from a different device. Steer an agent mid-response. Send a follow-up before the current response finishes. This requires bidirectional communication that is not coupled to the original request. This changes what's possible: @@ -75,10 +70,12 @@ Ably AI Transport implements durable sessions on top of [Ably channels](/docs/ch - Any participant can publish to the channel. Cancel, steer, interrupt can all happen through the same session. - Multiple participants subscribe to the same channel, and every participant sees every event. -In addition to these channel properties, the AI Transport SDK adds: +The core idea is that no participant is special. A client that drops and reconnects, a serverless agent that spins up for one turn and terminates, a second client joining from another device, an orchestrator agent delegating to sub-agents: all interact with the same session on equal terms. The session persists independently of any participant's connection lifecycle. + +The AI Transport SDK provides the abstractions that make this model practical: -- Turns that structure prompt-response cycles with clear boundaries, concurrent lifecycles, and scoped cancellation. -- A codec layer that maps between your AI framework's event types and Ably messages. -- A conversation tree that supports branching, edit, regenerate, and history navigation. +- A [codec layer](/docs/ai-transport/concepts/transport#codec) that bridges domain-specific message models (Vercel AI SDK's UIMessage, or any other) and Ably's native message primitives, including support for streamed token-by-token delivery. +- A [session layer](/docs/ai-transport/concepts/sessions) that materialises conversation state from the channel (or from an external store) into a branching [conversation tree](/docs/ai-transport/concepts/messages-and-conversation-tree) with views for pagination and branch navigation. +- A [transport layer](/docs/ai-transport/concepts/transport) that handles communication mechanics: publishing messages, routing streams, managing [turn lifecycle](/docs/ai-transport/concepts/turns), and delivering cancel signals. - React hooks for building UIs with streaming, pagination, and branch navigation. - Adapters that drop into various frameworks; for example AI Transport can be used with Vercel AI SDK's `useChat` with one line of code. From 5424207a7544e852b6b91921ae9bad83741c9726 Mon Sep 17 00:00:00 2001 From: zak Date: Fri, 17 Apr 2026 11:38:52 +0100 Subject: [PATCH 2/4] Add Vercel AI SDK framework guide Restore the Framework guides nav section with a comprehensive Vercel AI SDK guide that explains how the two SDKs integrate. The guide covers Vercel concepts (streamText, UIMessage, useChat, ChatTransport, tool calling), default transport limitations, what AI Transport adds (codec, useChatTransport, useMessageSync, server piping), how the layers fit together, integration paths, and what capabilities the combination unlocks. Replace the responsibilities table in the getting-started page with a link to the framework guide, and update cross-references between the two pages. --- src/data/nav/aitransport.ts | 9 ++ .../framework-guides/vercel-ai-sdk.mdx | 145 ++++++++++++++++++ .../getting-started/vercel-ai-sdk.mdx | 16 +- 3 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts index fcf001f514..58d45a2e30 100644 --- a/src/data/nav/aitransport.ts +++ b/src/data/nav/aitransport.ts @@ -55,6 +55,15 @@ export default { }, ], }, + { + name: 'Framework guides', + pages: [ + { + name: 'Vercel AI SDK', + link: '/docs/ai-transport/framework-guides/vercel-ai-sdk', + }, + ], + }, { name: 'Features', pages: [ diff --git a/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx b/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx new file mode 100644 index 0000000000..b50d8a58ae --- /dev/null +++ b/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx @@ -0,0 +1,145 @@ +--- +title: "Vercel AI SDK" +meta_description: "Understand how Ably AI Transport integrates with the Vercel AI SDK to add durable sessions, multi-device sync, and bidirectional control to your chat application." +meta_keywords: "AI Transport, Vercel AI SDK, useChat, ChatTransport, streamText, UIMessage, durable sessions, streaming, realtime" +--- + +Ably AI Transport integrates with the Vercel AI SDK to add durable sessions, multi-device sync, and bidirectional control to your chat application. This guide explains what each SDK does, how they connect, and what capabilities the combination unlocks. + +Ready to build? [Get started with Vercel AI SDK](/docs/ai-transport/getting-started/vercel-ai-sdk). + +## Understand the Vercel AI SDK + +The Vercel AI SDK is a toolkit for building AI-powered applications. It handles model interaction, streaming, and UI state management. The following concepts are the ones you need to understand for the AI Transport integration. + +### The provider system + +The AI SDK abstracts model providers behind a unified interface. You call `streamText()` with any provider (Anthropic, OpenAI, Google) and get the same API. Switching models is a one-line change. The provider handles the specifics of each model's API, authentication, and capabilities. + +### streamText + +[`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) is the core function for streaming AI responses. You pass it a model, a system prompt, and the conversation messages. It calls the model and returns a stream of events as the model generates its response token by token. The stream includes text deltas, tool call inputs, tool results, reasoning content, and lifecycle events (start, finish, error). On the server, `streamText()` is where the AI model interaction happens. + +### UIMessage and parts + +[`UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message) is Vercel's message model. Each message has a role (`user`, `assistant`, `system`) and an array of parts. Parts represent different types of content within a single message: text, reasoning, tool calls, tool results, files, and sources. Each part tracks its own streaming state (`streaming` or `done`), so the UI can show partial content as it arrives. + +A `UIMessageChunk` is one streaming event that contributes to a `UIMessage`. As the model generates a response, it emits a series of chunks (text-start, text-delta, text-end, tool-input-start, finish, and so on). The client accumulates these chunks into complete `UIMessage` objects with fully populated parts. + +### useChat + +[`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) is the main React hook for building chat UIs. It manages the array of `UIMessage` objects, sends messages to the server via a transport, handles streaming updates as chunks arrive, and tracks the conversation status (submitted, streaming, ready, error). It provides helpers for common operations: sending messages, regenerating responses, stopping a stream, and submitting tool results. + +### ChatTransport + +[`ChatTransport`](https://ai-sdk.dev/docs/ai-sdk-ui/transport) is the interface that `useChat` calls to send and receive messages. It defines two methods: `sendMessages` (submit messages and receive a stream of chunks back) and `reconnectToStream` (resume an interrupted stream). + +The default implementation sends an HTTP POST to your server endpoint and reads back a server-sent events (SSE) stream. This is where AI Transport plugs in: it provides an alternative `ChatTransport` implementation that routes messages through an Ably channel instead of a direct HTTP stream. + +### Tool calling + +Models can invoke tools that you define with a schema and an execute function. The model decides when to call a tool and generates the input parameters. The SDK executes the tool and feeds the result back to the model, which can then continue generating its response. Tool calls can require human approval before execution, creating approval gates in the conversation. + +## Understand the default transport and its limitations + +Without AI Transport, the message flow in a Vercel AI SDK application works as follows: + +1. `useChat` uses the default transport, which sends an HTTP POST to your server endpoint. +2. The server calls `streamText()`, which returns a stream of `UIMessageChunk` events. +3. The server converts this to an SSE response using [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response). +4. The client reads the SSE stream and reassembles the chunks into full `UIMessage` objects. + +This is a direct, point-to-point HTTP connection. The stream is coupled to the connection. This works for simple interactions, but creates limitations in production: + +- Streams die on disconnection. When a phone switches from Wi-Fi to cellular, a user refreshes the page, or a laptop lid closes mid-response, the stream fails. The model continues generating tokens, but there is no way to deliver them. +- Sessions do not span devices. The connection is exclusively between the requesting client and the server. A second tab or a phone cannot access the same stream. +- Clients cannot signal the agent. SSE is one-way: server to client. The only way for the client to communicate is to close the connection, which kills the stream. Cancel and resume are mutually exclusive. +- No persistence beyond the connection. When the connection ends, the stream is gone. There is no way to replay what happened or resume from where it left off. + +## Understand what AI Transport adds + +AI Transport implements the `ChatTransport` interface and is a drop-in replacement for the default HTTP transport. You use it with `useChat` without changing your application code: + + +```javascript +// Before: default HTTP transport +const { messages } = useChat() + +// After: Ably transport (codec is pre-bound) +const chatTransport = useChatTransport({ channel, clientId }) +const { messages } = useChat({ transport: chatTransport }) +``` + + +Instead of SSE streaming between client and server, tokens flow through an Ably channel. The HTTP request triggers the server, but the response is decoupled from it. + +The integration has four parts: + +1. `useChatTransport` uses a `UIMessageCodec` under the hood to encode Vercel's `UIMessageChunk` events to Ably messages. Every chunk type (text-delta, tool-input, finish, and others) maps to an Ably message with headers to track the metadata. The codec handles encoding on the server, decoding on the client, and reassembling the chunks into complete `UIMessage` objects. +2. `useChatTransport` is a wrapper that converts the Ably Core SDK [`ClientTransport`](/docs/ai-transport/api-reference/client-transport) object into the `ChatTransport` interface for use with `useChat`. +3. `useMessageSync` subscribes to the transport's conversation tree and pushes updates into `useChat`'s `setMessages`. This keeps Vercel's local state in sync with the authoritative state on the channel. This is required for features like multi-device sync and conversation branching, and is how the Ably SDK brings those features to Vercel's `useChat` without `useChat` natively supporting them. +4. On the server, `turn.streamResponse(result.toUIMessageStream())` pipes the model's output through the codec encoder to the Ably channel. The HTTP response returns immediately (status 200, empty body). The tokens are delivered to all connected clients through the channel, not through the HTTP response. + +## See how they fit together + +The architecture stacks four layers: + +1. Vercel AI SDK provides `useChat()`, `streamText()`, tool calls, and UI state management. +2. The `ChatTransport` interface is the plug-in point that Vercel designed for custom transports. +3. AI Transport implements `ChatTransport` and adds sessions, presence, recovery, and control. +4. Ably infrastructure provides the global edge network, ordering, and persistence. + +Vercel AI SDK provides: + +- Model orchestration (`streamText`, providers) +- UI state management (`useChat`, message arrays, status tracking) +- Tool calls and structured output +- The `ChatTransport` interface as the extension point + +Ably AI Transport provides: + +- Durable sessions on Ably channels +- Multi-device fan-out +- Reconnection and recovery +- Active turn tracking +- Bidirectional control (cancel, steer, interrupt) +- Ordering and persistence +- History and replay +- Token compaction + +## Choose an integration path + +Both paths use the same server code. The difference is client-side only. + +### Use the Vercel useChat path + +The simplest path. `useChatTransport` wraps the core transport for direct use with Vercel's `useChat` hook. `useMessageSync` pushes other clients' messages into `useChat` state. You get Vercel's message management with AI Transport's durable delivery. + +Use this path when you want the standard Vercel `useChat` developer experience with durable sessions added. The [Vercel AI SDK getting started guide](/docs/ai-transport/getting-started/vercel-ai-sdk) follows this path. + +### Use the Core SDK path + +Use AI Transport's React hooks (`useView`, `useSend`, `useRegenerate`, `useEdit`) directly instead of `useChat`. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. + +Use this path when you need branching UI, custom message rendering, or direct control over the conversation tree. The [Core SDK getting started guide](/docs/ai-transport/getting-started/core-sdk) follows this path. + +## Discover what this unlocks + +With AI Transport, your Vercel AI SDK application gains capabilities that are not possible with the default HTTP transport: + +- Streams survive disconnection. The client reconnects and resumes from where it left off. The agent continues publishing regardless of client connectivity. +- Multi-device sync. The same conversation is accessible on phone, laptop, and tablet, all in realtime. Any device that subscribes to the session sees every message. +- Conversation branching. Edit and regenerate create forks in a conversation tree, not destructive replacements. The full history of every branch is preserved. +- Bidirectional control. Cancel, interrupt, and steer agents mid-stream through the same session. No separate control channel required. +- Approval gates reach the user on any device, even after reconnecting. A pending tool approval persists on the session until someone acts on it. +- Concurrent turns with independent cancel handles. Multiple requests can stream simultaneously on the same session. +- Agent presence. Real-time visibility into agent status: thinking, streaming, idle, or offline. +- Push notifications for completed background tasks. Reach users who have left the app. +- History and replay. Load the full conversation on reconnect, page refresh, or new device join. + +## Read next -Vercel AI SDK handles intelligence and UI. AI Transport handles what happens between the model and every device. - -| Vercel AI SDK | Ably AI Transport | -| --- | --- | -| Model orchestration (`streamText`, providers) | Durable sessions (Ably channels) | -| UI state (`useChat`, message management) | Multi-device fan-out | -| Tool calls and structured output | Reconnection and recovery | -| Streaming via HTTP/SSE | Active turn tracking | -| `ChatTransport` interface | Bidirectional control | -| | Ordering and persistence | +Vercel AI SDK handles intelligence and UI. AI Transport handles what happens between the model and every device. See the [Vercel AI SDK framework guide](/docs/ai-transport/framework-guides/vercel-ai-sdk) for a deeper explanation of how the two SDKs fit together. Vercel built the `ChatTransport` interface as the extension point for custom transports. AI Transport implements `ChatTransport`, so you swap the transport layer without changing your application code: @@ -281,12 +271,12 @@ Both paths use the same server code. The difference is client-side only. The `useChat` path is the simplest. `useChatTransport` wraps the core transport for direct use with Vercel's `useChat` hook. `useMessageSync` pushes other clients' messages into `useChat` state. You get Vercel's message management with AI Transport's durable delivery. Use this path when you want the standard Vercel `useChat` developer experience with durable sessions added. This is the path the tutorial above follows. -The generic hooks path uses AI Transport's React hooks (`useView`, `useSend`, `useRegenerate`, `useEdit`) directly. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. Use this path when you need branching UI, custom message rendering, or direct control over the conversation tree. +The [Core SDK](/docs/ai-transport/getting-started/core-sdk) path uses AI Transport's React hooks (`useView`, `useSend`, `useRegenerate`, `useEdit`) directly instead of `useChat`. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. Use this path when you need branching UI, custom message rendering, or direct control over the conversation tree. ## Explore next @@ -351,7 +352,7 @@ A structured event describing a turn starting or ending. ```javascript type TurnLifecycleEvent = - | { type: 'x-ably-turn-start'; turnId: string; clientId: string } + | { type: 'x-ably-turn-start'; turnId: string; clientId: string; parent?: string; forkOf?: string } | { type: 'x-ably-turn-end'; turnId: string; clientId: string; reason: TurnEndReason } ``` diff --git a/src/pages/docs/ai-transport/api-reference/react-hooks.mdx b/src/pages/docs/ai-transport/api-reference/react-hooks.mdx index 34dfb13975..692837c0fb 100644 --- a/src/pages/docs/ai-transport/api-reference/react-hooks.mdx +++ b/src/pages/docs/ai-transport/api-reference/react-hooks.mdx @@ -32,36 +32,45 @@ import { ### useClientTransport -Create and memoize a `ClientTransport` instance. The transport is created on the first render and the same instance is returned on subsequent renders. The hook does not auto-close the transport on unmount; channel lifecycle is managed by the Ably provider. Call `transport.close()` explicitly if you need to tear down the transport independently of the channel lifecycle. +Access a `ClientTransport` from the nearest `TransportProvider`. The transport is created by the provider, not by this hook. When `channelName` is omitted, the innermost `TransportProvider` in the tree is used. ```javascript -function useClientTransport(options: ClientTransportOptions): ClientTransport +function useClientTransport(options?: { + channelName?: string, + skip?: boolean +}): ClientTransport ``` -See [ClientTransportOptions](/docs/ai-transport/api-reference/client-transport#client-transport-options) for available options. +| Property | Type | Description | +| --- | --- | --- | +| channelName | `string` | The channel name passed to the enclosing `TransportProvider`. Omit to use the nearest provider. | +| skip | `boolean` | When `true`, return a stub transport that throws on any access. Useful when auth is not yet resolved. | + +See [TransportProvider](/docs/ai-transport/api-reference/react-hooks#transport-provider) for setting up the provider. ### useView -Subscribe to a view and return the visible node list with pagination, navigation, and write operations. Accepts either a `ClientTransport` (uses its default view), a `View` directly, or `null`/`undefined`. Returns a `ViewHandle` that re-renders the component when the view updates. +Subscribe to a view and return the visible node list with pagination, navigation, and write operations. Pass `transport` to use a transport's default view, or `view` to subscribe to a specific view directly. When both are omitted, defaults to the nearest `TransportProvider`'s transport. Returns a `ViewHandle` that re-renders the component when the view updates. ```javascript -function useView( - source: ClientTransport | View | null | undefined, - options?: UseViewOptions -): ViewHandle +function useView(options?: { + transport?: ClientTransport | null, + view?: View | null, + limit?: number, + skip?: boolean +}): ViewHandle ``` -#### UseViewOptions - | Property | Type | Description | | --- | --- | --- | -| limit | `number` | Maximum number of older messages to load per page. Defaults to 100. | - -When `options` are provided, the hook auto-loads the first page on mount. +| transport | `ClientTransport \| null` | Client transport whose default view to subscribe to. Defaults to the nearest provider when omitted. | +| view | `View \| null` | A specific view to subscribe to directly. Takes priority over `transport`. | +| limit | `number` | Maximum number of older messages to load per page. When provided, auto-loads the first page on mount. | +| skip | `boolean` | When `true`, skip all subscriptions and return an empty handle. | #### ViewHandle @@ -88,7 +97,11 @@ Create an additional view of the conversation tree. Returns a `ViewHandle` with ```javascript -function useCreateView(transport: ClientTransport): ViewHandle +function useCreateView(options?: { + transport?: ClientTransport | null, + limit?: number, + skip?: boolean +}): ViewHandle ``` @@ -124,11 +137,13 @@ function useRegenerate(view: View): (messageId: string, options?: SendOptions) = ### useTree -Provide stable structural query callbacks backed by the transport's conversation tree. These are thin `useCallback` wrappers around the tree -- no local state or subscriptions. The hook does not re-render on tree changes; use `useView` for reactive updates. +Provide stable structural query callbacks backed by the transport's conversation tree. These are thin `useCallback` wrappers around the tree. The hook does not re-render on tree changes; use `useView` for reactive updates. ```javascript -function useTree(transport: ClientTransport): TreeHandle +function useTree(options?: { + transport?: ClientTransport +}): TreeHandle ``` @@ -146,7 +161,9 @@ Subscribe to active turns on the channel. Returns a reactive `Map` keyed by clie ```javascript -function useActiveTurns(transport: ClientTransport): Map> +function useActiveTurns(options?: { + transport?: ClientTransport | null +}): Map> ``` @@ -158,7 +175,10 @@ Subscribe to raw Ably messages on the transport's channel. Useful for building c ```javascript -function useAblyMessages(transport: ClientTransport): Ably.Message[] +function useAblyMessages(options?: { + transport?: ClientTransport, + skip?: boolean +}): Ably.InboundMessage[] ``` @@ -185,26 +205,17 @@ function useChatTransport(transport: ClientTransport, chatOptions?: ChatTranspor When you pass `VercelClientTransportOptions`, the hook creates a new `ClientTransport` internally with the codec pre-bound to `UIMessageCodec`. When you pass an existing `ClientTransport`, it wraps that instance as a `ChatTransport` without creating a new transport. -**Using options (creates transport internally):** - - -```javascript -const transport = useChatTransport({ channel }) -const { messages } = useChat({ transport }) -``` - - -**Using an existing ClientTransport:** +**Using an existing ClientTransport (recommended when you also need `useView`, `useActiveTurns`, or `useMessageSync`):** ```javascript -const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) +const transport = useClientTransport({ channelName: chatId }) const chatTransport = useChatTransport(transport) const { messages } = useChat({ transport: chatTransport }) ``` -Use the second form when you need direct access to the `ClientTransport` for features like active turn tracking or cancellation. See [ChatTransportOptions](/docs/ai-transport/api-reference/vercel#chat-transport-options) for available options. +See [ChatTransportOptions](/docs/ai-transport/api-reference/vercel#chat-transport-options) for available options. ### useMessageSync @@ -221,7 +232,7 @@ function useMessageSync( ```javascript -const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) +const transport = useClientTransport({ channelName: chatId }) const chatTransport = useChatTransport(transport) const { messages, setMessages } = useChat({ transport: chatTransport }) useMessageSync(transport, setMessages) diff --git a/src/pages/docs/ai-transport/api-reference/server-transport.mdx b/src/pages/docs/ai-transport/api-reference/server-transport.mdx index 306ff8775d..7a5fa06bd3 100644 --- a/src/pages/docs/ai-transport/api-reference/server-transport.mdx +++ b/src/pages/docs/ai-transport/api-reference/server-transport.mdx @@ -75,6 +75,7 @@ function close(): void | onAbort | optional | `(write: (event: TEvent) => Promise) => void \| Promise` | Callback fired when the turn is aborted. Receives a `write` function to publish final events before the abort finalizes. | | onMessage | optional | `(message: Ably.Message) => void` | Hook called before each Ably message is published in this turn. Mutate the message in place to add custom headers. | | onError | optional | `(error: ErrorInfo) => void` | Callback for errors during this turn. | +| signal | optional | `AbortSignal` | An external abort signal (typically the HTTP request's `req.signal`) that aborts this turn when fired. This allows platform-level cancellation (request cancellation, serverless function timeout) to stop LLM generation gracefully. | ## Turn diff --git a/src/pages/docs/ai-transport/api-reference/vercel.mdx b/src/pages/docs/ai-transport/api-reference/vercel.mdx index 3ad29a79c5..a671047fa4 100644 --- a/src/pages/docs/ai-transport/api-reference/vercel.mdx +++ b/src/pages/docs/ai-transport/api-reference/vercel.mdx @@ -95,34 +95,25 @@ In most cases you will use the `useChatTransport` hook instead of calling this f ## useChatTransport -React hook that creates a `ChatTransport` for use with Vercel's `useChat`. Accepts either a `ChatTransportOptions` object or an existing `ClientTransport` instance. +React hook that creates a `ChatTransport` for use with Vercel's `useChat`. Accepts either a `VercelClientTransportOptions` object (creates a transport internally) or an existing `ClientTransport` instance. Both forms accept an optional second argument for `ChatTransportOptions`. ```javascript import { useChatTransport } from '@ably/ai-transport/vercel/react' -function useChatTransport(options: ChatTransportOptions): ChatTransport -function useChatTransport(transport: ClientTransport): ChatTransport +function useChatTransport(options: VercelClientTransportOptions, chatOptions?: ChatTransportOptions): ChatTransport +function useChatTransport(transport: ClientTransport, chatOptions?: ChatTransportOptions): ChatTransport ``` -When you pass `ChatTransportOptions`, the hook creates a `ClientTransport` internally with the codec pre-bound to `UIMessageCodec`. When you pass an existing `ClientTransport`, it wraps that instance as a `ChatTransport` without creating a new transport. Use the second form when you need direct access to the `ClientTransport` for features like active turn tracking or cancellation. +When you pass `VercelClientTransportOptions`, the hook creates a `ClientTransport` internally with the codec pre-bound to `UIMessageCodec`. When you pass an existing `ClientTransport`, it wraps that instance as a `ChatTransport` without creating a new transport. Use the second form when you need direct access to the `ClientTransport` for features like active turn tracking, `useMessageSync`, or cancellation. See the [React hooks reference](/docs/ai-transport/api-reference/react-hooks#use-chat-transport) for full usage examples. ## ChatTransportOptions -Options for `useChatTransport` (from `@ably/ai-transport/vercel/react`). +Optional second argument to `useChatTransport` and `createChatTransport` for customizing request behavior. | Property | Required | Type | Description | | --- | --- | --- | --- | -| channel | required | `Ably.RealtimeChannel` | The Ably channel for the session. | -| clientId | optional | `string` | The client ID for this transport instance. | -| api | optional | `string` | URL of the API endpoint. | -| headers | optional | `Record` | Additional HTTP headers. | -| body | optional | `Record` | Additional request body fields. | -| credentials | optional | `RequestCredentials` | Credentials mode for fetch. | -| fetch | optional | `typeof fetch` | Custom fetch implementation. | -| messages | optional | `UIMessage[]` | Pre-loaded messages for history. | -| logger | optional | `Logger` | Logger instance. | | prepareSendMessagesRequest | optional | `(context: SendMessagesRequestContext) => { body?: Record; headers?: Record }` | Hook to customize the HTTP POST body and headers before sending to the API. Receives conversation context including history, new messages, and trigger type. | diff --git a/src/pages/docs/ai-transport/concepts/authentication.mdx b/src/pages/docs/ai-transport/concepts/authentication.mdx index a07921f69d..9c48881214 100644 --- a/src/pages/docs/ai-transport/concepts/authentication.mdx +++ b/src/pages/docs/ai-transport/concepts/authentication.mdx @@ -72,8 +72,9 @@ const channel = realtimeClient.channels.get('your-conversation'); import { useEffect, useState } from 'react'; import * as Ably from 'ably'; -import { AblyProvider, useChannel } from 'ably/react'; -import { useClientTransport } from '@ably/ai-transport/react'; +import { AblyProvider } from 'ably/react'; +import { TransportProvider, useClientTransport } from '@ably/ai-transport/react'; +import { UIMessageCodec } from '@ably/ai-transport/vercel'; // 1. Create an authenticated Ably client and wrap your app export function Providers({ children }) { @@ -98,10 +99,17 @@ export function Providers({ children }) { return {children}; } -// 2. Inside the provider, get a channel and pass it to the transport +// 2. Wrap with TransportProvider, then access the transport inside +function App({ conversationId }) { + return ( + + + + ); +} + function Chat({ conversationId }) { - const { channel } = useChannel({ channelName: conversationId }); - const transport = useClientTransport({ channel, codec: yourCodec }); + const transport = useClientTransport({ channelName: conversationId }); // ... } ``` @@ -203,19 +211,21 @@ await realtimeClient.auth.authorize(); ## Server endpoint authentication -When the client sends user messages, it makes an HTTP POST to the server endpoint. The `ClientTransportOptions` provide two mechanisms for authenticating these requests: +When the client sends user messages, it makes an HTTP POST to the server endpoint. `TransportProvider` accepts two props for authenticating these requests: **headers**: Static or dynamic HTTP headers sent with every POST. ```javascript -const transport = useClientTransport({ - channel, - codec: UIMessageCodec, - headers: () => ({ + ({ 'Authorization': `Bearer ${getAuthToken()}`, - }), -}) + })} +> + + ``` @@ -223,11 +233,13 @@ const transport = useClientTransport({ ```javascript -const transport = useClientTransport({ - channel, - codec: UIMessageCodec, - credentials: 'include', -}) + + + ``` diff --git a/src/pages/docs/ai-transport/features/agent-presence.mdx b/src/pages/docs/ai-transport/features/agent-presence.mdx index 5a2efa25da..2e9b4271cc 100644 --- a/src/pages/docs/ai-transport/features/agent-presence.mdx +++ b/src/pages/docs/ai-transport/features/agent-presence.mdx @@ -75,7 +75,7 @@ For richer status indicators, combine presence data with `useActiveTurns`. Prese ```javascript -const activeTurns = useActiveTurns(transport) +const activeTurns = useActiveTurns({ transport }) const agentStatus = useAgentPresence(channel) // your custom hook // Agent is streaming if it has active turns diff --git a/src/pages/docs/ai-transport/features/chain-of-thought.mdx b/src/pages/docs/ai-transport/features/chain-of-thought.mdx index 5c4e5a0168..d738c86f97 100644 --- a/src/pages/docs/ai-transport/features/chain-of-thought.mdx +++ b/src/pages/docs/ai-transport/features/chain-of-thought.mdx @@ -47,7 +47,7 @@ On the client, message nodes include both text and reasoning content. Render the ```javascript -const { nodes } = useView(transport) +const { nodes } = useView({ transport }) // Each node may contain text parts, reasoning parts, or both for (const node of nodes) { diff --git a/src/pages/docs/ai-transport/features/concurrent-turns.mdx b/src/pages/docs/ai-transport/features/concurrent-turns.mdx index 47369c4b50..9d585a8b46 100644 --- a/src/pages/docs/ai-transport/features/concurrent-turns.mdx +++ b/src/pages/docs/ai-transport/features/concurrent-turns.mdx @@ -65,7 +65,7 @@ With HTTP streaming, each request occupies a dedicated connection. Running two t ```javascript -const activeTurns = useActiveTurns(transport) +const activeTurns = useActiveTurns({ transport }) // Map> // Show a spinner next to each client that's streaming diff --git a/src/pages/docs/ai-transport/features/double-texting.mdx b/src/pages/docs/ai-transport/features/double-texting.mdx index 7a37291dfa..1e6a22b9af 100644 --- a/src/pages/docs/ai-transport/features/double-texting.mdx +++ b/src/pages/docs/ai-transport/features/double-texting.mdx @@ -36,7 +36,7 @@ If you want to prevent concurrent turns and instead queue messages, detect activ ```javascript -const activeTurns = useActiveTurns(transport) +const activeTurns = useActiveTurns({ transport }) const handleSend = async (text) => { if (activeTurns.size > 0) { diff --git a/src/pages/docs/ai-transport/features/history.mdx b/src/pages/docs/ai-transport/features/history.mdx index 7e180653a5..0a498815ff 100644 --- a/src/pages/docs/ai-transport/features/history.mdx +++ b/src/pages/docs/ai-transport/features/history.mdx @@ -18,7 +18,7 @@ The client transport loads history using `view.loadOlder()` with `untilAttach` f ```javascript -const { nodes, hasOlder, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loadOlder } = useView({ transport, limit: 30 }) // Load the next page of older messages if (hasOlder) { @@ -33,7 +33,7 @@ The `useView` hook handles history loading on mount: ```javascript -const { nodes, hasOlder, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loadOlder } = useView({ transport, limit: 30 }) ``` @@ -49,7 +49,7 @@ Load more messages as the user scrolls up. The `useView` hook provides everythin ```javascript -const { nodes, hasOlder, loading, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loading, loadOlder } = useView({ transport, limit: 30 }) function handleScrollToTop() { if (hasOlder && !loading) { diff --git a/src/pages/docs/ai-transport/features/human-in-the-loop.mdx b/src/pages/docs/ai-transport/features/human-in-the-loop.mdx index 0a94a67408..931dbb4d47 100644 --- a/src/pages/docs/ai-transport/features/human-in-the-loop.mdx +++ b/src/pages/docs/ai-transport/features/human-in-the-loop.mdx @@ -77,7 +77,7 @@ On the client, detect pending approval requests and present them to the user: ```javascript -const { nodes } = useView(transport) +const { nodes } = useView({ transport }) // Find the node containing a pending approval request const pendingNode = nodes diff --git a/src/pages/docs/ai-transport/features/interruption.mdx b/src/pages/docs/ai-transport/features/interruption.mdx index 0919f6866e..26c72ef238 100644 --- a/src/pages/docs/ai-transport/features/interruption.mdx +++ b/src/pages/docs/ai-transport/features/interruption.mdx @@ -34,9 +34,9 @@ Detect whether a turn is active, cancel it, then send a new message. This is the import { useActiveTurns, useClientTransport, useView } from '@ably/ai-transport/react' function Chat({ channel, clientId }) { - const transport = useClientTransport({ channel, clientId }) - const { nodes, send } = useView(transport) - const activeTurns = useActiveTurns(transport) + const transport = useClientTransport({ channelName: chatId }) + const { nodes, send } = useView({ transport }) + const activeTurns = useActiveTurns({ transport }) const handleSend = async (text) => { // If the agent is streaming, cancel first @@ -80,7 +80,7 @@ The `useActiveTurns` hook returns a `Map>` of all currentl ```javascript -const activeTurns = useActiveTurns(transport) +const activeTurns = useActiveTurns({ transport }) // Check if any turns are streaming const isStreaming = activeTurns.size > 0 diff --git a/src/pages/docs/ai-transport/features/multi-device.mdx b/src/pages/docs/ai-transport/features/multi-device.mdx index 77536ffd07..b5e826f61e 100644 --- a/src/pages/docs/ai-transport/features/multi-device.mdx +++ b/src/pages/docs/ai-transport/features/multi-device.mdx @@ -23,18 +23,10 @@ Minimal code - no special configuration needed: ```javascript // Client A (laptop) -const transport = useClientTransport({ - channel: ably.channels.get('shared-session'), - codec: UIMessageCodec, - clientId: 'user-laptop', -}) +const transport = useClientTransport({ channelName: chatId }) // Client B (phone) - same channel name, different device -const transport = useClientTransport({ - channel: ably.channels.get('shared-session'), - codec: UIMessageCodec, - clientId: 'user-phone', -}) +const transport = useClientTransport({ channelName: chatId }) ``` @@ -59,7 +51,7 @@ Track which clients have active turns using `useActiveTurns`: ```javascript -const activeTurns = useActiveTurns(transport) +const activeTurns = useActiveTurns({ transport }) // Map> // Check if any client is streaming @@ -91,7 +83,7 @@ A client that connects after the conversation has started loads the full history ```javascript -const { nodes, hasOlder, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loadOlder } = useView({ transport, limit: 30 }) ``` diff --git a/src/pages/docs/ai-transport/features/reconnection-and-recovery.mdx b/src/pages/docs/ai-transport/features/reconnection-and-recovery.mdx index 1a8c029d33..431ef12e7d 100644 --- a/src/pages/docs/ai-transport/features/reconnection-and-recovery.mdx +++ b/src/pages/docs/ai-transport/features/reconnection-and-recovery.mdx @@ -57,7 +57,7 @@ The client transport loads conversation history using Ably's `untilAttach` param ```javascript -const { nodes, hasOlder, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loadOlder } = useView({ transport, limit: 30 }) ``` @@ -67,7 +67,7 @@ To load older messages beyond the initial window: ```javascript -const { nodes, hasOlder, loadOlder } = useView(transport, { limit: 30 }) +const { nodes, hasOlder, loadOlder } = useView({ transport, limit: 30 }) // hasOlder indicates whether there are more messages to load if (hasOlder) { diff --git a/src/pages/docs/ai-transport/features/token-streaming.mdx b/src/pages/docs/ai-transport/features/token-streaming.mdx index b7bb0d9a91..5e0e320917 100644 --- a/src/pages/docs/ai-transport/features/token-streaming.mdx +++ b/src/pages/docs/ai-transport/features/token-streaming.mdx @@ -67,7 +67,7 @@ On the client, the view updates as tokens arrive: ```javascript -const { nodes } = useView(transport) +const { nodes } = useView({ transport }) // nodes contains messages with streaming text that updates in real time ``` @@ -121,7 +121,7 @@ With Vercel's `useChat`: ```javascript -const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) +const transport = useClientTransport({ channelName: chatId }) const chatTransport = useChatTransport(transport) const { messages } = useChat({ transport: chatTransport }) ``` @@ -131,7 +131,7 @@ With generic hooks: ```javascript -const { nodes } = useView(transport) +const { nodes } = useView({ transport }) // Each node.message contains the streamed content, updating in real time ``` diff --git a/src/pages/docs/ai-transport/features/tool-calling.mdx b/src/pages/docs/ai-transport/features/tool-calling.mdx index c18c8de4f6..d007e5de18 100644 --- a/src/pages/docs/ai-transport/features/tool-calling.mdx +++ b/src/pages/docs/ai-transport/features/tool-calling.mdx @@ -77,7 +77,7 @@ On the client, detect the pending tool call and submit the result using `view.up ```javascript -const { nodes } = useView(transport) +const { nodes } = useView({ transport }) // Find the node with a pending tool invocation const pendingNode = nodes diff --git a/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx b/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx index b50d8a58ae..cdf9319b9a 100644 --- a/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx +++ b/src/pages/docs/ai-transport/framework-guides/vercel-ai-sdk.mdx @@ -65,8 +65,9 @@ AI Transport implements the `ChatTransport` interface and is a drop-in replaceme // Before: default HTTP transport const { messages } = useChat() -// After: Ably transport (codec is pre-bound) -const chatTransport = useChatTransport({ channel, clientId }) +// After: Ably transport +const transport = useClientTransport({ channelName: chatId }) +const chatTransport = useChatTransport(transport) const { messages } = useChat({ transport: chatTransport }) ``` diff --git a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx index 41a6833794..38ea777a18 100644 --- a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx +++ b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx @@ -157,18 +157,15 @@ Create the file `app/chat.tsx`. This uses AI Transport's generic hooks directly ```javascript 'use client' -import { useChannel } from 'ably/react' import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react' -import { UIMessageCodec } from '@ably/ai-transport/vercel' import { useState } from 'react' -export function Chat({ chatId, clientId }) { - const { channel } = useChannel({ channelName: chatId }) +export function Chat({ chatId }) { const [input, setInput] = useState('') - const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) - const { messages, send, hasOlder, loadOlder } = useView(transport, { limit: 30 }) - const activeTurns = useActiveTurns(transport) + const transport = useClientTransport({ channelName: chatId }) + const { messages, send, hasOlder, loadOlder } = useView({ transport, limit: 30 }) + const activeTurns = useActiveTurns({ transport }) const isStreaming = activeTurns.size > 0 const handleSubmit = async (e) => { @@ -199,7 +196,7 @@ export function Chat({ chatId, clientId }) {
setInput(e.target.value)} placeholder="Type a message..." /> {isStreaming ? ( - + ) : ( )} @@ -217,13 +214,17 @@ Create the file `app/page.tsx`: ```javascript import { Providers } from './providers' +import { TransportProvider } from '@ably/ai-transport/react' +import { UIMessageCodec } from '@ably/ai-transport/vercel' import { Chat } from './chat' export default function Page() { const chatId = 'my-chat-session' return ( - + + + ) } diff --git a/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx b/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx index 682e74e3a9..64fad3488e 100644 --- a/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx +++ b/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx @@ -166,17 +166,14 @@ Create the file `app/chat.tsx`. The client uses Vercel's `useChat` hook with an 'use client' import { useChat } from '@ai-sdk/react' -import { useChannel } from 'ably/react' import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react' import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react' -import { UIMessageCodec } from '@ably/ai-transport/vercel' import { useState } from 'react' -export function Chat({ chatId, clientId }) { - const { channel } = useChannel({ channelName: chatId }) +export function Chat({ chatId }) { const [input, setInput] = useState('') - const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId }) + const transport = useClientTransport({ channelName: chatId }) const chatTransport = useChatTransport(transport) const { messages, setMessages, sendMessage, stop } = useChat({ id: chatId, @@ -184,9 +181,9 @@ export function Chat({ chatId, clientId }) { }) useMessageSync(transport, setMessages) - useView(transport, { limit: 30 }) + useView({ transport, limit: 30 }) - const activeTurns = useActiveTurns(transport) + const activeTurns = useActiveTurns({ transport }) const isStreaming = activeTurns.size > 0 return ( @@ -224,13 +221,17 @@ Create the file `app/page.tsx`: ```javascript import { Providers } from './providers' +import { TransportProvider } from '@ably/ai-transport/react' +import { UIMessageCodec } from '@ably/ai-transport/vercel' import { Chat } from './chat' export default function Page() { const chatId = 'my-chat-session' return ( - + + + ) } @@ -260,8 +261,9 @@ Vercel built the `ChatTransport` interface as the extension point for custom tra const { messages } = useChat() // After: Ably transport (everything else stays the same) -const transport = useChatTransport({ channel }) -const { messages } = useChat({ transport }) +const transport = useClientTransport({ channelName: chatId }) +const chatTransport = useChatTransport(transport) +const { messages } = useChat({ transport: chatTransport }) ``` From ec9174888e30738ab35b20648a1b66a1f002c1a9 Mon Sep 17 00:00:00 2001 From: zak Date: Fri, 17 Apr 2026 16:06:49 +0100 Subject: [PATCH 4/4] Clarify Core SDK page is framework-independent Reframe the Core SDK getting-started page so it does not read as a Vercel tutorial. Explain that streamText and UIMessageCodec are swappable: any model inference approach works with a matching codec. Use the core createServerTransport import with explicit codec to show where a custom codec plugs in. Move Core SDK above Vercel in the nav. Add Ably provider explainer. --- src/data/nav/aitransport.ts | 8 +++---- .../ai-transport/getting-started/core-sdk.mdx | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts index 58d45a2e30..28516d7e45 100644 --- a/src/data/nav/aitransport.ts +++ b/src/data/nav/aitransport.ts @@ -45,14 +45,14 @@ export default { { name: 'Getting started', pages: [ - { - name: 'Vercel AI SDK', - link: '/docs/ai-transport/getting-started/vercel-ai-sdk', - }, { name: 'Core SDK', link: '/docs/ai-transport/getting-started/core-sdk', }, + { + name: 'Vercel AI SDK', + link: '/docs/ai-transport/getting-started/vercel-ai-sdk', + }, ], }, { diff --git a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx index 38ea777a18..8ff81a045b 100644 --- a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx +++ b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx @@ -1,12 +1,12 @@ --- title: "Get started with the Core SDK" -meta_description: "Build a streaming AI chat app using AI Transport's generic React hooks without a framework wrapper. Full access to the conversation tree, branching, and pagination." +meta_description: "Build a streaming AI chat app using AI Transport's core React hooks. Full access to the conversation tree, branching, and pagination." meta_keywords: "AI Transport, Core SDK, React hooks, streaming, realtime, Ably, chat, LLM, conversation tree" --- -Build a streaming AI chat application using AI Transport's generic React hooks. This approach gives you direct access to the conversation tree, branching, and pagination without a framework wrapper like Vercel's `useChat`. +Build a streaming AI chat application using Ably AI Transport's core React hooks. This approach gives you direct access to the conversation tree, branching, and pagination through hooks like `useView`, `useSend`, `useRegenerate`, and `useEdit`. -If you want the simplest path with Vercel AI SDK, see [Get started with Vercel AI SDK](/docs/ai-transport/getting-started/vercel-ai-sdk) instead. +To use directly with Vercel's `useChat` for message management, see [Get started with Vercel AI SDK](/docs/ai-transport/getting-started/vercel-ai-sdk). ## Prerequisites @@ -67,7 +67,7 @@ export async function GET(req) { ## Step 2: Create an Ably provider -Create the file `app/providers.tsx`: +Create the file `app/providers.tsx`. The Ably provider creates an authenticated realtime client using the token endpoint and makes it available to all child components. AI Transport uses this client to connect to channels for durable sessions. ```javascript @@ -104,7 +104,9 @@ export function Providers({ clientId, children }) { ## Step 3: Create the server API route -Create the file `app/api/chat/route.ts`. The server creates a turn, publishes the user message, streams the LLM response through the Ably channel, and returns immediately: +Create the file `app/api/chat/route.ts`. The server creates a turn, publishes the user message, streams the LLM response through the Ably channel, and returns immediately. + +This example uses Vercel AI SDK's `streamText` for model orchestration and `UIMessageCodec` to encode the response for Ably. You can swap `streamText` for any model inference approach. You need a codec that converts your model's output into Ably messages; `UIMessageCodec` handles this for Vercel AI SDK's message types. ```javascript @@ -112,7 +114,8 @@ import { after } from 'next/server' import { streamText, convertToModelMessages } from 'ai' import { anthropic } from '@ai-sdk/anthropic' import Ably from 'ably' -import { createServerTransport } from '@ably/ai-transport/vercel' +import { createServerTransport } from '@ably/ai-transport' +import { UIMessageCodec } from '@ably/ai-transport/vercel' const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY }) @@ -120,7 +123,7 @@ export async function POST(req) { const { messages, history, id, turnId, clientId, forkOf, parent } = await req.json() const channel = ably.channels.get(id) - const transport = createServerTransport({ channel }) + const transport = createServerTransport({ channel, codec: UIMessageCodec }) const turn = transport.newTurn({ turnId, clientId, parent, forkOf }) await turn.start() @@ -151,7 +154,7 @@ export async function POST(req) { ## Step 4: Create the chat component -Create the file `app/chat.tsx`. This uses AI Transport's generic hooks directly rather than Vercel's `useChat`. The `useView` hook provides the visible messages, a `send` function, and pagination: +Create the file `app/chat.tsx`. This uses AI Transport's core hooks directly. The `useView` hook provides the visible messages, a `send` function, and pagination: ```javascript @@ -209,7 +212,7 @@ export function Chat({ chatId }) { ## Step 5: Wire it together -Create the file `app/page.tsx`: +Create the file `app/page.tsx`. `UIMessageCodec` is the codec for Vercel AI SDK's message types, matching the server's output format. If you use a different model inference layer, provide a different codec to `TransportProvider`. ```javascript @@ -240,7 +243,7 @@ Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the sam 3. Every client subscribed to the channel receives tokens in realtime. 4. If a client disconnects, it automatically reconnects and resumes from where it left off. -The `useView` hook subscribes to the conversation tree and returns the visible messages along the currently selected branch. Unlike `useChat`, you have direct access to the tree structure: branching, pagination, and sibling navigation. +The `useView` hook subscribes to the conversation tree and returns the visible messages along the currently selected branch. You have direct access to the tree structure: branching, pagination, and sibling navigation. ## Explore next