{"record":{"id":"47b8075cc2946cd8","repo":"JuliusBrussee/caveman","slug":"cave-run-stopped","errorCode":"cave_run_stopped","errorMessage":"cave_run_stopped","messagePattern":"cave_run_stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/agent/src/runtime.ts","lineNumber":1600,"sourceCode":"      if (usageFailure) throw usageFailure;\n      if (nestedUsage.incomplete) throw new Error(\"cave_nested_usage_incomplete\");\n      if (efficiencyPlan && reasoningUsageUnavailable) {\n        throw new Error(\"cave_reasoning_usage_unavailable\");\n      }\n      if (efficiencyPlan) {\n        enforceSemanticBudgets(contextBill(lowered.ir), outputTokens, efficiencyPlan);\n        if (reasoningTokens > efficiencyPlan.budgets.reasoning) {\n          throw new Error(\"cave_reasoning_budget_exceeded\");\n        }\n      }\n      // The hard model-call ceiling is a stop condition, not a failure: ending\n      // the run through the same graceful path as every other stop keeps the\n      // partial work and the receipt intact. Checked before the\n      // increment so exactly `maxModelCalls` calls are allowed.\n      if (modelCalls >= maxModelCalls) {\n        stopReason = \"call_budget_exhausted\";\n        refusalPending = true;\n        throw new Error(\"cave_run_stopped\");\n      }\n      modelCalls++;\n      // Between-calls stop point. Nothing is in flight here: the previous turn\n      // and its tools have finished and settled, and this call has not started.\n      const plan = () => decideNextCall({\n        meter: budgetMeter,\n        breakers,\n        deadlineAt,\n        selected,\n        context,\n        requestedOutputTokens: streamOptions?.maxTokens,\n        outputMaxTokens: definition.output?.maxTokens,\n        planOutputTokens: efficiencyPlan?.budgets.output,\n        restorableBytes: restorableRequestBytes(\n          conversationOriginals,\n          instructions,\n          originalInstructions,\n        ),","sourceCodeStart":1582,"sourceCodeEnd":1618,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L1582-L1618","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Raise the ceiling: run(input, { maxModelCalls: N }) with an integer >= 1","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","Enable breakers (RunOptions.breakers) to catch repeated-tool-call loops before they eat the call ceiling"],"exampleFix":"// before\nawait agent.run(input, { maxModelCalls: 4 }); // throws cave_run_stopped mid-task\n\n// after\nconst result = await agent.run(input, { maxModelCalls: 16 });\nif (result.stopReason === \"call_budget_exhausted\") {\n  // handle partial work using result and result.receipt\n}","handlingStrategy":"fallback","validationCode":"// Size the ceiling before the run from expected turn count.\nconst expectedTurns = estimateTurns(task); // your own heuristic\nconst opts = { maxModelCalls: Math.max(8, expectedTurns * 2) };","typeGuard":null,"tryCatchPattern":"let result;\ntry {\n  result = await agent.run(input, opts);\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_run_stopped\") {\n    // graceful stop: the framework records stopReason; if you caught it here,\n    // read the result/stopReason path your wrapper exposes and treat partial\n    // output as usable, optionally retrying with a raised ceiling\n  } else throw e;\n}","preventionTips":["Always read RunResult.stopReason instead of only catching throws","Set maxModelCalls with headroom over expected turns","Enable breakers to surface tool loops that waste call budget"],"tags":["limits","stop-condition","model-calls"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}