JuliusBrussee/caveman · error

cave_sandbox_subagent_cycle

cave_sandbox_subagent_cycle

Error message

cave_sandbox_subagent_cycle

What it means

While descending the subagent path, the worker keeps a `visited` set of definitions starting with the root. If the next child definition was already visited, the graph contains a delegation cycle, and the worker throws `cave_sandbox_subagent_cycle`. Agent graphs must be acyclic so termination and containment are provable.

Solutions

  1. Break the cycle: extract shared logic into a plain tool both agents use instead of cross-delegating.
  2. Restructure so delegation forms a DAG — a child must never reference (directly or transitively) an ancestor.
  3. If recursion is needed, implement it as runtime-level repeated invocation of a single-level graph, not a cyclic static definition.
  4. Audit the definitions in the cycle and remove the back-reference to the ancestor.

Example fix

// before
const a = agent("a").tool(subagent("to_b", b)).build();
const b = agent("b").tool(subagent("to_a", a)).build();

// after
const a = agent("a").tool(subagent("to_b", b)).build();
const b = agent("b").tool(sharedHelperTool).build();
Defensive patterns

Strategy: validation

Validate before calling

function assertAcyclic(root: AgentDefinition): void {
  const visiting = new Set<AgentDefinition>();
  const done = new Set<AgentDefinition>();
  const walk = (d: AgentDefinition): void => {
    if (visiting.has(d)) throw new Error("subagent delegation cycle detected");
    if (done.has(d)) return;
    visiting.add(d);
    for (const t of d.tools) {
      if (t.runtime?.kind === "subagent") walk(t.runtime.definition as AgentDefinition);
    }
    visiting.delete(d);
    done.add(d);
  };
  walk(root);
}

Try / catch

try {
  await run(agentGraph);
} catch (err) {
  if (err instanceof Error && err.message.includes("cave_sandbox_subagent_cycle")) {
    console.error("agent graph has a delegation cycle — restructure so children never reference ancestors");
  }
  throw err;
}

Prevention

When it happens

Trigger: Agent A's subagent tool delegates to B, and B (directly or transitively) delegates back to A, so the worker encounters a definition it has already visited while walking request.agentPath.

Common situations: Two agents defined in one module referencing each other's builders; a shared helper closing over a parent agent and reused as a child; refactor introducing mutual delegation (A→B→A) that previously worked at shallower depth; recursive 'orchestrator' patterns mistakenly built as static subagents.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/605b0a21ff21a20d. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/tool-worker.ts:121

  if (request.allowNetwork !== true) installNetworkDeny();
  const imported = await import(request.entry) as { default?: AgentDefinition; agent?: AgentDefinition };
  let definition = imported.default ?? imported.agent;
  if (!definition || definition.kind !== "agent") throw new Error("cave_sandbox_agent_export_missing");
  validateAgentGraph(definition);
  if (agentDefinitionSHA256(definition) !== request.rootDefinitionSha256) {
    throw new Error("cave_sandbox_definition_mismatch");
  }
  const visited = new Set<AgentDefinition>([definition]);
  for (const name of request.agentPath) {
    const delegated = definition.tools.filter((item) =>
      item.name === name && item.runtime?.kind === "subagent"
    );
    if (delegated.length !== 1) throw new Error("cave_sandbox_unknown_subagent");
    const child = delegated[0]!.runtime!.definition as AgentDefinition;
    if (!child || child.kind !== "agent") {
      throw new Error("cave_sandbox_subagent_definition_invalid");
    }
    if (visited.has(child)) throw new Error("cave_sandbox_subagent_cycle");
    visited.add(child);
    definition = child;
  }
  const selectedTools = definition.tools.filter((item) => item.name === request.tool);
  if (selectedTools.length !== 1 || selectedTools[0]!.runtime?.kind === "subagent") {
    throw new Error("cave_sandbox_unknown_tool");
  }
  const selected = selectedTools[0]!;
  if (toolDefinitionSHA256(selected) !== request.toolDefinitionSha256) {
    throw new Error("cave_sandbox_tool_definition_mismatch");
  }
  if (selected.effect !== "read" && request.allowSideEffects !== true) {
    throw new Error("cave_sandbox_side_effect_denied");
  }
  const value = await selected.execute(request.params as never, AbortSignal.timeout(selected.timeoutMs));
  writeResult({ ok: true, value });
} catch (error) {
  writeResult({ ok: false, code: failureCode(error) });

View on GitHub (pinned to 3ee70a1026)