JuliusBrussee/caveman · error
cave_budget_escalation_result_invalid
cave_budget_escalation_result_invalid
Error message
cave_budget_escalation_result_invalid
What it means
Thrown when a RunOptions.onBudgetExhausted handler is invoked on budget exhaustion (exactly one escalation attempt per exhaustion) and returns an invalid outcome. The contract accepts "stop" or an object { release: number, reason: string }; anything else — undefined, a string other than "stop", a missing/Non-number release, a missing reason — is rejected. A handler that keeps releasing slivers is also bounded to one attempt, so malformed responses must fail loudly rather than loop.
Source
Thrown at packages/agent/src/runtime.ts:1632
restorableBytes: restorableRequestBytes(
conversationOriginals,
instructions,
originalInstructions,
),
});
let decided: NextCallDecision;
try {
decided = plan();
// Escalation gets exactly one attempt per exhaustion: the handler either
// funds the next call or it does not, and a handler that keeps releasing
// slivers must not turn one stop into an unbounded loop.
if (decided.action === "stop" && decided.reason === "budget_exhausted" &&
budgetMeter !== undefined && typeof options.onBudgetExhausted === "function") {
const outcome = await options.onBudgetExhausted(budgetExhaustionContext(budgetMeter));
if (outcome !== "stop") {
if (!isRecord(outcome) || typeof outcome.release !== "number" ||
typeof outcome.reason !== "string") {
throw new Error("cave_budget_escalation_result_invalid");
}
budgetMeter.release(outcome.release, outcome.reason);
decided = plan();
}
}
} catch (error) {
// Pi answers a thrown streamFn with a synthesized terminal error turn,
// which would otherwise replace this cause with a generic provider
// failure. Record it first so the run reports what actually went wrong.
ladderFailure ??= error instanceof Error ? error : new Error(String(error));
throw ladderFailure;
}
if (decided.action === "stop") {
// Pi answers a thrown streamFn with a synthesized zero-usage error turn,
// so record the refusal before throwing or the stop becomes the usage
// failure that turn produces.
stopReason = decided.reason;
refusalPending = true;View on GitHub (pinned to 27d5a3981a)
Solutions
- Return exactly "stop" or { release: <finite number>, reason: "<string>" } from every code path of onBudgetExhausted
- Default to "stop" when the escalation logic errors or cannot decide
- Validate external approvals before returning them: typeof r.release === "number" && typeof r.reason === "string"
Example fix
// before
const result = await agent.run(input, {
budget: { maxUsd: 5 },
onBudgetExhausted: async () => {
const ok = await askHuman();
if (ok) return { release: 5 }; // missing reason
}, // falls through to undefined
});
// after
const result = await agent.run(input, {
budget: { maxUsd: 5 },
onBudgetExhausted: async () => {
const ok = await askHuman();
return ok ? { release: 5, reason: "human approved top-up" } : "stop";
},
}); Defensive patterns
Strategy: type-guard
Validate before calling
type EscalationOutcome = "stop" | { release: number; reason: string };
function validOutcome(v: unknown): EscalationOutcome {
if (v === "stop") return v;
if (typeof v === "object" && v !== null &&
typeof (v as any).release === "number" &&
typeof (v as any).reason === "string") {
return v as { release: number; reason: string };
}
return "stop"; // fail safe: malformed escalation stops rather than throwing
} Type guard
const isEscalationOutcome = (v: unknown): v is EscalationOutcome =>
v === "stop" ||
(typeof v === "object" && v !== null &&
typeof (v as { release?: unknown }).release === "number" &&
typeof (v as { reason?: unknown }).reason === "string"); Prevention
- Wrap handler bodies so every path returns "stop" or a validated object
- Validate outputs of humans/external systems before forwarding as escalation outcomes
- Remember the handler gets exactly one attempt per exhaustion — return a decisive answer
When it happens
Trigger: An async handler whose return path is missing (returns undefined); returning { release: "5" } (string) or { release: 5 } without reason; returning true or "continue".
Common situations: A handler that asks a human or another system for approval and forwards its answer unvalidated; a handler that conditionally forgets to return; refactoring from a boolean-based hook to the outcome-object contract.
Related errors
- cave_budget_denomination_ambiguous
- cave_budget_max_invalid
- cave_budget_initial_invalid
- cave_budget_output_floor_invalid
- cave_budget_on_exhausted_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/beb1304df299df8c.
Report an issue: GitHub.