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
- De-duplicate by name before registration (see validation snippet in the defense section)
- Namespace colliding tools: rename one to 'fs_read' vs 'web_read' via its factory call
- 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
- Merge toolkits through a name-keyed Map with explicit conflict resolution
- Namespace tools by domain (fs_read, web_read) when combining libraries
- Run a duplicate-name check in CI for programmatically assembled agent definitions
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
- cave_tool_definition_invalid
- cave_reserved_tool_name:${declared.name}
- caveman agent: invalid agent id ${JSON.stringify(options.id)
- caveman agent: duplicate tool name
- caveman agent: tool prefix cave_ is reserved by framework ($
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/030eca918d76be5d.
Report an issue: GitHub.