{"record":{"id":"b980168c88686aaa","repo":"JuliusBrussee/caveman","slug":"cave-subagent-invocation-limit","errorCode":null,"errorMessage":"cave_subagent_invocation_limit","messagePattern":"cave_subagent_invocation_limit","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":3830,"sourceCode":"  appliedPlan: AppliedPlan,\n): { description: string; input: TSchema } {\n  const segment = lowered.ir.segments.find((item) => item.id === `tool.${definition.name}`);\n  if (!segment) throw new Error(`cave_context_segment_missing:tool.${definition.name}`);\n  const body = appliedPlan.bodies.get(segment.bodyHandle);\n  if (!body) throw new Error(`cave_context_body_missing:tool.${definition.name}`);\n  const parsed = JSON.parse(new TextDecoder().decode(body)) as unknown;\n  if (!isRecord(parsed) || parsed.name !== definition.name ||\n      typeof parsed.description !== \"string\" || !isRecord(parsed.input)) {\n    throw new Error(`cave_tool_schema_invalid:${definition.name}`);\n  }\n  return { description: parsed.description, input: parsed.input as TSchema };\n}\n\nfunction admitSubagent(state: InvocationState): () => void {\n  const ledger = state.ledger;\n  if (ledger.maxInvocations !== undefined && ledger.admitted >= ledger.maxInvocations) {\n    ledger.invocationRejections++;\n    throw new Error(\"cave_subagent_invocation_limit\");\n  }\n  if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {\n    ledger.concurrencyRejections++;\n    throw new Error(\"cave_subagent_concurrency_limit\");\n  }\n  ledger.admitted++;\n  ledger.active++;\n  ledger.peakActive = Math.max(ledger.peakActive, ledger.active);\n  let released = false;\n  return () => {\n    if (released) return;\n    released = true;\n    ledger.active = Math.max(0, ledger.active - 1);\n  };\n}\n\nfunction childInvocationTrace(parent: InvocationTrace): InvocationTrace {\n  return Object.freeze({","sourceCodeStart":3812,"sourceCodeEnd":3848,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L3812-L3848","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["Raise or remove RunOptions.maxSubagentInvocations if the tree width is legitimate.","Reduce per-turn fan-out in the subagent tool definition so fewer descendants are admitted.","Inspect the root span attribute cave.agent.tree.invocation_limit_rejections to confirm this cap (not maxCalls or depth) is the one rejecting."],"exampleFix":"// before\nconst result = await run(agent, { input, maxSubagentInvocations: 4 });\n// agent's fan-out tool spawns 8 children -> cave_subagent_invocation_limit\n\n// after\nconst result = await run(agent, { input, maxSubagentInvocations: 8 });","handlingStrategy":"try-catch","validationCode":"// No public pre-check exists: the ledger is framework-internal.\n// Budget your tree width yourself before the run:\nconst expected = toolFanOut * levels;\nif (opts.maxSubagentInvocations !== undefined && expected > opts.maxSubagentInvocations) {\n  throw new Error(`fan-out ${expected} exceeds maxSubagentInvocations ${opts.maxSubagentInvocations}`);\n}","typeGuard":"const isInvocationLimit = (e: unknown): boolean =>\n  e instanceof Error && e.message === \"cave_subagent_invocation_limit\";","tryCatchPattern":"try {\n  await run(agent, { input, maxSubagentInvocations: 16 });\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_subagent_invocation_limit\") {\n    // tree cap hit: decide between widening the cap or trimming fan-out\n  } else throw error;\n}","preventionTips":["Estimate total descendant admissions (fan-out x depth) before setting maxSubagentInvocations.","Cap per-turn fan-out in subagent tools so a single turn cannot exhaust the tree budget.","Watch cave.agent.tree.invocation_limit_rejections on the root span during load tests."],"tags":["subagent","limits","runtime","fan-out"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}