Skip to content

fix(agent): dedupe duplicate tool names with once-per-streak warning#3409

Merged
Sayt-0 merged 1 commit into
mainfrom
fix/dedupe-duplicate-tool-names
Jul 2, 2026
Merged

fix(agent): dedupe duplicate tool names with once-per-streak warning#3409
Sayt-0 merged 1 commit into
mainfrom
fix/dedupe-duplicate-tool-names

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Jul 2, 2026

Copy link
Copy Markdown
Member

What

Deduplicate tool names in Agent.collectTools(): the first toolset in configuration order wins, and each collision is surfaced to the user once per streak with a warning naming both toolsets involved and the config remedies.

Fixes #2251.

Why

When several toolsets emit the same tool name, the duplicate reaches the provider and Anthropic rejects the request:

HTTP 400: {"type":"error","error":{"type":"invalid_request_error",
"message":"tools: Tool names must be unique."}}

Collisions happen in real configs:

  • the same built-in toolset type configured twice (e.g. two filesystem);
  • two unnamed MCP servers exposing the same tool names;
  • an MCP tool shadowing a built-in (e.g. fetch);
  • a collision introduced at runtime by a ToolListChanged notification.

#1969 fixed only the multi-LSP case (multiplexer) and 3528ab2 the multi-RAG case; the general problem remained.

Before After
Anthropic HTTP 400, zero tools usable Request accepted, first occurrence wins
Tolerant providers Silent arbitrary shadowing (dispatcher map, last wins) Deterministic first-wins
User feedback Cryptic 400 or nothing One warning per collision streak, with origins and remedies

How

  • Dedup happens during collection (not after flattening), while tools.DescribeToolSet is still in scope, so the warning attributes both the kept and the ignored origin.
  • First toolset in configuration order wins — the rule the MCP docs already promise (docs/tools/mcp/index.md, Toolset order matters callout) but the code never implemented.
  • Collisions are reported through a once-per-streak guard mirroring the existing start/list failure streaks: a persistent collision warns once (then slog.Debug), a resolved collision resets the streak, a new or reappearing one is reported as fresh.
  • No automatic renaming or prefixing. The warning points at the existing user-level remedies: a unique name: on the MCP toolset, or its tools: filter.

Two earlier attempts (#2278, #3122) were closed after review. This design addresses each objection raised there:

Objection Raised on How this PR addresses it
"We can't just remove tools like this" (jira fetch vs built-in fetch) #3122 Both tools are never reachable today: besides the Anthropic 400, the dispatcher resolves calls through a name-keyed map (pkg/runtime/toolexec/dispatcher.go), so one tool already silently shadows the other. This PR makes the shadowing deterministic and loudly reported instead of arbitrary and silent.
Renaming breaks MCP-injected instructions ("use the fetch tool") #3122 No renaming is done; resolving a collision stays an explicit user decision via the existing name: mechanism.
User-facing warnings would repeat on every turn #2278 review Once-per-streak guard; collectTools runs every turn but a persistent collision warns only once.
Warning lacks toolset origin #2278 bot review Dedup at collection time keeps toolset identity; the warning names the kept and the ignored origin.

Tests

  • TestAgentTools: new table case — duplicate names across toolsets collapse to a unique set, one warning per dropped duplicate.
  • TestAgentToolsDedupeKeepsFirstInOrder: first occurrence wins, first-seen order preserved.
  • TestAgentToolsCollisionWarningIdentifiesOrigins: warning names both toolsets and the name: remedy.
  • TestAgentToolsCollisionWarningOncePerStreak: warns once for a persistent collision, re-warns only for new or reappearing collisions across turns.

Verification

  • golangci-lint run pkg/agent/... and go run ./lint .: no issues.
  • go test ./...: passes except pre-existing failures in pkg/tools/builtin/fetch and pkg/tools/builtin/openapi (local network proxy answers 405 on non-public addresses; they fail identically on main).

Out of scope

  • The skill sub-session path (skillSubSessionTools in pkg/runtime/loop.go) appends extra toolset tools after Agent.Tools() and could theoretically reintroduce a duplicate; a shared dedup helper could cover it as a follow-up.
  • Static config-time validation (e.g. rejecting the same built-in type twice) — insufficient alone since MCP tool lists are only known at runtime.

@Sayt-0 Sayt-0 requested a review from a team as a code owner July 2, 2026 09:48
Agent.collectTools appended tools from every toolset verbatim, so two
toolsets emitting the same tool name (same built-in type twice, two
unnamed MCP servers, an MCP tool shadowing a built-in, or a collision
introduced by a ToolListChanged notification) produced a duplicate in
the request and Anthropic rejected it with HTTP 400 'tools: Tool names
must be unique.' On tolerant providers the collision was already broken
too: the dispatcher resolves calls through a name-keyed map, so only
one of the colliding tools was ever reachable.

Deduplicate during collection, keeping the tool from the first toolset
in configuration order, which the MCP docs already promise. Deduping at
collection time (instead of after flattening) keeps the toolset identity
so the warning can attribute both origins.

Collisions are surfaced with once-per-streak semantics mirroring the
existing start/list failure guards: a persistent collision warns once,
a collision that disappears resets the streak, and a new collision is
reported as fresh. The user-facing warning names the kept and ignored
toolsets and points at the config remedies ('name:' prefix on MCP
toolsets or the 'tools:' filter).

Fixes #2251

Signed-off-by: Sayt-0 <louis-dalmorocompta@docker.com>
@Sayt-0 Sayt-0 force-pushed the fix/dedupe-duplicate-tool-names branch from f4e1571 to ab0e5b1 Compare July 2, 2026 09:52
@aheritier aheritier added area/agent For work that has to do with the general agent loop/agentic features of the app area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 2, 2026
@Sayt-0 Sayt-0 merged commit 063b901 into main Jul 2, 2026
11 checks passed
@Sayt-0 Sayt-0 deleted the fix/dedupe-duplicate-tool-names branch July 2, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent For work that has to do with the general agent loop/agentic features of the app area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate tool names across toolsets cause Anthropic 400 errors

3 participants