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.

JavaScript

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:

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

Parameters

pipeResultrequiredStreamResult
The result returned by Run.pipe.
finishReasonrequiredPromiseLike<AI.FinishReason>
The 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; call Run.suspend so a continuation Invocation can resume the Run.
  • { reason: 'complete' | 'cancelled' } — the run terminated normally; pass the outcome to Run.end.
  • { reason: 'error', error } — the run failed; error is an Ably.ErrorInfo. Pass the outcome to Run.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.

JavaScript

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 });
}