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

  1. Return exactly "stop" or { release: <finite number>, reason: "<string>" } from every code path of onBudgetExhausted
  2. Default to "stop" when the escalation logic errors or cannot decide
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/beb1304df299df8c. Report an issue: GitHub.