JuliusBrussee/caveman · error

cave_reasoning_budget_exceeded

cave_reasoning_budget_exceeded

Error message

cave_reasoning_budget_exceeded

What it means

Thrown before a model call when an efficiency plan is active and the run's accumulated reasoning tokens already exceed efficiencyPlan.budgets.reasoning. The check runs at each call boundary (enforceSemanticBudgets also guards context and output there), so a model that burned its reasoning allocation ends the run rather than silently exceeding the plan. Unlike the model-call ceiling, this is a real failure, not a graceful stop.

Source

Thrown at packages/agent/src/runtime.ts:1590

          spendFailure ??= failure;
        }
      }
      finalMessage = message;
    };
    const streamFn: StreamFn = async (selected, context, streamOptions) => {
      if (options.signal?.aborted) {
        throw options.signal.reason ?? new Error("cave_run_aborted");
      }
      if (spendFailure) throw spendFailure;
      if (usageFailure) throw usageFailure;
      if (nestedUsage.incomplete) throw new Error("cave_nested_usage_incomplete");
      if (efficiencyPlan && reasoningUsageUnavailable) {
        throw new Error("cave_reasoning_usage_unavailable");
      }
      if (efficiencyPlan) {
        enforceSemanticBudgets(contextBill(lowered.ir), outputTokens, efficiencyPlan);
        if (reasoningTokens > efficiencyPlan.budgets.reasoning) {
          throw new Error("cave_reasoning_budget_exceeded");
        }
      }
      // The hard model-call ceiling is a stop condition, not a failure: ending
      // the run through the same graceful path as every other stop keeps the
      // partial work and the receipt intact. Checked before the
      // increment so exactly `maxModelCalls` calls are allowed.
      if (modelCalls >= maxModelCalls) {
        stopReason = "call_budget_exhausted";
        refusalPending = true;
        throw new Error("cave_run_stopped");
      }
      modelCalls++;
      // Between-calls stop point. Nothing is in flight here: the previous turn
      // and its tools have finished and settled, and this call has not started.
      const plan = () => decideNextCall({
        meter: budgetMeter,
        breakers,
        deadlineAt,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Raise budgets.reasoning in the plan (or recompile the plan so budgets match actual reasoning demands) and re-lock
  2. Reduce reasoning load: shorter histories, simpler decomposition, or a model/setting with lower thinking output
  3. If the plan is only advisory for this run, run without candidatePlan/lockedBuild

Example fix

// before
const plan = { ..., budgets: { reasoning: 2048, ... } };
await agent.run(hardInput, { candidatePlan: plan });

// after
const plan = { ..., budgets: { reasoning: 16384, ... } };
await agent.run(hardInput, { candidatePlan: plan });
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await agent.run(input, { candidatePlan });
} catch (e) {
  if (e instanceof Error && e.message === "cave_reasoning_budget_exceeded") {
    // recompile/re-lock with a larger budgets.reasoning, or split the task
  } else throw e;
}

Prevention

When it happens

Trigger: candidatePlan/lockedBuild with budgets.reasoning: N, and the model's thinking tokens across the run surpass N; high-difficulty prompts that make the model reason heavily across several calls.

Common situations: Plans budgeted from easy eval prompts applied to harder production prompts; a locked build whose reasoning budget was tuned for a shorter context; models that increased default thinking effort after a version bump.

Related errors


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