From 6fa073242edc937ef02b84960223ab62a6f6f77e Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 18:00:38 +0000
Subject: [PATCH 1/4] Initial plan
From 6ef60f85661426182a2104d50c95f3099b2d9bc0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 18:08:25 +0000
Subject: [PATCH 2/4] Implement duplicate tool call prevention with reasoning
support
Co-authored-by: wmeddie <122480+wmeddie@users.noreply.github.com>
---
agent_components.py | 98 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 98 insertions(+)
diff --git a/agent_components.py b/agent_components.py
index 947e273..9ab34d6 100644
--- a/agent_components.py
+++ b/agent_components.py
@@ -1008,6 +1008,45 @@ def _dispatch_llm_call(ctx, provider, model_name, conversation, temperature):
# --- End Standalone LLM Call Functions ---
+# --- Duplicate Tool Call Prevention Functions ---
+
+def normalize_tool_args(args_dict):
+ """Remove _nonce and _duplicate_reasoning from args for comparison"""
+ if args_dict is None:
+ return None
+ normalized = args_dict.copy()
+ normalized.pop('_nonce', None)
+ normalized.pop('_duplicate_reasoning', None)
+ return normalized
+
+def extract_nonce_and_reasoning(args_dict):
+ """Extract _nonce and _duplicate_reasoning from parsed args"""
+ if args_dict is None:
+ return None, None
+ return args_dict.get('_nonce'), args_dict.get('_duplicate_reasoning')
+
+def is_duplicate_tool_call(tool_name, args_dict, tool_history):
+ """Check if this tool call is a duplicate of a previous call"""
+ normalized_args = normalize_tool_args(args_dict)
+
+ for prev_call in tool_history:
+ if (prev_call['tool_name'] == tool_name and
+ prev_call['normalized_args'] == normalized_args):
+ return True
+ return False
+
+def is_reasoning_unique(reasoning, tool_history):
+ """Check if the reasoning is unique within the current tool history"""
+ if reasoning is None or reasoning.strip() == "":
+ return False
+
+ used_reasoning = [call.get('reasoning') for call in tool_history
+ if call.get('reasoning') is not None]
+ return reasoning not in used_reasoning
+
+# --- End Duplicate Tool Call Prevention Functions ---
+
+
@xai_component
class AgentRun(Component):
"""Run the agent with the given conversation.
@@ -1032,8 +1071,15 @@ class AgentRun(Component):
out_conversation: OutArg[list]
last_response: OutArg[str]
+
+ def __init__(self):
+ super().__init__()
+ # Track tool calls within this AgentRun instance to prevent duplicates
+ self.tool_call_history = []
def execute(self, ctx) -> None:
+ # Reset tool call history for this execution
+ self.tool_call_history = []
try:
self.do_execute(ctx)
except Exception as e:
@@ -1145,6 +1191,58 @@ def handle_tool_use(self, ctx, agent, conversation, content, standard_toolbelt,
pre_tool_text = content[:match.start()].strip()
conversation[-1]['content'] = pre_tool_text
+ # Parse tool arguments to check for duplicates
+ parsed_args, _ = parse_tool_args(tool_args_str)
+ nonce, reasoning = extract_nonce_and_reasoning(parsed_args)
+
+ # Check for duplicate tool calls
+ if is_duplicate_tool_call(tool_name, parsed_args, self.tool_call_history):
+ if reasoning is None or reasoning.strip() == "":
+ # Duplicate call without reasoning - block it
+ error_message = f"Duplicate tool call detected for '{tool_name}' with same arguments. To retry this tool, provide '_duplicate_reasoning' parameter with unique justification."
+ print(f"Blocked duplicate tool call: {tool_name} with args: {normalize_tool_args(parsed_args)}")
+
+ if is_openai_model(model_name):
+ conversation.append({"role": "system", "content": f"ERROR: {error_message}"})
+ else:
+ conversation.append({"role": "user", "content": f"SYSTEM:\nERROR: {error_message}"})
+
+ # Give on_thought a chance to see the error
+ self.out_conversation.value = conversation
+ if hasattr(self, 'on_thought') and self.on_thought:
+ SubGraphExecutor(self.on_thought).do(ctx)
+
+ return min(stress_level + 0.1, 1.5) # Increase stress due to blocked duplicate
+
+ elif not is_reasoning_unique(reasoning, self.tool_call_history):
+ # Duplicate call with non-unique reasoning - block it
+ error_message = f"Duplicate tool call detected for '{tool_name}' with previously used reasoning: '{reasoning}'. Please provide unique justification."
+ print(f"Blocked duplicate tool call with reused reasoning: {tool_name}")
+
+ if is_openai_model(model_name):
+ conversation.append({"role": "system", "content": f"ERROR: {error_message}"})
+ else:
+ conversation.append({"role": "user", "content": f"SYSTEM:\nERROR: {error_message}"})
+
+ # Give on_thought a chance to see the error
+ self.out_conversation.value = conversation
+ if hasattr(self, 'on_thought') and self.on_thought:
+ SubGraphExecutor(self.on_thought).do(ctx)
+
+ return min(stress_level + 0.1, 1.5) # Increase stress due to blocked duplicate
+
+ else:
+ # Duplicate call with unique reasoning - allow it
+ print(f"Allowing duplicate tool call '{tool_name}' with unique reasoning: '{reasoning}'")
+
+ # Record this tool call in history (before execution in case of errors)
+ tool_call_record = {
+ 'tool_name': tool_name,
+ 'normalized_args': normalize_tool_args(parsed_args),
+ 'reasoning': reasoning
+ }
+ self.tool_call_history.append(tool_call_record)
+
tool_result = None
error_message = None
From 82f011b7573158d3f57538aac4b4815b12393648 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 18:13:37 +0000
Subject: [PATCH 3/4] Fix whitespace handling in reasoning and add AgentLearn
support
Co-authored-by: wmeddie <122480+wmeddie@users.noreply.github.com>
---
agent_components.py | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/agent_components.py b/agent_components.py
index 9ab34d6..ff4b962 100644
--- a/agent_components.py
+++ b/agent_components.py
@@ -1040,9 +1040,11 @@ def is_reasoning_unique(reasoning, tool_history):
if reasoning is None or reasoning.strip() == "":
return False
- used_reasoning = [call.get('reasoning') for call in tool_history
- if call.get('reasoning') is not None]
- return reasoning not in used_reasoning
+ # Normalize reasoning by stripping whitespace for comparison
+ normalized_reasoning = reasoning.strip()
+ used_reasoning = [call.get('reasoning', '').strip() for call in tool_history
+ if call.get('reasoning') is not None and call.get('reasoning').strip() != ""]
+ return normalized_reasoning not in used_reasoning
# --- End Duplicate Tool Call Prevention Functions ---
@@ -1239,7 +1241,7 @@ def handle_tool_use(self, ctx, agent, conversation, content, standard_toolbelt,
tool_call_record = {
'tool_name': tool_name,
'normalized_args': normalize_tool_args(parsed_args),
- 'reasoning': reasoning
+ 'reasoning': reasoning.strip() if reasoning else None
}
self.tool_call_history.append(tool_call_record)
@@ -1489,8 +1491,15 @@ class AgentLearn(Component):
out_conversation: OutArg[list]
last_response: OutArg[str]
+
+ def __init__(self):
+ super().__init__()
+ # Track tool calls within this AgentLearn instance to prevent duplicates
+ self.tool_call_history = []
def execute(self, ctx) -> None:
+ # Reset tool call history for this execution
+ self.tool_call_history = []
agent = ctx['agent_' + self.agent_name.value]
model_name = agent['agent_model']
From 40034137f8701a7a5ab8b51da2a94b6db94bdf1a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 18:15:02 +0000
Subject: [PATCH 4/4] Add comprehensive documentation for duplicate tool call
prevention
Co-authored-by: wmeddie <122480+wmeddie@users.noreply.github.com>
---
DUPLICATE_PREVENTION.md | 89 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 89 insertions(+)
create mode 100644 DUPLICATE_PREVENTION.md
diff --git a/DUPLICATE_PREVENTION.md b/DUPLICATE_PREVENTION.md
new file mode 100644
index 0000000..0d64720
--- /dev/null
+++ b/DUPLICATE_PREVENTION.md
@@ -0,0 +1,89 @@
+# Duplicate Tool Call Prevention
+
+This feature prevents agents from calling the same tool multiple times in a row with identical inputs, which is a common failure mode in frontier models like GPT-5 and Gemini-Flash-2.5.
+
+## How It Works
+
+The system tracks tool calls within each `AgentRun` execution and blocks duplicates based on:
+- Tool name
+- Normalized arguments (excluding special parameters)
+
+## Special Parameters
+
+### `_duplicate_reasoning`
+Allows agents to justify why they need to call the same tool again with the same arguments.
+
+**JSON Format:**
+```json
+{
+ "query": "search term",
+ "_duplicate_reasoning": "trying with broader context"
+}
+```
+
+**XML Format:**
+```xml
+search term
+<_duplicate_reasoning>trying with broader context
+```
+
+### `_nonce`
+Reserved for future use. Currently ignored in duplicate detection but preserved in tool arguments.
+
+## Behavior
+
+### ✅ Allowed Cases
+1. **First call** - Any tool call is allowed initially
+2. **Different tool** - Calling a different tool is always allowed
+3. **Different arguments** - Same tool with different parameters is allowed
+4. **Unique reasoning** - Duplicate call with unique `_duplicate_reasoning`
+
+### ❌ Blocked Cases
+1. **Exact duplicate** - Same tool + same arguments without reasoning
+2. **With nonce only** - Same tool + same arguments + `_nonce` but no reasoning
+3. **Reused reasoning** - Same tool + same arguments + previously used reasoning
+
+## Examples
+
+```python
+# First call - allowed
+{"query": "project timeline"}
+
+# Exact duplicate - blocked
+{"query": "project timeline"}
+# Result: ERROR: Duplicate tool call detected...
+
+# With unique reasoning - allowed
+{"query": "project timeline", "_duplicate_reasoning": "searching with broader context"}
+
+# Reused reasoning - blocked
+{"query": "project timeline", "_duplicate_reasoning": "searching with broader context"}
+# Result: ERROR: Duplicate tool call detected for 'lookup_memory' with previously used reasoning...
+
+# Different arguments - allowed
+{"query": "project milestones"}
+```
+
+## Error Messages
+
+When duplicates are blocked, clear error messages guide the agent:
+
+- **No reasoning:** "Duplicate tool call detected for 'TOOL_NAME' with same arguments. To retry this tool, provide '_duplicate_reasoning' parameter with unique justification."
+
+- **Reused reasoning:** "Duplicate tool call detected for 'TOOL_NAME' with previously used reasoning: 'REASONING'. Please provide unique justification."
+
+## Implementation Details
+
+- Tracking scope: Within single `AgentRun` execution (resets each run)
+- Argument normalization: Removes `_nonce` and `_duplicate_reasoning` for comparison
+- Reasoning comparison: Whitespace normalized to prevent trivial bypasses
+- Stress level: Increases when duplicates are blocked to encourage different behavior
+- Format support: Both JSON and XML argument formats
+- Components: Works with both `AgentRun` and `AgentLearn`
+
+## Benefits
+
+🎯 **Prevents infinite loops** - Stops frontier models from getting stuck
+🧠 **Encourages thoughtful usage** - Agents must justify repeat calls
+🔄 **Allows justified retries** - Supports legitimate retry scenarios
+📝 **Provides clear feedback** - Error messages guide better behavior
\ No newline at end of file