JuliusBrussee/caveman · error

cave_duplicate_tool_name

cave_duplicate_tool_name

Error message

cave_duplicate_tool_name

What it means

Raised while validateAgentGraph visits tool definitions: two tools reachable from one agent share the same name. Tool names are the dispatch key for the model and the runtime, so duplicates would make calls ambiguous; this fires regardless of tool identity.

Solutions

  1. Deduplicate the tools array (e.g. new Map(tools.map(t => [t.name, t])).values()).
  2. Rename one of the colliding tools so names are unique within the agent.
  3. Check helper/composition code that appends tools — guard against adding an already-present name.

Example fix

// before
tools: [{ kind: "tool", name: "search", /* a */ }, { kind: "tool", name: "search", /* b */ }]
// after
tools: [{ kind: "tool", name: "search" }, { kind: "tool", name: "search_web" }]
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueToolNames(tools: ToolDefinition[]): void {
  const seen = new Set<string>();
  for (const t of tools) {
    if (seen.has(t.name)) throw new Error(`duplicate tool name: ${t.name}`);
    seen.add(t.name);
  }
}

Try / catch

try {
  validateAgentGraph(def);
} catch (e) {
  if (e instanceof Error && e.message === "cave_duplicate_tool_name") {
    console.error("Two tools share a name; deduplicate with a Map keyed by name");
  } else throw e;
}

Prevention

When it happens

Trigger: Adding the same tool object twice to tools, or two distinct tools accidentally given identical name strings; visit tracks names in a Set and throws on the second occurrence.

Common situations: Merging tool lists from multiple sources without deduplication, copy-pasting a tool entry with only its implementation changed, generated tool sets where a naming template collides, or appending a tool that was already registered by a helper.

Related errors


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

Appendix: source

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

    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);
      if (declared.name.startsWith("cave_")) {
        throw new Error(`cave_reserved_tool_name:${declared.name}`);
      }
      if (typeof Reflect.get(declared, TOOL_IMPLEMENTATION_SOURCE) !== "string") {
        throw new Error(`cave_untrusted_tool_definition:${declared.name}`);
      }
      if (declared.runtime?.kind !== "subagent") continue;
      const child = declared.runtime.definition as AgentDefinition;
      visit(child, depth + 1, childSandboxRequired);
    }
    active.delete(definition);
    memo.add(definition);
  };

  visit(root, 0, false);
}

View on GitHub (pinned to 3ee70a1026)