Skip to content

Commit 5937e09

Browse files
committed
fix(agent): stop treating a failed tool as a provider failure
tool_execution_retry raises AgentToolExecutionError once a tool has exhausted tool_retry_attempts (agent.py:4398). That call sits inside the try whose handler catches BadRequestError, InternalServerError, AuthenticationError and bare Exception, so the tool error landed there, was recorded as Agent.llm_error, and the whole generation was retried. Re-running the model cannot fix a broken tool. Measured on the unfixed source with a tool that always raises: "a tool failure re-ran the model 3 times", and the run ends with "Failed to generate a valid response after retry attempts", which points at the provider rather than the tool. Catch AgentToolExecutionError ahead of the generation handler: flush any tool results recorded before the failure, report it as Agent.tool_error rather than Agent.llm_error, write what happened into short_memory, and leave the retry loop. The run continues, so the next loop lets the model read the failure and choose differently. Closes kyegomez#1924
1 parent 60e192b commit 5937e09

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

swarms/structs/agent.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,36 @@ def _run(
16501650
loop_count=loop_count
16511651
)
16521652

1653+
except AgentToolExecutionError as e:
1654+
# A tool that already exhausted its own retries is not
1655+
# a provider failure. Re-running the model cannot fix
1656+
# it and costs another completion, so record it and
1657+
# leave the retry loop instead of falling into the
1658+
# generation handler below.
1659+
if use_transcript and turn_calls:
1660+
transcript.flush_tool_results(
1661+
turn_calls, turn_results
1662+
)
1663+
1664+
capture_error(
1665+
e,
1666+
self,
1667+
name="Agent.tool_error",
1668+
loop=loop_count,
1669+
)
1670+
1671+
self.short_memory.add(
1672+
role="Tool Executor",
1673+
content=(
1674+
f"Tool execution failed after "
1675+
f"{self.tool_retry_attempts} attempts: {e}"
1676+
),
1677+
)
1678+
1679+
# Exit the retry loop, not the run: the next loop lets
1680+
# the model read the failure and choose differently.
1681+
success = True
1682+
16531683
except (
16541684
BadRequestError,
16551685
InternalServerError,

tests/structs/test_agent.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,53 @@ def test_a_zero_or_none_attempt_count_still_runs_once(self):
11941194
), f"attempts={attempts!r} should still run once"
11951195

11961196

1197+
class TestToolFailureIsNotAnLLMError:
1198+
"""#1924: a tool that exhausted its own retries raised into the generation
1199+
handler, which logged it as Agent.llm_error and re-ran the model, so one
1200+
broken tool cost up to retry_attempts extra completions.
1201+
"""
1202+
1203+
@staticmethod
1204+
def _broken_tool(query: str) -> str:
1205+
"""Always fails.
1206+
1207+
Args:
1208+
query: anything at all.
1209+
"""
1210+
raise RuntimeError("tool is broken")
1211+
1212+
def test_a_failing_tool_does_not_re_run_the_model(self):
1213+
agent = _patched_agent(
1214+
"ToolFailAgent",
1215+
model_name="gpt-5.4",
1216+
dynamic_tools=False,
1217+
tools=[self._broken_tool],
1218+
retry_attempts=3,
1219+
)
1220+
1221+
llm_calls = []
1222+
1223+
def fake_call_llm(task=None, *args, **kwargs):
1224+
llm_calls.append(task)
1225+
return [
1226+
{
1227+
"type": "function",
1228+
"id": "call-1",
1229+
"function": {
1230+
"name": "_broken_tool",
1231+
"arguments": '{"query": "x"}',
1232+
},
1233+
}
1234+
]
1235+
1236+
agent.call_llm = fake_call_llm
1237+
agent.run("use the tool")
1238+
1239+
assert (
1240+
len(llm_calls) == 1
1241+
), f"a tool failure re-ran the model {len(llm_calls)} times"
1242+
1243+
11971244
class TestConcurrentExecutionPool:
11981245
"""#1793: both concurrent entry points referenced self.executor, which
11991246
__init__ never assigned — run_concurrent_tasks swallowed the AttributeError

0 commit comments

Comments
 (0)