JuliusBrussee/caveman · error · Error
cave_subagent_definition_cycle
cave_subagent_definition_cycle
Error message
cave_subagent_definition_cycle
What it means
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`.
Source
Thrown at packages/agent/src/definition-graph.ts:23
);
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");
names.add(declared.name);View on GitHub (pinned to 766dce6b13)
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.
Example fix
// before
a.tools = [tool({ runtime: subagent(b) })];
b.tools = [tool({ runtime: subagent(a) })]; // cycle
// after
b.tools = []; // b is a leaf; the host loop re-invokes a or b as needed Defensive patterns
Strategy: validation
Validate before calling
function hasDefinitionCycle(root: AgentDefinition): boolean {
const active = new Set<AgentDefinition>();
const seen = new Set<AgentDefinition>();
const visit = (node: AgentDefinition): boolean => {
if (active.has(node)) return true;
if (seen.has(node)) return false;
active.add(node);
for (const declared of node.tools ?? []) {
if (declared?.runtime?.kind === "subagent" &&
visit(declared.runtime.definition as AgentDefinition)) return true;
}
active.delete(node);
seen.add(node);
return false;
};
return visit(root);
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- cave_agent_definition_invalid
- cave_subagent_depth_limit
- cave_host_sandbox_nested_under_required
- caveman agent: subagent maxInputChars must be a positive int
- caveman agent: subagent maxCalls must be a positive integer
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/635044dc316e4828.
Report an issue: GitHub.