vercelRunOutcome
vercelRunOutcome resolves the outcome of a Vercel streamText response that was piped through Run.pipe. It returns a VercelRunOutcome 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.
Use it at the end of every Vercel route handler so the lifecycle event you publish matches what actually happened on the LLM side.
1
2
3
4
5
6
7
8
9
10
11
12
import { streamText } from 'ai';
import { vercelRunOutcome } from '@ably/ai-transport/vercel';
const result = streamText({ model, messages, tools });
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
}Resolve the run outcome
vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike<FinishReason>): Promise<VercelRunOutcome>The helper applies two rules:
- If
pipeResult.reasonis'cancelled'or'error', return it as-is. The transport already knows the run ended for a non-completion reason. An'error'outcome carriespipeResult.errorwrapped as anAbly.ErrorInfo. - If
pipeResult.reasonis'complete', awaitfinishReasonand translate:'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
finishReasonrejects, classify the rejection: abort-shaped errors return{ reason: 'cancelled' }; anything else (for exampleNoOutputGeneratedError) returns{ reason: 'error', error }with the rejection wrapped as anAbly.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.
Parameters
pipeResultrequiredStreamResultRun.pipe.finishReasonrequiredPromiseLike<AI.FinishReason>finishReason promise from a Vercel streamText result.Returns
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; callRun.suspendso a continuation Invocation can resume the Run.{ reason: 'complete' | 'cancelled' }— the run terminated normally; pass the outcome toRun.end.{ reason: 'error', error }— the run failed;erroris anAbly.ErrorInfo. Pass the outcome toRun.end.
error is present only on the 'error' arm. The non-'suspend' arms are assignable to RunEndParams, so after a 'suspend' guard the whole object passes straight to Run.end(outcome).
Example
A Vercel route handler that runs a tool-capable model, pipes the stream, and either suspends (for tool resolution) or ends the run with the correct reason.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import * as Ably from 'ably';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { Invocation } from '@ably/ai-transport';
import { createAgentSession, vercelRunOutcome } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
export async function POST(req: Request) {
const invocation = Invocation.fromJSON(await req.json());
const session = createAgentSession({
client: ably,
channelName: invocation.sessionName,
});
await session.connect();
const run = session.createRun(invocation, { signal: req.signal });
try {
await run.start();
await run.loadConversation();
const result = streamText({
model: openai('gpt-4'),
messages: run.messages,
tools: myTools,
});
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
}
} finally {
session.close();
}
return Response.json({ invocationId: run.invocationId });
}