JuliusBrussee/caveman · error
cave_provider_terminal
cave_provider_terminal
Error message
cave_provider_terminal_${finalMessage.stopReason} What it means
Thrown when the final assistant message carries stopReason "error" or "aborted": the provider itself terminated the run in a failure state. The error message interpolates the stopReason (cave_provider_terminal_error or cave_provider_terminal_aborted).
Source
Thrown at packages/agent/src/runtime.ts:2111
(efficiencyPlan !== undefined ||
executionContext.spendLedgers.length > 0 ||
usageFailure.message === "cave_provider_model_identity_mismatch" ||
usageFailure.message === "cave_provider_identity_missing")) {
throw usageFailure;
}
if (pendingSpendReservations.length > 0) {
markSpendIncomplete(executionContext.spendLedgers);
throw new Error("cave_subagent_spend_evidence_incomplete");
}
// A run stopped before its first call has no assistant message, and that is
// the honest outcome rather than missing evidence: the caller gets an empty
// answer, zero usage, and the reason the runtime declined to spend.
if (!finalMessage && stopReason === undefined) {
throw new Error("cave_incomplete_evidence: Pi emitted no final assistant message");
}
if (finalMessage &&
(finalMessage.stopReason === "error" || finalMessage.stopReason === "aborted")) {
throw new Error(`cave_provider_terminal_${finalMessage.stopReason}`);
}
const text = finalMessage === undefined ? "" : assistantText(finalMessage);
if (definition.output?.schema && finalMessage !== undefined) {
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
throw new Error("cave_output_schema_invalid_json");
}
if (!Value.Check(definition.output.schema, parsed)) {
throw new Error("cave_output_schema_mismatch");
}
}
if (appliedPlan.appliedTransformIDs.length > 0 && !cacheBoundaryKnown) {
cacheBust = true;
markRequestPassThrough(headers, appliedPlan, "cache_boundary_unobserved");
}
for (const child of nestedReceipts) receipt.recordSubagent(child);View on GitHub (pinned to 27d5a3981a)
Solutions
- Distinguish the variant: cave_provider_terminal_aborted means an abort (check your signals/timeouts); cave_provider_terminal_error means the provider errored
- For provider errors, apply backoff and retry the run
- For aborted, check who owns the AbortSignal and increase client/provider timeouts if the abort was timeout-driven
- Inspect the provider's native error details in logs for the underlying cause
Defensive patterns
Strategy: try-catch
Type guard
function isProviderTerminal(e: unknown): "error" | "aborted" | null {
if (!(e instanceof Error)) return null;
const m = /^cave_provider_terminal_(error|aborted)$/.exec(e.message);
return m ? (m[1] as "error" | "aborted") : null;
} Try / catch
try {
await agent.run(input, opts);
} catch (e) {
const kind = isProviderTerminal(e);
if (kind === "aborted") { /* check signals/timeouts */ }
else if (kind === "error") { /* backoff and retry the provider call */ }
else throw e;
} Prevention
- Set realistic provider timeouts so generation isn't aborted mid-stream
- Use exponential backoff for cave_provider_terminal_error retries
- Keep abort signals scoped so unrelated cancellations can't hit the run
When it happens
Trigger: finalMessage.stopReason === "error" (provider-side error ended the turn) or "aborted" (the call was aborted, e.g. via signal or provider timeout).
Common situations: Provider rate limits or internal errors surfacing as a terminal stop; AbortSignal firing during generation; provider timeouts; content-filter terminations.
Related errors
- caveman build: invalid .caveman/provider.json
- caveman build: set CAVE_MODEL when zero or multiple provider
- cave_provider_identity_missing
- cave_provider_model_identity_mismatch
- cave_subagent_spend_evidence_incomplete
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/41282a446d0a99a3.
Report an issue: GitHub.