JuliusBrussee/caveman · error · Error

cave_duplicate_tool_name

cave_duplicate_tool_name

Error message

cave_duplicate_tool_name

What it means

Within a single AgentDefinition, tool names must be unique — a Set tracks names as the tools array is walked and any repeat throws cave_duplicate_tool_name before the graph is used. Distinct agents may share tool names; the constraint is per-definition.

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 27d5a3981a)

Solutions

  1. De-duplicate by name before registration (see validation snippet in the defense section)
  2. Namespace colliding tools: rename one to 'fs_read' vs 'web_read' via its factory call
  3. When merging toolkits, build a Map keyed by name with explicit conflict resolution

Example fix

// before
const tools = [...fsToolkit, ...webToolkit]; // both contain name: "read"

// after
const byName = new Map();
for (const t of [...fsToolkit, ...webToolkit]) byName.set(t.name === "read" && byName.has("read") ? "web_read" : t.name, t);
const tools = [...byName.values()];
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueToolNames(tools: Array<{ name: string }>): 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);
  }
}

Type guard

function hasDuplicateToolNames(tools: Array<{ name: string }>): boolean {
  return new Set(tools.map((t) => t.name)).size !== tools.length;
}

Prevention

When it happens

Trigger: Registering the same tool instance twice in one agent's tools array, or two different tools with the same name (e.g. two 'search' tools from different modules) in one definition.

Common situations: Combining toolkits that each export a 'read' or 'bash' tool; spread-and-override assembly where the override was appended instead of replacing; default tools merged with custom ones of the same name.

Related errors


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