{"record":{"id":"9745727dae33f5e8","repo":"JuliusBrussee/caveman","slug":"caveman-agent-duplicate-tool-name","errorCode":null,"errorMessage":"caveman agent: duplicate tool name","messagePattern":"caveman agent: duplicate tool name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/index.ts","lineNumber":103,"sourceCode":"];\n\nexport function agent(options: {\n  id: string;\n  instructions: string | FileSource;\n  model: Auto | string | Model<Api>;\n  reasoning?: AgentDefinition[\"reasoning\"];\n  tools?: ToolDefinition[];\n  contexts?: ContextDefinition[];\n  memory?: MemoryDefinition;\n  output?: OutputDefinition;\n  sandbox?: AgentDefinition[\"sandbox\"];\n}): AgentDefinition {\n  if (!/^[a-z0-9][a-z0-9_-]{0,95}$/.test(options.id)) {\n    throw new Error(`caveman agent: invalid agent id ${JSON.stringify(options.id)}`);\n  }\n  const tools = Object.freeze([...(options.tools ?? [])]);\n  if (new Set(tools.map((item) => item.name)).size !== tools.length) {\n    throw new Error(\"caveman agent: duplicate tool name\");\n  }\n  const sandbox = options.sandbox ?? \"required\";\n  if (!SANDBOX_MODES.includes(sandbox)) {\n    throw new Error(`caveman agent: unknown sandbox mode ${JSON.stringify(sandbox)}`);\n  }\n  const reserved = tools.find((item) => item.name.startsWith(\"cave_\"));\n  if (reserved) {\n    throw new Error(\n      `caveman agent: tool prefix cave_ is reserved by framework (${reserved.name})`,\n    );\n  }\n  const definition: AgentDefinition = {\n    kind: \"agent\",\n    id: options.id,\n    instructions: options.instructions,\n    model: options.model,\n    reasoning: options.reasoning ?? \"low\",\n    tools,","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/index.ts#L85-L121","documentation":"agent() freezes the tools array and rejects definitions where two tools share the same name, since tool names must be unique for dispatch. It compares the size of the Set of tool names against the array length.","triggerScenarios":"agent({ tools: [searchTool, searchTool] }) — the same tool object passed twice; two distinct tool objects both named 'search'.","commonSituations":"Spreading a default tool list plus a custom list that both include a common tool; building tools in a loop that reuses a name; concatenating tool sets from multiple modules.","solutions":["Deduplicate the tools array (e.g. new Map(tools.map(t => [t.name, t])).values()) before passing it","Rename one of the conflicting tools to a unique name","Check how the tools array is assembled for accidental duplicates","Filter duplicates: tools.filter((t, i, arr) => arr.findIndex(x => x.name === t.name) === i)"],"exampleFix":"// before\nagent({ id: \"bot\", tools: [...baseTools, webSearchTool] }); // webSearchTool also in baseTools\n// after\nconst tools = [...new Map([...baseTools, webSearchTool].map(t => [t.name, t])).values()];\nagent({ id: \"bot\", tools });","handlingStrategy":"validation","validationCode":"const names = tools.map(t => t.name);\nif (new Set(names).size !== names.length) throw new Error(\"duplicate tool names before agent()\");","typeGuard":"const hasUniqueToolNames = (tools) => new Set(tools.map(t => t.name)).size === tools.length;","tryCatchPattern":"try { def = agent(opts); } catch (err) { if (err.message === \"caveman agent: duplicate tool name\") { opts.tools = dedupeByName(opts.tools); def = agent(opts); } else throw err; }","preventionTips":["Deduplicate merged tool lists by name before passing to agent()","Keep a registry keyed by tool name","Avoid spreading overlapping default/custom tool arrays blindly"],"tags":["validation","agent-definition","tools"],"backgroundTag":"invalid-argument-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}