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:
| stopReason | Meaning |
|---|---|
"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 |
// 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:
// 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:
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:
agent.abort(); // Cancel current generation/tool execution
await agent.waitForIdle(); // Wait for the run to fully settle13.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:
// 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:
// 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:
- Tool schema incompatibility — some OpenAI-compatible endpoints do not support strict mode or grammar tools;
- Message role ordering — some compatible endpoints require toolResult to be followed by an assistant message;
- Thinking parameters unsupported — the
reasoning_effortfield is rejected by the gateway; - Field name differences —
max_tokensvsmax_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";
AbortControllerpassing a signal oragent.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
- Implement a CLI with a "Stop Generation" button: press Ctrl+C to trigger abort and retain already-generated partial content.
- Write an automatic retry wrapper: when encountering
stopReason === 'error', retry with exponential backoff, up to 3 times. - 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.