Skip to content

registry.dispatch: 'Unknown tool' error for trivially-normalizable CamelCase/_tool-suffix drift from Claude family #14186

@JayGwod

Description

@JayGwod

Summary

The tool dispatch path in tools/registry.py::ToolRegistry.dispatch() rejects tool calls whose name field has cosmetic variations that are trivially resolvable, producing spurious Unknown tool: X errors and forcing the model into retry loops.

Observed variations from Claude 4.5 / 4.6 / 4.7:

Emitted name Canonical target
TodoTool_tool todo
Todo_tool todo
Patch_tool patch
BrowserClick_tool browser_click
Terminal_tool terminal
WriteFile_tool write_file

The drift pattern is CamelCase + trailing _tool suffix + mixed case. These are never ambiguous with any real tool name — all registered tools use lowercase snake_case.

Evidence of prevalence

  • [FEATURE] Wrong tool name calling from claude-sonnet-4-5 model mastra-ai/mastra#12581 — empirical 44-model study: Claude family fails tool-name conformance at ~30%, other families ≈ 0%.
  • anthropic/anthropic-cookbook#50235 — upstream advisory recommending normalization at the dispatch boundary.
  • Observed repeatedly in production gateway logs on this installation (openclaw, PID 2473729), especially after a prior dispatch error — the retry often worsens the drift.

Root cause

dispatch() does an exact-match lookup against the registry. A miss returns {"error": "Unknown tool: X"} immediately, with no attempt to normalize.

Proposed fix

Add a _normalize_tool_name() helper and call it as a fallback (only on miss — exact match still wins). Layered drift like TodoTool_tool needs fixpoint iteration:

pass 1: strip _tool suffix -> "TodoTool"
pass 2: camel->snake        -> "todo_tool"
pass 3: strip _tool suffix  -> "todo"        ✓ hit

Bounded MAX_ITER = 4. Logs a warning when normalization recovers a call so drift stays visible.

Safety properties:

  • Zero impact on well-formed calls — fallback is never reached when exact match succeeds.
  • No false matches — normalization produces lowercase snake_case; collisions with real tool names would already have been exact matches.
  • Truly unknown tools still error — e.g. NotARealTool_toolUnknown tool: NotARealTool_tool (verified).

PR

Will open from JayGwod/hermes-agent:fix/registry-tool-name-normalize.

Live-verified after gateway reload: all 5 drift patterns above route to the correct handler; a control case (NotARealTool_tool) still returns the expected error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundcomp/toolsTool registry, model_tools, toolsetssweeper:implemented-on-mainSweeper: behavior already present on current maintype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions