Skip to content

Commit df8c1c7

Browse files
committed
fix: implement duplicate_tool_call_threshold detection logic
Add missing duplicate tool call detection to prevent infinite loops when the agent repeatedly calls the same tool with identical arguments. Changes: - Add duplicate detection logic before tool execution - Check recent_tool_signatures for matching tool calls - Abort execution when threshold is exceeded (default: 3) - Send AgentEvent::Error to notify frontend - Return error message to LLM so it can adjust its approach - Add tracing for debugging This fixes a critical stability issue where the agent could get stuck in an infinite loop calling the same tool repeatedly (e.g., web_search with the same query). The detection logic: 1. Constructs tool signature: "tool_name:json_args" 2. Counts occurrences in recent_tool_signatures (last 8 calls) 3. If count >= duplicate_tool_call_threshold, abort with error 4. LLM receives error and can try a different approach Example: - Agent calls web_search("上海AI政策") 3 times - On 3rd call, threshold exceeded - Error returned: "Tool has been called 3 times with identical arguments" - Agent adjusts strategy and tries different parameters Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ffcc1f3 commit df8c1c7

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

core/src/agent.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,6 +3136,46 @@ impl AgentLoop {
31363136
// In streaming mode, ToolStart is sent when we receive ToolUseStart from LLM
31373137
// But we still need to send ToolEnd after execution
31383138

3139+
// Check for duplicate tool calls to prevent infinite loops
3140+
let tool_signature = format!(
3141+
"{}:{}",
3142+
tool_call.name,
3143+
serde_json::to_string(&tool_call.args).unwrap_or_default()
3144+
);
3145+
3146+
let duplicate_count = recent_tool_signatures
3147+
.iter()
3148+
.filter(|sig| sig.starts_with(&tool_signature))
3149+
.count();
3150+
3151+
if duplicate_count >= self.config.duplicate_tool_call_threshold as usize {
3152+
let error_msg = format!(
3153+
"Tool '{}' has been called {} times with identical arguments. \
3154+
Aborting to prevent infinite loop. Consider modifying your approach.",
3155+
tool_call.name, duplicate_count
3156+
);
3157+
3158+
tracing::warn!(
3159+
tool_name = tool_call.name.as_str(),
3160+
duplicate_count = duplicate_count,
3161+
threshold = self.config.duplicate_tool_call_threshold,
3162+
"Duplicate tool call threshold exceeded"
3163+
);
3164+
3165+
// Send error event
3166+
if let Some(tx) = &event_tx {
3167+
tx.send(AgentEvent::Error {
3168+
message: error_msg.clone(),
3169+
})
3170+
.await
3171+
.ok();
3172+
}
3173+
3174+
// Return error result to LLM so it can adjust its approach
3175+
messages.push(Message::tool_result(&tool_call.id, &error_msg, true));
3176+
continue;
3177+
}
3178+
31393179
// Check for malformed tool arguments from LLM (4.1 parse error recovery)
31403180
if let Some(parse_error) =
31413181
tool_call.args.get("__parse_error").and_then(|v| v.as_str())

0 commit comments

Comments
 (0)