diff --git a/src/pages/docs/ai-transport/api/errors.mdx b/src/pages/docs/ai-transport/api/errors.mdx
index f0c3246689..e3597de0d1 100644
--- a/src/pages/docs/ai-transport/api/errors.mdx
+++ b/src/pages/docs/ai-transport/api/errors.mdx
@@ -122,9 +122,11 @@ const run = agentSession.createRun(invocation, {
// End a Run with reason 'error' when piping fails.
try {
const result = await run.pipe(llmStream);
- await run.end(result.reason);
+ await run.end({ reason: result.reason });
} catch (error) {
- await run.end('error');
+ await run.end({ reason: 'error' });
}
```
+
+To give clients specific detail, optionally pass an `Ably.ErrorInfo`: `run.end({ reason: 'error', error: new Ably.ErrorInfo(message, code, statusCode) })`. Omit `error` to end in error without detail and the client sees a generic fallback; either way clients read it from `RunInfo.error` and the session `error` event.
diff --git a/src/pages/docs/ai-transport/api/index.mdx b/src/pages/docs/ai-transport/api/index.mdx
index 910457785b..74191c814c 100644
--- a/src/pages/docs/ai-transport/api/index.mdx
+++ b/src/pages/docs/ai-transport/api/index.mdx
@@ -89,7 +89,7 @@ The Vercel entry points re-export the core factories with the codec pre-bound. I
},
{
title: 'vercelRunOutcome',
- description: 'Map a Vercel streamText finish reason and a pipe result to the RunEndReason for 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',
},
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 bb3bf5830c..d32655072f 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
@@ -290,9 +290,25 @@ Use `suspend` instead of `end` when you want the run to come back. Use `end` onl
### End the run
-{`end(reason: RunEndReason): Promise`}
+{`end(params: RunEndParams): Promise`}
-Publish the `ai-run-end` event to the channel terminally and clean up. `reason` is one of `'complete'`, `'cancelled'`, or `'error'`. To pause a Run instead of ending it, use [`suspend`](#run-suspend).
+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).
+
+#### Parameters
+
+`RunEndParams` is discriminated on `reason`:
+
+- `{ reason: 'complete' | 'cancelled' }` — a non-error terminal reason; carries no `error`.
+- `{ reason: 'error', error? }` — the run ended in error; `error` is an optional `Ably.ErrorInfo` to surface to clients. Omit it to end in error without detail.
+
+
+
+| Property | Required | Description | Type |
+| --- | --- | --- | --- |
+| reason | required | The terminal reason. | `RunEndReason` |
+| error | optional | The terminal error to surface to clients. Allowed only when `reason` is `'error'`. | `Ably.ErrorInfo` |
+
+
## Close the session
@@ -350,7 +366,7 @@ Use [`loadConversation`](#load-conversation) before piping to the LLM in any con
## RunEndReason
-`'complete' | 'cancelled' | 'error'`. Passed to [`Run.end`](#run-end) and 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`.
@@ -385,9 +401,9 @@ export async function POST(req: Request) {
const llmStream = await callMyLLM(run.messages);
const result = await run.pipe(llmStream);
- await run.end(result.reason);
+ await run.end({ reason: result.reason });
} catch (err) {
- await run.end('error');
+ await run.end({ reason: 'error' });
throw err;
} finally {
session.close();
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 9a3c490c9f..85d2f4ce88 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,12 +1,12 @@
---
title: "vercelRunOutcome"
-meta_description: "API reference for vercelRunOutcome, the helper that maps a Vercel streamText finishReason and Run.pipe result to a RunEndReason or a 'suspend' sentinel."
-meta_keywords: "AI Transport, vercelRunOutcome, RunEndReason, streamText, finishReason, Vercel AI SDK, Ably"
+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 either a terminal [`RunEndReason`](/docs/ai-transport/api/javascript/core/agent-session#run-end-reason) you pass to [`Run.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end), or the sentinel `'suspend'` telling you to call [`Run.suspend`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend) instead.
+`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)).
-It preserves transport-level outcomes (`'cancelled'`, `'error'`) from the pipe result. When the pipe completed naturally, it awaits Vercel's `finishReason` and returns `'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 `'complete'` otherwise.
+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`.
Use it at the end of every Vercel route handler so the lifecycle event you publish matches what actually happened on the LLM side.
@@ -19,7 +19,7 @@ const result = streamText({ model, messages, tools });
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
-if (outcome === 'suspend') {
+if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
@@ -29,15 +29,15 @@ if (outcome === 'suspend') {
## Resolve the run outcome
-{`vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike): Promise`}
+{`vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike): Promise`}
The helper applies two rules:
-1. If `pipeResult.reason` is `'cancelled'` or `'error'`, return it as-is. The transport already knows the run ended for a non-completion reason.
+1. If `pipeResult.reason` is `'cancelled'` or `'error'`, return it as-is. The transport already knows the run ended for a non-completion reason. An `'error'` outcome carries `pipeResult.error` wrapped as an `Ably.ErrorInfo`.
2. If `pipeResult.reason` is `'complete'`, await `finishReason` and translate:
- - `'tool-calls'` returns `'suspend'`. The LLM requested tools the SDK did not auto-execute, so the run should pause for the next client-published tool result.
- - Any other Vercel finish reason returns `'complete'`.
- - If `finishReason` rejects, classify the rejection: abort-shaped errors return `'cancelled'`; anything else (for example `NoOutputGeneratedError`) returns `'error'`.
+ - `'tool-calls'` returns `{ reason: 'suspend' }`. The LLM requested tools the SDK did not auto-execute, so the run should pause for the next client-published tool result.
+ - 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.
@@ -63,7 +63,22 @@ The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason
### Returns
-`Promise`. Either a terminal reason (`'complete'`, `'cancelled'`, `'error'`) to pass to [`Run.end`](/docs/ai-transport/api/javascript/core/agent-session#run-end), or the sentinel `'suspend'` telling the caller to invoke [`Run.suspend`](/docs/ai-transport/api/javascript/core/agent-session#run-suspend) so a continuation Invocation can resume the Run.
+`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: '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).
+
+`error` is present only on the `'error'` arm. The non-`'suspend'` arms are assignable to [`RunEndParams`](/docs/ai-transport/api/javascript/core/agent-session#run-end-params), so after a `'suspend'` guard the whole object passes straight to `Run.end(outcome)`.
+
+
+
+| Property | Description | Type |
+| --- | --- | --- |
+| 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` |
+
+
## Example
@@ -103,7 +118,7 @@ export async function POST(req: Request) {
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
- if (outcome === 'suspend') {
+ if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
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 6e0d3c7220..c148f6d83a 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
@@ -59,7 +59,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| 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 |
-| status | Run lifecycle status. `'active'` while streaming, `'suspended'` while paused awaiting input, otherwise the `RunEndReason`. | `'active' \| 'suspended' \| RunEndReason` |
+| 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 |
| startSerial | Ably serial of the first observed message tagged with this run-id. | String or Undefined |
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 67701d9c82..23ba8bf330 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
@@ -78,6 +78,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| 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 |
diff --git a/src/pages/docs/ai-transport/features/agent-presence.mdx b/src/pages/docs/ai-transport/features/agent-presence.mdx
index 05194007cc..5967b68a03 100644
--- a/src/pages/docs/ai-transport/features/agent-presence.mdx
+++ b/src/pages/docs/ai-transport/features/agent-presence.mdx
@@ -42,7 +42,7 @@ app.post('/api/chat', async (req, res) => {
await channel.presence.update({ status: 'streaming' });
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
await channel.presence.leave();
session.close();
res.json({ ok: true });
diff --git a/src/pages/docs/ai-transport/features/branching.mdx b/src/pages/docs/ai-transport/features/branching.mdx
index 8ffc2085d2..52087c7051 100644
--- a/src/pages/docs/ai-transport/features/branching.mdx
+++ b/src/pages/docs/ai-transport/features/branching.mdx
@@ -131,9 +131,9 @@ try {
});
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
} catch (err) {
- await run.end('error');
+ await run.end({ reason: 'error' });
throw err;
} finally {
session.close();
diff --git a/src/pages/docs/ai-transport/features/cancellation.mdx b/src/pages/docs/ai-transport/features/cancellation.mdx
index 2950aca116..df2deddeb9 100644
--- a/src/pages/docs/ai-transport/features/cancellation.mdx
+++ b/src/pages/docs/ai-transport/features/cancellation.mdx
@@ -73,7 +73,7 @@ const result = streamText({
});
const { reason } = await run.pipe(result.toUIMessageStream());
-await run.end(reason);
+await run.end({ reason });
```
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 3fa5f57f7e..d41e3a8d7c 100644
--- a/src/pages/docs/ai-transport/features/chain-of-thought.mdx
+++ b/src/pages/docs/ai-transport/features/chain-of-thought.mdx
@@ -35,7 +35,7 @@ app.post('/api/chat', async (req, res) => {
});
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
session.close();
res.json({ ok: true });
});
diff --git a/src/pages/docs/ai-transport/features/concurrent-turns.mdx b/src/pages/docs/ai-transport/features/concurrent-turns.mdx
index c5176ba75d..a618447eda 100644
--- a/src/pages/docs/ai-transport/features/concurrent-turns.mdx
+++ b/src/pages/docs/ai-transport/features/concurrent-turns.mdx
@@ -54,7 +54,7 @@ app.post('/api/chat', async (req, res) => {
});
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
session.close();
res.json({ ok: true });
});
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 5ccb87897a..69b77c0a25 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
@@ -61,7 +61,7 @@ const result = streamText({
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
-if (outcome === 'suspend') {
+if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
diff --git a/src/pages/docs/ai-transport/features/push-notifications.mdx b/src/pages/docs/ai-transport/features/push-notifications.mdx
index 904bc55db4..221e3dbd54 100644
--- a/src/pages/docs/ai-transport/features/push-notifications.mdx
+++ b/src/pages/docs/ai-transport/features/push-notifications.mdx
@@ -52,7 +52,7 @@ app.post('/api/chat', async (req, res) => {
})
const { reason } = await run.pipe(result.toUIMessageStream())
- await run.end(reason)
+ await run.end({ reason })
session.close()
if (reason === 'complete') {
diff --git a/src/pages/docs/ai-transport/features/token-streaming.mdx b/src/pages/docs/ai-transport/features/token-streaming.mdx
index bfc722e490..b5c0e3dabb 100644
--- a/src/pages/docs/ai-transport/features/token-streaming.mdx
+++ b/src/pages/docs/ai-transport/features/token-streaming.mdx
@@ -93,7 +93,7 @@ const result = streamText({
});
const { reason } = await run.pipe(result.toUIMessageStream());
-await run.end(reason);
+await run.end({ reason });
session.close();
```
diff --git a/src/pages/docs/ai-transport/features/tool-calling.mdx b/src/pages/docs/ai-transport/features/tool-calling.mdx
index bed5759dcc..f644d8285f 100644
--- a/src/pages/docs/ai-transport/features/tool-calling.mdx
+++ b/src/pages/docs/ai-transport/features/tool-calling.mdx
@@ -40,7 +40,7 @@ const result = streamText({
});
const { reason } = await run.pipe(result.toUIMessageStream());
-await run.end(reason);
+await run.end({ reason });
```
@@ -70,7 +70,7 @@ const result = streamText({
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
-if (outcome === 'suspend') {
+if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
diff --git a/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx b/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx
index 44998c7ee1..7d66614ad9 100644
--- a/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx
+++ b/src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx
@@ -47,7 +47,7 @@ return createUIMessageStreamResponse({ stream: result.toUIMessageStream() });
// After: Ably transport
const { reason } = await run.pipe(result.toUIMessageStream());
-await run.end(reason);
+await run.end({ reason });
return Response.json({ invocationId: run.invocationId });
```
@@ -82,7 +82,7 @@ export async function POST(req) {
});
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
session.close();
});
diff --git a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx
index 56b5da1f5e..32849f8f6d 100644
--- a/src/pages/docs/ai-transport/getting-started/core-sdk.mdx
+++ b/src/pages/docs/ai-transport/getting-started/core-sdk.mdx
@@ -77,9 +77,9 @@ export async function POST(req) {
});
const { reason } = await run.pipe(result.toUIMessageStream());
- await run.end(reason);
+ await run.end({ reason });
} catch (err) {
- await run.end('error');
+ await run.end({ reason: 'error' });
throw err;
} finally {
session.close();
diff --git a/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx b/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx
index 620e1679e8..367e0e942d 100644
--- a/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx
+++ b/src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx
@@ -83,13 +83,13 @@ export async function POST(req) {
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
- if (outcome === 'suspend') {
+ if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
}
} catch (err) {
- await run.end('error');
+ await run.end({ reason: 'error' });
throw err;
} finally {
session.close();
diff --git a/src/pages/docs/ai-transport/internals/conversation-tree.mdx b/src/pages/docs/ai-transport/internals/conversation-tree.mdx
index 03edbe001d..a7a7a18aba 100644
--- a/src/pages/docs/ai-transport/internals/conversation-tree.mdx
+++ b/src/pages/docs/ai-transport/internals/conversation-tree.mdx
@@ -59,7 +59,7 @@ Every node in the tree is a `ConversationNode = InputNode | RunNode`. Narrow on
| `forkOf` | The node key of the node this Run replaces, or `undefined` if this Run is not a fork. Reply-run regenerate siblings do not use this; they group by shared parent. |
| `regeneratesCodecMessageId` | The `codec-message-id` of the assistant message this Run regenerates. Verbatim from the wire's `msg-regenerate`. |
| `clientId` | The Ably `clientId` of the client that started the Run, from `run-client-id`. |
-| `status` | `'active'` between `ai-run-start` and `ai-run-end`; `'suspended'` after `ai-run-suspend` until `ai-run-resume`; otherwise the terminal `RunEndReason`. |
+| `state` | A `RunNodeState` discriminated on `status`: `{ status }` is `'active'` between `ai-run-start` and `ai-run-end`, `'suspended'` after `ai-run-suspend` until `ai-run-resume`, otherwise a non-error terminal `RunEndReason`; the `{ status: 'error', error }` arm carries the terminal `Ably.ErrorInfo`. |
| `projection` | The codec's per-Run projection, folded from every event published under this `run-id`. |
| `invocationId` | The agent-minted invocation id observed from the most recent `ai-run-start` (or `ai-run-resume` for continuations). Empty string until run-start arrives. |
| `startSerial` / `endSerial` | Ably serials of the lifecycle events. Used for sibling ordering. |
diff --git a/src/pages/docs/ai-transport/internals/transport-patterns.mdx b/src/pages/docs/ai-transport/internals/transport-patterns.mdx
index ae02a71e34..c7bc8b215d 100644
--- a/src/pages/docs/ai-transport/internals/transport-patterns.mdx
+++ b/src/pages/docs/ai-transport/internals/transport-patterns.mdx
@@ -106,7 +106,7 @@ Cancel routing connects client cancel signals to the correct agent-side Runs. Th
3. The agent matches the cancel against its registered Runs. If the cancel arrives before run-start (the client cancelled before the agent minted the run id), the agent buffers it and fires once its input-event lookup resolves the input to the run.
4. The matched Run's `onCancel` hook (if configured) is invoked. If the hook returns `false`, the cancel is rejected silently and the Run continues.
5. If the cancel is accepted, the matched Run's `AbortController` fires. `run.abortSignal` flips to aborted; the LLM stream (if it was passed the same signal) stops; `Run.pipe()` resolves with `reason: 'cancelled'`; the encoder closes any in-flight streams with `status: 'cancelled'`.
-6. The agent code, in its `finally` block, calls `run.end('cancelled')`. The agent publishes `ai-run-end` with `run-reason: cancelled`.
+6. The agent code, in its `finally` block, calls `run.end({ reason: 'cancelled' })`. The agent publishes `ai-run-end` with `run-reason: cancelled`.
Cancel routing is per-Run. A cancel for one Run does not affect other active Runs on the same session; each Run has its own `AbortController`.