{"record":{"id":"c80ae7798c73ad70","repo":"can1357/oh-my-pi","slug":"tool-toolcall-name-not-found-c80ae7","errorCode":null,"errorMessage":"Tool \"${toolCall.name}\" not found","messagePattern":"Tool \"(.+?)\" not found","errorType":"validation","errorClass":"AIError.ToolNotFoundError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/utils/validation.ts","lineNumber":1902,"sourceCode":"\tfor (const [key, entry] of recovered) {\n\t\tif (!(key in out)) out[key] = entry;\n\t}\n\treturn { value: out, changed: true };\n}\n\nconst MAX_COERCION_PASSES = 5;\n\n/**\n * Finds a tool by name and validates the tool call arguments against its schema.\n * @param tools Array of tool definitions\n * @param toolCall The tool call from the LLM\n * @returns The validated arguments\n * @throws Error if tool is not found or validation fails\n */\nexport function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall[\"arguments\"] {\n\tconst tool = tools.find(t => t.name === toolCall.name);\n\tif (!tool) {\n\t\tthrow new AIError.ToolNotFoundError(toolCall.name);\n\t}\n\treturn validateToolArguments(tool, toolCall);\n}\n\n/** Cap per-field string lengths when embedding received args in an error message. */\nconst MAX_ERROR_ARG_STRING_LENGTH = 256;\n\nfunction truncateArgsForError(value: unknown): unknown {\n\tif (typeof value === \"string\") {\n\t\tif (value.length <= MAX_ERROR_ARG_STRING_LENGTH) return value;\n\t\treturn `${value.slice(0, MAX_ERROR_ARG_STRING_LENGTH)}… [truncated ${value.length - MAX_ERROR_ARG_STRING_LENGTH} chars]`;\n\t}\n\tif (Array.isArray(value)) return value.map(truncateArgsForError);\n\tif (value !== null && typeof value === \"object\") {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const [key, entry] of Object.entries(value)) out[key] = truncateArgsForError(entry);\n\t\treturn out;\n\t}","sourceCodeStart":1884,"sourceCodeEnd":1920,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/utils/validation.ts#L1884-L1920","documentation":"validateToolCall looks up the requested tool by name in the provided tools array and throws AIError.ToolNotFoundError when no tool matches. This happens after the model emitted a tool_call whose name is not registered in the current session/request, so its arguments can never be validated or executed.","triggerScenarios":"Calling validateToolCall(tools, toolCall) — or the higher-level runToolCall path — where toolCall.name does not exactly match any entry of tools: model hallucinated a tool name, the tool was removed/renamed between requests, or case/whitespace mismatch.","commonSituations":"Model hallucinating tool names not in the request; client-side tool registry trimmed for context but the model replayed an old tool call; renaming a tool server-side while resuming a stored conversation; multi-provider routing where the tool list sent differs from the one used for validation.","solutions":["Ensure the exact same tools array used in the LLM request is passed to validateToolCall","Log/inspect toolCall.name and compare against tool names for case or whitespace mismatches","Handle ToolNotFoundError in the tool-call loop and return an error message back to the model so it can recover instead of crashing","If a tool was renamed, map legacy names to new ones before validation"],"exampleFix":"// before\nconst args = validateToolCall(tools, toolCall); // throws if unknown\n// after\nlet args;\ntry {\n  args = validateToolCall(tools, toolCall);\n} catch (err) {\n  if (err instanceof AIError.ToolNotFoundError) {\n    return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}` };\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const known = new Set(tools.map(t => t.name));\nif (!known.has(toolCall.name)) {\n  return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}` };\n}","typeGuard":"function isKnownTool(tools: Tool[], toolCall: ToolCall): toolCall is ToolCall {\n  return tools.some(t => t.name === toolCall.name);\n}","tryCatchPattern":"try {\n  const args = validateToolCall(tools, toolCall);\n} catch (err) {\n  if (err instanceof AIError.ToolNotFoundError) {\n    return { toolCallId: toolCall.id, isError: true, content: `Unknown tool ${toolCall.name}` };\n  }\n  throw err;\n}","preventionTips":["Always pass the same tools array to the request and to validation","Trim tool lists only together with conversation history the model saw","Echo available tool names in the system prompt to reduce hallucination","Map legacy/renamed tool names before validation"],"tags":["tools","validation","tool-calling","llm"],"backgroundTag":"tool-not-found","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}