From e0f354e1a6285a31537c8a4d6aec03dc599857f5 Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Wed, 29 Jul 2026 09:21:56 -0700 Subject: [PATCH] fix(tools): let raise through the parallel native path, guard all handlers Chasing down CodeRabbit's note about callers of execute_single_native_tool_call turned up a fifth place this exception was being downgraded: the experimental executor's parallel branch wrapped future.result() in a broad except and folded the abort into a fake tool result, so the remaining parallel calls carried on. The sequential path and crew_agent_executor's parallel branch were already fine. Five separate handlers have swallowed this during review, so added a guard test asserting the passthrough at every site rather than trusting the next one gets spotted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG --- .../src/crewai/experimental/agent_executor.py | 4 +++ lib/crewai/tests/tools/test_tool_failure.py | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index fe1c36eae..4f1051a7e 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -1767,6 +1767,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): idx = future_to_idx[future] try: ordered_results[idx] = future.result() + except ToolExecutionFailedError: + # A deliberate stop: folding it into a tool result would + # let the remaining parallel calls carry on. + raise except Exception as e: tool_call = runnable_tool_calls[idx] info = extract_tool_call_info(tool_call) diff --git a/lib/crewai/tests/tools/test_tool_failure.py b/lib/crewai/tests/tools/test_tool_failure.py index 266117bb7..6eb24c85f 100644 --- a/lib/crewai/tests/tools/test_tool_failure.py +++ b/lib/crewai/tests/tools/test_tool_failure.py @@ -700,6 +700,32 @@ class TestRaisePolicySurvivesEveryWrapper: ).kickoff() assert not result.has_tool_failures + def test_every_broad_handler_around_tool_execution_lets_it_through(self) -> None: + """Guard against a new `except Exception` quietly downgrading an abort. + + Five separate handlers have swallowed this exception during review of + this PR, so assert the passthrough at each site rather than trusting + that the next one will be spotted. + """ + import inspect + + from crewai.agent.core import Agent as AgentCls + from crewai.agents.crew_agent_executor import CrewAgentExecutor + from crewai.agents.step_executor import StepExecutor + from crewai.experimental.agent_executor import AgentExecutor + + sites = [ + (AgentCls._execute_with_timeout, "_passthrough_exceptions"), + (StepExecutor.execute, "ToolExecutionFailedError"), + (AgentExecutor.execute_tool_action, "ToolExecutionFailedError"), + (AgentExecutor.execute_native_tool, "ToolExecutionFailedError"), + (CrewAgentExecutor._invoke_loop_react, "ToolExecutionFailedError"), + (CrewAgentExecutor._ainvoke_loop_react, "ToolExecutionFailedError"), + ] + for func, expected in sites: + source = inspect.getsource(func) + assert expected in source, f"{func.__qualname__} lost its passthrough" + def test_passthrough_tuple_includes_the_error(self) -> None: from crewai.agent.core import _passthrough_exceptions