can1357/oh-my-pi · error · AIError.ToolNotFoundError

Tool "${toolCall.name}" not found

Error message

Tool "${toolCall.name}" not found

What it means

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.

Source

Thrown at packages/ai/src/utils/validation.ts:1902

	for (const [key, entry] of recovered) {
		if (!(key in out)) out[key] = entry;
	}
	return { value: out, changed: true };
}

const MAX_COERCION_PASSES = 5;

/**
 * Finds a tool by name and validates the tool call arguments against its schema.
 * @param tools Array of tool definitions
 * @param toolCall The tool call from the LLM
 * @returns The validated arguments
 * @throws Error if tool is not found or validation fails
 */
export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"] {
	const tool = tools.find(t => t.name === toolCall.name);
	if (!tool) {
		throw new AIError.ToolNotFoundError(toolCall.name);
	}
	return validateToolArguments(tool, toolCall);
}

/** Cap per-field string lengths when embedding received args in an error message. */
const MAX_ERROR_ARG_STRING_LENGTH = 256;

function truncateArgsForError(value: unknown): unknown {
	if (typeof value === "string") {
		if (value.length <= MAX_ERROR_ARG_STRING_LENGTH) return value;
		return `${value.slice(0, MAX_ERROR_ARG_STRING_LENGTH)}… [truncated ${value.length - MAX_ERROR_ARG_STRING_LENGTH} chars]`;
	}
	if (Array.isArray(value)) return value.map(truncateArgsForError);
	if (value !== null && typeof value === "object") {
		const out: Record<string, unknown> = {};
		for (const [key, entry] of Object.entries(value)) out[key] = truncateArgsForError(entry);
		return out;
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure the exact same tools array used in the LLM request is passed to validateToolCall
  2. Log/inspect toolCall.name and compare against tool names for case or whitespace mismatches
  3. Handle ToolNotFoundError in the tool-call loop and return an error message back to the model so it can recover instead of crashing
  4. If a tool was renamed, map legacy names to new ones before validation

Example fix

// before
const args = validateToolCall(tools, toolCall); // throws if unknown
// after
let args;
try {
  args = validateToolCall(tools, toolCall);
} catch (err) {
  if (err instanceof AIError.ToolNotFoundError) {
    return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}` };
  }
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const known = new Set(tools.map(t => t.name));
if (!known.has(toolCall.name)) {
  return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}` };
}

Type guard

function isKnownTool(tools: Tool[], toolCall: ToolCall): toolCall is ToolCall {
  return tools.some(t => t.name === toolCall.name);
}

Try / catch

try {
  const args = validateToolCall(tools, toolCall);
} catch (err) {
  if (err instanceof AIError.ToolNotFoundError) {
    return { toolCallId: toolCall.id, isError: true, content: `Unknown tool ${toolCall.name}` };
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/c80ae7798c73ad70. Report an issue: GitHub.