PAIPAI

Tool Failures

ctx.fail for planned stops, throw for bugs, honor ctx.signal for Stop.

A tool can end in four ways besides returning output: fail, throw, cancel, suspend. Suspend is Human approval. The other three belong here.

ctx.fail — planned, model-visible

Use fail when the outcome is expected: missing record, closed gate, upstream that will not answer. The message reaches the model. PAI does not treat it as a runtime incident.

execute: async (ctx) => {
  const row = await ctx.context.records.get(ctx.input.id);
  if (!row) {
    return ctx.fail(`No record ${ctx.input.id}.`);
  }
  return row;
};

The client sees { code: "tool_failed", message: "No record …" }. The string is what the model reads.

The UI sees a tool part in output-error with that errorText. Tests use expectFailed() — see Tool tests.

throw — unexpected

Throw when the tool did not mean to stop: a database connection died, a bug. The thrown message, cut to 2,000 characters, becomes the part's errorText: the client shows it and the model reads it. Keep secrets and connection strings out of it, or replace it with mapError. PAI's own errors read The tool could not complete. instead. PAI reports the original on the server (onRuntimeError). Do not throw for "user typed a bad id."

ctx.signal — the user hit Stop

execute: async (ctx) => {
  return ctx.context.docs.search(ctx.input.query, {
    workspaceId: ctx.context.workspaceId,
    signal: ctx.signal,
  });
};

Pass signal into fetch, ORMs, and anything else that can abort. A tool that ignores it keeps running after Stop.

What the model sees

There is no part.state === "cancelled". Stop becomes output-error with part.cancelled === true. A suspend stays input-available with a pending action.

EndTool partModel
return outputstate: "output-available"the output
ctx.failstate: "output-error"the fail message
throwstate: "output-error" (the message, not the stack)the thrown message, or The tool could not complete. for PAI's own errors
abort / Stopstate: "output-error" and part.cancelledthe turn stopped
ctx.suspendstate: "input-available", pending action, runStatus: "waiting"blocked — Human approval
invalid input (the tool does not run)state: "output-error"Invalid input for tool <name>: and the validation issues from inputSchema, as the AI SDK sends them, up to 2,000 characters
the step ended before the model finished the input (the tool does not run)state: "output-error"Invalid input for tool <name>: and that the model ended the step before completing the input, or that the input was cut off at the output length limit. The run continues so the model can retry.
an earlier sequential call in the step failed (the tool does not run)state: "output-error"Not run: the earlier call to <tool> in this step failed.
run failed or loststate: "output-error"the call was interrupted and may have completed

Messages you write into inputSchema, such as a refine message, reach the model and the client as written.

Prefer a field on outputSchema ({ found: false }) when "not found" is a normal result you want to render as UI, not as an error.

Next

On this page