JuliusBrussee/caveman · error

cave_subagent_invocation_limit

Error message

cave_subagent_invocation_limit

What it means

Thrown by admitSubagent when the root-owned invocation ledger has already admitted maxInvocations subagent calls. It enforces RunOptions.maxSubagentInvocations, a monotonic cap on total descendant admissions across all tools and depths in one run. The check runs before admission, so a rejected call consumes no tree slot and increments ledger.invocationRejections (surfaced on the root span as cave.agent.tree.invocation_limit_rejections).

Source

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

  appliedPlan: AppliedPlan,
): { description: string; input: TSchema } {
  const segment = lowered.ir.segments.find((item) => item.id === `tool.${definition.name}`);
  if (!segment) throw new Error(`cave_context_segment_missing:tool.${definition.name}`);
  const body = appliedPlan.bodies.get(segment.bodyHandle);
  if (!body) throw new Error(`cave_context_body_missing:tool.${definition.name}`);
  const parsed = JSON.parse(new TextDecoder().decode(body)) as unknown;
  if (!isRecord(parsed) || parsed.name !== definition.name ||
      typeof parsed.description !== "string" || !isRecord(parsed.input)) {
    throw new Error(`cave_tool_schema_invalid:${definition.name}`);
  }
  return { description: parsed.description, input: parsed.input as TSchema };
}

function admitSubagent(state: InvocationState): () => void {
  const ledger = state.ledger;
  if (ledger.maxInvocations !== undefined && ledger.admitted >= ledger.maxInvocations) {
    ledger.invocationRejections++;
    throw new Error("cave_subagent_invocation_limit");
  }
  if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {
    ledger.concurrencyRejections++;
    throw new Error("cave_subagent_concurrency_limit");
  }
  ledger.admitted++;
  ledger.active++;
  ledger.peakActive = Math.max(ledger.peakActive, ledger.active);
  let released = false;
  return () => {
    if (released) return;
    released = true;
    ledger.active = Math.max(0, ledger.active - 1);
  };
}

function childInvocationTrace(parent: InvocationTrace): InvocationTrace {
  return Object.freeze({

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Raise or remove RunOptions.maxSubagentInvocations if the tree width is legitimate.
  2. Reduce per-turn fan-out in the subagent tool definition so fewer descendants are admitted.
  3. Inspect the root span attribute cave.agent.tree.invocation_limit_rejections to confirm this cap (not maxCalls or depth) is the one rejecting.

Example fix

// before
const result = await run(agent, { input, maxSubagentInvocations: 4 });
// agent's fan-out tool spawns 8 children -> cave_subagent_invocation_limit

// after
const result = await run(agent, { input, maxSubagentInvocations: 8 });
Defensive patterns

Strategy: try-catch

Validate before calling

// No public pre-check exists: the ledger is framework-internal.
// Budget your tree width yourself before the run:
const expected = toolFanOut * levels;
if (opts.maxSubagentInvocations !== undefined && expected > opts.maxSubagentInvocations) {
  throw new Error(`fan-out ${expected} exceeds maxSubagentInvocations ${opts.maxSubagentInvocations}`);
}

Type guard

const isInvocationLimit = (e: unknown): boolean =>
  e instanceof Error && e.message === "cave_subagent_invocation_limit";

Try / catch

try {
  await run(agent, { input, maxSubagentInvocations: 16 });
} catch (error) {
  if (error instanceof Error && error.message === "cave_subagent_invocation_limit") {
    // tree cap hit: decide between widening the cap or trimming fan-out
  } else throw error;
}

Prevention

When it happens

Trigger: Calling run() with RunOptions.maxSubagentInvocations set to N while the agent tree admits more than N subagent invocations in total (e.g. a fan-out tool spawns many children, or nested subagents each spawn more).

Common situations: Developer sets a small maxSubagentInvocations to bound cost, then adds a parallel-fan-out subagent tool whose children exceed the cap; or a recursive task delegation pattern admits more descendants than expected because the cap counts the whole tree, not per-level.

Related errors


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