From dce44114a0cf9e22d29efa55303cfd5135eb480a Mon Sep 17 00:00:00 2001 From: zak Date: Mon, 27 Jul 2026 14:54:10 +0100 Subject: [PATCH 1/2] Fix Vercel tool-calling docs for static tool-call encoding Statically-declared tools encode as `tool-${name}` parts, not `dynamic-tool`, so the client-side lookups that matched only `dynamic-tool` silently missed them. Match both representations (via an isToolPart guard and the AI SDK's getToolName) in the tool-calling and human-in-the-loop client examples, and correct the use-message-sync merge description to say the same. Rework the human-in-the-loop server example: a tool with no `execute` can only reach `input-available`, never the `approval-requested` state the client checks for. Replace it with the demo-grounded pattern of a single tool that has both `execute` and a per-call `needsApproval` gate (an approved call executes on the continuation instead of re-requesting). Update the "regular vs approval tool" FAQ to describe the two mechanisms accurately. Switch the remaining `typescript` code fences to `javascript` so the blocks render; AIT's language selector has no typescript entry. --- .../api/javascript/core/codec.mdx | 4 +- .../api/react/vercel/use-message-sync.mdx | 2 +- .../features/human-in-the-loop.mdx | 57 +++++++++++++------ .../ai-transport/features/tool-calling.mdx | 10 +++- .../frameworks/vercel-ai-sdk-ui.mdx | 2 +- .../getting-started/vercel-wdk.mdx | 10 ++-- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx index 44b9f7d0a6..656a5f3deb 100644 --- a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx +++ b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx @@ -12,7 +12,7 @@ The `Codec` interface is the bridge between an AI framework and Ably channel mes Implement `Codec` to integrate any AI framework with AI Transport. The SDK ships the Vercel codec via [`createUIMessageCodec()`](/docs/ai-transport/api/javascript/vercel/codec) for the Vercel AI SDK. For other frameworks, implement the methods below. -```typescript +```javascript import type { Codec } from '@ably/ai-transport'; const myCodec: Codec = { @@ -358,7 +358,7 @@ The reducer is a pure, stateless folding contract that `Codec` extends. A minimal codec skeleton that folds a single input and output variant and extracts a flat message list. -```typescript +```javascript import type { Codec, CodecInputEvent, diff --git a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx index 39032e5323..7c974ba9ef 100644 --- a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx +++ b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx @@ -53,7 +53,7 @@ The hook does two things: - Subscribes to the `ChatTransport`'s `onStreamingChange` event. While `streaming` is `true` (own-run stream consumed by `useChat`), the gate is closed and no `setMessages` calls are made. - Subscribes to the underlying `View`'s `'update'` event. When the view changes and the gate is open, the hook calls `setMessages` with an updater that merges the view's messages into the overlay. -The merge is per message, not a wholesale replace. For each assistant message, the merge preserves any `dynamic-tool` part in the overlay whose state is `output-available`, `output-error`, `approval-responded`, or `output-denied` while the tree's part is still in an unresolved state. That keeps local `addToolResult` and `addToolApprovalResponse` actions visible until the channel echo lands. +The merge is per message, not a wholesale replace. For each assistant message, the merge matches tool parts by `toolCallId` in either representation, a statically-declared tool's `tool-${name}` part or a dynamic tool's `dynamic-tool` part, and preserves any overlay part whose state is `output-available`, `output-error`, `approval-responded`, or `output-denied` while the tree's part is still in an unresolved state. That keeps local `addToolResult` and `addToolApprovalResponse` actions visible until the channel echo lands. ## When to use it 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 69b77c0a25..a6930ce2a5 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 @@ -31,25 +31,42 @@ The flow: ## Define an approval tool -On the server, define a tool without an `execute` function. The tool's description tells the LLM when to request approval: +On the server, define a tool with an `execute` function and a `needsApproval` gate. The AI SDK calls `needsApproval` per tool call: while it returns `true`, the SDK emits an approval request instead of running `execute`, so the tool pauses for the user. Make the gate per call rather than per tool name, so a call that has already been approved does not ask again on the continuation: ```javascript +// True once this specific tool call has an approved response in the +// conversation. `streamText` passes the model-message list, where an approval +// shows up as a `tool-approval-request` on an assistant message paired with a +// `tool-approval-response` on a later tool message, correlated by `approvalId`. +const isApprovedToolCall = (toolCallId, messages) => { + const approvalIdToToolCallId = new Map(); + for (const message of messages) { + if (message.role !== 'assistant' || typeof message.content === 'string') continue; + for (const part of message.content) { + if (part.type === 'tool-approval-request') { + approvalIdToToolCallId.set(part.approvalId, part.toolCallId); + } + } + } + for (const message of messages) { + if (message.role !== 'tool') continue; + for (const part of message.content) { + if (part.type !== 'tool-approval-response' || !part.approved) continue; + if (approvalIdToToolCallId.get(part.approvalId) === toolCallId) return true; + } + } + return false; +}; + const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), messages: conversationHistory, tools: { - requestApproval: { - description: 'Request user approval before executing a sensitive action', - inputSchema: z.object({ - action: z.string().describe('Description of the action to approve'), - details: z.string().describe('Additional context for the user'), - }), - // No execute function: requires client-side approval. - }, executeTransfer: { - description: 'Execute a bank transfer', + description: 'Execute a bank transfer. Requires user approval before running.', inputSchema: z.object({ amount: z.number(), recipient: z.string() }), + needsApproval: (_input, { toolCallId, messages }) => !isApprovedToolCall(toolCallId, messages), execute: async ({ amount, recipient }) => { return await processTransfer(amount, recipient); }, @@ -69,28 +86,32 @@ if (outcome.reason === 'suspend') { ``` -When the LLM decides an action needs approval, it invokes `requestApproval`. `streamText` finishes with `finishReason: 'tool-calls'`; [`vercelRunOutcome`](/docs/ai-transport/api/javascript/vercel/run-outcome) translates that to `'suspend'`, so the agent calls `run.suspend()` and the pending tool call stays on the channel for any connected client to act on. +When the LLM invokes `executeTransfer` and `needsApproval` returns `true`, `streamText` finishes with `finishReason: 'tool-calls'`; [`vercelRunOutcome`](/docs/ai-transport/api/javascript/vercel/run-outcome) translates that to `'suspend'`, so the agent calls `run.suspend()` and the pending tool call stays on the channel for any connected client to act on. On the continuation, the approval response is in the conversation, `needsApproval` returns `false`, and `execute` runs. ## Handle approval on the client -On the client, detect pending approval requests and present them to the user: +On the client, detect pending approval requests and present them to the user. A statically-declared tool arrives as a `tool-${name}` part, not `dynamic-tool`, so match both representations and read the name with the AI SDK's `getToolName`: ```javascript +import { getToolName } from 'ai'; + const { messages, runOf, send } = useView(); +const isToolPart = (p) => p.type === 'dynamic-tool' || p.type.startsWith('tool-'); + const pending = messages.find(({ message }) => message.parts?.some( - (p) => p.type === 'dynamic-tool' && p.toolName === 'requestApproval' && p.state === 'approval-requested', + (p) => isToolPart(p) && getToolName(p) === 'executeTransfer' && p.state === 'approval-requested', ), ); const pendingApproval = pending?.message.parts?.find( - (p) => p.type === 'dynamic-tool' && p.state === 'approval-requested', + (p) => isToolPart(p) && p.state === 'approval-requested', ); if (pending && pendingApproval) { - const { action, details } = pendingApproval.input; + const { amount, recipient } = pendingApproval.input; const runId = runOf(pending.codecMessageId).runId; const respond = async (approved) => { @@ -112,8 +133,8 @@ if (pending && pendingApproval) { return ( respond(true)} onReject={() => respond(false)} /> @@ -147,7 +168,7 @@ The agent's turn has already ended, so no connection or timeout is at risk. The ### How is this different from a regular tool call? -A regular client-executed tool runs as soon as the client receives the call. Human-in-the-loop blocks until a human submits the result. The mechanics are the same; the user experience is different. +A regular client-executed tool has no `execute` function, so its call sits in the `input-available` state and the client runs it and returns a `tool-result` as soon as it receives the call. An approval tool has an `execute` function gated by `needsApproval`, so its call sits in the `approval-requested` state until a human returns a `tool-approval-response`; the tool then runs on the server. Both suspend the run and resume on a continuation, but the approval path waits for a person. ### Can the agent see who approved it? diff --git a/src/pages/docs/ai-transport/features/tool-calling.mdx b/src/pages/docs/ai-transport/features/tool-calling.mdx index f14a716b05..ccc5de53df 100644 --- a/src/pages/docs/ai-transport/features/tool-calling.mdx +++ b/src/pages/docs/ai-transport/features/tool-calling.mdx @@ -80,19 +80,23 @@ if (outcome.reason === 'suspend') { [`vercelRunOutcome`](/docs/ai-transport/api/javascript/vercel/run-outcome) returns `'suspend'` when `streamText` finishes with `finishReason: 'tool-calls'`, so the agent suspends instead of ending. The pending tool call stays on the channel for any connected client to fulfil. -On the client, find the assistant message with the pending tool call and publish a `tool-result` input addressed to its `codecMessageId`. The codec folds the result onto the suspended assistant message, and the agent picks it up to continue the run: +On the client, find the assistant message with the pending tool call and publish a `tool-result` input addressed to its `codecMessageId`. The codec folds the result onto the suspended assistant message, and the agent picks it up to continue the run. + +A tool part arrives in one of two representations: a statically-declared tool (one defined in the `tools` object, like `getUserLocation` above) as `tool-${name}`, and a dynamic tool as `dynamic-tool`. Match both so the lookup works regardless of how the tool was declared: ```javascript const { messages, runOf, send } = useView(); +const isToolPart = (p) => p.type === 'dynamic-tool' || p.type.startsWith('tool-'); + const pending = messages.find(({ message }) => - message.parts?.some((p) => p.type === 'dynamic-tool' && p.state === 'input-available'), + message.parts?.some((p) => isToolPart(p) && p.state === 'input-available'), ); if (pending) { const toolCall = pending.message.parts.find( - (p) => p.type === 'dynamic-tool' && p.state === 'input-available', + (p) => isToolPart(p) && p.state === 'input-available', ); const location = await new Promise((resolve, reject) => { diff --git a/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-ui.mdx b/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-ui.mdx index e3b0816b68..532578ff0d 100644 --- a/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-ui.mdx +++ b/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-ui.mdx @@ -65,7 +65,7 @@ The integration has four parts: `createUIMessageCodec`, `createClientSession`, `createAgentSession`, and `createChatTransport` are generic over the AI SDK's three `UIMessage` type parameters: message metadata, custom data parts, and tools. Supply them once and your typed message flows through the session, so `view.getMessages()` returns messages whose `metadata`, data parts, and tool parts carry your types instead of the SDK defaults. Omit them and inference is unchanged. -```typescript +```javascript import { createClientSession } from '@ably/ai-transport/vercel'; import type * as AI from 'ai'; diff --git a/src/pages/docs/ai-transport/getting-started/vercel-wdk.mdx b/src/pages/docs/ai-transport/getting-started/vercel-wdk.mdx index f79b0d2584..bbffa3f5a5 100644 --- a/src/pages/docs/ai-transport/getting-started/vercel-wdk.mdx +++ b/src/pages/docs/ai-transport/getting-started/vercel-wdk.mdx @@ -58,7 +58,7 @@ This is a one-time setup per Ably app. Without the rule, the first token append Wrap your Next.js config with `withWorkflow`. It installs the bundler transforms for the `'use workflow'` and `'use step'` directives and serves the workflow runtime's handler endpoints under `/.well-known/workflow/v1/*`. -```typescript +```javascript import type { NextConfig } from 'next'; import { withWorkflow } from 'workflow/next'; @@ -79,7 +79,7 @@ The agent side is three files under `app/workflows/`: the workflow that orchestr Create `app/workflows/turn.ts`. The workflow is the deterministic orchestrator: it holds the Run's identity and does no channel I/O. `openRun` opens the Run; `runInference` runs each model call, the first and every follow-up. While an inference reports fresh server-tool calls, the workflow dispatches one `runTool` per call, then loops a follow-up `runInference`. Every terminal outcome has already been published on the wire by the step that produced it; the workflow only decides whether to schedule more steps. -```typescript +```javascript import { getWorkflowMetadata } from 'workflow'; import type { InvocationData } from '@ably/ai-transport'; import { failRun, openRun, runInference, runTool, type TurnIds } from './steps'; @@ -122,7 +122,7 @@ A `'use workflow'` function must stay deterministic. WDK re-executes it on every Create `app/workflows/tools.ts` with one server tool. It generates a whole-dollar price and throws when the price is odd, about half the time, so you can watch WDK retry the tool step in the inspector and watch the retry's re-rolled output supersede the failed attempt on the Ably channel. -```typescript +```javascript import { z } from 'zod'; import type { Tool } from 'ai'; @@ -152,7 +152,7 @@ Create `app/workflows/steps.ts`. Each `'use step'` function runs as a separate p Two structural rules keep the cross-process lifecycle sound. The step that produces an outcome publishes the matching Run lifecycle event in the same session it streamed with. And failure paths detach rather than end, leaving the Run open on the wire so a WDK retry can adopt it and publish a superseding attempt under the same `stepId`. -```typescript +```javascript import * as Ably from 'ably'; import { convertToModelMessages, stepCountIs, streamText } from 'ai'; import { getStepMetadata } from 'workflow'; @@ -339,7 +339,7 @@ async function publishTerminal(run: AgentRun, outcome: InferenceOutcome): Promis Create `app/api/chat/route.ts`. The route starts a workflow and returns immediately; the reply reaches the client over the channel, so the response body is informational only. -```typescript +```javascript import { start } from 'workflow/api'; import type { InvocationData } from '@ably/ai-transport'; import { chatWorkflow } from '../../workflows/turn'; From 3260f2cc419e32507bc8656dc3fa4973a866bf84 Mon Sep 17 00:00:00 2001 From: zak Date: Tue, 28 Jul 2026 11:58:40 +0100 Subject: [PATCH 2/2] Consolidate AI Transport concepts and restyle docs Fold six concept pages into the three that carry the model. Steps and invocations move into Runs, connections into Sessions, authentication into the getting-started guide, codecs into the codec architecture internals page, and infrastructure into Why AI Transport. Each old URL keeps a redirect, and the nav drops the removed entries. The concepts section now teaches session, run, and conversation tree rather than nine overlapping pages. Apply a house-style pass across the whole section: - Use "session" for the durable conversation. Reserve "channel" for places the reader needs channel-level detail: channel rules and namespaces, capability scoping, wire headers, and rate limits. - Lowercase the primitives (run, session, step, invocation, codec) in prose and mid-heading. They are concepts, not proper nouns. - Put links inline on the noun a reader would click instead of trailing "see X for detail" sentences, which only belong in Read next. - Fold contrast constructions into plain sentences, and replace metaphors that stand in for a literal statement. - Reserve "capability" for the Ably auth model and use "feature" elsewhere. Spell realtime as one word. Say "AI application". - Say whether each code block runs on the client or the agent. Correct a factual error on four core API pages, which said view.send() fires a POST to wake the agent. Only the Vercel ChatTransport does that. The core session is HTTP-free, so the application must POST the invocation itself, and client-session.mdx contradicted itself on the same page. Restore the steps diagram, orphaned when steps.mdx was folded in, to the Runs page beside the section it illustrates. Record the rules above in the write-docs skill, so a later pass reapplies them instead of undoing them. --- .claude/skills/write-docs/SKILL.md | 50 ++++-- src/data/nav/aitransport.ts | 26 +--- src/pages/docs/ai-transport/api/errors.mdx | 22 +-- src/pages/docs/ai-transport/api/index.mdx | 16 +- .../api/javascript/core/agent-session.mdx | 124 +++++++-------- .../api/javascript/core/client-session.mdx | 48 +++--- .../api/javascript/core/codec.mdx | 12 +- .../api/javascript/temporal/index.mdx | 12 +- .../api/javascript/vercel/chat-transport.mdx | 6 +- .../api/javascript/vercel/codec.mdx | 8 +- .../api/javascript/vercel/run-outcome.mdx | 10 +- .../ai-transport/api/react/core/providers.mdx | 2 +- .../api/react/core/use-ably-messages.mdx | 2 +- .../api/react/core/use-client-session.mdx | 4 +- .../api/react/core/use-create-view.mdx | 14 +- .../api/react/core/use-messages-with-seed.mdx | 14 +- .../ai-transport/api/react/core/use-tree.mdx | 26 ++-- .../ai-transport/api/react/core/use-view.mdx | 36 ++--- .../react/vercel/chat-transport-provider.mdx | 4 +- .../api/react/vercel/use-chat-transport.mdx | 4 +- .../api/react/vercel/use-message-sync.mdx | 14 +- .../ai-transport/concepts/authentication.mdx | 108 ------------- .../docs/ai-transport/concepts/codecs.mdx | 86 ----------- .../ai-transport/concepts/connections.mdx | 94 ----------- .../concepts/conversation-tree.mdx | 64 +++----- .../docs/ai-transport/concepts/index.mdx | 101 +++++------- .../ai-transport/concepts/infrastructure.mdx | 62 -------- .../ai-transport/concepts/invocations.mdx | 79 ---------- src/pages/docs/ai-transport/concepts/runs.mdx | 146 ++++++++---------- .../docs/ai-transport/concepts/sessions.mdx | 115 +++++++------- .../docs/ai-transport/concepts/steps.mdx | 96 ------------ .../ai-transport/features/agent-presence.mdx | 26 ++-- .../docs/ai-transport/features/branching.mdx | 30 ++-- .../ai-transport/features/cancellation.mdx | 30 ++-- .../features/chain-of-thought.mdx | 32 ++-- .../features/concurrent-turns.mdx | 50 +++--- .../features/database-hydration.mdx | 26 ++-- .../ai-transport/features/double-texting.mdx | 16 +- .../features/durable-execution.mdx | 68 ++++---- .../docs/ai-transport/features/history.mdx | 24 +-- .../features/human-in-the-loop.mdx | 16 +- .../features/interruption-and-steering.mdx | 88 +++++------ .../ai-transport/features/liveobjects.mdx | 26 ++-- .../ai-transport/features/multi-device.mdx | 46 +++--- .../features/optimistic-updates.mdx | 34 ++-- .../features/push-notifications.mdx | 12 +- .../features/reconnection-and-recovery.mdx | 28 ++-- .../ai-transport/features/token-streaming.mdx | 24 +-- .../ai-transport/features/tool-calling.mdx | 24 +-- .../docs/ai-transport/frameworks/temporal.mdx | 40 ++--- .../frameworks/vercel-ai-sdk-core.mdx | 36 ++--- .../frameworks/vercel-ai-sdk-ui.mdx | 42 +++-- .../ai-transport/frameworks/vercel-wdk.mdx | 74 ++++----- .../getting-started/authentication.mdx | 58 +++++-- .../getting-started/channel-rules.mdx | 10 +- .../ai-transport/getting-started/core-sdk.mdx | 28 ++-- .../ai-transport/getting-started/temporal.mdx | 50 +++--- .../getting-started/vercel-ai-sdk.mdx | 30 ++-- .../getting-started/vercel-wdk.mdx | 70 ++++----- .../docs/ai-transport/going-to-production.mdx | 39 +++-- src/pages/docs/ai-transport/index.mdx | 8 +- .../internals/codec-architecture.mdx | 44 ++++-- .../internals/conversation-tree.mdx | 36 ++--- .../docs/ai-transport/internals/index.mdx | 8 +- .../internals/transport-patterns.mdx | 22 +-- .../ai-transport/internals/wire-protocol.mdx | 56 +++---- src/pages/docs/ai-transport/roadmap.mdx | 20 +-- .../docs/ai-transport/troubleshooting.mdx | 34 ++-- .../why/http-streaming-and-ai.mdx | 18 +-- src/pages/docs/ai-transport/why/index.mdx | 50 +++--- 70 files changed, 1112 insertions(+), 1666 deletions(-) delete mode 100644 src/pages/docs/ai-transport/concepts/authentication.mdx delete mode 100644 src/pages/docs/ai-transport/concepts/codecs.mdx delete mode 100644 src/pages/docs/ai-transport/concepts/connections.mdx delete mode 100644 src/pages/docs/ai-transport/concepts/infrastructure.mdx delete mode 100644 src/pages/docs/ai-transport/concepts/invocations.mdx delete mode 100644 src/pages/docs/ai-transport/concepts/steps.mdx diff --git a/.claude/skills/write-docs/SKILL.md b/.claude/skills/write-docs/SKILL.md index ff54332047..9af7614822 100644 --- a/.claude/skills/write-docs/SKILL.md +++ b/.claude/skills/write-docs/SKILL.md @@ -38,15 +38,15 @@ Eighteen principles govern Ably docs pages, distilled from the AI Transport wire 9. **Concept pages convey what the layer requires.** The pattern is *problem → model → what this layer requires → code proof*. Name the technical properties (ordering, persistence, accumulation, fan-out, presence) and why each matters. 10. **Concept pages do not teach features.** One minimal code-proof sample max. No API surface walkthrough. If a concept page drifts into method-by-method content, it stops being a concept page and the feature pages around it feel thin. 11. **Features map to jobs to be done.** Each feature page maps to what a developer is trying to accomplish ("barge-in" = "let my users change direction mid-response"). Page names and intros use the JTBD framing. -12. **Framework pages: intentional design framing, not "missing features".** Frame the framework's scope as a deliberate boundary the framework chose, and Ably as the layer that fills the gap. Never "the framework can't do X" — always "the framework intentionally doesn't do X". Sibling framework pages (for example UI vs Core) use verbatim-aligned capability bullets. -13. **Positioning pages: open problem-first, turn positive.** "Why X" pages may lead with the problem, but must pivot to the capability section by mid-page. Pages that stay defensive end-to-end become gripe lists, not pitches. +12. **Framework pages: intentional design framing, not "missing features".** Frame the framework's scope as a deliberate boundary the framework chose, and Ably as the layer that fills the gap. Never "the framework can't do X" — always "the framework intentionally doesn't do X". Sibling framework pages (for example UI vs Core) use verbatim-aligned feature bullets. +13. **Positioning pages: open problem-first, turn positive.** "Why X" pages may lead with the problem, but must pivot to the feature section by mid-page. Pages that stay defensive end-to-end become gripe lists, not pitches. ### Mechanical (binary, agent-checkable) 14. **Layer 0 hook in the first 5 seconds.** Every page opens with a hook that answers "what is this, why should I care?". The `intro:` frontmatter feeds the auto-generated PageHeader; the body's first paragraph reinforces it. On feature pages, the hook is two sentences: outcome first ("Your users can change direction mid-response"), mechanism second ("AI Transport's session layer lets a client cancel and re-prompt without breaking the stream"). **Per-interface API reference pages are the one exception**: they drop `intro:` and let the opening paragraph carry the hook. Navigational API pages (the API reference hub, the errors page) keep `intro:` like every other page type. See the `## API reference pages` section for the full standard. 15. **Cover unhappy paths.** Every feature page has an edge-cases section. Race conditions, timeouts, network drops, capability-missing failures, what happens when the LLM errors. This is what separates trusted docs from marketing. 16. **FAQ with three to five real entries on feature pages.** Surface what developers actually ask. Do not pad to meet a count. -17. **No API keys in client code — but server-side agent code is the opposite.** In client (browser) code, never show an API key; use `authUrl: '/auth'` as the placeholder and link out to `concepts/authentication` (or the equivalent setup page) once. In **server-side or agent code** (durable-execution activities, agent routes, workers, anything that runs on your own infrastructure), do the reverse: construct the Realtime client with `new Ably.Realtime({ key: process.env.ABLY_API_KEY })`. An `authUrl` on the server is wrong — there is no browser to fetch a token, and the key is already trusted in that environment. When you see `authUrl` in an agent/server snippet, that is a bug to fix, not a rule to preserve. (The line 380 verification grep excludes `process.env`-sourced keys, so this pattern passes it.) +17. **No API keys in client code — but server-side agent code is the opposite.** In client (browser) code, never show an API key; use `authUrl: '/auth'` as the placeholder and link out to `getting-started/authentication` (or the equivalent setup page) once. In **server-side or agent code** (durable-execution activities, agent routes, workers, anything that runs on your own infrastructure), do the reverse: construct the Realtime client with `new Ably.Realtime({ key: process.env.ABLY_API_KEY })`. An `authUrl` on the server is wrong — there is no browser to fetch a token, and the key is already trusted in that environment. When you see `authUrl` in an agent/server snippet, that is a bug to fix, not a rule to preserve. (The line 380 verification grep excludes `process.env`-sourced keys, so this pattern passes it.) 18. **Visual rhythm.** Diagram, code block, card layout, or icon table every few paragraphs. No walls of text. ### Cross-product orientation @@ -57,7 +57,7 @@ AI Transport is the testbed for the new principles. Other Ably products will ali Pick the template that matches the page you are writing. -**Templates describe structure, not headings.** The numbered items below are the *section shape* — what each section covers and roughly in what order. They are not the literal H2 text for the page. A concept-page template item like "Problem statement" becomes a descriptive imperative on the page itself: `## Why Runs exist`, `## Why sessions exist`. "Model" becomes `## Understand the Run lifecycle`, `## Understand sessions and channels`. "Code proof" becomes `## Trigger a Run`, `## Attach to a session`. Adapt the heading to the specific concept; never copy the template label verbatim. Use imperative or descriptive verb phrases per the writing-style guide. +**Templates describe structure, not headings.** The numbered items below are the *section shape* — what each section covers and roughly in what order. They are not the literal H2 text for the page. A concept-page template item like "Problem statement" becomes a descriptive imperative on the page itself: `## Why runs exist`, `## Why sessions exist`. "Model" becomes `## Understand the run lifecycle`, `## Understand sessions and channels`. "Code proof" becomes `## Trigger a run`, `## Attach to a session`. Adapt the heading to the specific concept; never copy the template label verbatim. Use imperative or descriptive verb phrases per the writing-style guide. ### Feature page @@ -89,8 +89,8 @@ Concept pages convey the mental model. The pattern is *problem → model → wha A page that explains how Ably composes with a third-party framework (Vercel AI SDK, Temporal, Inngest, and so on). -1. **What the framework brings.** A capability table — what the framework owns by design. -2. **What Ably adds.** A capability table for what Ably layers on. **Sibling framework pages** are pages in the same `frameworks/` sub-tree that compete for the same developer JTBD (for example, Vercel AI SDK UI alongside a future Temporal page). Align bullets across siblings where the capability genuinely overlaps so a reader can compare like for like. Introduce framework-specific rows where the integration shape differs. Do not force parity when one framework simply does not own that capability. Comparability is the goal, not forced parity. +1. **What the framework brings.** A feature table, naming what the framework owns by design. +2. **What Ably adds.** A feature table for what Ably layers on. **Sibling framework pages** are pages in the same `frameworks/` sub-tree that compete for the same developer JTBD (for example, Vercel AI SDK UI alongside a future Temporal page). Align bullets across siblings where the feature genuinely overlaps so a reader can compare like for like. Introduce framework-specific rows where the integration shape differs. Do not force parity when one framework simply does not own that feature. Comparability is the goal, not forced parity. 3. **Where they connect.** The minimal integration code. The plug-in point (`ChatTransport`, codec, adapter). 4. **Scope and trade-offs.** Frame the framework's scope as intentional design. Never "the framework can't…". 5. **Read next.** Getting-started, sibling framework page, API reference for the integration. @@ -101,7 +101,7 @@ A page that explains how Ably composes with a third-party framework (Vercel AI S 1. **One-sentence opening.** The shape of the product, not the problem. 2. **Problem statement** (one or two paragraphs). What current solutions miss. -3. **Pivot.** A clearly-marked "How Ably solves this" or equivalent capability section. Everything below the pivot is positive-forward. +3. **Pivot.** A clearly-marked "How Ably solves this" or equivalent feature section. Everything below the pivot is positive-forward. 4. **Comparison table** (optional). Direct HTTP vs durable sessions, default cache vs Ably, and so on. Each row is checkable. 5. **How Ably implements this.** The primitives that back the model — link out to concepts. 6. **Read next.** Getting-started + concepts. @@ -362,13 +362,25 @@ Spelling to enforce on every page: Use the terms the product actually defines. Do not introduce vocabulary that is not part of the AI Transport domain — not internal-codebase jargon, and not generic AI/ML terms the docs have chosen not to use. A reader who has read the concept pages should never hit a word the docs never taught them. -- **Prefer the named primitives.** The concept pages define the vocabulary: [Session](/docs/ai-transport/concepts/sessions), [Connection](/docs/ai-transport/concepts/connections), [Run](/docs/ai-transport/concepts/runs), [Step](/docs/ai-transport/concepts/steps), [Invocation](/docs/ai-transport/concepts/invocations), [Codec](/docs/ai-transport/concepts/codecs), [conversation tree](/docs/ai-transport/concepts/conversation-tree), and view. Reach for these before inventing a synonym. +- **Prefer the named primitives.** The concept pages define the vocabulary: [session](/docs/ai-transport/concepts/sessions), [run](/docs/ai-transport/concepts/runs), [step](/docs/ai-transport/concepts/runs#steps), [invocation](/docs/ai-transport/concepts/runs#invocations), [conversation tree](/docs/ai-transport/concepts/conversation-tree), and view. Reach for these before inventing a synonym. Codec and connection no longer have their own concept pages: the codec is covered by the [codec API reference](/docs/ai-transport/api/javascript/core/codec) and [codec architecture](/docs/ai-transport/internals/codec-architecture), and connection behaviour lives on [sessions](/docs/ai-transport/concepts/sessions#connect). - **Do not leak internal jargon.** Words that name SDK internals but never appear in the concept or API docs (for example `projection`, `RunNode`, or other type names not documented for readers) read as jargon. Use the documented term instead: "the conversation" or "the Run's view", not "the projection"; "the Run" or "the conversation tree", not "`RunNode`". - **Do not smuggle in generic AI/ML vocab the docs avoid.** Describe a loop cycle as an "iteration", not a "pass". Describe a model invocation as a "model call" or "response", not an "inference pass". The verb "pass" (to pass an argument, hook, or signal) is fine; the noun "pass" (meaning a loop cycle) is not. -- **Respect the Run/turn split.** "Run" is the only primitive the docs explain. Use "turn" only in jobs-to-be-done framing (titles, intros, the "concurrent turns" feature name), never as a defined primitive alongside Run. +- **Respect the run/turn split.** "Run" is the only primitive the docs explain. Use "turn" only in jobs-to-be-done framing (titles, intros, the "concurrent turns" feature name), never as a defined primitive alongside run. +- **Lowercase the named primitives.** Write "run", "session", "step", "connection", "invocation", "codec": "the run ends", "every device on the session", "each step retries". This applies mid-heading too, because headings are sentence case: `## Close the run only once`, `## Look up a run by id`. Capitalise only where any word would be capitalised, which means the first word of a sentence or heading (`## Run the app`, `## Sessions do not span devices`) and API reference headings that name an actual type (`## Invocation`). Never capitalise mid-sentence to signal "this is the primitive". Exact type names stay in backticks with their real casing (`AgentRun`, `ClientSession`, `RunStep`). +- **"Capability" names the authentication model only.** Ably capabilities are the token permission model. Do not use "capability" or "capabilities" in a loose descriptive sense (a feature the SDK offers). Write "feature" or name the specific thing. Reserve "capability" for auth. +- **"AI application", never "AI app".** Readers do not call their software an "AI app". Write "AI application", or name what it is (the agent, the chat interface, your server). +- **Session is the default term for the durable conversation channel; use "channel" only for Ably-technical reasons.** When you mean the durable conversation a client attaches to, say Session. Reach for "Ably channel" only when the reader genuinely needs the channel-level detail (capabilities, channel naming, raw publish/subscribe, dashboard inspection). Do not drift between the two for the same idea. When a word is genuinely new and needed, define it on first use or link to the concept page that does. If you cannot point to where it is defined, it does not belong on the page. +### Intros, links, and code labelling + +- **No property or method names in the intro or Layer-0 hook.** The opening describes the outcome and the mechanism in plain language, not `run.pipe`, `createAgentSession`, or an option key. API names belong in the code samples and the body. If the first sentence of a page reads like a signature, rewrite it around what the reader gets. +- **No cross-page links in the opening paragraph.** The first paragraph must stand on its own. Reinforce principle 4 and principle 14: hook first, then link to concepts lower down. A reader should not have to leave the page before they have read what it is about. **Index and hub pages are the exception.** On a section index such as `concepts/index.mdx`, the opening paragraph names the concepts the section covers and links each one on first use. There the links are the content rather than a detour, so add them and do not strip them. +- **Inline links, not "see X for more".** Weave the link into the sentence at the phrase the reader would click. Do not append a bare "see [the codec page] for details" at the end of a paragraph when the link belongs on a noun inside it. A trailing "see also" is only acceptable in a dedicated Read next / Related section. +- **Label every code block as client or agent.** A reader must know where a snippet runs. Server-side or agent code (agent routes, workers, durable-execution activities) and client-side or browser code look similar but differ on auth (principle 17) and on which SDK surface they use. Say which it is in the sentence that introduces the block, or in a comment on the first line. +- **Consistent client and user.** "client" is the SDK, the connection, or the device; "user" is the human. Pick the right one for the actor you mean and do not drift between them mid-page. When the human acts through the UI, that is the user; when code attaches to a Session, that is the client. + ## Per-page workflow 1. **Identify the page type.** Pick the matching template above. @@ -412,6 +424,26 @@ grep -rn "—" "$P" # "realtime" is one word (no "real time" / "real-time") grep -rn "real[- ]time" "$P" +# "AI application", never "AI app" +grep -rn "AI app\b" "$P" + +# "Capability" reserved for the auth model — not a table header or loose descriptor +grep -rn "| Capability |" "$P" +grep -rniE "\bcapabilit(y|ies)\b" "$P" | grep -viE "token|auth|permission|channel" + +# Primitives stay lowercase except at the start of a sentence or heading. +# Matches a lowercase word or comma, then a capitalised primitive: "the Run", "a Session". +grep -rnE "[a-z,] (Run|Runs|Session|Sessions|Step|Steps|Invocation|Invocations|Codec|Codecs)\b" "$P" + +# No trailing "See [X]" pointer sentences. A pointer link only belongs in a +# Read next / Related section; elsewhere the link goes inline on the noun. +grep -rnE "\. See \[|; see \[|^- See \[|^See \[" "$P" + +# "Session" is the default term for the durable conversation. This grep is a +# review prompt, not a hard failure: every hit must justify channel-level detail +# (channel rules, namespaces, capabilities, wire protocol, rate limits, channelName). +grep -rn "the channel\b" "$P" + # No off-vocabulary terms. Noun "pass" (loop cycle), "inference pass", and internal # jargon that never appears in the concept/API docs. The verb "pass" (an argument, # hook, signal) is fine, so this targets the noun forms and known jargon only. diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts index 823fd14135..e558d692b5 100644 --- a/src/data/nav/aitransport.ts +++ b/src/data/nav/aitransport.ts @@ -73,38 +73,14 @@ export default { name: 'Sessions', link: '/docs/ai-transport/concepts/sessions', }, - { - name: 'Connections', - link: '/docs/ai-transport/concepts/connections', - }, { name: 'Runs', link: '/docs/ai-transport/concepts/runs', }, - { - name: 'Steps', - link: '/docs/ai-transport/concepts/steps', - }, - { - name: 'Invocations', - link: '/docs/ai-transport/concepts/invocations', - }, - { - name: 'Codecs', - link: '/docs/ai-transport/concepts/codecs', - }, { name: 'Conversation tree', link: '/docs/ai-transport/concepts/conversation-tree', }, - { - name: 'Authentication', - link: '/docs/ai-transport/concepts/authentication', - }, - { - name: 'Infrastructure', - link: '/docs/ai-transport/concepts/infrastructure', - }, ], }, { @@ -176,7 +152,7 @@ export default { link: '/docs/ai-transport/features/interruption-and-steering', }, { - name: 'LiveObjects State', + name: 'LiveObjects state', link: '/docs/ai-transport/features/liveobjects', }, { diff --git a/src/pages/docs/ai-transport/api/errors.mdx b/src/pages/docs/ai-transport/api/errors.mdx index 7fe4009de1..5accdee759 100644 --- a/src/pages/docs/ai-transport/api/errors.mdx +++ b/src/pages/docs/ai-transport/api/errors.mdx @@ -36,21 +36,21 @@ The `ErrorInfo` type is exported from `ably`. AI Transport surfaces it as the re | ----- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | 40000 | BadRequest | 400 | The request was malformed or missing required fields. | Check the request payload and ensure all required fields are present. | | 40003 | InvalidArgument | 400 | An argument passed to a session or run method was invalid. | Verify the arguments match the expected types and constraints. | -| 40160 | InsufficientCapability | 401 | The Ably channel rejected a publish for a capability reason. The token does not permit the operation. | Add the missing capability to the token. See [authentication](/docs/ai-transport/concepts/authentication). | +| 40160 | InsufficientCapability | 401 | The Ably channel rejected a publish for a capability reason. The token does not permit the operation. | Add the missing capability to the [token](/docs/ai-transport/getting-started/authentication). | ### Session and run errors | Code | Name | HTTP status | Description | Recovery | | ------ | ------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 104000 | EncoderRecoveryFailed | 500 | The encoder failed to recover after a publish error. One or more `updateMessage` calls could not recover a failed append pipeline. | End the Run with reason `'error'` and start a new one. The client sees the partial response up to the failure point. | -| 104001 | SessionSubscriptionError | 500 | A session-level channel subscription callback threw unexpectedly. | Check the subscription callback for unhandled exceptions. Inspect the underlying error for details. | -| 104002 | CancelListenerError | 500 | The cancel listener or `onCancel` hook threw while processing a cancel message. | Check the `onCancel` hook for unhandled exceptions. The Run may not respond to subsequent cancels. | -| 104003 | RunLifecycleError | 500 | A publish within a Run failed (lifecycle event, message, or codec event). | Check channel permissions and connection state. Surface the error to the calling HTTP handler so the client's pending send fails. | +| 104000 | EncoderRecoveryFailed | 500 | The encoder failed to recover after a publish error. One or more `updateMessage` calls could not recover a failed append pipeline. | End the run with reason `'error'` and start a new one. The client sees the partial response up to the failure point. | +| 104001 | SessionSubscriptionError | 500 | A session-level subscription callback threw unexpectedly. | Check the subscription callback for unhandled exceptions. Inspect the underlying error for details. | +| 104002 | CancelListenerError | 500 | The cancel listener or `onCancel` hook threw while processing a cancel message. | Check the `onCancel` hook for unhandled exceptions. The run may not respond to subsequent cancels. | +| 104003 | RunLifecycleError | 500 | A publish within a run failed (lifecycle event, message, or codec event). | Check channel permissions and connection state. Surface the error to the calling HTTP handler so the client's pending send fails. | | 104004 | SessionClosed | 400 | An operation was attempted on a session that has already been closed. | Create a new session. Do not reuse a closed session. | | 104005 | SessionSendFailed | 500 | A send failed: either the core's channel publish failed, or the Vercel chat transport's agent-invocation POST failed (network error or non-2xx response). | Check the API endpoint URL, network connectivity, and authentication. Inspect the underlying error for details. | -| 104006 | ChannelContinuityLost | 500 | The Ably channel lost message continuity. The channel entered `FAILED`, `SUSPENDED`, or `DETACHED`, or re-attached with `resumed: false`. Active streams can no longer be guaranteed to receive all events. | Surfaced via the session's `on('error')` on both the client and the agent. End active Runs; subsequent operations can resume once the channel reattaches. | +| 104006 | ChannelContinuityLost | 500 | The Ably channel lost message continuity. The channel entered `FAILED`, `SUSPENDED`, or `DETACHED`, or re-attached with `resumed: false`. Active streams can no longer be guaranteed to receive all events. | Surfaced via the session's `on('error')` on both the client and the agent. End active runs; subsequent operations can resume once the channel reattaches. | | 104007 | ChannelNotReady | 400 | An operation was attempted but the channel is not in a usable state (not `ATTACHED` or `ATTACHING`). | Wait for the channel to attach before retrying, or inspect channel state and connection state for the underlying cause. | -| 104008 | StreamError | 500 | An error occurred while piping a response stream to the channel. Either the source event stream threw (LLM provider rate limit, model error, network failure) or an underlying publish failed mid-stream. | Inspect `cause` for the provider error. End the Run with reason `'error'`. The error is also returned on `StreamResult.error` from `Run.pipe`. | +| 104008 | StreamError | 500 | An error occurred while piping a response stream to the session. Either the source event stream threw (LLM provider rate limit, model error, network failure) or an underlying publish failed mid-stream. | Inspect `cause` for the provider error. End the run with reason `'error'`. The error is also returned on `StreamResult.error` from `Run.pipe`. | ### Auth and channel errors @@ -62,9 +62,9 @@ Auth and channel errors come from the Ably platform, not AI Transport. The most | 40300 | Forbidden. | The token is valid but not authorised for this resource. | | 80000 | Channel attach failed. | Check the channel name and capability. | | 90000 | Internal channel error. | Retry the operation. If the error persists, contact support. | -| 93002 | `Can only update/delete/append messages on channels with mutableMessages enabled`. The namespace lacks the `mutableMessages` rule, so AI Transport cannot append stream tokens. This is the most common AI Transport setup failure. | Enable the **Message annotations, updates, deletes, and appends** rule on the namespace. See [Configure the channel rule](/docs/ai-transport/getting-started/channel-rules). | +| 93002 | `Can only update/delete/append messages on channels with mutableMessages enabled`. The namespace lacks the `mutableMessages` rule, so AI Transport cannot append stream tokens. This is the most common AI Transport setup failure. | Enable the **Message annotations, updates, deletes, and appends** [rule on the namespace](/docs/ai-transport/getting-started/channel-rules). | -The full Ably error code list lives at [Ably error codes](/docs/platform/errors/codes). +The [Ably error codes](/docs/platform/errors/codes) page lists every code the platform returns. ## Match an ErrorInfo against a code @@ -86,7 +86,7 @@ session.on('error', (error) => { ## Example -Handle session-level and run-level errors separately. Both the client and the agent subscribe to session errors with `session.on('error')`. The agent additionally uses the run runtime's `onError` for per-Run failures. +Handle session-level and run-level errors separately. Both the client and the agent subscribe to session errors with `session.on('error')`. The agent additionally uses the run runtime's `onError` for per-run failures. ```javascript @@ -116,7 +116,7 @@ const run = agentSession.createRun(invocation, { }, }); -// End a Run with reason 'error' when piping fails. +// End a run with reason 'error' when piping fails. try { const result = await run.pipe(llmStream); await run.end({ reason: result.reason }); diff --git a/src/pages/docs/ai-transport/api/index.mdx b/src/pages/docs/ai-transport/api/index.mdx index 3a428ce1ae..d8e957fde2 100644 --- a/src/pages/docs/ai-transport/api/index.mdx +++ b/src/pages/docs/ai-transport/api/index.mdx @@ -2,12 +2,12 @@ title: "API reference" meta_description: "API reference for Ably AI Transport. Sessions, codec, React hooks, Vercel integration, and errors." meta_keywords: "API reference, AI Transport, ClientSession, AgentSession, codec, React hooks, Vercel, errors, Ably" -intro: "Reference for every public API surface in the @ably/ai-transport package. The SDK is organised into four entry points so you only import what you need." +intro: "Reference for every public API in the @ably/ai-transport package. The SDK is organised into four entry points so you only import what you need." redirect_from: - /docs/ai-transport/api-reference --- -This reference covers every public API surface in the `@ably/ai-transport` package. The SDK is organised into four entry points so you only import what you need. +This reference covers every public API in the `@ably/ai-transport` package. The SDK is organised into four entry points so you only import what you need. ## Quick example @@ -29,8 +29,6 @@ await session.connect(); ``` -See the individual reference pages for the full API. - ## Entry points | Entry point | Import path | Contents | @@ -52,13 +50,13 @@ The Vercel entry points re-export the core factories with the codec pre-bound. I {[ { title: 'ClientSession', - description: 'Subscribe to a channel, build a conversation tree, send messages, and cancel Runs from the client.', + description: 'Subscribe to a session, build a conversation tree, send messages, and cancel runs from the client.', image: 'icon-product-pubsub', link: '/docs/ai-transport/api/javascript/core/client-session', }, { title: 'AgentSession', - description: 'Manage Run lifecycles, publish lifecycle events, and pipe streamed assistant output from the server.', + description: 'Manage run lifecycles, publish lifecycle events, and pipe streamed assistant output from the server.', image: 'icon-product-pubsub', link: '/docs/ai-transport/api/javascript/core/agent-session', }, @@ -89,7 +87,7 @@ The Vercel entry points re-export the core factories with the codec pre-bound. I }, { title: 'vercelRunOutcome', - description: 'Map a Vercel streamText finishReason and pipe result to a VercelRunOutcome for Run.suspend / Run.end.', + description: 'Map a Vercel streamText finishReason and pipe result to a VercelRunOutcome for run.suspend / run.end.', image: 'icon-tech-vercel', link: '/docs/ai-transport/api/javascript/vercel/run-outcome', }, @@ -128,13 +126,13 @@ The Vercel entry points re-export the core factories with the codec pre-bound. I }, { title: 'useMessagesWithSeed', - description: 'Reconcile a persisted conversation seed with the live channel and render the composed conversation.', + description: 'Reconcile a persisted conversation seed with the live session and render the composed conversation.', image: 'icon-tech-react', link: '/docs/ai-transport/api/react/core/use-messages-with-seed', }, { title: 'useTree', - description: 'Stable structural query callbacks for inspecting Runs outside the visible branch.', + description: 'Stable structural query callbacks for inspecting runs outside the visible branch.', image: 'icon-tech-react', link: '/docs/ai-transport/api/react/core/use-tree', }, diff --git a/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx b/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx index 46778b25e3..c534202bf8 100644 --- a/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx +++ b/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx @@ -1,15 +1,15 @@ --- title: "AgentSession" -meta_description: "API reference for the AI Transport AgentSession: factory, lifecycle methods, the Run interface, and the Invocation value object." -meta_keywords: "AI Transport, AgentSession, createAgentSession, Run, Invocation, createRun, pipe, end, Ably" +meta_description: "API reference for the AI Transport AgentSession: factory, lifecycle methods, the run interface, and the invocation value object." +meta_keywords: "AI Transport, AgentSession, createAgentSession, run, invocation, createRun, pipe, end, Ably" redirect_from: - /docs/ai-transport/api-reference/server-transport - /docs/ai-transport/api/javascript/server-transport --- -The `AgentSession` is the server-side counterpart to [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session). It subscribes to the channel for cancel signals and creates `AgentRun` instances that publish lifecycle events, user messages, and streamed assistant output. +The `AgentSession` is the server-side counterpart to `ClientSession`. It subscribes to the channel for cancel signals and creates `AgentRun` instances that publish lifecycle events, user messages, and streamed assistant output. -Construct one with `createAgentSession` from the core entry point. For Vercel `UIMessage` channels, use the pre-bound factory from [`@ably/ai-transport/vercel`](/docs/ai-transport/api/javascript/vercel/chat-transport) instead. +Construct one with `createAgentSession` from the core entry point. For Vercel `UIMessage` sessions, use the pre-bound factory from [`@ably/ai-transport/vercel`](/docs/ai-transport/api/javascript/vercel/chat-transport) instead. ```javascript @@ -40,7 +40,7 @@ await run.start(); | Property | Description | Type | | --- | --- | --- | | presence | The Ably presence object for the session's channel. Use it to see which clients are connected, for example to detect whether the requesting user is still online (`enter`, `leave`, `get`, `subscribe`). The session adds no semantics of its own (it is the same instance the channel exposes), and presence operations implicitly attach, so they work without first awaiting [`connect()`](#connect). | `Ably.RealtimePresence` | -| object | The Ably LiveObjects API for the session's channel. Use it to read and write shared `LiveMap` / `LiveCounter` state on the channel the session already uses; call `get()` to resolve the object. The session adds no semantics; it is the same instance the channel exposes. Operating on it requires the client to be constructed with the `LiveObjects` plugin from `ably/liveobjects` and the object modes to be requested via [`channelModes`](#constructor-params); without both, the underlying SDK throws. See [LiveObjects State](/docs/ai-transport/features/liveobjects). | `RealtimeObject` | +| object | The Ably [LiveObjects](/docs/ai-transport/features/liveobjects) API for the session's channel. Use it to read and write shared `LiveMap` / `LiveCounter` state on the channel the session already uses; call `get()` to resolve the object. The session adds no semantics; it is the same instance the channel exposes. Operating on it requires the client to be constructed with the `LiveObjects` plugin from `ably/liveobjects` and the object modes to be requested via [`channelModes`](#constructor-params); without both, the underlying SDK throws. | `RealtimeObject` | @@ -75,8 +75,8 @@ const session = createAgentSession({ | client | required | The Ably Realtime client. The caller owns its lifecycle; neither `session.end()` nor `session.detach()` closes the client. | `Ably.Realtime` | | channelName | required | The channel to publish to. The session owns this channel; do not also resolve it elsewhere with conflicting options. | String | | codec | required | The codec used to encode events and messages. | `Codec` | -| channelModes | optional | Extra channel modes to request on top of the modes AI Transport always needs. Pass `OBJECT_MODES` to use Ably LiveObjects via [`object`](#properties). Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on. See [LiveObjects State](/docs/ai-transport/features/liveobjects). | `Ably.ChannelMode[]` | -| historyPageSize | optional | Wire-message limit fetched per channel-history round trip, used by every `run.view` pagination on this session. Independent of `loadOlder`'s reveal `limit`: it tunes fetch cost, not reveal granularity. Defaults to 100. | Number | +| channelModes | optional | Extra channel modes to request on top of the modes AI Transport always needs. Pass `OBJECT_MODES` to use Ably [LiveObjects](/docs/ai-transport/features/liveobjects) via [`object`](#properties). Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on. | `Ably.ChannelMode[]` | +| historyPageSize | optional | Wire-message limit fetched per channel-history round trip, used by every `run.view` pagination on this session. Independent of `loadOlder`'s reveal `limit`: it tunes fetch cost rather than reveal granularity. Defaults to 100. | Number | | logger | optional | Logger instance for diagnostic output. | `Logger` | @@ -107,7 +107,7 @@ await session.connect(); {`createRun(invocation: Invocation, runtime?: RunRuntime): AgentRun`} -Create a new `AgentRun` for the input event named in the `Invocation`. Returns synchronously and publishes nothing to the channel until [`AgentRun.start`](#run-start) is called. The run is registered for cancel routing immediately so early cancels fire the `abortSignal`. +Create a new `AgentRun` for the input event named in the `Invocation`. Returns synchronously and publishes nothing to the session until [`AgentRun.start`](#run-start) is called. The run is registered for cancel routing immediately so early cancels fire the `abortSignal`. ```javascript @@ -131,7 +131,7 @@ const run = session.createRun(invocation, { signal: req.signal }); | Property | Description | Type | | --- | --- | --- | -| inputEventId | The specific input event on the channel that triggered this invocation. Run identity is resolved from that event's wire headers. | String | +| inputEventId | The specific input event on the session that triggered this invocation. Run identity is resolved from that event's wire headers. | String | | sessionName | Logical name of the session, used as the Ably channel name. | String | @@ -140,26 +140,26 @@ const run = session.createRun(invocation, { signal: req.signal }); | Property | Description | Type | | --- | --- | --- | -| invocationId | Override the invocation id for this Run. Defaults to a fresh `crypto.randomUUID()` (the normal path; one per HTTP request). Supply a non-empty value for deterministic ids in tests. | String | +| invocationId | Override the invocation id for this run. Defaults to a fresh `crypto.randomUUID()` (the normal path; one per HTTP request). Supply a non-empty value for deterministic ids in tests. | String | | runId | Override the run id for a fresh run. Defaults to a fresh `crypto.randomUUID()`. Continuations ignore this and read the existing `runId` off the triggering input event. Supply a non-empty value for deterministic ids in tests. | String | | signal | External `AbortSignal` (typically the HTTP request's `req.signal`) that cancels the run when fired. | `AbortSignal` | | onMessage | Called before each Ably message is published. Mutate the message in place to add custom headers under `extras.ai`. | `(message: Ably.Message) => void` | | onCancelled | Called when the run is cancelled. Receives a `write` function to publish final outputs before cancellation finalises. | `(write: (output: TOutput) => Promise) => void \| Promise` | | onCancel | Called when a cancel arrives. Return `true` to accept, `false` to reject. Defaults to accepting all. | `(req: CancelRequest) => Promise` | | onError | Called with non-fatal run-scoped errors. | `(error: Ably.ErrorInfo) => void` | -| onSteer | Called once per steering message as it folds into this Run, so the agent can race the arrival against an in-flight model call and decide whether to interrupt it. A hint only; the SDK never interrupts the model call itself, and [`hasInput`](#has-input) remains the authoritative check. Keep it synchronous and cheap. | `() => void` | +| onSteer | Called once per steering message as it folds into this run, so the agent can race the arrival against an in-flight model call and decide whether to interrupt it. A hint only; the SDK never interrupts the model call itself, and [`hasInput`](#has-input) remains the authoritative check. Keep it synchronous and cheap. | `() => void` | ### Returns -An `AgentRun` handle for publishing lifecycle events, user messages, and streamed output. See [AgentRun interface](#run) below. +An [`AgentRun`](#run) handle for publishing lifecycle events, user messages, and streamed output. ## Adopt an existing run {`adoptRun(identity: AdoptIdentity, runtime?: RunRuntime): AdoptedRun`} -Adopt an already-open Run by its identity so a fresh process can publish further Steps and lifecycle events for it. Returns synchronously and does no I/O; publishes nothing to the channel until [`AdoptedRun.load`](#adopted-load) resolves. Use it from a step, tool, or cleanup activity that runs in a separate process from the one that opened the Run. See [Durable execution](/docs/ai-transport/features/durable-execution). +Adopt an already-open run by its identity so a fresh process can publish further steps and lifecycle events for it. Returns synchronously and does no I/O; publishes nothing to the session until [`AdoptedRun.load`](#adopted-load) resolves. Use it from a step, tool, or cleanup activity that [runs in a separate process](/docs/ai-transport/features/durable-execution) from the one that opened the run. ```javascript @@ -177,8 +177,8 @@ await run.load(); | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| identity | required | The Run's identity, threaded across the process boundary by the workflow that opened it. | | -| runtime | optional | Per-Run hooks and an external abort signal. `runId` and `invocationId` overrides do not apply here; identity comes from the `identity` argument. |
| +| identity | required | The run's identity, threaded across the process boundary by the workflow that opened it. |
| +| runtime | optional | Per-run hooks and an external abort signal. `runId` and `invocationId` overrides do not apply here; identity comes from the `identity` argument. |
|
@@ -186,23 +186,23 @@ await run.load(); | Property | Description | Type | | --- | --- | --- | -| runId | The existing Run's id. Authoritative: unlike `createRun`'s continuation path, `AdoptedRun.load` does not re-key the Run from the trigger event's `run-id` header. | String | -| invocationId | This activity's invocation id (a step activity's id, or a cancel-cleanup id). Stamped on every event this process publishes for the Run. Independent of the Run's owner identity. | String | -| triggerEventId | The id of the event whose headers resolve the Run's write-time anchors, typically an `ai-input` for a normal turn. Every activity of an invocation resolves against the same trigger. | String | +| runId | The existing run's id. Authoritative: unlike `createRun`'s continuation path, `AdoptedRun.load` does not re-key the run from the trigger event's `run-id` header. | String | +| invocationId | This activity's invocation id (a step activity's id, or a cancel-cleanup id). Stamped on every event this process publishes for the run. Independent of the run's owner identity. | String | +| triggerEventId | The id of the event whose headers resolve the run's write-time anchors, typically an `ai-input` for a normal turn. Every activity of an invocation resolves against the same trigger. | String | ### Returns
-An `AdoptedRun` handle. Call [`load`](#adopted-load) to resolve the Run's write context off the channel and adopt it for publishing. +An `AdoptedRun` handle. Call [`load`](#adopted-load) to resolve the run's write context off the session and adopt it for publishing. ## AdoptedRun.load {`load(options?: { timeoutMs?: number }): Promise`} -Resolve the Run's write context from the channel and adopt the Run for publishing in this process without emitting a fresh opening event. Awaits the Run's `ai-run-start` on the channel (paging history as needed), pins [`run.view`](#run) to the triggering branch, and checks the Run's status: an active Run is adopted; a suspended or terminal Run rejects. Idempotent; a second call is a no-op. +Resolve the run's write context from the session and adopt the run for publishing in this process without emitting a fresh opening event. Awaits the run's `ai-run-start` on the session (paging history as needed), pins [`run.view`](#run) to the triggering branch, and checks the run's status: an active run is adopted; a suspended or terminal run rejects. Idempotent; a second call is a no-op. -Rejects with `InvalidArgument` when the Run is suspended (resume via `createRun().start()` on a continuation invocation) or terminal (read-only). Rejects with `InputEventNotFound` when the Run's `ai-run-start` is not observed within `timeoutMs`, which is a workflow-ordering error: the adopting activity ran before the opener published. This is retryable. +Rejects with `InvalidArgument` when the run is suspended (resume via `createRun().start()` on a continuation invocation) or terminal (read-only). Rejects with `InputEventNotFound` when the run's `ai-run-start` is not observed within `timeoutMs`, which is a workflow-ordering error: the adopting activity ran before the opener published. This is retryable. ### Parameters @@ -210,17 +210,17 @@ Rejects with `InvalidArgument` when the Run is suspended (resume via `createRun( | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| options.timeoutMs | optional | How long to wait for the Run's `ai-run-start` before rejecting. Defaults to `30000`. | Number | +| options.timeoutMs | optional | How long to wait for the run's `ai-run-start` before rejecting. Defaults to `30000`. | Number | ### Returns -`Promise`. Resolves once the Run is adopted for publishing. The returned `AdoptedRun` retains the full `AgentRun` publish surface (`createStep`, `pipe`, `suspend`, `end`) but omits `start` (the Run was opened elsewhere; publishing another opening event would corrupt its lifecycle). +`Promise`. Resolves once the run is adopted for publishing. The returned `AdoptedRun` retains every `AgentRun` publish method (`createStep`, `pipe`, `suspend`, `end`) but omits `start` (the run was opened elsewhere; publishing another opening event would corrupt its lifecycle). ## AgentRun -The handle returned by [`createRun`](#create-run). It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the agent's lifecycle surface. +The handle returned by [`createRun`](#create-run). It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the agent's lifecycle methods. ### Properties @@ -228,14 +228,14 @@ The handle returned by [`createRun`](#create-run). It extends the shared `BaseRu | Property | Description | Type | | --- | --- | --- | -| runId | The Run's unique identifier. Known synchronously on the agent (it mints the id for a fresh run, or reads it off the triggering input event for a continuation). | String | -| status | The Run's lifecycle status, read live off the tree. | `RunStatus` | +| runId | The run's unique identifier. Known synchronously on the agent (it mints the id for a fresh run, or reads it off the triggering input event for a continuation). | String | +| status | The run's lifecycle status, read live off the tree. | `RunStatus` | | error | The terminal error, present exactly when `status` is `'error'`. | `Ably.ErrorInfo` or Undefined | -| messages | All of this Run's messages: its triggering input followed by its streamed output (across any suspend and resume), deduplicated by `codecMessageId`. The unit to persist. See [Hydrate the conversation](#conversation-hydration). | `TMessage[]` | +| messages | All of this run's messages: its triggering input followed by its streamed output (across any suspend and resume), deduplicated by `codecMessageId`. The unit to [persist and hydrate](#conversation-hydration). | `TMessage[]` | | invocationId | The invocation id minted by the agent for this `createRun` call (one per HTTP request). Readable synchronously; the application returns it on the HTTP response. The agent stamps it on every event it publishes for this invocation. | String | -| abortSignal | `AbortSignal` scoped to this Run. Fires when a cancel event arrives. | `AbortSignal` | -| view | A read-only `View` of the conversation branch this Run belongs to, from its triggering input back to the conversation root. Use it to reconstruct the conversation to feed the model. See [Hydrate the conversation](#conversation-hydration). | `View` | -| located | Resolves once the Run's triggering input (named in its invocation) has been observed on the channel, whether through the live subscription or by paging history with `view.loadOlder()`. [`start`](#run-start) awaits it internally; await it directly only to read the trigger before deciding how to start. | `Promise` | +| abortSignal | `AbortSignal` scoped to this run. Fires when a cancel event arrives. | `AbortSignal` | +| view | A read-only `View` of the conversation branch this run belongs to, from its triggering input back to the conversation root. Use it to [reconstruct the conversation](#conversation-hydration) to feed the model. | `View` | +| located | Resolves once the run's triggering input (named in its invocation) has been observed on the session, whether through the live subscription or by paging history with `view.loadOlder()`. [`start`](#run-start) awaits it internally; await it directly only to read the trigger before deciding how to start. | `Promise` | @@ -243,7 +243,7 @@ The handle returned by [`createRun`](#create-run). It extends the shared `BaseRu {`start(): Promise`} -Wait until the Run's triggering input has been observed on the channel (see [`located`](#run)), then publish the opening lifecycle event (`ai-run-start`, or `ai-run-resume` for a continuation). Must be called before `pipe`, `suspend`, or `end`. +Wait until the run's triggering input has been observed on the session (see [`located`](#run)), then publish the opening lifecycle event (`ai-run-start`, or `ai-run-resume` for a continuation). Must be called before `pipe`, `suspend`, or `end`. There is no built-in deadline: `start()` does not time out waiting for the trigger. It rejects only if the run is cancelled or the session is closed before the trigger is observed. Race it against your own timeout if you need one. @@ -251,7 +251,7 @@ There is no built-in deadline: `start()` does not time out waiting for the trigg {`hasInput(): boolean`} -Drives the agent's loop: returns `true` before the Run has produced any output (the triggering input always needs a first response), and again whenever a steering message has folded into the Run since the previous check. Returns `false` once the Run has produced output and no steering message is pending, or once [`abortSignal`](#run) has fired. +Drives the agent's loop: returns `true` before the run has produced any output (the triggering input always needs a first response), and again whenever a steering message has folded into the run since the previous check. Returns `false` once the run has produced output and no steering message is pending, or once [`abortSignal`](#run) has fired. Calling `hasInput()` drains any pending steering messages: the next output the agent pipes stamps their codec-message-ids, resolving each steering client's [`outcome`](/docs/ai-transport/features/interruption-and-steering#steer) as consumed. There is no observe-only variant; treat every call as a commitment to respond to whatever it reports. @@ -269,7 +269,7 @@ await run.end({ reason: 'complete' }); {`pipe(stream: ReadableStream, options?: PipeOptions): Promise`} -Pipe a `ReadableStream` of outputs through the encoder to the channel. Returns when the stream completes, is cancelled, or errors. Does NOT call `end()`; the caller must call `end()` after `pipe()` returns. +Pipe a `ReadableStream` of outputs through the encoder to the session. Returns when the stream completes, is cancelled, or errors. Does NOT call `end()`; the caller must call `end()` after `pipe()` returns. #### Parameters @@ -309,9 +309,9 @@ Pipe a `ReadableStream` of outputs through the encoder to the channel. Returns w {`createStep(options?: StepOptions): RunStep`} -Create a [`RunStep`](#step): a re-attemptable unit of agent work within this Run. Use it when a retry of the same logical unit must supersede the failed attempt's channel output rather than append beside it, typically inside a workflow-engine activity. Returns synchronously and does no I/O; [`RunStep.start`](#step-start) publishes the opening event. +Create a [`RunStep`](#step): a re-attemptable unit of agent work within this run. Use it when a retry of the same logical unit must supersede the failed attempt's output rather than append beside it, typically inside a workflow-engine activity. Returns synchronously and does no I/O; [`RunStep.start`](#step-start) publishes the opening event. -`options.stepId` controls retry coalescing. Omit it for the common in-process case: the SDK assigns an invocation-scoped id, and an in-process retry after a `'failed'` close reuses that id. Supply an explicit `stepId` when the same logical Step re-attempts in a separate process. Source it from the workflow engine's own stable per-activity id (a [Temporal](/docs/ai-transport/frameworks/temporal) activity id, a [Vercel WDK](/docs/ai-transport/frameworks/vercel-wdk) step id). See [Durable execution](/docs/ai-transport/features/durable-execution). +`options.stepId` controls retry coalescing. Omit it for the common in-process case: the SDK assigns an invocation-scoped id, and an in-process retry after a `'failed'` close reuses that id. Supply an explicit `stepId` when the same logical step [re-attempts in a separate process](/docs/ai-transport/features/durable-execution). Source it from the workflow engine's own stable per-activity id (a [Temporal](/docs/ai-transport/frameworks/temporal) activity id, a [Vercel WDK](/docs/ai-transport/frameworks/vercel-wdk) step id). ```javascript @@ -322,7 +322,7 @@ await step.end(); ``` -The Run must be open first, via [`start`](#run-start) or an adopting [`load`](#adopted-load). Only one Step may be active on a Run at a time; `step.start()` rejects if another Step is still open. If a Step is left open, `run.end()` auto-closes it. +The run must be open first, via [`start`](#run-start) or an adopting [`load`](#adopted-load). Only one step may be active on a run at a time; `step.start()` rejects if another step is still open. If a step is left open, `run.end()` auto-closes it. #### Parameters @@ -339,19 +339,19 @@ The Run must be open first, via [`start`](#run-start) or an adopting [`load`](#a | Property | Description | Type | | --- | --- | --- | | stepId | A stable identifier used to coalesce retries. A fresh attempt under an existing `stepId` supersedes the prior attempt's output. Omit for in-process work; supply the workflow engine's own stable per-activity id for cross-process retries. | String | -| stepClientId | The `clientId` to attribute this Step to. Omit for the common case; the SDK inherits the prior Step's value (sticky), defaulting to the triggering input's publisher for the Run's first Step. Supply an explicit value when a steering message incorporates a fresh input mid-run. | String | +| stepClientId | The `clientId` to attribute this step to. Omit for the common case; the SDK inherits the prior step's value (sticky), defaulting to the triggering input's publisher for the run's first step. Supply an explicit value when a steering message incorporates a fresh input mid-run. | String | #### Returns -A [`RunStep`](#step) handle whose lifecycle mirrors the Run: call `start()` to publish `ai-step-start`, `pipe()` or `send()` to publish output, then `end()` to publish `ai-step-end`. +A [`RunStep`](#step) handle whose lifecycle mirrors the run: call `start()` to publish `ai-step-start`, `pipe()` or `send()` to publish output, then `end()` to publish `ai-step-end`. ### Suspend the run {`suspend(): Promise`} -Publish the `ai-run-suspend` event to the channel, pausing the Run pending external input (a tool approval, a human-in-the-loop response). The Run is not terminal: `RunInfo.status` becomes `'suspended'`, and a continuation Invocation resumes it via `ai-run-resume`. +Publish the `ai-run-suspend` event to the session, pausing the run pending external input (a tool approval, a human-in-the-loop response). The run is not terminal: `RunInfo.status` becomes `'suspended'`, and a continuation invocation resumes it via `ai-run-resume`. Use `suspend` instead of `end` when you want the run to come back. Use `end` only for terminal outcomes. @@ -359,7 +359,7 @@ Use `suspend` instead of `end` when you want the run to come back. Use `end` onl {`end(params: RunEndParams): Promise`} -Publish the `ai-run-end` event to the channel terminally and clean up. `params` is a [`RunEndParams`](#run-end-params) object carrying the terminal `reason` and, when `reason` is `'error'`, an optional `error`. To pause a Run instead of ending it, use [`suspend`](#run-suspend). +Publish the `ai-run-end` event to the session terminally and clean up. `params` is a [`RunEndParams`](#run-end-params) object carrying the terminal `reason` and, when `reason` is `'error'`, an optional `error`. To pause a run instead of ending it, use [`suspend`](#run-suspend). #### Parameters @@ -379,7 +379,7 @@ Publish the `ai-run-end` event to the channel terminally and clean up. `params` ## RunStep -The handle returned by [`AgentRun.createStep`](#create-step). A `RunStep` brackets one re-attemptable unit of agent output on the channel with an `ai-step-start` and an `ai-step-end`. Its `stepId` is stable across retries of the same Step: a retried `ai-step-start` under the same id supersedes the prior attempt's output instead of appending to it. See [Steps](/docs/ai-transport/concepts/steps). +The handle returned by [`AgentRun.createStep`](#create-step). A `RunStep` brackets one re-attemptable [unit of agent output](/docs/ai-transport/concepts/runs#steps) on the session with an `ai-step-start` and an `ai-step-end`. Its `stepId` is stable across retries of the same step: a retried `ai-step-start` under the same id supersedes the prior attempt's output instead of appending to it. ### Properties @@ -387,8 +387,8 @@ The handle returned by [`AgentRun.createStep`](#create-step). A `RunStep` bracke | Property | Description | Type | | --- | --- | --- | -| stepId | This Step's id. Stable across retry attempts of the same Step. | String | -| abortSignal | The Run's `AbortSignal` (the same instance as [`AgentRun.abortSignal`](#run)); there is no per-Step abort. Fires when a cancel arrives for this Run. | `AbortSignal` | +| stepId | This step's id. Stable across retry attempts of the same step. | String | +| abortSignal | The run's `AbortSignal` (the same instance as [`AgentRun.abortSignal`](#run)); there is no per-step abort. Fires when a cancel arrives for this run. | `AbortSignal` | @@ -396,13 +396,13 @@ The handle returned by [`AgentRun.createStep`](#create-step). A `RunStep` bracke {`start(): Promise`} -Publish `ai-step-start`, opening the Step for output. Call once, after the Run is open (via [`start`](#run-start) or an adopting [`load`](#adopted-load)) and before [`pipe`](#step-pipe) or [`send`](#step-send). Idempotent; a second call is a no-op. Rejects if another Step is already active on the Run (only one Step may be open at a time), or if the Run has ended. +Publish `ai-step-start`, opening the step for output. Call once, after the run is open (via [`start`](#run-start) or an adopting [`load`](#adopted-load)) and before [`pipe`](#step-pipe) or [`send`](#step-send). Idempotent; a second call is a no-op. Rejects if another step is already active on the run (only one step may be open at a time), or if the run has ended. ### Pipe outputs {`pipe(stream: ReadableStream, options?: PipeOptions): Promise`} -Pipe an output stream through the encoder to the channel, stamping every output with this Step's `step-id` and its attempt's `start-serial`. Otherwise identical to [`AgentRun.pipe`](#pipe): resolves when the stream completes, is cancelled, or errors. A stream error returns `{ reason: 'error' }` rather than throwing, and marks the Step `'failed'` when [`end`](#step-end) closes it. +Pipe an output stream through the encoder to the session, stamping every output with this step's `step-id` and its attempt's `start-serial`. Otherwise identical to [`AgentRun.pipe`](#pipe): resolves when the stream completes, is cancelled, or errors. A stream error returns `{ reason: 'error' }` rather than throwing, and marks the step `'failed'` when [`end`](#step-end) closes it. #### Parameters @@ -411,7 +411,7 @@ Pipe an output stream through the encoder to the channel, stamping every output | Parameter | Required | Description | Type | | --- | --- | --- | --- | | stream | required | The output stream from your LLM call. | `ReadableStream` | -| options | optional | Per-stream overrides. A per-output `resolveWriteOptions` merges over the Step's default headers, so a normal override leaves `step-id` intact. | | +| options | optional | Per-stream overrides. A per-output `resolveWriteOptions` merges over the step's default headers, so a normal override leaves `step-id` intact. |
|
@@ -423,9 +423,9 @@ Pipe an output stream through the encoder to the channel, stamping every output {`send(output: TOutput): Promise`} -Publish a single discrete output as one assistant message on the channel, stamped with this Step's `step-id` and its attempt's `start-serial`. Use it when the output is already resolved (a tool result, a data payload, a metadata event) rather than a streamed source. Each `send` mints its own `codec-message-id`, so N calls produce N assistant messages, not one. For streamed output from a long-running source, use [`pipe`](#step-pipe) instead. +Publish a single discrete output as one assistant message on the session, stamped with this step's `step-id` and its attempt's `start-serial`. Use it when the output is already resolved (a tool result, a data payload, a metadata event) rather than a streamed source. Each `send` mints its own `codec-message-id`, so N calls produce N assistant messages rather than one. For streamed output from a long-running source, use [`pipe`](#step-pipe) instead. -The Step must be active (started, not ended). Rejects otherwise. A publish failure throws. +The step must be active (started and not yet ended). Rejects otherwise. A publish failure throws. #### Parameters
@@ -441,9 +441,9 @@ The Step must be active (started, not ended). Rejects otherwise. A publish failu {`end(params?: StepEndParams): Promise`} -Publish `ai-step-end`, closing the Step. Idempotent; a second call is a no-op. Omit `params` to derive the reason: `'cancelled'` if the Run was cancelled (its `abortSignal` fired), otherwise `'failed'` if any [`pipe`](#step-pipe) errored, otherwise `'complete'`. Pass an explicit `reason` to override. +Publish `ai-step-end`, closing the step. Idempotent; a second call is a no-op. Omit `params` to derive the reason: `'cancelled'` if the run was cancelled (its `abortSignal` fired), otherwise `'failed'` if any [`pipe`](#step-pipe) errored, otherwise `'complete'`. Pass an explicit `reason` to override. -A Step terminal is not a Run terminal. Drive the Run to [`suspend`](#run-suspend) or [`end`](#run-end) afterwards. If a Step is left open, `run.end()` auto-closes it so observers are never stranded, but an explicit `end()` is clearer and lets you set the reason. +A step terminal is not a run terminal. Drive the run to [`suspend`](#run-suspend) or [`end`](#run-end) afterwards. If a step is left open, `run.end()` auto-closes it so observers are never stranded, but an explicit `end()` is clearer and lets you set the reason. #### Parameters @@ -459,7 +459,7 @@ A Step terminal is not a Run terminal. Drive the Run to [`suspend`](#run-suspend | Property | Description | Type | | --- | --- | --- | -| reason | The terminal reason. Omit to derive it: `'cancelled'` if the Run was cancelled (its `abortSignal` fired), otherwise `'failed'` if any `pipe` errored, otherwise `'complete'`. Pass an explicit value to override. | `'complete' \| 'failed' \| 'cancelled'` | +| reason | The terminal reason. Omit to derive it: `'cancelled'` if the run was cancelled (its `abortSignal` fired), otherwise `'failed'` if any `pipe` errored, otherwise `'complete'`. Pass an explicit value to override. | `'complete' \| 'failed' \| 'cancelled'` | @@ -499,7 +499,7 @@ unsubscribe(); {`detach(): Promise`} -Unsubscribe from cancel messages, abort every active Run's controller (firing their `abortSignal`), detach the channel this session attached, and clean up. Publishes no Run terminal: any still-open Run is left as-is on the channel, to be resumed or cleaned up by another process. This is the escape hatch a durable in-flight activity uses to hand a Run off to the next activity mid-workflow. For a teardown that also closes open Runs, use [`end`](#session-end). +Unsubscribe from cancel messages, abort every active run's controller (firing their `abortSignal`), detach the channel this session attached, and clean up. Publishes no run terminal: any still-open run is left as-is on the session, to be resumed or cleaned up by another process. A durable in-flight activity uses this to leave a run open for the next activity to adopt mid-workflow. For a teardown that also closes open runs, use [`end`](#session-end). The detach is best-effort: a failure (for example, the channel is already `FAILED`) is swallowed and does not reject. Idempotent. @@ -511,15 +511,15 @@ await session.detach(); ### Returns -`Promise`. Resolves once the detach completes. See [Durable execution](/docs/ai-transport/features/durable-execution) for when to prefer `detach` over `end`. +`Promise`. Resolves once the detach completes. [Durable execution](/docs/ai-transport/features/durable-execution) covers when to prefer `detach` over `end`. ## End the session {`end(): Promise`} -Gracefully tear down the session. For every still-open Run this session owns, close its open Step (if any), then publish `ai-run-end` with `reason: 'cancelled'`, then do everything [`detach`](#detach) does. A forgotten `run.end()` on a fire-and-forget turn still closes every observer's stream this way, rather than leaving it stuck on `streaming`. +Gracefully tear down the session. For every still-open run this session owns, close its open step (if any), then publish `ai-run-end` with `reason: 'cancelled'`, then do everything [`detach`](#detach) does. A forgotten `run.end()` on a fire-and-forget turn still closes every observer's stream this way, rather than leaving it stuck on `streaming`. -An open Run ends `'cancelled'`. Not `'complete'` (that would falsely mark an unfinished turn as done), not `'suspend'` (that would hang observers with no resumer; preserve-for-resume is [`detach`](#detach)'s job), not `'error'`. Use `end` as the normal teardown for a non-durable agent. A durable in-flight activity uses [`detach`](#detach) instead, to hand a still-open Run off to the next activity without terminating it. +An open run always ends `'cancelled'`, never `'complete'` (that would falsely mark an unfinished turn as done), `'suspend'` (that would hang observers with no resumer; preserve-for-resume is [`detach`](#detach)'s job), or `'error'`. Use `end` as the normal teardown for a non-durable agent. A durable in-flight activity uses [`detach`](#detach) instead, leaving a still-open run for the next activity to pick up without terminating it. ```javascript @@ -533,7 +533,7 @@ await session.end(); ## Invocation -A value object wrapping the JSON body a client sends to the agent's HTTP endpoint to start a Run. +A value object wrapping the JSON body a client sends to the agent's HTTP endpoint to start a run. ### Build from JSON @@ -564,7 +564,7 @@ const invocation = Invocation.fromJSON(data); | Property | Description | Type | | --- | --- | --- | -| inputEventId | Identifier for the input event on the channel that triggered this invocation. Run identity is resolved from that event's wire headers, not from the body. | String | +| inputEventId | Identifier for the input event on the session that triggered this invocation. Run identity is resolved from that event's wire headers rather than from the body. | String | | sessionName | Logical name of the session, used as the Ably channel name. | String | @@ -573,8 +573,8 @@ const invocation = Invocation.fromJSON(data); Two accessors expose conversation content, at different scopes: -- `run.messages` is all of this Run's own messages: its triggering input plus its streamed output (across any suspend and resume). This is the unit to persist, not the value to feed the model in a multi-turn conversation. -- `run.view` is a read-only, leaf-pinned `View` of this Run's full branch, from its triggering input back to the conversation root. This is the value to feed the model. +- `run.messages` is all of this run's own messages: its triggering input plus its streamed output (across any suspend and resume). This is the unit to persist rather than the value to feed the model in a multi-turn conversation. +- `run.view` is a read-only, leaf-pinned `View` of this run's full branch, from its triggering input back to the conversation root. This is the value to feed the model. `run.view` includes an ancestor turn only once its run has completed. An ancestor that is still active, suspended, cancelled, or errored is omitted, along with the input it replied to, so a dangling tool call from a concurrent or interrupted turn can't invalidate the prompt. The current run is always included, and an omitted ancestor reappears once it completes. @@ -597,13 +597,13 @@ For database-backed hydration, page `run.view` back only to the newest stored me ## RunEndReason -`'complete' | 'cancelled' | 'error'`. The terminal-reason discriminant: it is the `reason` field of the [`RunEndParams`](#run-end-params) you pass to [`Run.end`](#run-end), the `reason` on [`StreamResult`](#pipe-returns), and the value reflected on `RunInfo.status` once the Run terminates. +`'complete' | 'cancelled' | 'error'`. The terminal-reason discriminant: it is the `reason` field of the [`RunEndParams`](#run-end-params) you pass to [`Run.end`](#run-end), the `reason` on [`StreamResult`](#pipe-returns), and the value reflected on `RunInfo.status` once the run terminates. -A Run that pauses for external input (tool approval, human-in-the-loop) uses [`Run.suspend`](#run-suspend) instead of `end`, which publishes `ai-run-suspend` and leaves the Run alive at `RunInfo.status === 'suspended'`. A continuation Invocation resumes it via `ai-run-resume`. +A run that pauses for external input (tool approval, human-in-the-loop) uses [`Run.suspend`](#run-suspend) instead of `end`, which publishes `ai-run-suspend` and leaves the run alive at `RunInfo.status === 'suspended'`. A continuation invocation resumes it via `ai-run-resume`. ## Example -An HTTP handler that sets up the session, creates a Run, rebuilds the conversation from `run.view`, pipes the LLM stream, and ends the Run. +An HTTP handler that sets up the session, creates a run, rebuilds the conversation from `run.view`, pipes the LLM stream, and ends the run. ```javascript diff --git a/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx b/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx index 01112c78f1..1c21cfc7ea 100644 --- a/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx +++ b/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx @@ -9,7 +9,7 @@ redirect_from: The `ClientSession` subscribes to an Ably channel, decodes incoming messages through a codec, and builds a conversation tree. It owns the channel attach and the cancel-publish path, exposes a default branch-aware `View` for rendering, and lets you derive additional views over the same tree. -Construct one with `createClientSession` from the core entry point. For Vercel `UIMessage` channels, use the pre-bound factory from [`@ably/ai-transport/vercel`](/docs/ai-transport/api/javascript/vercel/chat-transport) instead. +Construct one with `createClientSession` from the core entry point. For Vercel `UIMessage` sessions, use the pre-bound factory from [`@ably/ai-transport/vercel`](/docs/ai-transport/api/javascript/vercel/chat-transport) instead. ```javascript @@ -35,10 +35,10 @@ await session.connect(); | Property | Description | Type | | --- | --- | --- | -| tree | The complete conversation tree. Holds every known Run node and emits events on any change. Use `view` in most cases; reach for `tree` for low-level inspection. | | +| tree | The complete conversation tree. Holds every known run node and emits events on any change. Use `view` in most cases; use `tree` for low-level inspection. |
| | view | The default paginated, branch-aware view for rendering. Events scope to the visible messages. |
| | presence | The Ably presence object for the session's channel. Use it to see which clients are connected (`enter`, `leave`, `get`, `subscribe`). The session adds no semantics of its own (it is the same instance the channel exposes), and presence operations implicitly attach, so they work without first awaiting [`connect()`](#connect). | `Ably.RealtimePresence` | -| object | The Ably LiveObjects API for the session's channel. Use it to read and write shared `LiveMap` / `LiveCounter` state on the channel the session already uses; call `get()` to resolve the object. The session adds no semantics; it is the same instance the channel exposes. Operating on it requires the client to be constructed with the `LiveObjects` plugin from `ably/liveobjects` and the object modes to be requested via [`channelModes`](#constructor-params); without both, the underlying SDK throws. See [LiveObjects State](/docs/ai-transport/features/liveobjects). | `RealtimeObject` | +| object | The Ably [LiveObjects](/docs/ai-transport/features/liveobjects) API for the session's channel. Use it to read and write shared `LiveMap` / `LiveCounter` state on the channel the session already uses; call `get()` to resolve the object. The session adds no semantics; it is the same instance the channel exposes. Operating on it requires the client to be constructed with the `LiveObjects` plugin from `ably/liveobjects` and the object modes to be requested via [`channelModes`](#constructor-params); without both, the underlying SDK throws. | `RealtimeObject` |
@@ -46,7 +46,7 @@ await session.connect(); | Property | Description | Type | | --- | --- | --- | -| getRunNode | `(runId: string) => RunNode \| undefined`. Get a Run by id. | Function | +| getRunNode | `(runId: string) => RunNode \| undefined`. Get a run by id. | Function | | getNodeByCodecMessageId | `(codecMessageId: string) => ConversationNode \| undefined`. Get the input or run node that owns a given codec-message-id. Narrow on `kind` (`'input'` or `'run'`) before reading kind-specific fields. | Function | | getSiblingNodes | `(key: string) => ConversationNode[]`. The sibling group for a node key: edit versions for an input node, regenerate siblings for a reply run. Ordered oldest-first by serial; single-element when there are no siblings; empty when the key is unknown. | Function | | on | `(event: 'update' \| 'ably-message' \| 'run' \| 'output', handler) => () => void`. Subscribe to tree changes. Returns an unsubscribe function. | Function | @@ -58,14 +58,14 @@ await session.connect(); | Property | Description | Type | | --- | --- | --- | | getMessages | `() => CodecMessage[]`. Visible messages along the selected branch, each paired with its `codecMessageId`. Read the domain object from each entry's `message` field; correlate back to the transport (run lookups, branch navigation, continuation routing) via `codecMessageId`. | Function | -| runs | `() => RunInfo[]`. Visible Runs along the selected branch. | Function | +| runs | `() => RunInfo[]`. Visible runs along the selected branch. | Function | | runOf | `(codecMessageId: string) => RunInfo \| undefined`. Run that owns the message. | Function | -| run | `(runId: string) => RunInfo \| undefined`. Direct Run lookup by id. | Function | +| run | `(runId: string) => RunInfo \| undefined`. Direct run lookup by id. | Function | | branchSelection | `(codecMessageId: string) => BranchHandle`. The branch siblings at the anchor, plus a `select(index)` verb to switch between them. | Function | | hasOlder | `() => boolean`. Whether older messages can be revealed. | Function | | loadOlder | `(limit?: number) => Promise[]>`. Reveal older messages, resolving to the revealed page (oldest-first); `[]` when nothing older was revealed. | Function | | loadUntil | `(predicate, signal?) => Promise[]>`. Page older history back until `predicate` matches a message (the seam), then resolve to the messages newer than it. Drives database-backed hydration. | Function | -| send | `(events, options?) => Promise>`. Send an input on the channel and fire a POST. At most one new message per send (it mints a fresh Run); the array form carries only wire-only inputs (tool results, approvals) for a continuation. Wrap a domain message via `codec.createUserMessage` to send a fresh user message. | Function | +| send | `(events, options?) => Promise>`. Publish an input on the session. The core session is HTTP-free, so wake the agent by POSTing `run.toInvocation().toJSON()` to your agent endpoint. At most one new message per send (it mints a fresh run); the array form carries only wire-only inputs (tool results, approvals) for a continuation. Wrap a domain message via `codec.createUserMessage` to send a fresh user message. | Function | | regenerate | `(messageId, options?) => Promise>`. Regenerate an assistant message. | Function | | edit | `(messageId, inputs, options?) => Promise>`. Edit a user message. | Function | | on | `(event: 'update' \| 'ably-message' \| 'run', handler) => () => void`. Subscribe to view updates, raw Ably messages for visible nodes, or run lifecycle events. Returns an unsubscribe function. | Function | @@ -104,8 +104,8 @@ const session = createClientSession({ | client | required | The Ably Realtime client. The caller owns its lifecycle; `session.close()` does not close the client. The session's identity is read from this client's `auth.clientId` at publish time, stamped on the wire as the run-owner / input-owner id so other clients can attribute messages. A connection with no concrete clientId (anonymous, or a wildcard `*` token) publishes without one. | `Ably.Realtime` | | channelName | required | The channel to subscribe to and publish cancel signals on. The session owns this channel; do not also resolve it elsewhere with conflicting options. | String | | codec | required | The codec used to encode and decode events and messages. | `Codec` | -| channelModes | optional | Extra channel modes to request on top of the modes AI Transport always needs. Pass `OBJECT_MODES` to use Ably LiveObjects via [`object`](#properties). Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on. See [LiveObjects State](/docs/ai-transport/features/liveobjects). | `Ably.ChannelMode[]` | -| historyPageSize | optional | Wire-message limit fetched per channel-history round trip when paging older history through `view.loadOlder()`, shared by every view on the session. Independent of `loadOlder`'s reveal `limit`: it tunes fetch cost, not reveal granularity. Defaults to 100. | Number | +| channelModes | optional | Extra channel modes to request on top of the modes AI Transport always needs. Pass `OBJECT_MODES` to use Ably [LiveObjects](/docs/ai-transport/features/liveobjects) via [`object`](#properties). Omit to attach with the default mode set. The session requests the union, so extra modes never drop the modes AI Transport relies on. | `Ably.ChannelMode[]` | +| historyPageSize | optional | Wire-message limit fetched per channel-history round trip when paging older history through `view.loadOlder()`, shared by every view on the session. Independent of `loadOlder`'s reveal `limit`: it tunes fetch cost rather than reveal granularity. Defaults to 100. | Number | | logger | optional | Logger instance for diagnostic output. | `Logger` | @@ -158,7 +158,7 @@ session.view.branchSelection(messageId).index; // 0 {`cancel(runId: string): Promise`} -Publish a cancel signal for the specified Run. The agent receives the cancel through its own channel subscription and ends the Run with reason `'cancelled'`. +Publish a cancel signal for the specified run. The agent receives the cancel through its own channel subscription and ends the run with reason `'cancelled'`. ```javascript @@ -188,17 +188,17 @@ await clientRun.cancel(); | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| runId | required | The Run to cancel. Typically obtained from `ClientRun.runId` (after awaiting `clientRun.started`), or from a `RunInfo.runId` in `view.runs()`. | String | +| runId | required | The run to cancel. Typically obtained from `ClientRun.runId` (after awaiting `clientRun.started`), or from a `RunInfo.runId` in `view.runs()`. | String | ### Returns
-`Promise`. Resolves once the cancel message has been published. The cancel is best-effort: if the agent has already ended the Run, the cancel is a no-op. +`Promise`. Resolves once the cancel message has been published. The cancel is best-effort: if the agent has already ended the run, the cancel is a no-op. ## ClientRun -The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the client's control surface, including the run-scoped [`steer`](#steer) verb. +The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the client's control methods, including the run-scoped [`steer`](#steer) verb. ### Properties @@ -206,10 +206,10 @@ The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It | Property | Description | Type | | --- | --- | --- | -| runId | The Run's unique identifier, minted by the agent. Empty until the agent's `ai-run-start` is observed; await [`started`](#client-run-properties) before reading it. | String | -| status | The Run's lifecycle status, read live off the tree. | `RunStatus` | +| runId | The run's unique identifier, minted by the agent. Empty until the agent's `ai-run-start` is observed; await [`started`](#client-run-properties) before reading it. | String | +| status | The run's lifecycle status, read live off the tree. | `RunStatus` | | error | The terminal error, present exactly when `status` is `'error'`. | `Ably.ErrorInfo` or Undefined | -| messages | This Run's whole turn: its originating input followed by the Run's output, deduplicated by `codecMessageId`. | `TMessage[]` | +| messages | This run's whole turn: its originating input followed by the run's output, deduplicated by `codecMessageId`. | `TMessage[]` | | started | Resolves when the agent's `ai-run-start` (or `ai-run-resume`) is observed, the point at which `runId` is populated. No built-in deadline; race it against your own timeout. | `Promise` | | inputCodecMessageId | The triggering input's codec-message-id, owned by the client the moment it publishes. Stream routing and cancel key on this, so it is known synchronously, unlike `runId`. | String | | inputEventId | The input event's unique identifier, stamped on the published input event and forwarded in the POST body so the agent can locate the trigger. | String | @@ -220,7 +220,7 @@ The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It {`steer(input: TInput): SteerResult`} -Publish a codec input event that targets this Run. The steering message carries this Run's `run-id` so the agent folds it into the active Run rather than starting a new Run. Pass the same shape [`view.send`](#properties) accepts, typically `codec.createUserMessage(...)`. The SDK awaits `runId` internally, so this is safe to call as soon as the handle is returned. Once an `ai-run-end` has folded for this Run the handle is dead and further `steer()` calls return immediately-rejected promises. See [Interruption and steering](/docs/ai-transport/features/interruption-and-steering). +Publish a codec input event that targets this run. The [steering message](/docs/ai-transport/features/interruption-and-steering) carries this run's `run-id` so the agent folds it into the active run rather than starting a new run. Pass the same shape [`view.send`](#properties) accepts, typically `codec.createUserMessage(...)`. The SDK awaits `runId` internally, so this is safe to call as soon as the handle is returned. Once an `ai-run-end` has folded for this run the handle is dead and further `steer()` calls return immediately-rejected promises. ```javascript @@ -247,14 +247,14 @@ const { consumed, runTerminalReason } = await outcome; #### Returns -`SteerResult`. Two promises: `published` for the channel-publish acknowledgement, and `outcome` for the consumed determination resolved at the Run's next terminal event. +`SteerResult`. Two promises: `published` for the channel-publish acknowledgement, and `outcome` for the consumed determination resolved at the run's next terminal event.
| Property | Description | Type | | --- | --- | --- | | published | Resolves when the steering message is published to the channel, carrying the Ably-assigned `serial`. Rejects if the publish (or the internal `runId` await) fails. | `Promise<{ serial: string \| undefined }>` | -| outcome | A `Promise` that resolves once a terminal lifecycle event (`ai-run-end`, or `ai-run-suspend` for a consumed steering message) folds for the Run. Rejects if the handle dies first (for example the session closes). | | +| outcome | A `Promise` that resolves once a terminal lifecycle event (`ai-run-end`, or `ai-run-suspend` for a consumed steering message) folds for the run. Rejects if the handle dies first (for example the session closes). |
|
@@ -262,8 +262,8 @@ const { consumed, runTerminalReason } = await outcome; | Property | Description | Type | | --- | --- | --- | -| consumed | `true` when the steering message's codec-message-id appears in the union of `steer-codec-message-ids` stamps on the Run's responses (the agent's loop had it visible when it produced that response), `false` when it never appeared before `ai-run-end`. For an `ai-run-suspend` a not-consumed steering message stays pending. | Boolean | -| runTerminalReason | How the Run ended, present when the outcome was determined by an `ai-run-end`. Absent when determined by an `ai-run-suspend`, since the Run has not ended. | `RunEndReason` | +| consumed | `true` when the steering message's codec-message-id appears in the union of `steer-codec-message-ids` stamps on the run's responses (the agent's loop had it visible when it produced that response), `false` when it never appeared before `ai-run-end`. For an `ai-run-suspend` a not-consumed steering message stays pending. | Boolean | +| runTerminalReason | How the run ended, present when the outcome was determined by an `ai-run-end`. Absent when determined by an `ai-run-suspend`, since the run has not ended. | `RunEndReason` | @@ -271,13 +271,13 @@ const { consumed, runTerminalReason } = await outcome; {`cancel(): Promise`} -Cancel this specific Run. Keyed by [`inputCodecMessageId`](#client-run-properties), which the client owns synchronously, so a cancel issued before the agent mints the `runId` is still honoured (the agent buffers it and fires it once its input-event watcher matches the trigger). Resolves once the cancel is published; it does not wait for [`started`](#client-run-properties). +Cancel this specific run. Keyed by [`inputCodecMessageId`](#client-run-properties), which the client owns synchronously, so a cancel issued before the agent mints the `runId` is still honoured (the agent buffers it and fires it once its input-event watcher matches the trigger). Resolves once the cancel is published; it does not wait for [`started`](#client-run-properties). ### Build the invocation
{`toInvocation(): Invocation`} -Build the [`Invocation`](/docs/ai-transport/api/javascript/core/agent-session#invocation) pointer for this Run, carrying only `inputEventId` and the session's channel name. POST `run.toInvocation().toJSON()` to your agent endpoint to wake the agent; run identity lives on the channel, not in the invocation body. +Build the [`Invocation`](/docs/ai-transport/api/javascript/core/agent-session#invocation) pointer for this run, carrying only `inputEventId` and the session's channel name. POST `run.toInvocation().toJSON()` to your agent endpoint to wake the agent; run identity lives on the channel rather than in the invocation body. ## Subscribe to session errors @@ -317,7 +317,7 @@ unsubscribe(); Tear down the session. Unsubscribe from the channel, close active streams, clear handlers, and prevent further operations. -`close()` is local-state-only. The server keeps streaming until its Runs end on their own. To stop in-progress Runs, call [`cancel`](#cancel) for each before `close()`. +`close()` is local-state-only. The server keeps streaming until its runs end on their own. To stop in-progress runs, call [`cancel`](#cancel) for each before `close()`. ```javascript diff --git a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx index 656a5f3deb..a045ee8c3a 100644 --- a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx +++ b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx @@ -1,13 +1,13 @@ --- title: "Codec" -meta_description: "API reference for the AI Transport Codec interface. Build custom codecs to integrate any AI framework with Ably channels." -meta_keywords: "AI Transport, Codec, Encoder, Decoder, DecodedMessage, CodecInputEvent, CodecOutputEvent, Reducer, custom codec, Ably" +meta_description: "API reference for the AI Transport codec interface. Build custom codecs to integrate any AI framework with Ably channels." +meta_keywords: "AI Transport, codec, Encoder, Decoder, DecodedMessage, CodecInputEvent, CodecOutputEvent, Reducer, custom codec, Ably" redirect_from: - /docs/ai-transport/api-reference/codec - /docs/ai-transport/api/javascript/codec --- -The `Codec` interface is the bridge between an AI framework and Ably channel messages. It describes the wire as a flat stream of input and output events and folds those events into a per-Run projection that the SDK extracts messages from for the conversation tree. +The `Codec` interface is the bridge between an AI framework and Ably channel messages. It describes the wire as a flat stream of input and output events and folds those events into a per-run projection that the SDK extracts messages from for the conversation tree. Implement `Codec` to integrate any AI framework with AI Transport. The SDK ships the Vercel codec via [`createUIMessageCodec()`](/docs/ai-transport/api/javascript/vercel/codec) for the Vercel AI SDK. For other frameworks, implement the methods below. @@ -35,11 +35,11 @@ const myCodec: Codec = { | Property | Description | Type | | --- | --- | --- | -| init | Build an empty initial projection. Called once per Run before any events are folded. | `() => TProjection` | +| init | Build an empty initial projection. Called once per run before any events are folded. | `() => TProjection` | | fold | Fold one event into the projection and return the updated projection. May mutate `state` in place. | `(state, event, meta) => TProjection` | | createEncoder | Create a stateful encoder bound to the given channel writer. Takes optional . | `(channel, options?) => Encoder` | | createDecoder | Create a stateful decoder that maps inbound Ably messages to typed inputs and outputs. Each call returns a
. | `() => Decoder` | -| getMessages | Extract the per-message list from a projection as `{ codecMessageId, message }` pairs. The SDK uses the pairs to correlate rendered messages back to the wire id; the View concatenates them across the visible Run chain. | `(projection) => CodecMessage[]` | +| getMessages | Extract the per-message list from a projection as `{ codecMessageId, message }` pairs. The SDK uses the pairs to correlate rendered messages back to the wire id; the View concatenates them across the visible run chain. | `(projection) => CodecMessage[]` | | createUserMessage | Wrap a `TMessage` as the codec's `UserMessage` variant for publishing on the `ai-input` wire. Returns the codec's `TInput` so the result is usable as a `View.send` argument without a cast. | `(message) => TInput` | | createRegenerate | Build a `Regenerate` input that targets an assistant message. The View calls this from `regenerate()`. | `(target, parent) => TInput` | | createToolResult | Optional. Build a `ToolResult` input addressed at the assistant codec-message containing the tool call. The codec defines the payload shape (for example the Vercel layer's `{ toolCallId, output }`). | `(codecMessageId, payload) => TInput` | @@ -213,7 +213,7 @@ Tagged result of decoding one inbound Ably message. The codec routes by the wire ## Well-known input variants -Codec input variants extend the `CodecInputEvent` base. The SDK reserves a handful of well-known `kind` literals so callers can publish a user message, regenerate an assistant message, or resolve a tool call without inventing their own shapes. Codec-specific variants pick any other literal. (Edits ride the user-message path with a `forkOf` routing header; there is no separate `'edit'` kind on the wire.) +Codec input variants extend the `CodecInputEvent` base. The SDK reserves a handful of well-known `kind` literals so callers can publish a user message, regenerate an assistant message, or resolve a tool call without inventing their own shapes. Codec-specific variants pick any other literal. (Edits go on the user-message path with a `forkOf` routing header; there is no separate `'edit'` kind on the wire.)
diff --git a/src/pages/docs/ai-transport/api/javascript/temporal/index.mdx b/src/pages/docs/ai-transport/api/javascript/temporal/index.mdx index 2969426433..ff64390de5 100644 --- a/src/pages/docs/ai-transport/api/javascript/temporal/index.mdx +++ b/src/pages/docs/ai-transport/api/javascript/temporal/index.mdx @@ -18,11 +18,11 @@ The subpath declares `@temporalio/activity` as an optional peer dependency; inst {`stepIdFor(invocationId: string): string`} -Read the current Temporal activity's id and combine it with the Run's invocation id to produce a `stepId` that survives retries and never collides across workflows. Pass the result to [`AgentRun.createStep`](/docs/ai-transport/api/javascript/core/agent-session#create-step). +Read the current Temporal activity's id and combine it with the run's invocation id to produce a `stepId` that survives retries and never collides across workflows. Pass the result to [`AgentRun.createStep`](/docs/ai-transport/api/javascript/core/agent-session#create-step). -Temporal's `activityId` is unique within a single workflow. AI Transport's Step supersede semantics operate at the whole-Run lifetime, so bare `activityId`s would collide when two workflows publish to the same Run (a suspend followed by a continuation), and the SDK would treat the two workflows' first Steps as retries of the same Step. Prefixing with the Run's invocation id keeps each Step's identity globally distinct while still letting a retry of the same activity coalesce cleanly. +Temporal's `activityId` is unique within a single workflow. AI Transport's step supersede semantics operate at the whole-run lifetime, so bare `activityId`s would collide when two workflows publish to the same run (a suspend followed by a continuation), and the SDK would treat the two workflows' first steps as retries of the same step. Prefixing with the run's invocation id keeps each step's identity globally distinct while still letting a retry of the same activity coalesce cleanly. -The helper reads `Context.current().info.activityId` internally. Call it from inside a Temporal activity, not from workflow code. +The helper reads `Context.current().info.activityId` internally. Call it from inside a Temporal activity rather than from workflow code. ```javascript @@ -57,7 +57,7 @@ export async function runInferenceStep(input) { | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| invocationId | required | The Run's invocation id, sourced from the SDK-supplied `input.ids.invocationId`. This value is also typically used as the Temporal `workflowId` when the workflow is started, so the two ids match. | String | +| invocationId | required | The run's invocation id, sourced from the SDK-supplied `input.ids.invocationId`. This value is also typically used as the Temporal `workflowId` when the workflow is started, so the two ids match. | String |
@@ -67,7 +67,7 @@ export async function runInferenceStep(input) { ## Example -A worker registering an activity that adopts an in-flight Run, opens a Step under the Temporal-derived id, and publishes a discrete tool result: +A worker registering an activity that adopts an in-flight run, opens a step under the Temporal-derived id, and publishes a discrete tool result: ```javascript @@ -106,4 +106,4 @@ export async function runToolStep({ ids, invocation, toolCall, output }) { ``` -A retry of the same activity re-enters the code with the same Temporal `activityId`, so `stepIdFor` returns the same id and the retry's `ai-step-start` supersedes the failed attempt's output on the channel. See [Durable execution](/docs/ai-transport/features/durable-execution) for the full pattern. +A retry of the same activity re-enters the code with the same Temporal `activityId`, so `stepIdFor` returns the same id and the retry's `ai-step-start` [supersedes the failed attempt's output](/docs/ai-transport/features/durable-execution) on the session. diff --git a/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx b/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx index 2b40e470ca..b8fb58bc27 100644 --- a/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx +++ b/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx @@ -7,7 +7,7 @@ redirect_from: - /docs/ai-transport/api/javascript/vercel --- -The Vercel entry point pre-binds the [Vercel codec](/docs/ai-transport/api/javascript/vercel/codec) and exposes a `ChatTransport` adapter that satisfies the contract Vercel AI SDK's `useChat` hook expects. Use these factories whenever you build a chat UI on top of `useChat` so you do not have to wire the codec yourself. +The Vercel entry point pre-binds the Vercel codec and exposes a `ChatTransport` adapter that satisfies the contract Vercel AI SDK's `useChat` hook expects. Use these factories whenever you build a chat UI on top of `useChat` so you do not have to wire the codec yourself. ```javascript @@ -27,13 +27,13 @@ const chatTransport = createChatTransport(session); ``` -React-side wiring (`ChatTransportProvider`, `useChatTransport`, `useMessageSync`) lives at [`@ably/ai-transport/vercel/react`](/docs/ai-transport/api/react/vercel/use-chat-transport). +The React-side wiring (`ChatTransportProvider`, `useChatTransport`, `useMessageSync`) is in [`@ably/ai-transport/vercel/react`](/docs/ai-transport/api/react/vercel/use-chat-transport). ## Create a Vercel client session {`function createClientSession(options: VercelClientSessionOptions): ClientSession`} -A pre-bound version of the core [`createClientSession`](/docs/ai-transport/api/javascript/core/client-session#constructor) with the Vercel codec already supplied. The `api` default is set on [`createChatTransport`](#create-chat-transport), not here. +A pre-bound version of the core [`createClientSession`](/docs/ai-transport/api/javascript/core/client-session#constructor) with the Vercel codec already supplied. The `api` default is set on [`createChatTransport`](#create-chat-transport) rather than here. ```javascript diff --git a/src/pages/docs/ai-transport/api/javascript/vercel/codec.mdx b/src/pages/docs/ai-transport/api/javascript/vercel/codec.mdx index 339787ed13..7e1b0ba1f0 100644 --- a/src/pages/docs/ai-transport/api/javascript/vercel/codec.mdx +++ b/src/pages/docs/ai-transport/api/javascript/vercel/codec.mdx @@ -4,7 +4,7 @@ meta_description: "API reference for createUIMessageCodec, the factory for the p meta_keywords: "Vercel AI SDK, AI Transport, createUIMessageCodec, UIMessageCodec, VercelInput, VercelOutput, VercelProjection, codec, Ably" --- -`createUIMessageCodec()` returns the pre-built codec for the Vercel AI SDK. The returned codec implements [`Codec`](/docs/ai-transport/api/javascript/core/codec) so a session can encode `UIMessageChunk` events out and decode them back into `UIMessage` objects without a custom implementation. +`createUIMessageCodec()` returns the pre-built codec for the Vercel AI SDK. The returned codec implements `Codec` so a session can encode `UIMessageChunk` events out and decode them back into `UIMessage` objects without a custom implementation. You rarely call `createUIMessageCodec()` yourself. The Vercel-pre-bound [`createClientSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-client-session) and [`createAgentSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-agent-session) already supply the codec. Call it explicitly when you build a codec wrapper, run the encoder or decoder outside of a session, or compose the codec into a different codec. @@ -20,7 +20,7 @@ const projection = codec.init(); ## Properties -`createUIMessageCodec()` is a factory, not a type. Each call returns a fresh, stateless codec value that satisfies the full [`Codec`](/docs/ai-transport/api/javascript/core/codec) interface for the Vercel `TInput`, `TOutput`, `TProjection`, and `TMessage` parameters listed below. Because the codec is stateless, hold a single instance at module scope (or, in React, `useMemo(() => createUIMessageCodec(), [])`) and reuse it rather than calling the factory inline on every render. +`createUIMessageCodec()` is a factory rather than a type. Each call returns a fresh, stateless codec value that satisfies the full [`Codec`](/docs/ai-transport/api/javascript/core/codec) interface for the Vercel `TInput`, `TOutput`, `TProjection`, and `TMessage` parameters listed below. Because the codec is stateless, hold a single instance at module scope (or, in React, `useMemo(() => createUIMessageCodec(), [])`) and reuse it rather than calling the factory inline on every render. @@ -41,7 +41,7 @@ const projection = codec.init(); ## Type parameters -`createUIMessageCodec` is generic over the AI SDK's three `UIMessage` type parameters — `createUIMessageCodec()`. Supplying concrete types specialises the codec so `getMessages` (and a session's `view.getMessages()`) return messages whose `metadata`, data parts, and tool parts carry your types instead of the SDK defaults; omitting them preserves the default inference. The same parameters thread through [`createClientSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-client-session), [`createAgentSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-agent-session), and [`createChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-chat-transport). See [Typed messages](/docs/ai-transport/frameworks/vercel-ai-sdk-ui#typed-messages) for the end-to-end pattern. +`createUIMessageCodec` is generic over the AI SDK's three `UIMessage` type parameters, as `createUIMessageCodec()`. Supplying concrete types specialises the codec so `getMessages` (and a session's `view.getMessages()`) return messages whose `metadata`, data parts, and tool parts carry your types instead of the SDK defaults; omitting them preserves the default inference. The same parameters thread through [`createClientSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-client-session), [`createAgentSession`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-agent-session), and [`createChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport#create-chat-transport), and [typed messages](/docs/ai-transport/frameworks/vercel-ai-sdk-ui#typed-messages) shows the end-to-end pattern. `VercelInput`, `VercelOutput`, and `VercelProjection` are likewise generic over the same three parameters, each defaulting to the SDK default. The tables below show the default instantiation. @@ -65,7 +65,7 @@ const projection = codec.init(); | Property | Description | Type | | --- | --- | --- | -| TProjection | Per-Run projection. Carries the `{ codecMessageId, message }` pair list plus internal stream-tracker state and a high-water-mark serial for idempotency. The SDK does not inspect this shape. Reach for `getMessages` instead. | `{ messages: CodecMessage[], ... }` | +| TProjection | Per-run projection. Carries the `{ codecMessageId, message }` pair list plus internal stream-tracker state and a high-water-mark serial for idempotency. The SDK does not inspect this shape. Use `getMessages` instead. | `{ messages: CodecMessage[], ... }` |
diff --git a/src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx b/src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx index 46245cf41e..2ba0e143ea 100644 --- a/src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx +++ b/src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx @@ -1,10 +1,10 @@ --- title: "vercelRunOutcome" -meta_description: "API reference for vercelRunOutcome, the helper that maps a Vercel streamText finishReason and Run.pipe result to a VercelRunOutcome object for Run.suspend or Run.end." +meta_description: "API reference for vercelRunOutcome, the helper that maps a Vercel streamText finishReason and run.pipe result to a VercelRunOutcome object for run.suspend or run.end." meta_keywords: "AI Transport, vercelRunOutcome, VercelRunOutcome, RunEndParams, RunEndReason, streamText, finishReason, Vercel AI SDK, Ably" --- -`vercelRunOutcome` resolves the outcome of a Vercel `streamText` response that was piped through [`Run.pipe`](/docs/ai-transport/api/javascript/core/agent-session#pipe). It returns a [`VercelRunOutcome`](#vercel-run-outcome-returns) object discriminated on `reason`: when `reason` is `'suspend'`, call [`Run.suspend`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend); otherwise pass the whole outcome to [`Run.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end) (the non-`'suspend'` arms are assignable to [`RunEndParams`](/docs/ai-transport/api/javascript/core/agent-session#run-end-params)). +`vercelRunOutcome` resolves the outcome of a Vercel `streamText` response that was piped through `Run.pipe`. It returns a [`VercelRunOutcome`](#vercel-run-outcome-returns) object discriminated on `reason`: when `reason` is `'suspend'`, call `Run.suspend`; otherwise pass the whole outcome to `Run.end` (the non-`'suspend'` arms are assignable to `RunEndParams`). It preserves transport-level outcomes (`'cancelled'`, `'error'`) from the pipe result. When the pipe completed naturally, it awaits Vercel's `finishReason` and returns `{ reason: 'suspend' }` for `'tool-calls'` (the LLM requested tools the SDK did not auto-execute, so the run should pause for the next client-published tool result), or `{ reason: 'complete' }` otherwise. An `'error'` outcome carries the wrapped `error`. @@ -39,7 +39,7 @@ The helper applies two rules: - Any other Vercel finish reason returns `{ reason: 'complete' }`. - If `finishReason` rejects, classify the rejection: abort-shaped errors return `{ reason: 'cancelled' }`; anything else (for example `NoOutputGeneratedError`) returns `{ reason: 'error', error }` with the rejection wrapped as an `Ably.ErrorInfo`. -The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason` with the abort signal's reason when the stream is aborted before any step completes, and with `NoOutputGeneratedError` when the model produced nothing at all. Without the guard the rejection would bubble out of the route handler, skip `Run.end`, and leave the run with no `ai-run-end` event on the channel. +The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason` with the abort signal's reason when the stream is aborted before any step completes, and with `NoOutputGeneratedError` when the model produced nothing at all. Without the guard the rejection would bubble out of the route handler, skip `Run.end`, and leave the run with no `ai-run-end` event on the session. ### Parameters @@ -65,7 +65,7 @@ The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason `Promise`. A `VercelRunOutcome` is an object discriminated on `reason` with three arms: -- `{ reason: 'suspend' }`: the LLM requested tools the SDK did not auto-execute. Call [`Run.suspend`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend) so a continuation Invocation can resume the Run. +- `{ reason: 'suspend' }`: the LLM requested tools the SDK did not auto-execute. Call [`Run.suspend`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend) so a continuation invocation can resume the run. - `{ reason: 'complete' | 'cancelled' }`: the run terminated normally. Pass the outcome to [`Run.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end). - `{ reason: 'error', error }`: the run failed. `error` is an `Ably.ErrorInfo`. Pass the outcome to [`Run.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end). @@ -75,7 +75,7 @@ The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason | Property | Description | Type | | --- | --- | --- | -| reason | The terminal reason, or `'suspend'` to pause the Run. | `'suspend' \| 'complete' \| 'cancelled' \| 'error'` | +| reason | The terminal reason, or `'suspend'` to pause the run. | `'suspend' \| 'complete' \| 'cancelled' \| 'error'` | | error | The terminal error, present only when `reason` is `'error'`. | `Ably.ErrorInfo` | diff --git a/src/pages/docs/ai-transport/api/react/core/providers.mdx b/src/pages/docs/ai-transport/api/react/core/providers.mdx index a967093aa9..a9ecfdb0d6 100644 --- a/src/pages/docs/ai-transport/api/react/core/providers.mdx +++ b/src/pages/docs/ai-transport/api/react/core/providers.mdx @@ -38,7 +38,7 @@ The provider reads the Ably Realtime client from the surrounding ` If `createClientSession` throws, the error is stored on the slot alongside an undefined session. `useClientSession` surfaces it as [`sessionError`](/docs/ai-transport/api/react/core/use-client-session#returns) so the component tree does not crash. -The provider also renders an ably-js `` for the session's channel, so ably-js's channel hooks (`usePresence`, `usePresenceListener`, `useChannel`) work for any descendant without wrapping the subtree in your own ``. Pass the same `channelName` you gave the provider. See [Agent presence](/docs/ai-transport/features/agent-presence#react). +The provider also renders an ably-js `` for the session's channel, so ably-js's channel hooks (`usePresence`, `usePresenceListener`, `useChannel`) work for any descendant without wrapping the subtree in your own ``, which is what [agent presence in React](/docs/ai-transport/features/agent-presence#react) relies on. Pass the same `channelName` you gave the provider. ### Props diff --git a/src/pages/docs/ai-transport/api/react/core/use-ably-messages.mdx b/src/pages/docs/ai-transport/api/react/core/use-ably-messages.mdx index 08bd91a35a..b757a587f3 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-ably-messages.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-ably-messages.mdx @@ -46,7 +46,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api The accumulated raw Ably messages in chronological order. Resets to an empty array when the resolved session changes. -Each `InboundMessage` carries the wire fields you would see from `channel.subscribe`: `name`, `data`, `serial`, `clientId`, `extras`, `timestamp`, and so on. The full type lives in the [Ably SDK reference](/docs/pub-sub/api/javascript/realtime/messages#message). +Each `InboundMessage` carries the wire fields you would see from `channel.subscribe`: `name`, `data`, `serial`, `clientId`, `extras`, `timestamp`, and so on. The [Ably SDK reference](/docs/pub-sub/api/javascript/realtime/messages#message) documents the full type. ## Example diff --git a/src/pages/docs/ai-transport/api/react/core/use-client-session.mdx b/src/pages/docs/ai-transport/api/react/core/use-client-session.mdx index d5838c7061..1c135bfe12 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-client-session.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-client-session.mdx @@ -7,7 +7,7 @@ redirect_from: - /docs/ai-transport/api/react/use-client-transport --- -`useClientSession` reads a `ClientSession` from the nearest [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider). It is a thin context reader that never throws during render. If no matching provider exists or the session failed to construct, the hook returns a stub session and a populated `sessionError` so the component can render an error state without an error boundary. +`useClientSession` reads a `ClientSession` from the nearest `ClientSessionProvider`. It is a thin context reader that never throws during render. If no matching provider exists or the session failed to construct, the hook returns a stub session and a populated `sessionError` so the component can render an error state without an error boundary. ```javascript @@ -51,7 +51,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api {`session: ClientSession`} -The resolved [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session). All session methods are available, including `view`, `tree`, `connect`, `cancel`, `createView`, `on`, and `close`. The provider already invokes `connect()` once per mount, so application code rarely calls it directly. The hooks `useView`, `useTree`, `useCreateView`, and `useAblyMessages` resolve their session through the same provider, so reach for them whenever you want reactive state rather than the raw session. +The resolved [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session). All session methods are available, including `view`, `tree`, `connect`, `cancel`, `createView`, `on`, and `close`. The provider already invokes `connect()` once per mount, so application code rarely calls it directly. The hooks `useView`, `useTree`, `useCreateView`, and `useAblyMessages` resolve their session through the same provider, so use them whenever you want reactive state rather than the raw session. ## Read the session error diff --git a/src/pages/docs/ai-transport/api/react/core/use-create-view.mdx b/src/pages/docs/ai-transport/api/react/core/use-create-view.mdx index 21ecb3ee91..fc7433a035 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-create-view.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-create-view.mdx @@ -7,7 +7,7 @@ redirect_from: - /docs/ai-transport/api/react/use-create-view --- -`useCreateView` creates a new [`ClientView`](/docs/ai-transport/api/javascript/core/client-session#create-view) over the same conversation tree as the session's default view, then subscribes to it the same way [`useView`](/docs/ai-transport/api/react/core/use-view) does. Each created view has its own branch selections and pagination state. The view is closed automatically on unmount or when the session reference changes. +`useCreateView` creates a new `ClientView` over the same conversation tree as the session's default view, then subscribes to it the same way `useView` does. Each created view has its own branch selections and pagination state. The view is closed automatically on unmount or when the session reference changes. Use this hook when you need two independent renderings of the same conversation, for example, a split-pane diff between two regenerate siblings, or a secondary panel that pages older history without affecting the main scroll position. @@ -37,7 +37,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| session | optional | A client session to create a view from. Defaults to the nearest provider. Pass `null` to defer creation. | `ClientSession` | +| session | optional | A client session to create a view from, wrapping its [`createView`](/docs/ai-transport/api/javascript/core/client-session#create-view) method. Defaults to the nearest provider. Pass `null` to defer creation. | `ClientSession` | | limit | optional | Number of older codecMessages to reveal per page. When provided, auto-loads the first page on mount. | Number | | skip | optional | When `true`, skip view creation and return an empty handle immediately. | Boolean | @@ -56,17 +56,17 @@ Returns the same [`ViewHandle`](/docs/ai-transport/api/react/core/use-view#retur | loading | Whether a page load is currently in progress. | Boolean | | loadError | Set when the most recent `loadOlder` call failed. | `Ably.ErrorInfo` or Undefined | | loadOlder | Reveal older messages, resolving to the revealed page (oldest-first); `[]` when nothing was revealed. | `() => Promise[]>` | -| runOf | Look up the `RunInfo` for the Run that owns a `codecMessageId`. | `(codecMessageId) => RunInfo \| undefined` | -| run | Direct lookup by Run id. | `(runId) => RunInfo \| undefined` | -| runs | Snapshot of the visible Runs along the selected branch, in chronological order. | `() => RunInfo[]` | +| runOf | Look up the `RunInfo` for the run that owns a `codecMessageId`. | `(codecMessageId) => RunInfo \| undefined` | +| run | Direct lookup by run id. | `(runId) => RunInfo \| undefined` | +| runs | Snapshot of the visible runs along the selected branch, in chronological order. | `() => RunInfo[]` | | branchSelection | Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb. | `(codecMessageId) => BranchHandle` | -| send | Send an input on the channel and fire a POST (at most one new message per send). Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. | `(events, options?) => Promise>` | +| send | Publish an input on the session, at most one new message per send. The core session is HTTP-free, so wake the agent by POSTing `run.toInvocation().toJSON()` to your agent endpoint. Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. | `(events, options?) => Promise>` | | regenerate | Regenerate an assistant message, using this view's branch. | `(messageId, options?) => Promise>` | | edit | Edit a user message, forking from this view's branch. | `(messageId, inputs, options?) => Promise>` | -See [`useView`](/docs/ai-transport/api/react/core/use-view#returns) for the field-by-field behaviour of each property. Every method behaves the same way, scoped to the view this hook created. +[`useView`](/docs/ai-transport/api/react/core/use-view#returns) documents the field-by-field behaviour of each property. Every method behaves the same way, scoped to the view this hook created. ## Example diff --git a/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx b/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx index 65f919a8d7..1fe970497d 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx @@ -1,10 +1,10 @@ --- title: "useMessagesWithSeed" -meta_description: "Reconcile a persisted conversation seed with the live AI Transport channel from React and render the composed conversation." +meta_description: "Reconcile a persisted conversation seed with the live AI Transport session from React and render the composed conversation." meta_keywords: "AI Transport, React, useMessagesWithSeed, seed, hydration, loadUntil, Ably" --- -`useMessagesWithSeed` reconciles a persisted conversation seed with the live Ably channel and returns the composed conversation, oldest-first: the seed followed by the live tail that has not yet been seeded. It is the core primitive behind database-backed [hydration](/docs/ai-transport/features/database-hydration), and the Vercel `useMessageSync` builds on it. +`useMessagesWithSeed` reconciles a persisted conversation seed with the live session and returns the composed conversation, oldest-first: the seed followed by the live tail that has not yet been seeded. It is the core primitive behind database-backed hydration, and the Vercel `useMessageSync` builds on it. Pass the `view` from a resolved session. [`useClientSession()`](/docs/ai-transport/api/react/core/providers#client-session-provider) returns a handle whose `session.view` is a `View`. @@ -22,7 +22,7 @@ const messages = useMessagesWithSeed({ This hook is typically used within a [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider), which resolves the session whose `view` you pass in. -The hook takes the newest seed message's id, via `getMessageId`, as the seam and drives [`View.loadUntil`](/docs/ai-transport/api/javascript/core/client-session) to page the channel back until that id reappears. It then composes the seed followed by the live tail, dropping the single overlapping message at the seam. With no seed (an empty array) it surfaces the live channel window unchanged. +The hook takes the newest seed message's id, via `getMessageId`, as the seam and drives [`View.loadUntil`](/docs/ai-transport/api/javascript/core/client-session) to page the session back until that id reappears. It then composes the seed followed by the live tail, dropping the single overlapping message at the seam. With no seed (an empty array) it surfaces the live session window unchanged. ## Parameters @@ -32,10 +32,10 @@ The hook takes the newest seed message's id, via `getMessageId`, as the seam and | Parameter | Required | Description | Type | | --- | --- | --- | --- | -| view | optional | The view over the live channel to reconcile against, for example `session.view`, or `undefined` before the session or view resolves. The hook then surfaces the seed as-is. | `View` or Undefined | -| seed | required | The persisted conversation, oldest-first. Compared by content, so passing a fresh array each render, for example `data ?? []`, is safe. An empty array is a loaded-but-empty conversation and surfaces the live channel window unchanged. While the seed is still loading, set `skip` instead of passing `[]`. | `TMessage[]` | -| getMessageId | required | Returns a message's stable domain id, the seam key shared between your store and the channel. The transport's internal `codecMessageId` is never persisted. | `(message: TMessage) => string` | -| skip | optional | Holds the reconciliation while the seed is still loading, for example during an async store fetch. When `true` the hook does not page the channel and returns `[]`. Clear it once the seed has loaded. Defaults to `false`. | Boolean | +| view | optional | The view over the live session to reconcile against, for example `session.view`, or `undefined` before the session or view resolves. The hook then surfaces the seed as-is. | `View` or Undefined | +| seed | required | The persisted conversation, oldest-first. Compared by content, so passing a fresh array each render, for example `data ?? []`, is safe. An empty array is a loaded-but-empty conversation and surfaces the live session window unchanged. While the seed is still loading, set `skip` instead of passing `[]`. | `TMessage[]` | +| getMessageId | required | Returns a message's stable domain id, the seam key shared between your store and the session. The transport's internal `codecMessageId` is never persisted. | `(message: TMessage) => string` | +| skip | optional | Holds the reconciliation while the seed is still loading, for example during an async store fetch. When `true` the hook does not page the session and returns `[]`. Clear it once the seed has loaded. Defaults to `false`. | Boolean | diff --git a/src/pages/docs/ai-transport/api/react/core/use-tree.mdx b/src/pages/docs/ai-transport/api/react/core/use-tree.mdx index c148f6d83a..7a7cee358c 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-tree.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-tree.mdx @@ -7,9 +7,9 @@ redirect_from: - /docs/ai-transport/api/react/use-tree --- -`useTree` exposes stable structural query callbacks backed by a session's [`Tree`](/docs/ai-transport/api/javascript/core/client-session). The returned methods are thin `useCallback` wrappers around `session.tree` and never trigger re-renders on their own. +`useTree` exposes stable structural query callbacks backed by a session's `Tree`. The returned methods are thin `useCallback` wrappers around `session.tree` and never trigger re-renders on their own. -Use this hook when you need to inspect tree structure outside the visible branch (for example, to count edit or regenerate siblings on a node before showing navigation arrows). Branch navigation for the currently visible chain lives on [`useView`](/docs/ai-transport/api/react/core/use-view). +Use this hook when you need to inspect tree structure outside the visible branch (for example, to count edit or regenerate siblings on a node before showing navigation arrows). [`useView`](/docs/ai-transport/api/react/core/use-view) covers branch navigation for the currently visible chain. ```javascript @@ -43,7 +43,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api | Property | Description | Type | | --- | --- | --- | -| getRunNode | Get a Run by `runId`, or `undefined` if not found. | | +| getRunNode | Get a run by `runId`, or `undefined` if not found. |
| | getNodeByCodecMessageId | Get the input or run node that owns a given `codecMessageId`, or `undefined` if not observed. Narrow on `kind` (`'input'` or `'run'`) before reading kind-specific fields. | `(codecMessageId) => ConversationNode \| undefined` | | getSiblingNodes | Get the sibling group for a node key (edit versions for an input node, regenerate siblings for a reply run). Ordered oldest-first by serial; single-element array when the node has no siblings; empty when the key is unknown. | `(key) => ConversationNode[]` | @@ -54,24 +54,24 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api | Property | Description | Type | | --- | --- | --- | | kind | Discriminator. Always `'run'`. | `'run'` | -| runId | The Run's unique identifier. Minted by the agent. | String | -| parentCodecMessageId | The codec-message-id of the input node this Run is rooted at (the user prompt the agent replied to). `undefined` for the root Run. | String or Undefined | -| forkOf | The node key of the node this Run replaces, or `undefined` if this Run is not a fork. | String or Undefined | -| regeneratesCodecMessageId | The codec-message-id this Run regenerates, or `undefined` for non-regenerate Runs. | String or Undefined | -| clientId | Identity of the Ably client that started this Run. | String | +| runId | The run's unique identifier. Minted by the agent. | String | +| parentCodecMessageId | The codec-message-id of the input node this run is rooted at (the user prompt the agent replied to). `undefined` for the root run. | String or Undefined | +| forkOf | The node key of the node this run replaces, or `undefined` if this run is not a fork. | String or Undefined | +| regeneratesCodecMessageId | The codec-message-id this run regenerates, or `undefined` for non-regenerate runs. | String or Undefined | +| clientId | Identity of the Ably client that started this run. | String | | state | Run lifecycle state, discriminated on `status`. `{ status }` is `'active'` while streaming, `'suspended'` while paused awaiting input, or a non-error terminal `RunEndReason`; the `'error'` arm pairs `status: 'error'` with the terminal `error`. | `RunNodeState` | -| projection | Per-Run codec projection. Folded by the Tree from every event published under this run-id. | `TProjection` | -| invocationId | The agent-minted `invocationId` observed for this Run, adopted from the `ai-run-start` event. Empty string until run-start arrives, or if the wire didn't carry an invocation-id. | String | +| projection | Per-run codec projection. Folded by the Tree from every event published under this run-id. | `TProjection` | +| invocationId | The agent-minted `invocationId` observed for this run, adopted from the `ai-run-start` event. Empty string until run-start arrives, or if the wire didn't carry an invocation-id. | String | | startSerial | Ably serial of the first observed message tagged with this run-id. | String or Undefined | | endSerial | Ably serial of the run-end lifecycle event. | String or Undefined |
-## Look up a Run by id
+## Look up a run by id {`getRunNode(runId: string): RunNode | undefined`} -Return the full [`RunNode`](#returns) record for a given `runId`, or `undefined` if the Run has not been observed. Use this when you need fields the View's [`RunInfo`](/docs/ai-transport/api/react/core/use-view#returns) does not expose (parent / fork relationships, the raw projection, serials). +Return the full [`RunNode`](#returns) record for a given `runId`, or `undefined` if the run has not been observed. Use this when you need fields the View's [`RunInfo`](/docs/ai-transport/api/react/core/use-view#returns) does not expose (parent / fork relationships, the raw projection, serials). ## Look up a node by codec-message-id @@ -89,7 +89,7 @@ Ordered oldest-first by serial. Returns a single-element array when the node has ## Example -A run-history sidebar that lists every observed Run and lights up sibling navigation where alternatives exist. +A run-history sidebar that lists every observed run and lights up sibling navigation where alternatives exist. ```javascript diff --git a/src/pages/docs/ai-transport/api/react/core/use-view.mdx b/src/pages/docs/ai-transport/api/react/core/use-view.mdx index 8f17b461e1..2b325bdea4 100644 --- a/src/pages/docs/ai-transport/api/react/core/use-view.mdx +++ b/src/pages/docs/ai-transport/api/react/core/use-view.mdx @@ -60,11 +60,11 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api | loading | Whether a page load is currently in progress. | Boolean | | loadError | Set when the most recent `loadOlder` call failed. Cleared automatically on the next successful load. | `Ably.ErrorInfo` or Undefined | | loadOlder | Reveal older messages, resolving to the revealed page (oldest-first). Returns `[]` when nothing was revealed: already loading, no view resolved, history exhausted, or the load failed. | `() => Promise[]>` | -| runOf | Look up the `RunInfo` for the Run that owns the given `codecMessageId`. | | -| run | Direct lookup by Run id. |
| -| runs | Snapshot of the visible Runs along the selected branch, in chronological order. Returns `[]` when the view isn't resolved. | `() => RunInfo[]` | +| runOf | Look up the `RunInfo` for the run that owns the given `codecMessageId`. |
| +| run | Direct lookup by run id. |
| +| runs | Snapshot of the visible runs along the selected branch, in chronological order. Returns `[]` when the view isn't resolved. | `() => RunInfo[]` | | branchSelection | Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb to navigate it. Always returns a safe handle. |
| -| send | Send an input on the channel and fire a POST. At most one new message per send; the array form carries only wire-only inputs (tool results, approvals). Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. Takes optional
. | `(events, options?) => Promise>` | +| send | Publish an input on the session. The core session is HTTP-free, so wake the agent by POSTing `run.toInvocation().toJSON()` to your agent endpoint. At most one new message per send; the array form carries only wire-only inputs (tool results, approvals). Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. Takes optional
. | `(events, options?) => Promise>` | | regenerate | Regenerate an assistant message using this view's branch for history. | `(messageId, options?) => Promise>` | | edit | Edit a user message, forking from this view's branch. | `(messageId, inputs, options?) => Promise>` | @@ -74,11 +74,11 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api | Property | Description | Type | | --- | --- | --- | -| runId | The Run's unique identifier. | String | -| clientId | Identity of the Ably client that started this Run. Empty string when the wire didn't carry an owner client id. | String | -| status | Run lifecycle status. `'active'` while streaming, `'suspended'` while paused awaiting input, otherwise the `RunEndReason` the Run terminated with. | `'active' \| 'suspended' \| RunEndReason` | -| error | The terminal error, present exactly when `status` is `'error'`. Carries the agent-stamped error detail so a UI can show why a Run failed. | `Ably.ErrorInfo` | -| invocationId | The agent-minted `invocationId` observed for this Run, adopted from `ai-run-start`. Empty string until run-start arrives, or if the wire didn't carry an invocation-id. | String | +| runId | The run's unique identifier. | String | +| clientId | Identity of the Ably client that started this run. Empty string when the wire didn't carry an owner client id. | String | +| status | Run lifecycle status. `'active'` while streaming, `'suspended'` while paused awaiting input, otherwise the `RunEndReason` the run terminated with. | `'active' \| 'suspended' \| RunEndReason` | +| error | The terminal error, present exactly when `status` is `'error'`. Carries the agent-stamped error detail so a UI can show why a run failed. | `Ably.ErrorInfo` | +| invocationId | The agent-minted `invocationId` observed for this run, adopted from `ai-run-start`. Empty string until run-start arrives, or if the wire didn't carry an invocation-id. | String |
@@ -112,23 +112,23 @@ Reveal older messages into the view, resolving to the revealed page (oldest-firs On failure, `loadError` is set. On the next successful load, `loadError` is cleared automatically. -## Look up a Run by message id
+## Look up a run by message id {`runOf(codecMessageId: string): RunInfo | undefined`} -Look up the [`RunInfo`](#returns) for the Run that owns the given `codecMessageId`. Returns `undefined` when the codec-message-id has not been observed. +Look up the [`RunInfo`](#returns) for the run that owns the given `codecMessageId`. Returns `undefined` when the codec-message-id has not been observed. -## Look up a Run by id +## Look up a run by id {`run(runId: string): RunInfo | undefined`} -Direct lookup by Run id. Symmetric with [`runOf`](#run-of) for callers that already hold a `runId`. +Direct lookup by run id. Symmetric with [`runOf`](#run-of) for callers that already hold a `runId`. ## Resolve a branch selection {`branchSelection(codecMessageId: string): BranchHandle`} -Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb to navigate it. Always returns a safe handle: for branch anchors with N siblings, `siblings` carries every sibling Run's view of the anchor slot; for non-anchor messages, `siblings` is `[thisMessage]` and `hasSiblings` is `false`. +Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb to navigate it. Always returns a safe handle: for branch anchors with N siblings, `siblings` carries every sibling run's view of the anchor slot; for non-anchor messages, `siblings` is `[thisMessage]` and `hasSiblings` is `false`. ## Select a sibling @@ -146,21 +146,21 @@ view.branchSelection(codecMessageId).select(1); {`send(events: TInput | TInput[], options?: SendOptions): Promise>`} -Send an input on the channel and fire a POST. Each `TInput` carries its own routing metadata (`parent`, `target`, `codecMessageId`). +Publish an input on the session. The core session is HTTP-free, so wake the agent by POSTing `run.toInvocation().toJSON()` to your agent endpoint. Each `TInput` carries its own routing metadata (`parent`, `target`, `codecMessageId`). -To send a fresh user message, build a `UserMessage` input. Use the codec factory (`codec.createUserMessage(message)`) or build the literal directly: `{ kind: 'user-message', message }`. A send introduces at most one new message: exactly one `UserMessage` for a fresh send (which mints a new Run), or none for a continuation. The array form exists only to carry the wire-only inputs that resolve a single assistant turn (`tool-result`, `tool-result-error`, `tool-approval-response`); pair it with `options.runId` to extend a suspended Run. Passing more than one new message rejects with `InvalidArgument`. +To send a fresh user message, build a `UserMessage` input. Use the codec factory (`codec.createUserMessage(message)`) or build the literal directly: `{ kind: 'user-message', message }`. A send introduces at most one new message: exactly one `UserMessage` for a fresh send (which mints a new run), or none for a continuation. The array form exists only to carry the wire-only inputs that resolve a single assistant turn (`tool-result`, `tool-result-error`, `tool-approval-response`); pair it with `options.runId` to extend a suspended run. Passing more than one new message rejects with `InvalidArgument`. ## Regenerate an assistant message {`regenerate(messageId: string, options?: SendOptions): Promise>`} -Regenerate an assistant message. Creates a new Run that targets the message and threads under its parent user message. Both ids are computed automatically from this view's branch. +Regenerate an assistant message. Creates a new run that targets the message and threads under its parent user message. Both ids are computed automatically from this view's branch. ## Edit a user message {`edit(messageId: string, inputs: TInput | TInput[], options?: SendOptions): Promise>`} -Edit a user message. Creates a new Run that forks the target message with the replacement inputs. `forkOf`, `parent`, and history are computed automatically from this view's branch. +Edit a user message. Creates a new run that forks the target message with the replacement inputs. `forkOf`, `parent`, and history are computed automatically from this view's branch. ## Example diff --git a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx index c1b3d8e813..152ce9d4e6 100644 --- a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx +++ b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx @@ -7,7 +7,7 @@ redirect_from: - /docs/ai-transport/api/react/chat-transport-provider --- -`ChatTransportProvider` is the entry point for the Vercel React integration. It wraps children in a `ClientSessionProvider` bound to the Vercel codec, constructs a [`ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport) over that session, and exposes both through React context. Descendants call [`useChatTransport`](/docs/ai-transport/api/react/vercel/use-chat-transport) to get the transport (and the session) for Vercel `useChat`. +`ChatTransportProvider` is the entry point for the Vercel React integration. It wraps children in a `ClientSessionProvider` bound to the Vercel codec, constructs a `ChatTransport` over that session, and exposes both through React context. Descendants call `useChatTransport` to get the transport (and the session) for Vercel `useChat`. The Realtime client is read from the surrounding ``. The session is created once on first render. The chat transport is recreated only when `chatOptions` changes. @@ -66,7 +66,7 @@ function App() { The underlying `ClientSession` is created once on first render via `useRef`, and `connect()` runs from a `useEffect`. The session is closed when the provider truly unmounts. -Because it wraps [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider), it also renders an ably-js `` for the channel, so ably-js's `usePresence`, `usePresenceListener`, and `useChannel` hooks work in the subtree. See [Agent presence](/docs/ai-transport/features/agent-presence#react). +Because it wraps [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider), it also renders an ably-js `` for the channel, so ably-js's `usePresence`, `usePresenceListener`, and `useChannel` hooks work in the subtree for [agent presence in React](/docs/ai-transport/features/agent-presence#react). The `ChatTransport` itself is not closed on unmount: its `close()` delegates to `ClientSession.close()`, which the inner `ClientSessionProvider` already calls. Auto-closing here would double-close in React Strict Mode. diff --git a/src/pages/docs/ai-transport/api/react/vercel/use-chat-transport.mdx b/src/pages/docs/ai-transport/api/react/vercel/use-chat-transport.mdx index 37783206df..de7b6ada5f 100644 --- a/src/pages/docs/ai-transport/api/react/vercel/use-chat-transport.mdx +++ b/src/pages/docs/ai-transport/api/react/vercel/use-chat-transport.mdx @@ -7,7 +7,7 @@ redirect_from: - /docs/ai-transport/api/react/use-chat-transport --- -`useChatTransport` reads a [`ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport#chat-transport) and its underlying [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session) from the nearest `ChatTransportProvider`. Pass the returned `chatTransport` to Vercel AI SDK's `useChat({ transport })` and use `session` for everything else (cancel, tree inspection, raw message access). +`useChatTransport` reads a `ChatTransport` and its underlying `ClientSession` from the nearest `ChatTransportProvider`. Pass the returned `chatTransport` to Vercel AI SDK's `useChat({ transport })` and use `session` for everything else (cancel, tree inspection, raw message access). The hook is a thin context reader; it does not create or manage any session or transport state. When no provider is found or the session failed to construct, the hook returns stubs along with populated `sessionError` and `chatTransportError` so the component can render an error state without an error boundary. @@ -72,7 +72,7 @@ Post-construction errors (for example send failures or channel continuity loss) ## Example -A chat component wired through `useChat` with a stop button that cancels the active Run through the underlying session. +A chat component wired through `useChat` with a stop button that cancels the active run through the underlying session. ```javascript diff --git a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx index 7c974ba9ef..533482b37c 100644 --- a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx +++ b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx @@ -7,9 +7,9 @@ redirect_from: - /docs/ai-transport/api/react/use-message-sync --- -`useMessageSync` subscribes to view updates and syncs them into Vercel AI SDK's `useChat` overlay through the `setMessages` updater. It is how observer clients (other tabs, other devices) catch up with messages that arrive on the channel rather than from `useChat`'s own `sendMessage` calls. +`useMessageSync` subscribes to view updates and syncs them into Vercel AI SDK's `useChat` overlay through the `setMessages` updater. It is how observer clients (other tabs, other devices) catch up with messages that arrive on the session rather than from `useChat`'s own `sendMessage` calls. -During an active own-run stream the sync is gated so it does not race the AI SDK's internal `write()` and produce an ID mismatch. When the stream ends, the gate opens and the next view update is merged in. Tool resolutions in the local overlay that have not yet echoed back through the channel are preserved during the merge. +During an active own-run stream the sync is gated so it does not race the AI SDK's internal `write()` and produce an ID mismatch. When the stream ends, the gate opens and the next view update is merged in. Tool resolutions in the local overlay that have not yet echoed back through the session are preserved during the merge. ```javascript @@ -36,7 +36,7 @@ This hook must be used within a `ChatTransportProvider` (exported from `@ably/ai | Parameter | Required | Description | Type | | --- | --- | --- | --- | | setMessages | required | The `setMessages` updater function from `useChat()`. Called with an updater that returns the next overlay. | `(updater: (prev: UIMessage[]) => UIMessage[]) => void` | -| messages | optional | The application's seeded conversation, typically `useChat()`'s `messages` where persisted history was supplied via `useChat({ messages })`. When non-empty, the hook reconciles it with the live channel: it takes the newest entry's `id` as the seam, pages the channel back to it, and composes stored history with the live tail with no duplicate. Omit it, or pass `[]`, for the full live channel history. See [database hydration](/docs/ai-transport/features/database-hydration). | `UIMessage[]` | +| messages | optional | The application's seeded conversation for [database hydration](/docs/ai-transport/features/database-hydration), typically `useChat()`'s `messages` where persisted history was supplied via `useChat({ messages })`. When non-empty, the hook reconciles it with the live session: it takes the newest entry's `id` as the seam, pages the session back to it, and composes stored history with the live tail with no duplicate. Omit it, or pass `[]`, for the full live session history. | `UIMessage[]` | | channelName | optional | Channel name of the `ChatTransportProvider` to observe. Omit to use the nearest provider. | String | | skip | optional | When `true`, skip all subscriptions. | Boolean | @@ -53,15 +53,15 @@ The hook does two things: - Subscribes to the `ChatTransport`'s `onStreamingChange` event. While `streaming` is `true` (own-run stream consumed by `useChat`), the gate is closed and no `setMessages` calls are made. - Subscribes to the underlying `View`'s `'update'` event. When the view changes and the gate is open, the hook calls `setMessages` with an updater that merges the view's messages into the overlay. -The merge is per message, not a wholesale replace. For each assistant message, the merge matches tool parts by `toolCallId` in either representation, a statically-declared tool's `tool-${name}` part or a dynamic tool's `dynamic-tool` part, and preserves any overlay part whose state is `output-available`, `output-error`, `approval-responded`, or `output-denied` while the tree's part is still in an unresolved state. That keeps local `addToolResult` and `addToolApprovalResponse` actions visible until the channel echo lands. +The merge runs per message rather than as a wholesale replace. For each assistant message, the merge matches tool parts by `toolCallId` in either representation, a statically-declared tool's `tool-${name}` part or a dynamic tool's `dynamic-tool` part, and preserves any overlay part whose state is `output-available`, `output-error`, `approval-responded`, or `output-denied` while the tree's part is still in an unresolved state. That keeps local `addToolResult` and `addToolApprovalResponse` actions visible until the session echo lands. ## When to use it -Use `useMessageSync` whenever you mount a `ChatTransportProvider` and render messages through `useChat`. Without it, observer tabs or devices that did not originate a Run never see its messages because `useChat`'s state is local to each `useChat` instance. +Use `useMessageSync` whenever you mount a `ChatTransportProvider` and render messages through `useChat`. Without it, observer tabs or devices that did not originate a run never see its messages because `useChat`'s state is local to each `useChat` instance. If you render messages directly from [`useView`](/docs/ai-transport/api/react/core/use-view) instead of from `useChat`, you do not need `useMessageSync`; `useView` already reflects the view state. -To seed the conversation from your own database, pass the loaded history as `messages`, both to `useChat` and to `useMessageSync`. The hook reconciles the seed with the live channel at the seam, so a reloaded conversation shows its stored history plus the live tail exactly once. See [database hydration](/docs/ai-transport/features/database-hydration). +To [seed the conversation from your own database](/docs/ai-transport/features/database-hydration), pass the loaded history as `messages`, both to `useChat` and to `useMessageSync`. The hook reconciles the seed with the live session at the seam, so a reloaded conversation shows its stored history plus the live tail exactly once. ```javascript @@ -72,7 +72,7 @@ useMessageSync({ messages: seed, setMessages }); ## Example -A complete Vercel-React chat with sync wired in. A second browser tab pointing at the same `channelName` sees every message as soon as it arrives on the channel. +A complete Vercel-React chat with sync wired in. A second browser tab pointing at the same `channelName` sees every message as soon as it arrives on the session. ```javascript diff --git a/src/pages/docs/ai-transport/concepts/authentication.mdx b/src/pages/docs/ai-transport/concepts/authentication.mdx deleted file mode 100644 index 05503dad5a..0000000000 --- a/src/pages/docs/ai-transport/concepts/authentication.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -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, capabilities" -intro: "AI Transport authenticates at three layers: an Ably token for the channel, HTTP credentials for the agent endpoint, and a server-side hook that authorises cancel signals." -redirect_from: - - /docs/ai-transport/how-it-works/authentication - - /docs/ai-transport/sessions-identity/identifying-users-and-agents ---- - -Authentication in AI Transport operates at three levels: Ably token authentication for channel access, HTTP credentials for agent endpoint authorisation, and cancel authorisation for controlling who can stop a Run. Each layer is independent and addresses a different trust boundary. - -For the practical setup (signing tokens, wiring `authCallback`, securing the agent POST), see [Set up authentication](/docs/ai-transport/getting-started/authentication). - -## The three layers - -| Layer | What it protects | How it works | -| --- | --- | --- | -| Ably token | Channel publish, subscribe, history, presence. | Your server signs a short-lived JWT that names the user's `clientId` and capabilities. The client SDK fetches it via `authCallback` and refreshes before expiry. | -| Agent endpoint | The HTTP POST that wakes the agent. | Your application's normal endpoint auth: cookies, bearer tokens, signed headers. Carries no AI-Transport-specific semantics. | -| Cancel authorisation | Who can stop a Run. | An `onCancel` hook on the agent's `RunRuntime`. The agent inspects the cancel sender and accepts or rejects. | - -The Ably token controls *what can land on the channel*. The agent endpoint auth controls *who can trigger a Run*. Cancel authorisation controls *who can stop one that's already running*. Different concerns, separately enforced. - -## Authentication flow - -1. Your auth server authenticates the user. -2. Your auth server issues an Ably-compatible token (JWT format is recommended for most apps). -3. The client SDK fetches tokens through `authCallback` and refreshes them automatically before expiry. -4. The authenticated Ably Realtime client provides channels to AI Transport via the session providers. - - - -## AI Transport capabilities - -Capabilities are permissions on the Ably channel. When you issue a token for an AI Transport user, the capability claim names which operations they can perform on which channels. - -| Feature | Required capabilities | -|---------|----------------------| -| Send user messages to channel | `publish` | -| Receive streamed tokens | `subscribe` | -| Replay history on reconnect | `subscribe`, `history` | -| Cancel a Run | `publish` | -| Read and write shared objects | `object-subscribe`, `object-publish` | -| All AI Transport features | `publish`, `subscribe`, `history` | - -A token that's missing a capability fails silently at the operation site, not at construction. Capability mismatches are one of the most common setup mistakes; see [troubleshooting: capability or token scope mismatch](/docs/ai-transport/troubleshooting#capability-mismatch). - -## Channel-scoped capabilities - -Capabilities are keyed by channel name pattern. The same token can grant different operations on different channels: - -* `my-conversation`: a specific conversation channel. -* `conversations:*`: all channels in the `conversations:` namespace. -* `*`: all channels. - -Scope tokens to the smallest channel set the user needs. A user's token granting access to one conversation should name that conversation, not the whole namespace. - -## Token lifecycle and permission updates - -- With `authCallback` or `authUrl`, token refresh is automatic. The SDK fetches a new token before the current one expires. -- To change a user's capabilities mid-session, issue a new token from your auth server and re-authenticate the client: - - - ```javascript - await realtimeClient.auth.authorize(); - ``` - - -- To immediately remove access, [revoke issued tokens](/docs/auth/revocation). -- If your capability JSON is too large for a JWT claim, or must remain confidential, use native [Ably Tokens](/docs/auth/token/ably-tokens) instead. - -## Agent endpoint authentication - -The HTTP POST that wakes the agent is a separate request from anything on the Ably channel. It carries application credentials (a session cookie, a bearer token, or whatever your endpoint normally expects). The Ably token does not authenticate this POST; the agent endpoint authenticates it through your own application auth. - -On the core flow, the application owns the POST and sets credentials in its `fetch` call. On the Vercel flow, [`ChatTransportProvider`](/docs/ai-transport/api/react/vercel/chat-transport-provider) accepts a `credentials` prop and a `chatOptions.prepareSendMessagesRequest` hook that returns `{ body?, headers? }` per request, which the transport merges into every invocation POST. - -## Cancel authorisation - -When a client publishes a cancel signal, the agent decides whether to honour it. The `onCancel` hook on `RunRuntime` receives a `CancelRequest` carrying the raw Ably cancel `message` and the `runId` it targets. Compare the cancel sender's `clientId` against whichever identity your application considers authorised; here, the user identity resolved from the inbound HTTP request: - - -```javascript -const userId = await authenticateUser(req); - -const run = session.createRun(invocation, { - signal: req.signal, - onCancel: async (request) => { - return request.message.clientId === userId; - }, -}); -``` - - -Return `true` to allow the cancel (the `abortSignal` fires, the stream closes). Return `false` to reject it (the Run continues). If `onCancel` is not provided, every cancel is accepted. - -The Ably token's `clientId` is verified by the Ably service before the cancel reaches the agent, so the `clientId` on the cancel message is trustworthy; the agent can authorise off it without independent verification. - -## Read next - -The codec model has two generics that name the direction of travel: - -- `TInput extends CodecInputEvent`: anything the client sends to the agent. User messages, regenerate requests, tool results, tool approvals. -- `TOutput extends CodecOutputEvent`: anything the agent streams back. Text deltas, tool calls, finish signals, framework-specific events. - -Each direction has its own wire scope: inputs publish on `ai-input`, outputs publish on `ai-output`. The SDK enforces the distinction at the type level: you can't accidentally publish an output where an input is expected. - -The flow on each side is symmetric. The client sends a domain message; the codec wraps it as a `TInput`, encodes it onto the channel, and the agent decodes it back into the same `TInput` shape it can act on. The agent streams `TOutput`s back; the codec encodes each one onto the channel, and the client decodes them and folds them through `codec.fold` into the per-Run `TProjection` that the View renders. - -The codec implements the encoder and decoder. The SDK calls them. Custom domain logic (what each event *means*, how to fold them into a projection, how to derive `TMessage[]` from the projection) lives in the codec, not in the SDK. - -## Well-known input variants - -The SDK ships a vocabulary of `TInput` variants that show up in every framework. Each variant is a tagged union identified by its `kind` field. A codec must support these so the SDK's higher-level methods (`view.send`, `view.regenerate`, `view.edit`) can call into them without a per-framework adapter: - -| Variant | `kind` value | What it triggers | -| --- | --- | --- | -| `UserMessage` | `'user-message'` | A fresh user turn. | -| `Regenerate` | `'regenerate'` | Regenerate an assistant message. | -| `ToolResult` | `'tool-result'` | Deliver a successful tool result back to the agent. | -| `ToolResultError` | `'tool-result-error'` | Deliver a tool failure. | -| `ToolApprovalResponse` | `'tool-approval-response'` | Approve or deny a pending tool call. | - -The tool variants carry a codec-defined `payload`; the core knows only the routing (`kind`, `codecMessageId`) and lets the codec own the shape of the payload (for example, the Vercel layer's `{ toolCallId, output }` for `tool-result`). - -Codecs implement `createUserMessage(message)` and `createRegenerate(target, parent)` to bridge from the SDK's higher-level methods into well-typed `TInput`s, and optionally `createToolResult`, `createToolResultError`, `createToolApprovalResponse` for the tool variants. Edits go on the wire as a fresh `UserMessage` published with `view.edit` (which routes via the `forkOf` header on the user-message path) rather than a separate variant. Custom codecs can add their own variants on top, picking any `kind` value other than the reserved ones above. - -## What the codec layer requires - -| Property | Why it matters | -| --- | --- | -| Direction-explicit | Inputs and outputs live on separate wire names (`ai-input` / `ai-output`). The type system mirrors this with `TInput` / `TOutput`. Mixing the two would corrupt the stream-append pipeline and the agent's input-event lookup. | -| Deterministic | Two participants running the same codec over the same channel log produce the same projection. Encoding the same event twice produces equivalent wire messages. Without determinism, multi-device sync wouldn't converge. | -| Stream-aware | Many `TOutput`s are streamed (token deltas). The encoder must distinguish single-publish messages from append sequences and reconstruct streaming state from Ably's create + append + update lifecycle on decode. | -| Projection-driven | The codec owns a per-Run `TProjection` (its own opaque state, typically the message list under construction). The SDK folds decoded events through `codec.fold` to keep it up to date; the View materialises rendered messages from each Run's projection via `codec.getMessages(projection)`, which returns `CodecMessage[]` (each pair carries the codec-message-id alongside the domain message). | -| Header tiering | Codec-specific fields ride under `extras.ai.codec`; transport-level headers (run-id, status, role) ride under `extras.ai.transport`. The codec uses `codecHeaders` and `transportHeaders` on `MessagePayload` / `StreamPayload` to control which tier its fields land in. | - -## Write a custom codec - -The shape of a minimal custom codec, with full bodies omitted (just the contract): - - -```javascript -const myCodec = { - init() { return []; }, - fold(projection, event, meta) {/* ... */}, - createEncoder(channel, options) {/* ... */}, - createDecoder() {/* ... */}, - getMessages(projection) { - // Return [{ codecMessageId, message }] pairs so the SDK can correlate - // each rendered message back to its wire id. - return projection.map((entry) => ({ codecMessageId: entry.codecMessageId, message: entry.message })); - }, - createUserMessage(message) { - return { kind: 'user-message', message }; - }, - createRegenerate(target, parent) { - return { kind: 'regenerate', target, parent }; - }, -}; -``` - - -Most users don't write a custom codec; the bundled [Vercel codec](/docs/ai-transport/api/javascript/vercel/codec) covers the Vercel AI SDK end-to-end. See the [Codec API reference](/docs/ai-transport/api/javascript/core/codec) for the full interface. - -## Read next - -Multiple things need to talk to a session: a user's browser tab, the user's second device, an agent's HTTP handler, an agent's retry process after a serverless cold start. Each one needs to subscribe to the channel, decode messages through the codec, route cancels to the right [Run](/docs/ai-transport/concepts/runs), and tear down cleanly when its work is done, without disrupting the session for anyone else. The connection is the lifecycle-owning object that does all of this. - -## Understand the connection types - -The two connection types differ in what they own and how long they live: - -| Connection | Lifetime | Owns | Doesn't own | -| --- | --- | --- | --- | -| `ClientSession` | Long-lived (the lifetime of the user's app or tab). | Channel subscription, the read path into the tree, default `view`, cancel publish path. | The channel itself, the Ably client, the conversation state. | -| `AgentSession` | Typically short-lived (one HTTP handler invocation). | Channel attach for cancel routing, the write path for Run lifecycle events and the streamed response. | Long-lived state, retry coordination across invocations. | - -Both expose `connect()` and the SDK's lifecycle gate: methods that publish or subscribe throw `InvalidArgument` until `connect()` resolves. They differ on teardown: a `ClientSession` disposes with `close()`, an `AgentSession` with `end()` (or `detach()` to hand an in-flight Run to another process). - -## What the connection layer requires - -| Property | Why it matters | -| --- | --- | -| Lifecycle gate | `connect()` separates construction from use. Until it resolves the SDK can't guarantee the channel is attached or that publishes will be delivered. The gate prevents subtle races where a `view.send` lands before the subscription is in place. | -| Independence | Each connection has its own subscription, branch selections, and pagination window. A second `ClientSession` in another tab doesn't see the first one's branch state. | -| Cancel routing | Cancels are channel publishes that name a `runId`. Both `ClientSession.cancel(runId)` and the agent's `Run.abortSignal` rely on the connection being attached to the right channel. | -| Codec binding | The codec is wired into the connection at construction. Switching codecs means a new connection. | -| Clean teardown | Disposal releases the subscription and clears handlers locally. `ClientSession.close()` and `AgentSession.detach()` do not end Runs on the wire; active Runs keep running until the agent ends them. `AgentSession.end()` first closes the agent's own open Runs as `'cancelled'`, then detaches. | - -## Understand connection lifecycle - -A connection moves through four stages. Construct it with `createClientSession` or `createAgentSession`. Call `connect()` to subscribe to the channel. On a `ClientSession`, write through `view.send`, `view.regenerate`, and `view.edit`, cancel through `cancel(runId)`, derive views with `createView()`, and subscribe to non-fatal errors with `on('error', ...)`. Tear it down with `close()` on a `ClientSession`, or `end()` (or `detach()`) on an `AgentSession`. - -`connect()` is the gate. Every operation that touches the channel throws `InvalidArgument` until the connect promise resolves. `connect()` is also idempotent: subsequent calls return the same promise, useful when a component mounts twice or a retry path re-runs initialisation. - -## Coexist with other participants - -Several `ClientSession` instances can be open against the same session simultaneously. Each one independently: - -- Materialises the tree from the channel (the same tree, on the wire). -- Selects its own branch via the [`view.branchSelection`](/docs/ai-transport/api/javascript/core/client-session#properties) handle's `select` method. -- Holds its own pagination window over older messages. - -What they share is the channel-backed state. A message one client publishes lands on every other client's tree via the channel subscription. A cancel one client publishes is observed by the agent (and by every other client) through their own subscriptions. - -The `AgentSession` is symmetric: an agent process can hold one short-lived `AgentSession` per HTTP invocation, or a long-lived one that serves multiple Runs. Either pattern works because the channel, not the connection, owns the conversation. - -## Open a connection - -A minimal client-side connection: - - -```javascript -import * as Ably from 'ably'; -import { createClientSession } from '@ably/ai-transport'; -import { createUIMessageCodec } from '@ably/ai-transport/vercel'; - -const ably = new Ably.Realtime({ authUrl: '/api/auth/token' }); - -const session = createClientSession({ - client: ably, - channelName: 'conversation-42', - codec: createUIMessageCodec(), -}); - -await session.connect(); -// session.view, session.tree, session.cancel(...) are now safe to use -``` - - -The agent-side mirror. See the [AgentSession reference](/docs/ai-transport/api/javascript/core/agent-session) for the full pattern. - -## Bind a codec to the connection - -A connection is bound to a codec at construction. The codec is what makes the same connection carry Vercel `UIMessage`, an OpenAI-style payload, or a custom domain shape. See the [Codecs concept](/docs/ai-transport/concepts/codecs) for the model; see the [Codec reference](/docs/ai-transport/api/javascript/core/codec) for the interface. - -## 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 pointers to parent or sibling messages. +A message is one unit of content in the conversation: a user's input, an assistant's response, a tool call, or a tool result. Each message has a unique identity and a position in the tree defined by pointers to its parent or its siblings. -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. +A message arrives on the session either as a single publish or as a sequence of operations that share one message ID. A streamed assistant response starts with a publish and builds up through a series of appends, one per token, until a final append marks it complete. Those appends are not separate messages. ## 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. +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) is a node in the tree, whether it is currently visible or not. -### Use the tree as the source of truth +The tree is not a linear list. When you regenerate a response, the new response becomes a sibling of the old one instead of replacing it. When you edit a message, the edited version forks from the same parent as the original, and the earlier version stays in the tree as a sibling on an alternative branch. [Conversation branching](/docs/ai-transport/features/branching) covers how to create branches and navigate between siblings. -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. +Messages are ordered by their serial, so tree construction is deterministic. The same sequence of messages always produces the same tree, which is why two clients reading the same session agree on the shape of the conversation. ## 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 +When a conversation has been regenerated three times at one point, the tree holds all three responses as siblings. The view selects one and presents the conversation as though that response is the only one, while still allowing navigation to the others. -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. +Each view handles three jobs on top of that selection: -### Interact through a view +- Branch selection. Each client holds its own selections, so two clients looking at the same session view different branches. +- Pagination. Older messages are withheld and loaded on demand. The view tracks the boundary between visible and withheld messages and provides the means to widen the window. +- Scoped events. The view emits update and lifecycle events only for the branch it is showing, so a component subscribed to a view re-renders only when something it displays changes. -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. +You also write through the view. Sending a message, regenerating a response, and editing a message are all operations on a view, and the view uses its current position in the tree to set the correct parent and fork pointers on whatever it publishes. ## Work with the tree and views -The session exposes the default view as a property. Iterate the visible messages on the selected branch through `getMessages()`; each entry pairs the domain `message` with its `codecMessageId`: +On the client, the session exposes a default view as a property. Iterate the visible messages on the selected branch through `getMessages()`, where each entry pairs the domain message with its codec message id: ```javascript +// Client: read the visible messages on the selected branch. const view = session.view; for (const { codecMessageId, message } of view.getMessages()) { @@ -79,11 +58,12 @@ for (const { codecMessageId, message } of view.getMessages()) { ``` -Additional views over the same tree are created with `session.createView()`. The tree behind the view holds every branch; the view selects one path through it. See [conversation branching](/docs/ai-transport/features/branching) for creating branches with edit and regenerate and navigating between siblings. +Create additional views over the same tree with `session.createView()`. ## Read next - -The [session](/docs/ai-transport/concepts/sessions) is the persistent, shared state of a conversation. It exists independently of any participant: clients connect and disconnect, agents spin up and terminate, and the session endures. - -Every other concept on this page operates on or within a session. - -## A connection attaches to the session - -A [connection](/docs/ai-transport/concepts/connections) is one participant's hold on the session. `ClientSession` for client applications is long-lived and subscribes to the channel for the duration of the user's app. `AgentSession` for agent processes is typically short-lived and handles one HTTP invocation at a time. - -Multiple connections attach to the same session simultaneously and dispose independently. - -## A Run is one unit of agent work - -A [Run](/docs/ai-transport/concepts/runs) is AI Transport's unit of work for one turn: one prompt-response cycle. It has an identity, a lifecycle, and an end reason. The Run is the level at which you start, observe, and cancel work. - -A session contains many Runs. Runs coexist on the same session and have independent lifecycles. - -## A Step is the retry unit within a Run - -A [Step](/docs/ai-transport/concepts/steps) is one re-attemptable unit of agent output inside a Run: a single inference, a single tool result, a single scheduled activity. A retry under the same `stepId` supersedes the failed attempt on the channel rather than appending beside it. - -Steps are what make Runs safe to execute inside a durable workflow engine such as [Temporal](/docs/ai-transport/frameworks/temporal) or [Vercel WDK](/docs/ai-transport/frameworks/vercel-wdk). See [Durable execution](/docs/ai-transport/features/durable-execution). - -## An Invocation triggers a Run - -An [Invocation](/docs/ai-transport/concepts/invocations) is the trigger a client posts to the agent endpoint to start or continue a Run. It carries the `inputEventId` and the session name; run identity is resolved from the triggering input event on the channel. - -The same Run can be triggered by many Invocations: tool results, regenerates, retries. The agent mints `runId` (for a fresh run) and a fresh `invocationId` per HTTP request, and returns both on the response so the caller can observe them. - -## The codec translates between your framework and Ably - -A [codec](/docs/ai-transport/concepts/codecs) is the boundary between your domain (text deltas, tool calls, finish events for whatever framework you use) and Ably's message primitives. The codec is direction-typed: `TInput` for client-to-agent events, `TOutput` for agent-to-client events. - -The SDK ships a vocabulary of well-known input variants the codec supports so the standard operations (send, regenerate, edit, tool result, tool approval) work across any framework. - -## The conversation tree holds every branch - -The [conversation tree](/docs/ai-transport/concepts/conversation-tree) is how Runs and messages are organised inside the session. Every Run and every message, including every branch from an edit or regenerate, is preserved as a node in the tree. - -A view is a linear path through the tree: the conversation as a participant sees it. The tree is what makes branching, edit, and regenerate work without losing history. - -## Authentication runs at three layers - -[Authentication](/docs/ai-transport/concepts/authentication) covers three concerns at once: an Ably token for channel access, HTTP headers for the agent endpoint, and a server-side hook that authorises cancel signals. - -The first two are standard Ably auth; the third is specific to how AI Transport handles cancellation across participants. - -## The infrastructure carries it at scale - -The [infrastructure](/docs/ai-transport/concepts/infrastructure) page describes the four guarantees the session layer depends on: integrity (exactly-once, in order), reliability (multi-region, no single point of failure), performance (low latency wherever the user connects from), and availability (horizontal scale). - -If you are evaluating whether the session layer will hold in production, this is the page to read. +The [session](/docs/ai-transport/concepts/sessions) is the durable conversation that clients and agents attach to, and it outlives any connection. A [run](/docs/ai-transport/concepts/runs) is one turn of agent work inside a session, with its own identity, lifecycle, and end reason. The [conversation tree](/docs/ai-transport/concepts/conversation-tree) is how the session's messages are organised, which is what lets a user edit or regenerate a message without losing what came before. + +![Diagram showing two clients and a phone attached to one session, the conversation tree of runs inside it, the Ably channel as the append-only log underneath, and the invocation the client posts to the agent](../../../../images/content/diagrams/ait-concepts-overview.png) + + + +## Topics + + +{[ + { + title: 'Sessions', + description: 'The durable conversation, how it materialises from an Ably channel, and the ClientSession and AgentSession objects your code attaches with.', + link: '/docs/ai-transport/concepts/sessions', + }, + { + title: 'Runs', + description: 'One turn of agent work: its lifecycle and end reason, the invocation that triggers it, and the steps it publishes output through.', + link: '/docs/ai-transport/concepts/runs', + }, + { + title: 'Conversation tree', + description: 'How messages form a branching history, and how a view selects one linear path through it for each client.', + link: '/docs/ai-transport/concepts/conversation-tree', + }, +]} + ## Read next - -A durable session layer must be dependable and predictable. When a user reconnects mid-stream and resumes, the token stream must be exactly correct: no duplicates, no gaps, no reordering. When a tab reloads, the state must be exactly as it was when streaming started. When an agent crashes and recovers, retries must be invisible to the user. - -The platform provides exactly-once delivery and guaranteed message ordering across distributed infrastructure. Token streams are persisted and accumulated, so a reconnecting client gets the assembled state, not a sequence of deltas to reassemble. Operations are idempotent, so an agent retry after a crash is architecturally invisible. These properties are engineered at the protocol level, not implemented as application logic. - -See [the four pillars of dependability](https://ably.com/four-pillars-of-dependability) for the detailed technical claims. - -## Reliability: always on, in every region - -The session layer is a single point of failure for every AI conversation in your application. If it goes down, every user across every session is affected at once. The platform is engineered so this does not happen. - -Session state is present in every region simultaneously, not held in one region and replicated elsewhere. Publish and subscribe operations stay low-latency wherever the participant connects from, because the state lives in their region. When a region fails, there is nothing to relocate: the state already exists in other regions. No developer action is required. - -This is the architectural choice that distinguishes the platform. Designs that keep state in one region and fail over elsewhere accept a window of regional outage. The platform avoids that window by keeping state present everywhere. - -The platform has maintained 99.999% availability with zero global downtime over a multi-year operating history. - -## Performance and latency: low latency wherever the user is - -Token streams should arrive as fast as the LLM generates them. The infrastructure should not add perceptible latency. Users connecting from different regions should experience the same quality. - -The platform runs a globally distributed edge network. Clients connect to the nearest edge node, and because state is present in every region, every operation (token delivery, presence update, control signal) is served locally rather than crossing the network to a single region. The platform handles protocol negotiation automatically (WebSocket with fallback for restrictive networks) so connectivity works without developer configuration. - -See [latency](/docs/platform/architecture/latency) for the detailed performance characteristics. - -## Availability: no ceiling, no capacity planning - -Session state is stateful: that is what makes sessions useful (persistence, recovery, presence) and what makes them hard to run at scale. - -The platform scales horizontally. There is no ceiling, no capacity planning, and no sharding decision to make. Sessions are replicated in the global cluster. The infrastructure absorbs demand spikes and regional failures without developer intervention. - -Presence (whether a device is online, an agent is healthy, a session is active) is a first-class infrastructure primitive. Presence state is managed using data types that resolve consistently across regions without introducing extra latency. No polling is required. - -See [platform scalability](/docs/platform/architecture/platform-scalability) for the underlying architecture. - -## Security - -The platform includes DDoS protection, hardened network layers, token-based authentication, and an active bug bounty programme with independent security researchers. A dedicated security engineering team owns the posture. Ably is SOC 2 Type II certified and HIPAA compliant. See [security and compliance](https://ably.com/security) for the current certifications. - -## SDKs and integrations - -Multi-language SDKs provide the same session and the same guarantees whether your agent is in Python, your client is in React, or your mobile app is in Swift. Session data integrates into third-party systems through webhooks, Kafka, or other integrations. - -See [the SDK directory](/docs/sdks) and [integrations](/docs/integrations) for the full list. - -## Read next - -The client and the agent are separated by an unreliable network and a stateless HTTP boundary. The client publishes input on the channel and your application code posts to the agent endpoint. The agent receives the POST, possibly minutes later, and needs to know exactly which work to do: - -- Which input event triggered the work, so the agent's [`AgentRun.start`](/docs/ai-transport/api/javascript/core/agent-session#run-start) can wait for the exact event on the channel. -- Which channel, since the agent doesn't know the session's channel name except via the trigger. - -These two identifiers are the Invocation's payload (`inputEventId`, `sessionName`). They're carried in the HTTP POST body so the agent has them before the channel is observable. Everything else lives on the channel: run identity is resolved from the triggering input event's wire headers (the agent mints `runId` for a fresh run, or reads the existing `runId` off the input event for a continuation). The `invocationId` is minted by the agent per HTTP request. - -## The Invocation model - -An `InvocationData` is the wire shape: the JSON body in the POST. - -| Field | What it carries | -| --- | --- | -| `inputEventId` | The input event on the channel that triggered this invocation. The agent's `AgentRun.start()` waits until the channel message carrying this `event-id` has been observed, whether it arrives live or is paged in from channel history. | -| `sessionName` | The session's logical name, used as the Ably channel name. | - -The `Invocation` class is the runtime view of that data. The client side uses `ClientRun.toInvocation()` to obtain one from a returned `ClientRun`; the agent side uses `Invocation.fromJSON(data)` to construct one from the parsed POST body, then hands it to `session.createRun(invocation, runtime?)`. `createRun` mints the `invocationId` (one per HTTP request) and, for a fresh run, the `runId`; for a continuation the agent reads the existing `runId` off the triggering input event's wire headers. The application returns `run.runId` and `run.invocationId` on the HTTP response so the caller can observe them. - -End-to-end: the client publishes the input event on the channel and gets back a `ClientRun`. The client knows `inputCodecMessageId` immediately, before the agent has minted the run; `clientRun.runId` is populated later, once the agent's `ai-run-start` event lands (await `clientRun.started`). The application POSTs `clientRun.toInvocation().toJSON()` to the agent endpoint. The agent rebuilds the Invocation, calls `createRun` (which mints `invocationId` and resolves `runId` either by minting for a fresh run or by reading the input event for a continuation), and `run.start()` waits until the matching input event has been observed on the channel, whether it arrives live or is paged in from channel history. Output events the agent publishes carry the `run-id`, `invocation-id`, and `input-codec-message-id` headers; the client reads `clientRun.runId` once `ai-run-start` lands. The application returns `run.runId` and `run.invocationId` on the HTTP response so the original caller can observe the agent-minted identifiers directly. - -## What the Invocation layer requires - -| Property | Why it matters | -| --- | --- | -| Stable identity | The triggering input event must be addressable from both sides. The client stamps `event-id` on the channel publish, and your code posts the same `inputEventId` in the body. If they diverged, the agent's lookup would fail. | -| Deterministic generation | The client mints `inputEventId` and `codecMessageId` inside `view.send()` (and the other write methods), each defaulting to `crypto.randomUUID()`. The agent mints `runId` and `invocationId` per request; supply `RunRuntime.runId` / `RunRuntime.invocationId` to override them in tests. | -| Idempotency | The agent endpoint may receive the same Invocation more than once (network retry, queue redelivery). The agent must treat duplicate Invocations safely, typically by checking whether the input event has already produced a Run on the channel. | -| Decoupled timing | The POST may arrive before, simultaneously with, or after the input event on the channel. The agent's `AgentRun.start()` waits until the input event has been observed, whether it is paged in from channel history or arrives live, so the order doesn't matter. | -| Continuation support | A tool-result delivery or a regenerate produces a new Invocation; the client stamps the existing `run-id` on the new input event's wire headers when publishing. The agent's `createRun` reads that `run-id` from the input event instead of minting a fresh one, and the SDK treats it as a continuation. | - -## Receive an Invocation - -The agent's HTTP handler: - - -```javascript -import { Invocation } from '@ably/ai-transport'; - -export async function POST(req) { - const data = await req.json(); - const invocation = Invocation.fromJSON(data); - - const session = createAgentSession({ /* ... */ }); - await session.connect(); - - const run = session.createRun(invocation, { signal: req.signal }); - await run.start(); // waits for the input event with id `invocation.inputEventId` - // ... - - return Response.json({ runId: run.runId, invocationId: run.invocationId }); -} -``` - - -On the client, the [`view.send`](/docs/ai-transport/api/javascript/core/client-session#properties) call generates the identifiers, stamps them on the channel publish, and returns the `ClientRun`. Your application code (or the [Vercel `ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport) wrapper) calls `clientRun.toInvocation().toJSON()` to build the POST body and sends it to the agent endpoint. - -## Read next -## Why Runs exist +An agent's work in response to a single prompt is not atomic. It reasons over time, gathers external information, executes tools, and waits for humans or other systems to reply. Across that span several parties need to agree on which work is which, when it started, when it ended, and whether it was cancelled. Those parties include the user's other devices, a second browser tab, and a serverless function that restarts mid-execution. The run is the primitive that gives them all one identity to agree on. -An agent's work in response to a single user prompt isn't atomic. It can involve reasoning over time, gathering external information, executing tools, and waiting for humans or other systems to respond. Across that span, multiple participants (multiple devices, multiple browser tabs, a serverless function that restarts mid-execution) need to agree on *which work is which*, *when it started*, *when it ended*, and *whether it was cancelled*. The Run is the SDK primitive that carries this identity end-to-end. +## Understand the run model -## The Run model +A run is made up of: +- A unique `runId`. +- A number of lifecycle events for that run, including `ai-run-start` and `ai-run-end`. +- An owner, which is the `clientId` of the Ably client that published `ai-run-start`. +- An end state, which is one of `'complete'`, `'cancelled'`, or `'error'`. -A Run has four invariants that the SDK enforces on every channel: +Between the lifecycle events, the run owns a series of messages on the session: the user input that triggered it, the agent's streamed output, and any tool-call or tool-result messages. The [conversation tree](/docs/ai-transport/concepts/conversation-tree) groups those messages together so the UI can render the run as one turn. -- A unique `runId`. -- A bracketing pair of lifecycle events on the channel: `ai-run-start` and `ai-run-end`. -- An owner: the `clientId` of the Ably client that published `ai-run-start`. -- A terminal `RunEndReason` once it ends: `'complete'`, `'cancelled'`, or `'error'`. (A Run that pauses awaiting input takes `RunInfo.status === 'suspended'` via `AgentRun.suspend()`, which is non-terminal.) +## Understand the run lifecycle -Within those brackets, the Run owns a window of channel messages: the user input that triggered it, the agent's streamed outputs, any tool-call or tool-result messages. The conversation [Tree](/docs/ai-transport/concepts/conversation-tree) groups those messages into a `RunNode`; the [View](/docs/ai-transport/api/javascript/core/client-session#properties) surfaces projection-free [`RunInfo`](/docs/ai-transport/api/javascript/core/client-session#properties) snapshots for the UI. +A run starts, has some content produced within it, and ends. Once a run ends, it will not be started again. But runs can be suspended while waiting for external input, and resumed when that input arrives. +The run has a status which reflects each phase: -Inside those brackets, the Run is composed of one or more [Steps](/docs/ai-transport/concepts/steps). Each Step is a re-attemptable unit of work with its own `ai-step-start` / `ai-step-end` bracket. A retry of a Step under the same `stepId` supersedes the failed attempt on the channel rather than appending, which is what makes a Run safe to run inside a durable workflow engine. +- `'active'` while the agent is working. The SDK sets this when the run first starts, and again whenever a suspended run resumes. +- `'suspended'` while the run waits for input, such as a tool approval or a human-in-the-loop response. The run is not over, and a later user input can reactivate it. +- A terminal `RunEndReason` once the run finishes. `'complete'` is the success path, `'cancelled'` is set when the run is cancelled, and `'error'` is set when reasoning, output streaming, or a tool execution fails unrecoverably. -A Run is triggered by an [`Invocation`](/docs/ai-transport/concepts/invocations): the trigger the client posts to the agent endpoint that says "create or continue a Run with these identifiers." The Invocation is a separate concept because the same Run can be re-triggered (a tool result, a regenerate request); each trigger is one Invocation. +The run is also the unit of user-cancellation. When a user cancels a request or a prompt, they are cancelling the whole run. Internal failures such as an LLM stream dying, a retrying model call, or a serverless cold start fail do not fail a run and can be retried. -## What the Run layer requires +## Read a run from either side -| Property | Why it matters | -| --- | --- | -| Stable identity | The same `runId` must be readable to every participant: the client that started the Run, every other connected client, the agent process. Without it, cancellation has no target and observation has no scope. | -| Lifecycle brackets | `ai-run-start`, `ai-run-suspend`, `ai-run-resume`, and `ai-run-end` mark the Run on the wire. A view filtering "active Runs" reads `RunInfo.status === 'active'`; `'suspended'` and the terminal `RunEndReason` values cover the other states. | -| Cancel routing | A cancel message names a `runId`. The agent's [`Run.abortSignal`](/docs/ai-transport/api/javascript/core/agent-session#run) fires only for matching cancels, so unrelated Runs on the same session continue. | -| Durable execution | An agent can restart its process mid-Run (serverless cold start, container redeploy). The new process rehydrates by calling [`session.adoptRun`](/docs/ai-transport/api/javascript/core/agent-session#adopt-run), publishing further output as a [Step](/docs/ai-transport/concepts/steps) with a stable `stepId` so a retry supersedes the failed attempt. See [Durable execution](/docs/ai-transport/features/durable-execution). | -| Multiple participants | Several clients may observe the same Run (multi-device, support handover). They all see the lifecycle events; they all hold the same `RunInfo`. | +Both sides of a run expose the same read model, so the same accessor means the same thing on the client and on the agent. A client's `view.send()` returns a `ClientRun` and an agent's `createRun()` returns an `AgentRun`. Each has: -## Understand the Run lifecycle +- `runId`, the run's identifier. The agent knows it synchronously. On the client it is empty until the agent's run-start is observed, so await `clientRun.started` first. +- `status`, the lifecycle status, read live off the conversation tree. +- `error`, the terminal error, present exactly when `status` is `'error'`. +- `messages`, all of the run's messages, its triggering input followed by its streamed output across any suspend and resume. This is the unit to persist, which [database hydration](/docs/ai-transport/features/database-hydration) covers. -A Run progresses from start to terminal end. Along the way it can pause to wait for external input and be resumed when that input arrives. Each phase is reflected on `RunInfo.status`: +Each side then adds its own verbs. `ClientRun` adds `started` and `cancel()`. `AgentRun` adds `located`, which resolves once the triggering input has been observed on the session, along with the lifecycle methods `start()`, `pipe()`, `suspend()`, and `end()`. -- `'active'` while the agent is working. Set when the Run first starts and again whenever a paused Run is resumed. -- `'suspended'` while the Run is paused awaiting input (a tool approval, a human-in-the-loop response). The Run is not over; a later trigger re-activates it. -- A terminal `RunEndReason` once the Run finishes: - - `'complete'` is the success path. - - `'cancelled'` is set when the Run is cancelled. - - `'error'` is set when reasoning, output streaming, or a tool execution fails unrecoverably. +## Trigger a run with an invocation -A Run is the unit of cancellation. There is no user-facing cancel below the Run level. Internal failures (a stream dying, a model call retrying, a serverless cold start) fail the *execution attempt*, not the Run; the Run stays active and the SDK retries underneath. +Starting a run takes two steps, because the input and the trigger travel by different routes. The client publishes the user's input on the session, and your application posts to your agent endpoint to wake the agent. That POST is the invocation. -## Read a Run from either side +The SDK does not make the POST for you. `clientRun.toInvocation().toJSON()` gives you the body, which carries the id of the input event and the session name so the agent knows which session to attach to and which event to wait for. The bundled Vercel [`ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport) will make the request for you if you use it, which is part of the Vercel ChatTransport contract. -Both sides of a Run expose the same read-model, so the same accessor means the same thing on the client and the agent. A client's `view.send()` returns a `ClientRun`; an agent's `createRun()` returns an `AgentRun`. Each carries: - -- `runId`: the Run's identifier. Known synchronously on the agent; on the client it is empty until the agent's run-start is observed (await `clientRun.started` first). -- `status`: the lifecycle status, one of `'active'`, `'suspended'`, `'complete'`, `'cancelled'`, or `'error'`, read live off the [conversation tree](/docs/ai-transport/concepts/conversation-tree). -- `error`: the terminal error, present exactly when `status` is `'error'`. -- `messages`: all of the Run's messages, its triggering input followed by its streamed output (across any suspend and resume). This is the unit to persist; see [database hydration](/docs/ai-transport/features/database-hydration). - -Each side then adds its own verbs. The client's `ClientRun` adds `started` and `cancel()`; the agent's `AgentRun` adds `located` (resolves once the triggering input has been observed on the channel) and the lifecycle methods `start()`, `pipe()`, `suspend()`, and `end()`. - -## Trigger a Run - -A minimal client-side send returns a `ClientRun` that exposes the Run's identity and a per-Run cancel. Each send introduces at most one new message and triggers exactly one Run. The application then POSTs `clientRun.toInvocation().toJSON()` to its agent endpoint to wake the agent; see [Invocations](/docs/ai-transport/concepts/invocations). +On the client, publish the input and then wake the agent: ```javascript +// Client: publish the input, then wake the agent. const clientRun = await session.view.send(createUIMessageCodec().createUserMessage({ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }], })); -// The agent mints the runId on the server, so clientRun.runId is empty until -// the agent's run-start is observed. Await `started`, then read the id. +await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(clientRun.toInvocation().toJSON()), +}); + +// The agent mints the runId, so read it once run-start has been observed. await clientRun.started; console.log('Run started:', clientRun.runId); - -// Stop button: clientRun.cancel() works immediately, even before -// the run-start has been observed. -await clientRun.cancel(); ``` -On the agent side, the symmetric primitive is [`AgentSession.createRun(invocation, runtime?)`](/docs/ai-transport/api/javascript/core/agent-session#create-run). The agent mints `runId` (for a fresh run) or reads the existing `runId` off the triggering input event (for a continuation), and stamps it on every event it publishes; the client reads it from `clientRun.runId` once `clientRun.started` resolves. +On the agent side, `Invocation.fromJSON(body)` rebuilds the invocation and `session.createRun(invocation)` creates the run. The session generates a `runId` for a new run, and for a continuation it reads the existing `runId` off the triggering input event. The [AgentSession reference](/docs/ai-transport/api/javascript/core/agent-session) shows the full handler. -## Steer a Run +Ordering between the two routes does not matter. `run.start()` waits until the input event has been observed, whether it arrives live or is paged in from history, so the POST can land before, with, or after the user's input is published to the session. -While a Run is active, a client can send a follow-up message into it. This is called *steering* the Run. The agent picks up the new message on the next loop iteration; no new Run starts. +One run can be triggered more than once. A tool result, a regenerate request, and a retry after a serverless cold start each produce another invocation against the same `runId`. `clientRun.cancel()` works throughout, including before the run-start has been observed. -The follow-up has the same shape as the input that started the Run. It joins the conversation tree under the same `RunNode` and every connected client sees it. Unlike a fresh `view.send`, it carries the active Run's `runId`, which tells the agent to add it to the existing Run. +## Publish output as steps -Steering messages don't change how a Run ends. The terminal `RunEndReason` reports how the Run finished. Each steering message also gets its own outcome, so the client can tell which ones the agent saw before the Run ended and which it didn't. +A run publishes its output through steps rather than writing to the session directly. A step brackets one contiguous unit of output: the tokens of one model call, the payload of one tool result, or any other burst of writes the agent code frames as a single unit. Every output message carries the id of the step it belongs to, and each step has its own `ai-step-start` and `ai-step-end` events and its own terminal reason of `'complete'`, `'failed'`, or `'cancelled'`. - -```javascript -const activeRun = await session.view.send(createUIMessageCodec().createUserMessage({ - id: crypto.randomUUID(), - role: 'user', - parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }], -})); +![Diagram of a run on the session between an agent that publishes and a client that subscribes. The run contains three steps: step s1 attempt A ends failed, step s1 attempt B retries and supersedes attempt A to end complete, then step s2 publishes a tool result and ends complete, before the run ends complete.](../../../../images/content/diagrams/ait-concepts-steps.png) -const { published, outcome } = activeRun.steer(createUIMessageCodec().createUserMessage({ - id: crypto.randomUUID(), - role: 'user', - parts: [{ type: 'text', text: 'Make it 5 days, and keep it under £800.' }], -})); +Exactly one step is active on a run at a time, and the run stays open across them. A step ending is not a run ending, so the agent code decides whether to open another step, suspend, or end the run. -await published; // The steering message has been published to the channel. +Steps exist so that a retry has a safe boundary. Two `ai-step-start` events under the same `stepId` are the same step re-attempting, and the later attempt supersedes the earlier one's output rather than appending beside it. That is what lets a run execute inside a workflow engine such as [Temporal](/docs/ai-transport/frameworks/temporal) or [Vercel WDK](/docs/ai-transport/frameworks/vercel-wdk), where each retryable activity publishes its own step under an id the engine keeps stable. [Durable execution](/docs/ai-transport/features/durable-execution) covers that pattern, including how a fresh process adopts a run that another process opened. But you can re-use the same steps pattern for stanard LLM model request retries even if you don't use a durable execution framework. -const { consumed } = await outcome; // Resolves when the Run ends. -console.log('Reached the agent:', consumed); -``` - +## Steer a run + +Steering allows a client to send a follow-up message into a run while it is still active. The message carries the active run's `runId`, so the agent adds it to the existing run and picks it up on the next loop iteration instead of starting a new run. [Interruption and steering](/docs/ai-transport/features/interruption-and-steering#steer) covers how to call it and how to write the agent's loop. -See [interruption and steering](/docs/ai-transport/features/interruption-and-steering#steer) for how to call `steer()`, what `outcome` contains, and how to write the agent's loop. +Steering allows you to change the direction of an LLM agent without invoking an entirely new agent. -## Run concurrent Runs +## Run several runs at once -A session can hold multiple Runs in flight at the same time. They share the channel; they don't share state. See [concurrent turns](/docs/ai-transport/features/concurrent-turns) for the patterns that arise when more than one Run is `'active'`. +A session holds multiple runs in flight at the same time. They share the session and they do not share state, and [concurrent turns](/docs/ai-transport/features/concurrent-turns) covers the patterns that arise when more than one run is active. ## Read next +## Understand sessions and channels -A session is built on top of an Ably channel. A session is not the same thing as a channel. +A session is built on top of an Ably channel, and the two are not the same thing. -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 channel is a durable, ordered, append-only log of messages. Every event in the session passes through the channel in realtime, and every connected client receives it. Messages on the channel all have a total order defined by their serial, which is a unique identifier the Ably service assigns to the message on publish. Two clients reading the same log therefore 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, and the useful state is the result of applying them. +The session is the structured conversation that those events produce. ## Materialise a session -A session can be materialised from two sources. +A session materialises from one of two sources. -By default, the session materialises 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. +By default it materialises from the Ably channel, which serves as both the live delivery layer and the historical record. When channel history retention covers the session's lifetime, the channel alone is enough and you need no external storage or configuration. -Alternatively, you 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. +Alternatively you supply historical messages from your own database, and the channel provides only live and in-progress activity. Use this when channel retention is shorter than the conversation, or when you need to index conversation data in your own systems. The session merges the two into one consistent state, and [database hydration](/docs/ai-transport/features/database-hydration) covers how to wire it up. -Materialisation is not a simple replay of the log. Certain events on the channel affect how prior events are interpreted. A cancel event changes how the turn is represented and an 'edit' event changes the content of a user prompt, even though those preceding messages still exist on the channel. The channel retains the full, unedited log, and the materialisation process applies these events as instructions that reshape what the session contains. The session is the result of interpreting the log. +Materialisation is more than a replay of the log. Some events change how earlier events are interpreted. A cancel event changes how a run is represented, and an edit event changes the content of a user prompt, even though the original messages are still on the channel. The channel keeps the full unedited log, and materialisation applies these events as instructions that reshape what the session contains. -## Understand what the session layer requires +## Understand what a session depends on -In direct HTTP streaming over Server-Sent Events 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. +In direct HTTP streaming over Server-Sent Events or a WebSocket, the stream is the connection, so the stream dies when the connection dies. A session is an independently addressable resource that agents write to and clients subscribe to, and it outlives the connections attached to it. -Delivering this reliably requires several specific properties: +Delivering that reliably depends on five properties of the channel underneath: | Property | Why it matters | | --- | --- | -| Independently addressable | Any participant connects to the session by name. There is no requirement that the client that initiated a request is the one that receives the response. | -| Persistent | Messages outlive any single connection. An agent that crashes and restarts can resume publishing. A client that 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 reconnects and resumes from the exact point of disconnection without replaying the entire conversation or re-invoking the agent. | -| Bidirectional | Any participant publishes to the session at any time. This is what makes cancel, steer, 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 sub-agents all publish independently without routing through a single bottleneck. | +| Persistent | Messages outlive any single connection. An agent that restarts resumes publishing and a client that reconnects resumes consuming, because neither end holds the session state in memory. | +| Ordered and resumable | Messages have a total order. A client that drops mid-stream reconnects and resumes from the exact point of disconnection without replaying the conversation or re-invoking the agent. | +| Bidirectional | Any client publishes to the session at any time. This is what makes cancel, steering, and multi-client interaction possible. | +| Fan-out | Several clients subscribe to the same session at once. A second tab, a phone, or a client joining an hour later all receive the same ordered stream of activity. | +| Multiplexed | Several concurrent [runs](/docs/ai-transport/concepts/runs) coexist on one session. An orchestrating agent and its sub-agents publish independently without routing through a single bottleneck. | -These properties enable three capabilities that direct HTTP streaming does not provide: +## Connect to a session -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 communicates 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. +Your code reaches a session through a session object, either `ClientSession` or `AgentSession`. +- `ClientSession` runs in the browser for as long as the user's tab is open, subscribes to the channel, and publishes user input and cancel signals. +- `AgentSession` usually runs for one HTTP handler invocation, and it publishes the run lifecycle events and the streamed response. -A session is materialised by attaching a client session to the channel that backs it: +Neither object owns the conversation. Each one is a connection to a session that lives on the channel, so several agents or clients can connect and disconnect to the same session at once and independently. + +Both are constructed and then connected, as this client-side example shows: ```javascript import * as Ably from 'ably'; -import { createClientSession } from '@ably/ai-transport/vercel'; +import { createClientSession } from '@ably/ai-transport'; +import { createUIMessageCodec } from '@ably/ai-transport/vercel'; + +const ably = new Ably.Realtime({ authUrl: '/auth' }); -const ably = new Ably.Realtime({ authUrl: '/api/auth/token' }); +const session = createClientSession({ + client: ably, + channelName: 'conversation-42', + codec: createUIMessageCodec(), +}); -const session = createClientSession({ client: ably, channelName: 'conversation-42' }); await session.connect(); +// session.view, session.tree, and session.cancel(...) are now safe to use. ``` -`createClientSession` from `@ably/ai-transport/vercel` is pre-bound with the Vercel codec. The core entry point at `@ably/ai-transport` exposes the same factory with `codec` required. The core SDK never POSTs; the application wakes its agent by POSTing `run.toInvocation().toJSON()` (or by using `createChatTransport`, which does it for you). See [Set up authentication](/docs/ai-transport/getting-started/authentication) for how `authUrl` is wired up. - -## 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 handles 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. +Every operation that touches the channel throws `InvalidArgument` until the connect promise resolves, which stops a `view.send` landing before the subscription is in place. `connect()` is also idempotent, so a component that mounts twice gets the same promise back. Tear a client down with `close()` and an agent with `end()`. An agent that hands an in-flight run to another process uses `detach()` instead, which [durable execution](/docs/ai-transport/features/durable-execution#close-once) covers. -New participants join at any time. A second client attaching to the channel hydrates the full session from history and receives live updates going forward. No handshake or coordination is required between participants. +The `codec` argument is the translation layer between your framework's events and Ably messages. The Vercel codec is bundled, and `createClientSession` imported from `@ably/ai-transport/vercel` comes pre-bound with it, so you can leave the argument out. Writing your own is covered in [codec architecture](/docs/ai-transport/internals/codec-architecture). -To see which participants are currently connected, use presence: the session channel carries Ably Presence, exposed directly as `session.presence`. See [Agent presence](/docs/ai-transport/features/agent-presence). - -The session channel also carries Ably LiveObjects, exposed directly as `session.object`, for shared mutable state that the user and the agent both read and write, such as the page the user is on or a record they have selected. The agent reacts to it without polling, and the user sees the agent's own state change in real time. See [LiveObjects State](/docs/ai-transport/features/liveobjects). - -## Support 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. - -## Detach or end a session +## Share a session across participants -An `AgentSession` exposes two teardown methods. Pick the one that matches whether the current process owns the Run's terminal. +The session is the unit of sharing. A second client joins the session, an agent hydrates the session to build context for a model call, and every published message goes to the session. No client needs to be present for any other to work, and no arrival or departure corrupts the state. -Use [`session.detach()`](/docs/ai-transport/api/javascript/core/agent-session#detach) when the Run is intentionally left open on the channel for another process to continue. It unsubscribes from cancels, aborts local abort controllers, and releases the channel without publishing any Run terminal. This is the teardown a durable-workflow activity uses between Steps so the next activity can adopt the Run. +Agent lifecycle does not affect the session. An agent hydrates the session, works through a run, and terminates. The session survives because it lives on the channel rather than in the agent's memory, so a different agent instance handles the next run with the same state. Clients are equally resilient. A client that drops its connection loses nothing, because on reconnect the Ably connection resumes from the last received serial and any messages published during the gap are delivered. -Use [`session.end()`](/docs/ai-transport/api/javascript/core/agent-session#session-end) for the final teardown of a turn that runs in a single process, or the outermost catch of a workflow when activity retries are exhausted. It closes every still-open Run this session owns as `'cancelled'` and then detaches. A fire-and-forget turn that forgets its own `run.end()` still unsticks every observer's UI this way. +New clients join at any time. A second client attaching to the channel hydrates the full session from history and receives live updates from then on, with no handshake between clients. -The two are not interchangeable. Calling `end()` mid-workflow marks the Run terminal, so the next activity's `adoptRun` rejects with `InvalidArgument` because the Run is read-only. See [Durable execution](/docs/ai-transport/features/durable-execution) for the full pattern. +The session channel carries two Ably features directly. [Presence](/docs/ai-transport/features/agent-presence) is exposed as `session.presence` and tells you which participants are currently connected. [LiveObjects](/docs/ai-transport/features/liveobjects) is exposed as `session.object` and holds shared mutable state that the user and the agent both read and write, such as the record the user has selected. The agent reacts to it without polling, and the user sees the agent's changes in realtime. ## Read next - -A Run is rarely one continuous stream. A single turn typically produces several discrete pieces of output: the tokens of an initial LLM response, the payload of a tool result, the tokens of a follow-up LLM response, and so on. The channel needs to represent each of those as a distinct, addressable unit rather than an undifferentiated slab of `ai-output` messages between `ai-run-start` and `ai-run-end`. - -The Step is the SDK's answer. Every output message carries a `step-id` naming the Step it was produced in, and every Step is bracketed by `ai-step-start` and `ai-step-end` events so an observer can tell one unit from the next. The Run becomes a container of Steps rather than a container of raw output. - -That structure is what makes a retry safe. Because a Step has stable identity, two `ai-step-start` events under the same `stepId` are the same Step re-attempting rather than two distinct pieces of output. Between the two attempts, the message that was published later wins, so a retry's output supersedes the failed attempt on the channel rather than appending beside it. Without a sub-Run unit with its own identity, there would be no safe place to draw a retry boundary within a Run. - -## Understand the Step model - -Every Step within a Run has four invariants: - -- A `stepId` that is stable across retry attempts of the same Step. -- A bracketing pair of lifecycle events on the channel: `ai-step-start` and `ai-step-end`. -- A terminal `StepEndReason` once it closes: `'complete'`, `'failed'`, or `'cancelled'`. -- One Step active at a time on a given Run. The next Step opens after the previous one ends. - -The Run remains open across Steps. A Step ending is not a Run ending; the agent code drives the Run to [`suspend()`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend) or [`end()`](/docs/ai-transport/api/javascript/core/agent-session#run-end) after the last Step closes. - -Two kinds of Steps sit on a Run: - -- Implicit Steps opened by [`AgentRun.pipe`](/docs/ai-transport/api/javascript/core/agent-session#pipe). Each `pipe` call opens its own fresh Step lazily at the first output, stamps the stream, and closes on stream end. Two `pipe` calls produce two independent Steps. Implicit Steps never supersede; each is a fresh Step with a fresh id. -- Explicit Steps opened by [`AgentRun.createStep`](/docs/ai-transport/api/javascript/core/agent-session#create-step). Returns a `RunStep` handle. Pass `{ stepId }` to make the id stable across cross-process retries and gain the supersede semantics. - -## What the Step layer requires - -| Property | Why it matters | -| --- | --- | -| Stable stepId | A retry's `ai-step-start` must land under the same `stepId` as the failed attempt. Otherwise the retry's output publishes under a fresh id and both attempts remain in the conversation. Supply the workflow engine's own stable id ([Temporal](/docs/ai-transport/frameworks/temporal) activity id, [Vercel WDK](/docs/ai-transport/frameworks/vercel-wdk) step id) on cross-process retries. | -| Supersede on retry | When two attempts share a `stepId`, the message that was published later wins. The client's View surfaces only the winning attempt. | -| One Step at a time | Exactly one Step may be active on a Run. [`RunStep.start`](/docs/ai-transport/api/javascript/core/agent-session#step-start) rejects if another Step is still open. | -| Terminal auto-close | If agent code forgets to close a Step, [`AgentRun.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end) auto-closes the open Step before publishing the Run terminal. No observer's UI is stranded on `streaming`. | -| Independent from Run terminal | A Step ending as `'failed'` does not end the Run. The Run stays active for the next Step; the agent code decides whether to retry, suspend, or end the Run itself. | - -## Publish Steps across processes - -The Steps of one Run are not always published from the same process. A workflow engine runs each Step as a separate activity, a retry lands in a fresh invocation, a suspended Run resumes its next Step in a background worker. Steps compose across those boundaries because a Step's output and lifecycle events live on the channel, not in the process that published them. Any process that attaches a session to the channel and enters the Run can publish the next Step. - -A process publishes its Steps through an [`AgentSession`](/docs/ai-transport/api/javascript/core/agent-session) bound to the channel: construct it with `createAgentSession`, attach with `connect`. Every `ai-step-start`, `ai-step-end`, and `ai-output` inside a Step goes out through that session. The session keeps no state of its own; the open Run and its Steps live on the channel, which is what makes them visible to the next process. - -A Step always belongs to a Run, so a process must be inside that Run before it can publish one. The process that publishes the first Step opens the Run with `session.createRun(invocation)` and `start()`. A later process enters the already-open Run with `session.adoptRun(...)` and `load()`, which recovers the Run's state from channel history so it can publish further Steps without re-opening the Run. Adoption also routes channel cancels to this process, so a cancel ends the Step it is currently publishing as `'cancelled'`. See [Runs](/docs/ai-transport/concepts/runs) for the identifiers this threads across the boundary. - -Once it has published its Step, the process releases the channel. `session.detach()` releases it without publishing anything, leaving the Run open so the next process can adopt it and publish the next Step. `session.end()` closes any Step and Run this session still holds open as `'cancelled'`, then detaches. A process handling one Step mid-Run uses `detach`; the process that publishes the final Step, and a failure-catch cleanup, uses `end`. See [Detach or end a session](/docs/ai-transport/concepts/sessions#detach-vs-end). - -## Coalesce retries under a stable stepId - -When a Run's Steps run across workflow-engine activities, each activity follows the same shape: adopt the Run, create the Step under the workflow's activity id, pipe the LLM stream, close the Step. A retry re-enters the same code with the same activity id, so the retry's Step lands under the same `stepId` and supersedes the earlier attempt. - - -```javascript -import { createAgentSession } from '@ably/ai-transport'; -import { stepIdFor } from '@ably/ai-transport/temporal'; - -async function runInferenceStep({ runId, invocationId, triggerEventId }) { - const session = createAgentSession({ /* ... */ }); - await session.connect(); - - const run = session.adoptRun({ runId, invocationId, triggerEventId }); - await run.load(); - - const step = run.createStep({ stepId: stepIdFor(invocationId) }); - await step.start(); - await step.pipe(llmStream); - await step.end(); - - await session.detach(); -} -``` - - -`stepIdFor(invocationId)` reads the Temporal activity id from the activity context and prefixes it with the Run's invocation id so it stays unique across workflows. Other durable execution frameworks pass their own stable id in the same slot. - -Omit `stepId` for the common in-process case. The SDK assigns an invocation-scoped id, and an in-process retry after a `'failed'` close reuses that id automatically. - -## Read next -Both [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session) and [`AgentSession`](/docs/ai-transport/api/javascript/core/agent-session) expose presence directly as `session.presence`, with the standard `enter()`, `update()`, `leave()`, `get()`, and `subscribe()` operations. Presence operations attach the session's channel for you, so you can call them without first awaiting `connect()`. +Both [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session) and [`AgentSession`](/docs/ai-transport/api/javascript/core/agent-session) expose presence directly as `session.presence`, with the standard `enter()`, `update()`, `leave()`, `get()`, and `subscribe()` operations. Presence operations attach the session for you, so you can call them without first awaiting `connect()`. -The agent enters presence with its initial status, then updates that status as it moves through a turn (receiving a message, thinking, streaming, finishing) and leaves when it shuts down. Every connected client receives those updates in real time. +The agent enters presence with its initial status, then updates that status as it moves through a turn (receiving a message, thinking, streaming, finishing) and leaves when it shuts down. Every connected client receives those updates in realtime. ```javascript @@ -77,11 +77,11 @@ const agent = members.find((m) => m.clientId === 'agent'); ``` -You can put whatever your UI needs into presence data: a coarse `status`, a progress percentage, the name of the tool the agent is currently calling. Presence carries the agent's self-report; the conversation itself carries the run lifecycle. +You can put whatever your UI needs into presence data: a coarse `status`, a progress percentage, the name of the tool the agent is currently calling. ## Combine presence with active runs -For richer status indicators, combine presence data with the active runs on the view. Presence tells you the agent's self-reported state; `session.view.runs()` tells you which runs are actually in progress: +On the client, combine presence data with the active runs on the view for richer status indicators. Presence tells you the agent's self-reported state; `session.view.runs()` tells you which runs are actually in progress: ```javascript @@ -119,15 +119,15 @@ function AgentStatus() { - An agent that exits without calling `presence.leave()` (for example, a crashed process) is automatically removed from presence after a timeout. The agent is treated as present until the timeout fires. Wire a graceful shutdown that calls `leave()` for the best user experience. - A serverless agent that comes up for one turn and tears down should enter and leave presence per turn; entering once and leaving once at the end is fine for a long-running agent. -- Presence updates do not guarantee strict ordering with channel messages. A `streaming` presence update sometimes arrives slightly after the first token. Drive the UI off `session.view.runs()` for run-level state (active, suspended, terminal) and use presence for higher-level status the agent self-reports. +- Presence updates do not guarantee strict ordering with the messages on the session. A `streaming` presence update sometimes arrives slightly after the first token. Drive the UI off `session.view.runs()` for run-level state (active, suspended, terminal) and use presence for higher-level status the agent self-reports. - Multi-agent setups need a unique `clientId` per agent. Two agents with the same `clientId` collide in the presence set. -- A client without `presence` capability cannot subscribe to updates. Capability scoping is part of [authentication](/docs/ai-transport/concepts/authentication). +- A client without `presence` capability cannot subscribe to updates. Capability scoping is part of [authentication](/docs/ai-transport/getting-started/authentication). ## FAQ ### Does presence cost a message? -Presence enter, update, and leave each consume a message on the channel. See [the platform pricing](/docs/platform/pricing) for current rates. +Presence enter, update, and leave each consume a message on the channel, billed at the current [message rates](/docs/platform/pricing). ### Can clients enter presence too? @@ -139,11 +139,11 @@ Until Ably's presence timeout fires (currently around 15 seconds). Active connec ### What is the difference between presence and the view's active runs? -Presence is self-reported by the agent. `session.view.runs()` is observable from the channel by inspecting run lifecycle events. Presence reports intent; active runs report fact. Both together produce richer status. +Presence is self-reported by the agent. `session.view.runs()` is observable from the session by inspecting run lifecycle events. Both together produce richer status. ### Can I pause inference when no users are connected? -Yes. Subscribe to presence and check whether any non-agent participants are present. If none, end the run or short-circuit the LLM call. This is one of the cost-saving patterns presence enables. +Yes. Subscribe to presence and check whether any non-agent participants are present. If none, end the run or short-circuit the LLM call. ## Related features diff --git a/src/pages/docs/ai-transport/features/branching.mdx b/src/pages/docs/ai-transport/features/branching.mdx index 2c242349f9..42026e15cf 100644 --- a/src/pages/docs/ai-transport/features/branching.mdx +++ b/src/pages/docs/ai-transport/features/branching.mdx @@ -7,11 +7,11 @@ redirect_from: - /docs/ai-transport/features/edit-and-regenerate --- -Your users can revise the past without losing it. The original message stays, the new content forks alongside, and the UI flips between branches. AI Transport handles the tree shape on the wire; you wire up edit, regenerate, and the navigation arrows. +Your users can revise the past without losing it. The original message stays while the new content forks alongside it, and the UI flips between branches. AI Transport handles the tree shape on the wire; you build the edit, regenerate, and navigation controls. ![Diagram showing a branching conversation tree with edit and regenerate forks creating sibling branches from anchored messages](../../../../images/content/diagrams/ait-concepts-conversation-tree.png) -A minimal regenerate: +On the client, a minimal regenerate: ```javascript @@ -23,13 +23,13 @@ await view.regenerate(assistantMessageId); Every node on the channel carries `parent` and (optionally) `fork-of` or `msg-regenerate` headers. The [conversation tree](/docs/ai-transport/concepts/conversation-tree) reads these to build the branching structure: an edit produces a sibling `InputNode` whose `forkOf` points at the original user message's `codec-message-id`; a regenerate produces a same-parent sibling `RunNode` whose `regeneratesCodecMessageId` points at the original assistant message. -Branches are message-anchored: the branch decision lives on a `codecMessageId`, not on a `runId`. UIs render navigation arrows next to the bubble: the user's prompt for edit forks, the assistant slot for regenerate groups. +The branch decision is anchored to a `codecMessageId` rather than a `runId`. UIs render navigation arrows next to the bubble: the user's prompt for edit forks, the assistant slot for regenerate groups. -The View resolves the branch state on demand. Call `view.branchSelection(codecMessageId)` for any message and get back a handle: `{ hasSiblings, siblings, index, selected, select }`. Call the handle's `select(index)` to switch. +The view resolves the branch state on demand. Call `view.branchSelection(codecMessageId)` for any message and get back a handle: `{ hasSiblings, siblings, index, selected, select }`. Call the handle's `select(index)` to switch. ## Regenerate -Regenerate creates a sibling Run of an assistant message and starts a fresh turn from the same user prompt. The original response stays in the tree. +Regenerate creates a sibling run of an assistant message and starts a fresh turn from the same user prompt. The original response stays in the tree. On the client: ```javascript @@ -38,11 +38,11 @@ await regenerate(assistantMessageId); ``` -The SDK publishes a `Regenerate` well-known input variant that points at the assistant `codecMessageId`. The agent receives the [Invocation](/docs/ai-transport/concepts/invocations), creates a new Run with `regeneratesCodecMessageId` set, and streams the alternative response. +The SDK publishes a `Regenerate` well-known input variant that points at the assistant `codecMessageId`. The agent receives the [Invocation](/docs/ai-transport/concepts/runs#invocations), creates a new run with `regeneratesCodecMessageId` set, and streams the alternative response. ## Edit a user message -Edit replaces a user message and starts a new turn from that point. The original user message and everything below it stays in the tree as a separate branch. +Edit replaces a user message and starts a new turn from that point. The original user message and everything below it stays in the tree as a separate branch. On the client: ```javascript @@ -69,7 +69,7 @@ await view.edit(messageId, [ ``` -The SDK publishes the replacement inputs as fresh `UserMessage`s with the `forkOf` header pointing at the `codecMessageId` being replaced. The agent forks the Run and starts a fresh response from the edited content. +The SDK publishes the replacement inputs as fresh `UserMessage`s with the `forkOf` header pointing at the `codecMessageId` being replaced. The agent forks the run and starts a fresh response from the edited content. ## Navigate between siblings @@ -93,13 +93,13 @@ function BranchNav({ codecMessageId, view }) { ``` -`branchSelection` always returns a usable object: `hasSiblings: false` for messages that aren't branch anchors, `index: 0` for unknown ids. Safe to call on every rendered bubble without conditionals. +`branchSelection` always returns a usable object: `hasSiblings: false` for messages that aren't branch anchors, `index: 0` for unknown ids. You can call it on every rendered bubble without conditionals. When the user selects a different sibling, the view recomputes the visible branch. Every message below the selection point re-renders to reflect the chosen path. ## Side-by-side branches -Multiple views over the same conversation tree have independent branch selections, so different parts of the UI can render different branches simultaneously: +On the client, multiple views over the same conversation tree have independent branch selections, so different parts of the UI can render different branches simultaneously: ```javascript @@ -114,7 +114,7 @@ This is the pattern for comparison UIs where the user wants to see two regenerat ## Agent-side handling -The agent doesn't usually need bespoke branching logic. The Invocation arrives, `createRun` returns a Run pinned to the right branch, and draining `run.view` walks the ancestor chain along the selected branch automatically: +The agent doesn't usually need bespoke branching logic. The invocation arrives, `createRun` returns a run pinned to the right branch, and draining `run.view` walks the ancestor chain along the selected branch automatically: ```javascript @@ -148,16 +148,16 @@ try { ``` -`run.view` is pinned to this Run's branch, so draining it yields the complete branch (ancestor turns and the current user input) in order. Pass it straight to the LLM. +`run.view` is pinned to this run's branch, so draining it yields the complete branch (ancestor turns and the current user input) in order. Pass it straight to the LLM. ## Edge cases and unhappy paths -- Editing or regenerating mid-stream cancels nothing automatically. Call `session.cancel(runId)` on the active Run first if you do not want both to run. +- Editing or regenerating mid-stream cancels nothing automatically. Call `session.cancel(runId)` on the active run first if you do not want both to run. - Branch selection is per-view. Two devices on the same session can see different branches simultaneously; the tree is shared, the selection is per-view. - Deeply branched trees can have many siblings. A user can navigate forever; cap or hide branch navigation in your UI if your app has a preferred branch. - A regenerate against an edited prompt still works. The new branch attaches at the same anchor regardless of what siblings exist. - The visible branch recomputes when a sibling is selected through the branch handle's `select`. Avoid heavy work in the render path; updates are frequent during streaming. -- An optimistic edit is folded with the published edit. See [optimistic updates](/docs/ai-transport/features/optimistic-updates) for how the SDK reconciles the local insertion with the wire confirmation. +- An optimistic edit is folded with the published edit. The SDK [reconciles the local insertion with the wire confirmation](/docs/ai-transport/features/optimistic-updates). ## FAQ @@ -183,6 +183,6 @@ Tree depth is bounded by the conversation length. Branch breadth grows with edit ## Related features -- [Conversation tree](/docs/ai-transport/concepts/conversation-tree): the data structure branches live in. +- [Conversation tree](/docs/ai-transport/concepts/conversation-tree): the data structure that holds branches. - [Multi-device sessions](/docs/ai-transport/features/multi-device): edits and regenerations sync across devices. - [Optimistic updates](/docs/ai-transport/features/optimistic-updates): how local-first edits reconcile with the published tree. diff --git a/src/pages/docs/ai-transport/features/cancellation.mdx b/src/pages/docs/ai-transport/features/cancellation.mdx index 98c279ea87..0aacc1a674 100644 --- a/src/pages/docs/ai-transport/features/cancellation.mdx +++ b/src/pages/docs/ai-transport/features/cancellation.mdx @@ -2,16 +2,16 @@ title: "Cancellation" meta_description: "Cancel AI responses mid-stream with Ably AI Transport. Scoped cancel signals, server-side authorization, and graceful abort handling." meta_keywords: "AI cancellation, cancel streaming, abort signal, AI Transport, cancel filter, Ably" -intro: "Your users can stop an agent mid-response without breaking the session. AI Transport sends cancel as a signal on the channel, so other turns continue and the session stays open." +intro: "Your users can stop an agent mid-response without breaking the session. AI Transport sends cancel as a signal on the session, so other turns continue and the session stays open." redirect_from: - /docs/ai-transport/messaging/completion-and-cancellation --- -Cancellation is a turn-level operation. The client publishes a cancel signal on the Ably channel; the agent receives it on the run with the matching `runId` and fires its abort signal. Unlike closing an HTTP connection, cancellation is an explicit signal: the session remains intact, other runs continue, and both sides handle cleanup gracefully. +Cancellation is a turn-level operation. The client publishes a cancel signal on the session; the agent receives it on the run with the matching `runId` and fires its abort signal. Cancellation is an explicit signal, so the session remains intact and other runs continue while both sides clean up gracefully. -![Diagram showing a cancel signal stopping the in-progress Run](../../../../images/content/diagrams/ait-cancellation.png) +![Diagram showing a cancel signal stopping the in-progress run](../../../../images/content/diagrams/ait-cancellation.png) -A minimal cancel: +On the client, a minimal cancel: ```javascript @@ -21,7 +21,7 @@ await activeRun.cancel(); ## How it works -Sessions are bidirectional, so a cancel is just a signal on the channel. The client publishes a cancel message keyed on the triggering input's `codec-message-id` (the synchronous handle the client owns from send time). Once the agent has resolved the cancel to a registered Run, that Run's `abortSignal` fires. The LLM stream stops, the run ends with reason `'cancelled'`, and every subscriber receives the lifecycle update. A cancel published before the agent has minted the run-id is still honoured: the agent buffers it and fires once the input-event lookup resolves. +Sessions are bidirectional, so a cancel is just a signal on the session. The client publishes a cancel message keyed on the triggering input's `codec-message-id` (the synchronous handle the client owns from send time). Once the agent has resolved the cancel to a registered run, that run's `abortSignal` fires. The LLM stream stops, the run ends with reason `'cancelled'`, and every subscriber receives the lifecycle update. A cancel published before the agent has assigned the run-id is still honoured: the agent buffers it and fires once the input-event lookup resolves. ```javascript @@ -43,7 +43,7 @@ const result = streamText({ ## Cancel one run, several, or all -`activeRun.cancel()` targets the run the client just kicked off. To cancel several runs, iterate the visible Runs and cancel each by id: +`activeRun.cancel()` targets the run the client just started. To cancel several runs, iterate the visible runs on the client and cancel each by id: ```javascript @@ -66,7 +66,7 @@ Every run exposes an `abortSignal` that fires when the run is cancelled. Pass it const run = session.createRun(invocation, { signal: req.signal }); // Drain run.view for the full conversation to feed the model. run.messages -// is only this Run's own turn. +// is only this run's own turn. while (run.view.hasOlder()) await run.view.loadOlder(); await run.start(); @@ -118,7 +118,7 @@ const run = session.createRun(invocation, { ## Cancel on close -`ClientSession.close()` is local-state-only: it does not cancel runs on the wire. To stop in-progress runs before closing, cancel them explicitly: +`ClientSession.close()` is local-state-only: it does not cancel runs on the wire. On the client, cancel in-progress runs explicitly before closing: ```javascript @@ -132,15 +132,15 @@ await session.close(); - Cancellation is asynchronous. A few more tokens arrive after `cancel()` returns and before the server's `abortSignal` fires. Render them on the cancelled turn. - The server is responsible for honouring the abort signal. A tool invocation that does not check the signal continues to run until it completes. -- Cancel signals from a client without the channel [publish capability](/docs/auth/capabilities#capability-operations) will silently fail. Verify capabilities on the [authentication](/docs/ai-transport/concepts/authentication) endpoint. +- Cancel signals from a client without the channel [publish capability](/docs/auth/capabilities#capability-operations) will silently fail. Verify capabilities on the [authentication](/docs/ai-transport/getting-started/authentication) endpoint. - An `onCancel` that returns `false` does not notify the requesting client. Surface the rejection through your own application protocol if the user needs to know. -- A cancel sent before the turn starts is delivered to the channel and accumulated; the server applies it as soon as the turn is created. +- A cancel sent before the turn starts is delivered to the session and accumulated; the server applies it as soon as the turn is created. ## FAQ ### Why use cancel signals instead of closing the connection? -Closing the connection disconnects a client from the session. The session and connection are distinct and not coupled. A cancel signal notifies the agent to stop the stream but leaves the session intact, so the next message starts a new turn immediately, on every connected device. See [reconnection and recovery](/docs/ai-transport/features/reconnection-and-recovery) for how clients that disconnect mid-stream can reconnect and resume. +Closing the connection disconnects a client from the session. The session and connection are distinct and not coupled. A cancel signal notifies the agent to stop the stream but leaves the session intact, so the next message starts a new turn immediately, on every connected device. Clients that disconnect mid-stream [reconnect and resume](/docs/ai-transport/features/reconnection-and-recovery). ### Can a user on another device cancel my turn? @@ -150,16 +150,16 @@ Yes, if your `onCancel` hook authorises it. The default accepts all cancel reque The turn cancels once. Subsequent matching signals are no-ops; the abort signal does not refire. -### How do I tell a cancelled run apart from one that finished normally? +### How do I distinguish a cancelled run from one that finished normally? -`run.end(reason)` reports the reason on the channel. Clients receive it through the view's `run` lifecycle event. The reason is `'cancelled'` for a cancel and `'complete'` for a normal finish. +`run.end(reason)` reports the reason on the session. Clients receive it through the view's `run` lifecycle event. The reason is `'cancelled'` for a cancel and `'complete'` for a normal finish. ### Does cancel cost a message? -The cancel signal is a published message on the channel. See [pricing](/docs/platform/pricing) for current rates. +The cancel signal is a published message on the channel, billed at the current [message rates](/docs/platform/pricing). ## Related features -- [Interruption and steering](/docs/ai-transport/features/interruption-and-steering): steer the active Run, cancel and re-prompt, or send alongside. +- [Interruption and steering](/docs/ai-transport/features/interruption-and-steering): steer the active run, cancel and re-prompt, or send alongside. - [Concurrent turns](/docs/ai-transport/features/concurrent-turns): multiple turns with independent cancel handles. - [Token streaming](/docs/ai-transport/features/token-streaming): what gets cancelled when the abort fires. 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 f6577b3aaa..a9c0b17754 100644 --- a/src/pages/docs/ai-transport/features/chain-of-thought.mdx +++ b/src/pages/docs/ai-transport/features/chain-of-thought.mdx @@ -1,21 +1,21 @@ --- title: "Chain of thought" -meta_description: "Stream reasoning and thinking content alongside responses with Ably AI Transport. Display chain-of-thought in real time." +meta_description: "Stream reasoning and thinking content alongside responses with Ably AI Transport. Display chain-of-thought in realtime." meta_keywords: "chain of thought, reasoning, thinking, AI Transport, Ably, streaming reasoning" -intro: "Your users see the agent's reasoning as it streams, side by side with the response. AI Transport multiplexes reasoning and text streams within the same Run." +intro: "Your users see the agent's reasoning as it streams, side by side with the response. AI Transport multiplexes reasoning and text streams within the same run." redirect_from: - /docs/ai-transport/messaging/chain-of-thought --- -Chain of thought streams reasoning content alongside the main response text. The codec supports multiple stream types within a single Run. Text and reasoning are delivered as separate streams that render independently in the UI. +Chain of thought streams reasoning content alongside the main response text. The codec supports multiple stream types within a single run. Text and reasoning are delivered as separate streams that render independently in the UI. -![Diagram showing reasoning and response tokens streaming as parallel parts within the same Run](../../../../images/content/diagrams/ait-chain-of-thought.png) +![Diagram showing reasoning and response tokens streaming as parallel parts within the same run](../../../../images/content/diagrams/ait-chain-of-thought.png) ## How it works -When an LLM produces reasoning or thinking tokens, the codec multiplexes them alongside text tokens on the same Ably channel. Each stream type is tagged so the client routes reasoning content to one part of the UI and response text to another. +When an LLM produces reasoning or thinking tokens, the codec multiplexes them alongside text tokens on the same session. Each stream type is tagged so the client routes reasoning content to one part of the UI and response text to another. -With the Vercel AI SDK integration, reasoning arrives as a separate `reasoning` stream type within the UI message stream: +On the agent, with the Vercel AI SDK integration, reasoning arrives as a separate `reasoning` stream type within the UI message stream: ```javascript @@ -48,7 +48,7 @@ app.post('/api/chat', async (req, res) => { ``` -No additional server configuration is needed. If the model produces reasoning tokens, the codec encodes them as a distinct stream within the run. +No extra server configuration is needed. If the model produces reasoning tokens, the codec encodes them as a distinct stream within the run. ## Display reasoning in the UI @@ -70,7 +70,7 @@ for (const { message } of messages) { ``` -Both streams update in real time. Users see the reasoning appear as the model thinks, followed by (or alongside) the response text. +Both streams update in realtime. Users see the reasoning appear as the model thinks, followed by (or alongside) the response text.