Skip to content

Chapter 13 · Stop Reasons, Abort, and Error Handling ​

Chapter goals: Understand all stopReasons of AssistantMessage, master AbortController for proactive abort, continuing a session after abort, and debugging Provider payloads.

13.1 Stop Reasons Overview ​

At the end of every generation, AssistantMessage.stopReason tells you why it ended:

stopReasonMeaning
"pending"Appears only in partial messages; the final stop reason is not yet known
"stop"The model's final message this round; ended naturally
"length"Output hit the max token limit and was truncated
"toolUse"The model is calling a tool, waiting for tool results
"error"An error occurred during generation
"aborted"The request was cancelled by the abort signal
typescript
// Branch handling based on stop reason
if (message.stopReason === 'length') {
  // Truncated: prompt the user or use continue to write more
  console.warn('Output was truncated by maxTokens');
} else if (message.stopReason === 'toolUse') {
  // Need to execute the tool and feed the result back
  console.log('Model is waiting for tool results');
}

responseId is not guaranteed to exist

AssistantMessage may carry a responseId (provider upstream identifier), but do not assume it exists on all providers.

13.2 Errors Never Throw from Stream Functions ​

An important design in pi-ai: request failures never throw exceptions from stream functions. All failures, including abort and tool validation errors, are communicated through error events + the final message's stopReason:

typescript
// Error handling: listen to error events + check the final message
for await (const event of models.stream(model, context)) {
  if (event.type === 'error') {
    // event.reason is "error" or "aborted"
    console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
  }
}

// Check the final message after the stream ends
if (message.stopReason === 'error' || message.stopReason === 'aborted') {
  console.log('Failure reason:', message.errorMessage);
}

This "errors are data" design lets retry logic be written completely synchronously after stream consumption, without wrapping the async loop in try/catch.

13.3 AbortController for Proactive Abort ​

Use the standard AbortController to cancel an in-progress request — for example, when the user clicks a "Stop Generation" button:

typescript
const controller = new AbortController();

// Auto-abort after 2 seconds (simulating a timeout)
setTimeout(() => controller.abort(), 2000);

const response = await models.complete(model, context, {
  signal: controller.signal, // Pass in the abort signal
});

// After abort, stopReason is aborted; partial content may already be present
if (response.stopReason === 'aborted') {
  console.log('Request was aborted:', response.errorMessage);
  // response.content may already have partial generated content; keep selectively
}

On the Agent class it is even simpler — call agent.abort() directly to cancel the current operation, then use waitForIdle() to wait for full stop:

typescript
agent.abort();             // Cancel current generation/tool execution
await agent.waitForIdle(); // Wait for the run to fully settle

13.4 Continuing a Session After Abort ​

Aborting does not destroy session state. continue() resumes execution from the current context, without adding new messages — suitable for retries after errors:

typescript
// After abort or error, retry from the current state
// Note: the last message in context must be user or toolResult, not assistant
await agent.continue();

continue() preconditions

continue() requires the last message in context to be user or toolResult. If the last message is an assistant message (e.g. you want to continue writing after a normal end), use prompt() to append a new user message instead.

13.5 Debugging Provider Payloads ​

When a request fails inexplicably, the first step is to look at the actual request body sent out. pi-ai provides a debug hook to print the Provider payload:

typescript
// Debug: inspect the real payload transformed and sent to the provider
const stream = models.stream(model, context, {
  // After enabling debug, inspect the request body in the callback
  onPayload: (payload) => {
    console.log(JSON.stringify(payload, null, 2));
  },
});

Common troubleshooting checklist:

  1. Tool schema incompatibility — some OpenAI-compatible endpoints do not support strict mode or grammar tools;
  2. Message role ordering — some compatible endpoints require toolResult to be followed by an assistant message;
  3. Thinking parameters unsupported — the reasoning_effort field is rejected by the gateway;
  4. Field name differences — max_tokens vs max_completion_tokens.

Most of these compatibility differences can be corrected via the model's compat flag bits (detailed in the next chapter).

Chapter Summary ​

  • Six stopReasons cover natural end, truncation, tool waiting, errors, and aborts;
  • Errors never throw from stream functions; they are uniformly communicated through error events and stopReason — "errors are data";
  • AbortController passing a signal or agent.abort() can both abort; partial content can still be retained;
  • continue() resumes from the current state; the precondition is the last message is user/toolResult;
  • Debug by printing Provider payloads; focus on tool schema, role ordering, and thinking parameters.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What does stopReason "toolUse" mean?

2. What happens when a request fails (e.g. network error) in pi-ai?

3. What is the precondition for calling agent.continue()?

4. What is the most direct way to abort an in-progress Agent operation?

🛠️ Hands-on Practice ​

  1. Implement a CLI with a "Stop Generation" button: press Ctrl+C to trigger abort and retain already-generated partial content.
  2. Write an automatic retry wrapper: when encountering stopReason === 'error', retry with exponential backoff, up to 3 times.
  3. Intentionally send an incompatible tool schema to an OpenAI-compatible endpoint and use payload debugging to find the rejected field.

With error handling solved, Chapter 14 teaches you how to connect to any OpenAI-compatible endpoint.