Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/pages/docs/ai-transport/api/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
```
</Code>

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.
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
26 changes: 21 additions & 5 deletions src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,25 @@ Use `suspend` instead of `end` when you want the run to come back. Use `end` onl

### End the run <a id="run-end"/>

<MethodSignature>{`end(reason: RunEndReason): Promise<void>`}</MethodSignature>
<MethodSignature>{`end(params: RunEndParams): Promise<void>`}</MethodSignature>

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 <a id="run-end-params"/>

`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.

<Table id='RunEndParams'>

| 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` |

</Table>

## Close the session <a id="close"/>

Expand Down Expand Up @@ -350,7 +366,7 @@ Use [`loadConversation`](#load-conversation) before piping to the LLM in any con

## RunEndReason <a id="run-end-reason"/>

`'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`.

Expand Down Expand Up @@ -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();
Expand Down
39 changes: 27 additions & 12 deletions src/pages/docs/ai-transport/api/javascript/vercel/run-outcome.mdx
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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);
Expand All @@ -29,15 +29,15 @@ if (outcome === 'suspend') {

## Resolve the run outcome <a id="vercel-run-outcome"/>

<MethodSignature>{`vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike<FinishReason>): Promise<RunEndReason | 'suspend'>`}</MethodSignature>
<MethodSignature>{`vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike<FinishReason>): Promise<VercelRunOutcome>`}</MethodSignature>

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.

Expand All @@ -63,7 +63,22 @@ The rejection guard matters: Vercel AI SDK v6 rejects `streamText().finishReason

### Returns <a id="vercel-run-outcome-returns"/>

`Promise<RunEndReason | 'suspend'>`. 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<VercelRunOutcome>`. 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)`.

<Table id='VercelRunOutcome' hidden>

| 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` |

</Table>

## Example <a id="example"/>

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/api/react/core/use-tree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions src/pages/docs/ai-transport/api/react/core/use-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

</Table>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/agent-presence.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/ai-transport/features/branching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/cancellation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const result = streamText({
});

const { reason } = await run.pipe(result.toUIMessageStream());
await run.end(reason);
await run.end({ reason });
```
</Code>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/chain-of-thought.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/concurrent-turns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/human-in-the-loop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/ai-transport/features/token-streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const result = streamText({
});

const { reason } = await run.pipe(result.toUIMessageStream());
await run.end(reason);
await run.end({ reason });
session.close();
```
</Code>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/ai-transport/features/tool-calling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const result = streamText({
});

const { reason } = await run.pipe(result.toUIMessageStream());
await run.end(reason);
await run.end({ reason });
```
</Code>

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/ai-transport/frameworks/vercel-ai-sdk-core.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
```
</Code>
Expand Down Expand Up @@ -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();
});

Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/ai-transport/getting-started/core-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/ai-transport/getting-started/vercel-ai-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down