createToolResultFork

createToolResultFork builds the input and the send options a client needs to answer a suspended tool call. With the Vercel codec a client tool result opens a new reply run rather than re-entering the suspended run, so two clients answering the same call get separate sibling branches instead of colliding on one shared run.

You need it when you use the generic hooks and call view.send yourself. The ChatTransport builds the same input and send options internally, so an application using useChat never calls createToolResultFork.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

// Client code.
import { createToolResultFork } from '@ably/ai-transport/vercel';

const { input, sendOptions } = createToolResultFork({
  runMessages,
  parentCodecMessageId,
  toolCallId,
  result: { output },
  supersedesRunId,
});

const forked = await view.send([input], sendOptions);

Build a tool-result fork

createToolResultFork(params: { runMessages, parentCodecMessageId, toolCallId, result, supersedesRunId }): { input: VercelSessionInput, sendOptions: SendOptions }

The fork's parameters all come from the tree, because message order alone does not identify the suspended run. view.runOf(codecMessageId) gives the run that owns the assistant message holding the tool call, and tree.getRunNode(runId) gives the node whose parentCodecMessageId and projection the fork needs.

The returned input addresses a fresh assistant codec-message-id and carries a copy of the suspended run's whole message list, each entry re-created under a new id. createToolResultFork copies the whole run rather than only the current assistant message, which keeps context across sequential client tool calls, so a run that already resolved two tool calls carries both into the fork.

Parameters

runMessagesrequiredCodecMessage<UIMessage>[]
The suspended run's full message list, read as codec.getMessages(node.projection). The entry carrying toolCallId becomes the result's target.
parentCodecMessageIdrequiredString
The fork's structural parent, which is the suspended run's own input node from node.parentCodecMessageId. It roots the fork as a same-parent sibling of the suspended run instead of a detached root.
toolCallIdrequiredString
The tool call being resolved.
resultrequiredToolCallResolution
The resolution.
supersedesRunIdrequiredString
The runId of the suspended run this fork resolves, from node.runId. The tree then hides that run from branch selection.

Exactly one of output and errorMessage is set.

Returns

{ input: VercelSessionInput, sendOptions: SendOptions }. Pass both to view.send([input], sendOptions).

The send options carry parent, role: 'assistant', and supersedes, and deliberately carry no runId. The agent creates the fork's run id when it publishes ai-run-start, and the tree reconciles the client's optimistic reply run to it by the tool result's codec-message-id. The assistant role marks the run-less input as a reconstructed assistant turn, so the tree reads it as a reply run rather than a user prompt.

createToolResultFork throws an ErrorInfo with code 40003 when no entry in runMessages carries toolCallId, since the run does not own that call.

Example

Resolve a pending client tool call, then wake the agent so it reads the result from the session:

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

// Client code.
import { createToolResultFork, createUIMessageSessionCodec } from '@ably/ai-transport/vercel';

const codec = createUIMessageSessionCodec();

async function resolveToolCall(view, tree, pending, toolCallId, output) {
  const node = tree.getRunNode(view.runOf(pending.codecMessageId).runId);
  if (!node) return;

  const { input, sendOptions } = createToolResultFork({
    runMessages: codec.getMessages(node.projection),
    parentCodecMessageId: node.parentCodecMessageId,
    toolCallId,
    result: { output },
    supersedesRunId: node.runId,
  });

  const forked = await view.send([input], sendOptions);

  await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify(forked.toInvocation().toJSON()),
  });
}