{"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":"Error","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/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/index.ts#L85-L121","documentation":"Thrown by the agent() builder (packages/agent/src/index.ts:103): two or more tools in the agent's tools array share the same name. Tool names are the dispatch keys for the model, so duplicates within one agent are rejected at construction time.","triggerScenarios":"Calling agent({ tools: [...] }) where two ToolDefinition objects have the same name property — e.g. including the same tool twice, or two differently-configured tools with an identical name.","commonSituations":"Spreading a base tool list and appending an override of the same tool; copy-pasting a tool definition and forgetting to rename; composing tools from multiple modules that both define 'search'.","solutions":["Rename one of the colliding tools so every name in the array is unique.","If you meant to override, replace the original entry instead of appending.","Deduplicate by name before calling agent() when tool lists are assembled dynamically."],"exampleFix":"// before\nagent({ tools: [searchTool, searchToolWithFilters /* both named \"search\" */] });\n\n// after\nagent({ tools: [tool({ ...searchTool, name: \"search\" }), tool({ ...searchToolWithFilters, name: \"search_filtered\" })] });","handlingStrategy":"validation","validationCode":"function hasDuplicateToolNames(tools: { name: string }[]): boolean {\n  return new Set(tools.map((t) => t.name)).size !== tools.length;\n}\nif (hasDuplicateToolNames(tools)) {\n  const seen = new Set<string>();\n  const dup = tools.find((t) => seen.has(t.name) || !seen.add(t.name))!.name;\n  throw new Error(`duplicate tool name: ${dup}`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Deduplicate dynamically assembled tool lists by name before agent().","Replace a tool to override it instead of appending a second copy.","Assert uniqueness in a shared helper used by all tool-registry code."],"tags":["validation","naming","tools"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}