JuliusBrussee/caveman · error · Error

cave_subagent_depth_limit

cave_subagent_depth_limit

Error message

cave_subagent_depth_limit

What it means

The definition graph permits at most 8 levels of nested subagent definitions (visit is called with depth+1 for each subagent tool). Deeper chains are rejected to prevent runaway recursion and unbounded spawn trees, similar to agent-loop depth limits in other frameworks.

Source

Thrown at packages/agent/src/definition-graph.ts:22

  "@caveman-ai/agent:tool-implementation-source",
);

export function validateAgentGraph(root: AgentDefinition): void {
  // Memoize per inherited containment posture: the same child definition
  // reached under a sandbox-required ancestor must be re-checked, not skipped.
  const visited = [new Set<AgentDefinition>(), new Set<AgentDefinition>()];
  const active = new Set<AgentDefinition>();

  const visit = (
    definition: AgentDefinition,
    depth: number,
    sandboxRequired: boolean,
  ): void => {
    if (!definition || definition.kind !== "agent" ||
        !Array.isArray(definition.tools)) {
      throw new Error("cave_agent_definition_invalid");
    }
    if (depth > 8) throw new Error("cave_subagent_depth_limit");
    if (active.has(definition)) throw new Error("cave_subagent_definition_cycle");
    // Host mode is an opt-in the root makes for itself. A descendant cannot use
    // it to run closures outside an ancestor's required containment.
    if (sandboxRequired && definition.sandbox === "host") {
      throw new Error("cave_host_sandbox_nested_under_required");
    }
    const memo = visited[sandboxRequired ? 1 : 0]!;
    if (memo.has(definition)) return;
    active.add(definition);
    const childSandboxRequired = sandboxRequired ||
      definition.sandbox === "required";
    const names = new Set<string>();
    for (const declared of definition.tools) {
      if (!declared || declared.kind !== "tool" ||
          typeof declared.name !== "string") {
        throw new Error("cave_tool_definition_invalid");
      }
      if (names.has(declared.name)) throw new Error("cave_duplicate_tool_name");

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Flatten the hierarchy: replace deep delegation chains with direct tool calls or a wider (not deeper) fan-out
  2. If a level is only a pass-through wrapper, eliminate it and give its parent the child's tools directly
  3. Instrument your builder to report the effective depth of generated graphs before registration

Example fix

// before: 9-level chain a1->a2->...->a9
const a9 = agent({ kind: "agent", tools: [...] });
// ... each level wraps the next ...

// after: flatten — a1 holds the leaf tools directly
const a1 = agent({ kind: "agent", tools: [leafTool1, leafTool2] });
Defensive patterns

Strategy: validation

Validate before calling

function graphDepth(def: { tools: Array<{ runtime?: { kind?: string; definition?: unknown } }> }, seen = new Set()): number {
  if (seen.has(def)) return 0; // cycle handled separately by the validator
  seen.add(def);
  let max = 0;
  for (const t of def.tools) {
    if (t.runtime?.kind === "subagent") {
      max = Math.max(max, 1 + graphDepth(t.runtime.definition as never, new Set(seen)));
    }
  }
  return max;
}
if (graphDepth(root) > 8) throw new Error("definition graph exceeds subagent depth limit of 8");

Prevention

When it happens

Trigger: Chaining 9+ agent definitions where each level's tool runtime references the next: A's tool has runtime.kind 'subagent' pointing to B, B's to C, and so on past depth 8.

Common situations: Auto-generated orchestrator hierarchies; recursive self-referencing definitions that unexpectedly traverse as a chain; delegation chains built by composition helpers that add a wrapper level per feature.

Related errors


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