JuliusBrussee/caveman · error

cave_subagent_call_budget

cave_subagent_call_budget

Error message

cave_subagent_call_budget

What it means

Each subagent tool name has a per-execution call budget tracked in usage.calls; before dispatching, the runtime compares the current call count against runtime.maxCalls and throws cave_subagent_call_budget when it has already been reached. The counter is incremented synchronously right after the check so parallel dispatch cannot double-spend the budget.

Solutions

  1. Raise runtime.maxCalls for that subagent tool in the configuration
  2. Restructure the prompt/workflow so fewer subagent invocations are needed (batch work into one call)
  3. Use a differently-named subagent tool for additional budget
  4. Cache subagent results and reuse them instead of re-dispatching

Example fix

// before
runtime: { kind: "subagent", maxCalls: 3 }
// after
runtime: { kind: "subagent", maxCalls: 10 }
Defensive patterns

Strategy: validation

Validate before calling

const used = getUsage().calls.get(subagentToolName) ?? 0;
if (used >= subagentRuntime.maxCalls) {
  // reuse cached result or skip dispatch
}

Try / catch

try {
  await dispatchSubagent({ task });
} catch (e) {
  if (e.message === "cave_subagent_call_budget") {
    result = getCachedSubagentResult(subagentToolName) ?? synthesizeWithoutSubagent();
  } else throw e;
}

Prevention

When it happens

Trigger: Invoking the same subagent tool more than runtime.maxCalls times within one parent agent execution (per usage.calls ledger) — e.g. a maxCalls: 3 budget hit on the 4th call.

Common situations: Looping agent that keeps re-dispatching the same researcher subagent; maxCalls configured too low for the task; retried tool calls consuming the budget after partial failures.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/5ffb2a7c341dca5c. Report an issue: GitHub.

Appendix: source

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

  toolDefinition: ToolDefinition,
  params: unknown,
  signal: AbortSignal | undefined,
  parentOptions: InternalRunOptions,
  usage: NestedUsage,
  executionContext: InternalExecutionContext,
  parentMeter: BudgetMeter | undefined,
  parentDeadlineAt: number | undefined,
): Promise<unknown> {
  const runtime = toolDefinition.runtime;
  if (runtime?.kind !== "subagent") throw new Error("cave_subagent_runtime_missing");
  if (params === null || typeof params !== "object" || Array.isArray(params) ||
      typeof (params as { task?: unknown }).task !== "string") {
    throw new Error("cave_subagent_arguments_invalid");
  }
  const task = (params as { task: string }).task;
  if (task.length > runtime.maxInputChars) throw new Error("cave_subagent_input_limit");
  const calls = usage.calls.get(toolDefinition.name) ?? 0;
  if (calls >= runtime.maxCalls) throw new Error("cave_subagent_call_budget");
  // Reserve synchronously before any await so parallel Pi tool dispatch cannot
  // pass the same maxCalls check twice.
  usage.calls.set(toolDefinition.name, calls + 1);
  const depth = executionContext.depth;
  const depthLimit = Math.min(
    parentOptions.maxSubagentDepth ?? DEFAULT_SUBAGENT_DEPTH_LIMIT,
    ABSOLUTE_SUBAGENT_DEPTH_LIMIT,
  );
  if (depth + 1 > depthLimit) throw new Error("cave_subagent_depth_limit");
  // The wallet is carved here, still synchronously, for the same reason: two
  // subagents dispatched in one turn must not both be funded out of the same
  // remaining budget.
  const walletAmount = parentMeter === undefined
    ? undefined
    : parentMeter.denomination === "usd" ? runtime.maxCostUsd : runtime.maxTokens;
  if (parentMeter !== undefined && walletAmount === undefined) {
    throw new Error("cave_subagent_wallet_denomination_unavailable");
  }

View on GitHub (pinned to 3ee70a1026)