JuliusBrussee/caveman · warning

cave_run_stopped

cave_run_stopped

Error message

cave_run_stopped

What it means

Thrown by streamFn when modelCalls reaches maxModelCalls (the caller override or the derived default). This is a stop condition, not a failure: stopReason is set to "call_budget_exhausted" and refusalPending records the refusal, so the run ends through the graceful path with partial work and the receipt intact. Exactly maxModelCalls calls are allowed — the check runs before the increment.

Source

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

      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,
        selected,
        context,
        requestedOutputTokens: streamOptions?.maxTokens,
        outputMaxTokens: definition.output?.maxTokens,
        planOutputTokens: efficiencyPlan?.budgets.output,
        restorableBytes: restorableRequestBytes(
          conversationOriginals,
          instructions,
          originalInstructions,
        ),

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Raise the ceiling: run(input, { maxModelCalls: N }) with an integer >= 1
  2. Inspect RunResult.stopReason === "call_budget_exhausted" and the receipt to see which tool loop consumed the calls, then fix the agent's instructions/tool design so it converges
  3. Enable breakers (RunOptions.breakers) to catch repeated-tool-call loops before they eat the call ceiling

Example fix

// before
await agent.run(input, { maxModelCalls: 4 }); // throws cave_run_stopped mid-task

// after
const result = await agent.run(input, { maxModelCalls: 16 });
if (result.stopReason === "call_budget_exhausted") {
  // handle partial work using result and result.receipt
}
Defensive patterns

Strategy: fallback

Validate before calling

// Size the ceiling before the run from expected turn count.
const expectedTurns = estimateTurns(task); // your own heuristic
const opts = { maxModelCalls: Math.max(8, expectedTurns * 2) };

Try / catch

let result;
try {
  result = await agent.run(input, opts);
} catch (e) {
  if (e instanceof Error && e.message === "cave_run_stopped") {
    // graceful stop: the framework records stopReason; if you caught it here,
    // read the result/stopReason path your wrapper exposes and treat partial
    // output as usable, optionally retrying with a raised ceiling
  } else throw e;
}

Prevention

When it happens

Trigger: A tool-looping conversation hitting the default ceiling of 64 model calls (no plan), or the plan-derived ceiling; or hitting a caller-set maxModelCalls: N.

Common situations: Agents that loop on tools without converging; ceilings set too low for the task's natural turn count; plans whose retry_cascade_reserve implies a small call ceiling.

Related errors


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