Skip to content

Commit ce248e5

Browse files
committed
AI Transport docs: drain run.view before run.start()
run.start() awaits run.located, and on a cold start the triggering input only folds into the tree once the caller pages run.view — so draining after start() can hang. Reorder every agent example to drain (loadOlder loop) and build the conversation before calling run.start(), matching the SDK's own v0.4.0 docs (streaming.md, database-hydration.md) and the AgentRun.start contract. Affects agent-session, run-outcome, the agent-route feature pages, both frameworks pages, and both getting-started guides. database-hydration already used the correct loadUntil-then-start order. Also: chain-of-thought used "turn" for the Run lifecycle unit (abort signal, stream multiplexing, post-completion) inconsistently with the rest of the page — align to "Run". Fix "client-side React hooks" casing on the Vercel AI SDK UI page. AIT-1090
1 parent 43ff9e1 commit ce248e5

12 files changed

Lines changed: 68 additions & 54 deletions

File tree

src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Subscribe to non-fatal session errors with [`on('error')`](#on) rather than a co
9191

9292
<MethodSignature>{`connect(): Promise<void>`}</MethodSignature>
9393

94-
Subscribe to the channel and implicitly attach. The subscribe is unfiltered so channel-history-replayed input events fold into the tree, where each run's input-event watcher can match its trigger. Idempotent: subsequent calls return the same promise. All `AgentRun` methods (`start`, `pipe`, `suspend`, `end`) throw `InvalidArgument` until `connect()` has been called.
94+
Attaches and subscribes to the channel backing the session. Idempotent: subsequent calls return the same promise. All `AgentRun` methods (`start`, `pipe`, `suspend`, `end`) throw `InvalidArgument` until `connect()` has been called.
9595

9696
<Code>
9797
```javascript
@@ -107,7 +107,7 @@ await session.connect();
107107

108108
<MethodSignature>{`createRun(invocation: Invocation, runtime?: RunRuntime<TOutput>): AgentRun<TOutput, TProjection, TMessage>`}</MethodSignature>
109109

110-
Create a new `AgentRun` from an `Invocation`. Returns synchronously and arms the run's input-event watcher (a passive pre-scan of the tree plus a listener for the trigger's arrival); it 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`.
110+
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`.
111111

112112
<Code>
113113
```javascript
@@ -167,21 +167,21 @@ The handle returned by [`createRun`](#create-run). It extends the shared `BaseRu
167167
| 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 |
168168
| status | The Run's lifecycle status, read live off the tree. | `RunStatus` |
169169
| error | The terminal error, present exactly when `status` is `'error'`. | `Ably.ErrorInfo` or Undefined |
170-
| messages | This Run's whole turn: its triggering input message followed by its own streamed output, deduplicated by `codecMessageId`. The unit to persist. See [Hydrate the conversation](#conversation-hydration). | `TMessage[]` |
170+
| 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[]` |
171171
| 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 |
172172
| abortSignal | `AbortSignal` scoped to this Run. Fires when a cancel event arrives. | `AbortSignal` |
173-
| view | Read-only, leaf-pinned `View<TMessage>` of this Run's branch, from its triggering input back to the conversation root. Drain it with `loadOlder()` for ancestor context to feed the model. Empty until the trigger folds into the tree. See [Hydrate the conversation](#conversation-hydration). | `View<TMessage>` |
174-
| located | Resolves when this Run's triggering input folds into the tree (live or via a `view.loadOlder()` page). [`start`](#run-start) awaits it internally; await it directly only to read the trigger before deciding how to start. | `Promise<void>` |
173+
| view | A read-only `View<TMessage>` of the conversation branch this Run belongs to, from its triggering input back to the conversation root. Use it to reconstruct the conversation to feed the model. See [Hydrate the conversation](#conversation-hydration). | `View<TMessage>` |
174+
| 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<void>` |
175175

176176
</Table>
177177

178178
### Start the run <a id="run-start"/>
179179

180180
<MethodSignature>{`start(): Promise<void>`}</MethodSignature>
181181

182-
Await [`located`](#run) (the run's input-event watcher resolves the trigger, whether it arrives live or already sits in channel history), then read the trigger's wire headers and publish the opening lifecycle event (`ai-run-start`, or `ai-run-resume` for a continuation). Must be called before `pipe`, `suspend`, or `end`.
182+
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`.
183183

184-
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 folds in. Race it against your own timeout if you need one.
184+
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.
185185

186186
### Pipe the response stream <a id="pipe"/>
187187

@@ -333,20 +333,23 @@ const invocation = Invocation.fromJSON(data);
333333

334334
Two accessors expose conversation content, at different scopes:
335335

336-
- `run.messages` is this Run's own turn: its triggering input plus its streamed output. This is the unit to persist, not the value to feed the model in a multi-turn conversation.
336+
- `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.
337337
- `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.
338338

339-
To rebuild the prior conversation for the model, drain `run.view` with `loadOlder()` (the sole history driver) for as much ancestor context as you want, then read `getMessages()`:
339+
`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.
340+
341+
To rebuild the prior conversation for the model, drain `run.view` with `loadOlder()` for as much ancestor context as you want, then read `getMessages()`:
340342

341343
<Code>
342344
```javascript
343-
await run.start();
344-
345-
// Drain the branch back to the root for full context.
345+
// Rebuild the conversation from run.view before run.start(): draining pages in
346+
// this run's triggering input (otherwise run.start() awaits it arriving live).
346347
while (run.view.hasOlder()) {
347348
await run.view.loadOlder();
348349
}
349350
const conversation = run.view.getMessages().map(({ message }) => message);
351+
352+
await run.start();
350353
```
351354
</Code>
352355

@@ -384,14 +387,15 @@ export async function POST(req: Request) {
384387
const run = session.createRun(invocation, { signal: req.signal });
385388

386389
try {
387-
await run.start();
388-
389-
// Drain the run's branch back to the root for the model context.
390+
// Rebuild the conversation from run.view before run.start(): draining pages
391+
// in this run's triggering input (otherwise run.start() awaits it live).
390392
while (run.view.hasOlder()) {
391393
await run.view.loadOlder();
392394
}
393395
const conversation = run.view.getMessages().map(({ message }) => message);
394396

397+
await run.start();
398+
395399
const llmStream = await callMyLLM(conversation);
396400
const result = await run.pipe(llmStream);
397401
await run.end({ reason: result.reason });

src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,15 @@ export async function POST(req: Request) {
106106
const run = session.createRun(invocation, { signal: req.signal });
107107

108108
try {
109-
await run.start();
110-
111-
// Rebuild the prior conversation from run.view to feed the model.
109+
// Rebuild the conversation from run.view before run.start(): draining pages
110+
// in this run's triggering input (otherwise run.start() awaits it live).
112111
while (run.view.hasOlder()) {
113112
await run.view.loadOlder();
114113
}
115114
const conversation = run.view.getMessages().map(({ message }) => message);
116115

116+
await run.start();
117+
117118
const result = streamText({
118119
model: openai('gpt-4'),
119120
messages: conversation,

src/pages/docs/ai-transport/features/agent-presence.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ app.post('/api/chat', async (req, res) => {
3232
// Enter presence so every connected client sees what the agent is doing.
3333
await session.presence.enter({ status: 'thinking' });
3434

35-
await run.start();
36-
37-
// Drain run.view for the prior conversation to feed the model.
35+
// Rebuild the conversation from run.view before run.start(): draining pages in
36+
// this run's triggering input (otherwise run.start() awaits it arriving live).
3837
while (run.view.hasOlder()) {
3938
await run.view.loadOlder();
4039
}
4140
const conversation = run.view.getMessages().map(({ message }) => message);
4241

42+
await run.start();
43+
4344
const result = streamText({
4445
model: openai('gpt-4o'),
4546
messages: conversation,

src/pages/docs/ai-transport/features/branching.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,15 @@ const invocation = Invocation.fromJSON(await req.json());
122122
const run = session.createRun(invocation, { signal: req.signal });
123123

124124
try {
125-
await run.start();
126-
127-
// Drain run.view along this branch back to the root.
125+
// Rebuild the conversation from run.view before run.start(): draining pages in
126+
// this run's triggering input (otherwise run.start() awaits it arriving live).
128127
while (run.view.hasOlder()) {
129128
await run.view.loadOlder();
130129
}
131130
const conversation = run.view.getMessages().map(({ message }) => message);
132131

132+
await run.start();
133+
133134
const result = streamText({
134135
model: anthropic('claude-sonnet-4-20250514'),
135136
messages: conversation,

src/pages/docs/ai-transport/features/chain-of-thought.mdx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title: "Chain of thought"
33
meta_description: "Stream reasoning and thinking content alongside responses with Ably AI Transport. Display chain-of-thought in real time."
44
meta_keywords: "chain of thought, reasoning, thinking, AI Transport, Ably, streaming reasoning"
5-
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 turn."
5+
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."
66
redirect_from:
77
- /docs/ai-transport/messaging/chain-of-thought
88
---
99

10-
Chain of thought streams reasoning content alongside the main response text. The codec supports multiple stream types within a single turn. Text and reasoning are delivered as separate streams that render independently in the UI.
10+
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.
1111

1212
![Diagram showing reasoning and response tokens streaming as parallel parts within the same Run](../../../../images/content/diagrams/ait-chain-of-thought.png)
1313

@@ -25,14 +25,15 @@ app.post('/api/chat', async (req, res) => {
2525
await session.connect();
2626
const run = session.createRun(invocation, { signal: req.signal });
2727

28-
await run.start();
29-
30-
// Drain run.view for the prior conversation to feed the model.
28+
// Rebuild the conversation from run.view before run.start(): draining pages in
29+
// this run's triggering input (otherwise run.start() awaits it arriving live).
3130
while (run.view.hasOlder()) {
3231
await run.view.loadOlder();
3332
}
3433
const conversation = run.view.getMessages().map(({ message }) => message);
3534

35+
await run.start();
36+
3637
const result = streamText({
3738
model: anthropic('claude-sonnet-4-20250514'),
3839
messages: conversation,
@@ -95,18 +96,18 @@ Yes. Reasoning is a separate part type. Filter it out at the render layer. The c
9596

9697
### Does cancelling cut off reasoning too? <a id="faq-cancel"/>
9798

98-
Yes. Both the reasoning and text streams share the turn's abort signal.
99+
Yes. Both the reasoning and text streams share the Run's abort signal.
99100

100101
### Are reasoning tokens charged the same as text? <a id="faq-pricing"/>
101102

102103
Yes. The channel does not distinguish between part types for billing. The cost depends on the published message count after rollup.
103104

104-
### How do I render reasoning differently after the turn finishes? <a id="faq-post-render"/>
105+
### How do I render reasoning differently after the Run finishes? <a id="faq-post-render"/>
105106

106-
The `node.message.parts` array stays available after the turn ends. Hide or collapse reasoning when the streaming flag flips to false.
107+
The `node.message.parts` array stays available after the Run ends. Hide or collapse reasoning when the streaming flag flips to false.
107108

108109
## Related features <a id="related"/>
109110

110111
- [Token streaming](/docs/ai-transport/features/token-streaming): how text tokens are streamed and accumulated.
111-
- [Tool calling](/docs/ai-transport/features/tool-calling): another multi-part stream type within a turn.
112+
- [Tool calling](/docs/ai-transport/features/tool-calling): another multi-part stream type within a Run.
112113
- [Codec API](/docs/ai-transport/api/javascript/core/codec): reference for the codec that multiplexes reasoning and text streams.

src/pages/docs/ai-transport/features/concurrent-turns.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ app.post('/api/chat', async (req, res) => {
4444
await session.connect();
4545
const run = session.createRun(invocation, { signal: req.signal });
4646

47-
await run.start();
48-
49-
// Drain run.view for the prior conversation to feed the model.
47+
// Rebuild the conversation from run.view before run.start(): draining pages in
48+
// this run's triggering input (otherwise run.start() awaits it arriving live).
5049
while (run.view.hasOlder()) {
5150
await run.view.loadOlder();
5251
}
5352
const conversation = run.view.getMessages().map(({ message }) => message);
5453

54+
await run.start();
55+
5556
const result = streamText({
5657
model: anthropic('claude-sonnet-4-20250514'),
5758
messages: conversation,

src/pages/docs/ai-transport/features/push-notifications.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ app.post('/api/chat', async (req, res) => {
4242
await session.connect()
4343
const run = session.createRun(invocation, { signal: req.signal })
4444

45-
await run.start()
46-
47-
// Drain run.view for the prior conversation to feed the model.
45+
// Rebuild the conversation from run.view before run.start(): draining pages in
46+
// this run's triggering input (otherwise run.start() awaits it arriving live).
4847
while (run.view.hasOlder()) {
4948
await run.view.loadOlder()
5049
}
5150
const conversation = run.view.getMessages().map(({ message }) => message)
5251

52+
await run.start()
53+
5354
const result = streamText({
5455
model: openai('gpt-4o'),
5556
messages: conversation,

src/pages/docs/ai-transport/features/token-streaming.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,15 @@ const session = createAgentSession({ client: ably, channelName: invocation.sessi
8383
await session.connect();
8484
const run = session.createRun(invocation, { signal: req.signal });
8585

86-
await run.start();
87-
88-
// Drain run.view for the prior conversation to feed the model.
86+
// Rebuild the conversation from run.view before run.start(): draining pages in
87+
// this run's triggering input (otherwise run.start() awaits it arriving live).
8988
while (run.view.hasOlder()) {
9089
await run.view.loadOlder();
9190
}
9291
const conversation = run.view.getMessages().map(({ message }) => message);
9392

93+
await run.start();
94+
9495
const result = streamText({
9596
model: anthropic('claude-sonnet-4-20250514'),
9697
messages: conversation,

src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,15 @@ export async function POST(req) {
7878
const run = session.createRun(invocation, { signal: req.signal });
7979

8080
after(async () => {
81-
await run.start();
82-
83-
// Drain run.view for the prior conversation to feed the model.
81+
// Rebuild the conversation from run.view before run.start(): draining pages
82+
// in this run's triggering input (otherwise run.start() awaits it live).
8483
while (run.view.hasOlder()) {
8584
await run.view.loadOlder();
8685
}
8786
const conversation = run.view.getMessages().map(({ message }) => message);
8887

88+
await run.start();
89+
8990
const result = streamText({
9091
model: anthropic('claude-sonnet-4-20250514'),
9192
messages: await convertToModelMessages(conversation),

src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-ui.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Vercel AI SDK UI"
33
meta_description: "How Ably AI Transport integrates with Vercel AI SDK UI (@ai-sdk/react) to add durable sessions and multi-device sync to useChat."
44
meta_keywords: "AI Transport, Vercel AI SDK UI, useChat, ChatTransport, durable sessions, React, multi-device"
5-
intro: "Vercel AI SDK UI gives you client side react hooks like useChat for building chat interfaces. AI Transport plugs into useChat as a ChatTransport, so the same UI gets durable sessions, multi-device sync, and bidirectional control."
5+
intro: "Vercel AI SDK UI gives you client-side React hooks like useChat for building chat interfaces. AI Transport plugs into useChat as a ChatTransport, so the same UI gets durable sessions, multi-device sync, and bidirectional control."
66
redirect_from:
77
- /docs/ai-transport/framework-guides/vercel-ai-sdk
88
---

0 commit comments

Comments
 (0)