Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,22 @@ func (a *Agent) collectTools(ctx context.Context) ([]tools.Tool, error) {

agentTools = append(agentTools, a.tools...)

// Deduplicate tools by name, keeping the first occurrence.
// Duplicate tool names cause provider API errors (e.g. Anthropic 400).
seen := make(map[string]struct{}, len(agentTools))
deduped := make([]tools.Tool, 0, len(agentTools))
for _, t := range agentTools {
if _, exists := seen[t.Name]; exists {
slog.Warn("Duplicate tool name; keeping first occurrence",
Comment thread
Anubhav200311 marked this conversation as resolved.
Comment thread
Anubhav200311 marked this conversation as resolved.
"agent", a.Name(), "tool", t.Name)
a.addToolWarning(fmt.Sprintf("duplicate tool %q: keeping first occurrence", t.Name))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM/LIKELY] New addToolWarning call in dedup loop extends pre-existing unsynchronized access to pendingWarnings

a.addToolWarning(...) appends to a.pendingWarnings (a plain []string with no mutex). This pattern already existed in collectTools (line 235) and ensureToolSetsAreStarted (line 281) before this PR, but the new dedup block adds two additional unsynchronized append sites within the same hot path.

If two goroutines call Tools() or StartedTools() concurrently — which is plausible in a multi-agent or event-driven context — they both invoke collectTools, both reach this loop, and both call addToolWarning without synchronization, creating a data race on the backing array of the slice.

Impact: In practice the risk is low because agent methods are typically not called concurrently from different goroutines, and this is a pre-existing structural gap rather than a new one. However, the new dedup code is the first time duplicate-tool warnings can be generated during normal (non-error) operation, making this path more likely to be exercised.

Suggestion: Guard pendingWarnings with a sync.Mutex, or — if concurrent use is not a design goal — document that Tools() / StartedTools() must not be called concurrently.

continue
}
seen[t.Name] = struct{}{}
deduped = append(deduped, t)
}
agentTools = deduped

if a.addDescriptionParameter {
agentTools = tools.AddDescriptionParameter(agentTools)
}
Expand Down
15 changes: 15 additions & 0 deletions pkg/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ func TestAgentTools(t *testing.T) {
wantToolCount: 0,
wantWarnings: 0,
},
{
name: "duplicate tool names are deduplicated",
toolsets: []tools.ToolSet{
newStubToolSet(nil, []tools.Tool{
{Name: "read_file", Parameters: map[string]any{}},
{Name: "write_file", Parameters: map[string]any{}},
}, nil),
newStubToolSet(nil, []tools.Tool{
{Name: "read_file", Parameters: map[string]any{}},
{Name: "shell", Parameters: map[string]any{}},
}, nil),
},
wantToolCount: 3,
wantWarnings: 1,
},
}

for _, tt := range tests {
Expand Down
Loading