{"record":{"id":"635044dc316e4828","repo":"JuliusBrussee/caveman","slug":"cave-subagent-definition-cycle","errorCode":"cave_subagent_definition_cycle","errorMessage":"cave_subagent_definition_cycle","messagePattern":"cave_subagent_definition_cycle","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/definition-graph.ts","lineNumber":23,"sourceCode":");\n\nexport function validateAgentGraph(root: AgentDefinition): void {\n  // Memoize per inherited containment posture: the same child definition\n  // reached under a sandbox-required ancestor must be re-checked, not skipped.\n  const visited = [new Set<AgentDefinition>(), new Set<AgentDefinition>()];\n  const active = new Set<AgentDefinition>();\n\n  const visit = (\n    definition: AgentDefinition,\n    depth: number,\n    sandboxRequired: boolean,\n  ): void => {\n    if (!definition || definition.kind !== \"agent\" ||\n        !Array.isArray(definition.tools)) {\n      throw new Error(\"cave_agent_definition_invalid\");\n    }\n    if (depth > 8) throw new Error(\"cave_subagent_depth_limit\");\n    if (active.has(definition)) throw new Error(\"cave_subagent_definition_cycle\");\n    // Host mode is an opt-in the root makes for itself. A descendant cannot use\n    // it to run closures outside an ancestor's required containment.\n    if (sandboxRequired && definition.sandbox === \"host\") {\n      throw new Error(\"cave_host_sandbox_nested_under_required\");\n    }\n    const memo = visited[sandboxRequired ? 1 : 0]!;\n    if (memo.has(definition)) return;\n    active.add(definition);\n    const childSandboxRequired = sandboxRequired ||\n      definition.sandbox === \"required\";\n    const names = new Set<string>();\n    for (const declared of definition.tools) {\n      if (!declared || declared.kind !== \"tool\" ||\n          typeof declared.name !== \"string\") {\n        throw new Error(\"cave_tool_definition_invalid\");\n      }\n      if (names.has(declared.name)) throw new Error(\"cave_duplicate_tool_name\");\n      names.add(declared.name);","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/definition-graph.ts#L5-L41","documentation":"A subagent definition graph must be a DAG. `validateAgentGraph` keeps a path-scoped `active` set while recursing; revisiting a definition that is still on the current path means the graph contains itself — infinite recursion at runtime — so it throws `cave_subagent_definition_cycle`.","triggerScenarios":"Agent A's tools include a subagent whose `definition` is A itself; or mutual delegation A→B→A via shared references (e.g. two coordinator definitions embedding each other).","commonSituations":"Factory functions whose returned definition closes over itself; two \"coordinator\" agents that each delegate to the other; wiring shared singletons into each other during refactoring.","solutions":["Break the cycle: extract the shared leaf work into a third definition both agents reference (DAG shape).","Do not assign a definition (directly or transitively) into its own `tools`.","If agents must alternate turns, drive that loop from the orchestrating code at runtime instead of static nesting."],"exampleFix":"// before\na.tools = [tool({ runtime: subagent(b) })];\nb.tools = [tool({ runtime: subagent(a) })]; // cycle\n// after\nb.tools = []; // b is a leaf; the host loop re-invokes a or b as needed","handlingStrategy":"validation","validationCode":"function hasDefinitionCycle(root: AgentDefinition): boolean {\n  const active = new Set<AgentDefinition>();\n  const seen = new Set<AgentDefinition>();\n  const visit = (node: AgentDefinition): boolean => {\n    if (active.has(node)) return true;\n    if (seen.has(node)) return false;\n    active.add(node);\n    for (const declared of node.tools ?? []) {\n      if (declared?.runtime?.kind === \"subagent\" &&\n          visit(declared.runtime.definition as AgentDefinition)) return true;\n    }\n    active.delete(node);\n    seen.add(node);\n    return false;\n  };\n  return visit(root);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep the definition graph a DAG: shared leaf definitions are fine, self/mutual nesting is not.","Drive multi-step delegation from host code at runtime instead of encoding loops statically."],"tags":["subagent","cycle","graph","recursion"],"backgroundTag":"circular-dependency-detected","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}