vercelRunOutcome

vercelRunOutcome resolves the outcome of a Vercel streamText response that was piped through Run.pipe. It returns either a terminal RunEndReason you pass to Run.end, or the sentinel 'suspend' telling you to call Run.suspend instead.

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.

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 === 'suspend') {
  await run.suspend();
} else {
  await run.end(outcome);
}

Resolve the run outcome

vercelRunOutcome(pipeResult: StreamResult, finishReason: PromiseLike<FinishReason>): Promise<RunEndReason | 'suspend'>

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

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<RunEndReason | 'suspend'>. Either a terminal reason ('complete', 'cancelled', 'error') to pass to Run.end, or the sentinel 'suspend' telling the caller to invoke Run.suspend so a continuation Invocation can resume the Run.

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 === 'suspend') {
      await run.suspend();
    } else {
      await run.end(outcome);
    }
  } finally {
    session.close();
  }

  return Response.json({ invocationId: run.invocationId });
}