From 76b5f72e81844e7ee592db87317a96f2e05636ca Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Wed, 4 Feb 2026 20:34:08 -0300 Subject: [PATCH] Fix tool error causing double event scope pop (#4373) When a tool raises an error, both ToolUsageErrorEvent and ToolUsageFinishedEvent were being emitted. Since both events pop the event scope stack, this caused the agent scope to be incorrectly popped along with the tool scope. --- .../src/crewai/agents/crew_agent_executor.py | 30 ++++---- .../src/crewai/experimental/agent_executor.py | 30 ++++---- lib/crewai/src/crewai/tools/tool_usage.py | 8 +- lib/crewai/tests/events/test_event_context.py | 38 ++++++++- lib/crewai/tests/tools/test_tool_usage.py | 77 +++++++++++++++++++ 5 files changed, 152 insertions(+), 31 deletions(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index c0f24516a..647596f2a 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -814,6 +814,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): agent_key=agent_key, ), ) + error_event_emitted = False track_delegation_if_needed(func_name, args_dict, self.task) @@ -896,6 +897,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): error=e, ), ) + error_event_emitted = True elif max_usage_reached and original_tool: # Return error message when max usage limit is reached result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore." @@ -923,20 +925,20 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): color="red", ) - # Emit tool usage finished event - crewai_event_bus.emit( - self, - event=ToolUsageFinishedEvent( - output=result, - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - started_at=started_at, - finished_at=datetime.now(), - ), - ) + if not error_event_emitted: + crewai_event_bus.emit( + self, + event=ToolUsageFinishedEvent( + output=result, + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + started_at=started_at, + finished_at=datetime.now(), + ), + ) # Append tool result message tool_message: LLMMessage = { diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index b9d8adccc..98e8b5bdb 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -689,6 +689,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): agent_key=agent_key, ), ) + error_event_emitted = False track_delegation_if_needed(func_name, args_dict, self.task) @@ -764,6 +765,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): error=e, ), ) + error_event_emitted = True elif max_usage_reached and original_tool: # Return error message when max usage limit is reached result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore." @@ -792,20 +794,20 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): color="red", ) - # Emit tool usage finished event - crewai_event_bus.emit( - self, - event=ToolUsageFinishedEvent( - output=result, - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - started_at=started_at, - finished_at=datetime.now(), - ), - ) + if not error_event_emitted: + crewai_event_bus.emit( + self, + event=ToolUsageFinishedEvent( + output=result, + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + started_at=started_at, + finished_at=datetime.now(), + ), + ) # Append tool result message tool_message: LLMMessage = { diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index e5a9e6154..b6ce5adb6 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -270,6 +270,7 @@ class ToolUsage: result = None # type: ignore should_retry = False available_tool = None + error_event_emitted = False try: if self.tools_handler and self.tools_handler.cache: @@ -408,6 +409,7 @@ class ToolUsage: except Exception as e: self.on_tool_error(tool=tool, tool_calling=calling, e=e) + error_event_emitted = True self._run_attempts += 1 if self._run_attempts > self._max_parsing_attempts: self._telemetry.tool_usage_error(llm=self.function_calling_llm) @@ -435,7 +437,7 @@ class ToolUsage: result = self._format_result(result=result) finally: - if started_event_emitted: + if started_event_emitted and not error_event_emitted: self.on_tool_use_finished( tool=tool, tool_calling=calling, @@ -500,6 +502,7 @@ class ToolUsage: result = None # type: ignore should_retry = False available_tool = None + error_event_emitted = False try: if self.tools_handler and self.tools_handler.cache: @@ -638,6 +641,7 @@ class ToolUsage: except Exception as e: self.on_tool_error(tool=tool, tool_calling=calling, e=e) + error_event_emitted = True self._run_attempts += 1 if self._run_attempts > self._max_parsing_attempts: self._telemetry.tool_usage_error(llm=self.function_calling_llm) @@ -665,7 +669,7 @@ class ToolUsage: result = self._format_result(result=result) finally: - if started_event_emitted: + if started_event_emitted and not error_event_emitted: self.on_tool_use_finished( tool=tool, tool_calling=calling, diff --git a/lib/crewai/tests/events/test_event_context.py b/lib/crewai/tests/events/test_event_context.py index 071e1a34d..2a69ca1ee 100644 --- a/lib/crewai/tests/events/test_event_context.py +++ b/lib/crewai/tests/events/test_event_context.py @@ -177,4 +177,40 @@ class TestTriggeredByScope: raise ValueError("test error") except ValueError: pass - assert get_triggering_event_id() is None \ No newline at end of file + assert get_triggering_event_id() is None + + +def test_agent_scope_preserved_after_tool_error_event() -> None: + from crewai.events import crewai_event_bus + from crewai.events.types.tool_usage_events import ( + ToolUsageErrorEvent, + ToolUsageStartedEvent, + ) + + push_event_scope("crew-1", "crew_kickoff_started") + push_event_scope("task-1", "task_started") + push_event_scope("agent-1", "agent_execution_started") + + crewai_event_bus.emit( + None, + ToolUsageStartedEvent( + tool_name="test_tool", + tool_args={}, + agent_key="test_agent", + ) + ) + + crewai_event_bus.emit( + None, + ToolUsageErrorEvent( + tool_name="test_tool", + tool_args={}, + agent_key="test_agent", + error=ValueError("test error"), + ) + ) + + crewai_event_bus.flush() + + assert get_current_parent_id() == "agent-1" + diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index c6dfad58b..b68a41666 100644 --- a/lib/crewai/tests/tools/test_tool_usage.py +++ b/lib/crewai/tests/tools/test_tool_usage.py @@ -10,7 +10,9 @@ from crewai import Agent, Task from crewai.events.event_bus import crewai_event_bus from crewai.events.types.tool_usage_events import ( ToolSelectionErrorEvent, + ToolUsageErrorEvent, ToolUsageFinishedEvent, + ToolUsageStartedEvent, ToolValidateInputErrorEvent, ) from crewai.tools import BaseTool @@ -744,3 +746,78 @@ def test_tool_usage_finished_event_with_cached_result(): assert isinstance(event.started_at, datetime.datetime) assert isinstance(event.finished_at, datetime.datetime) assert event.type == "tool_usage_finished" + + +def test_tool_error_does_not_emit_finished_event(): + from crewai.tools.tool_calling import ToolCalling + + class FailingTool(BaseTool): + name: str = "Failing Tool" + description: str = "A tool that always fails" + + def _run(self, **kwargs) -> str: + raise ValueError("Intentional failure") + + failing_tool = FailingTool().to_structured_tool() + + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.verbose = False + mock_agent.fingerprint = None + mock_agent.i18n.tools.return_value = {"name": "Add Image"} + mock_agent.i18n.errors.return_value = "Error: {error}" + mock_agent.i18n.slice.return_value = "Available tools: {tool_names}" + + mock_task = MagicMock() + mock_task.delegations = 0 + mock_task.name = "Test Task" + mock_task.description = "A test task" + mock_task.id = "test-task-id" + + mock_action = MagicMock() + mock_action.tool = "failing_tool" + mock_action.tool_input = "{}" + + tool_usage = ToolUsage( + tools_handler=MagicMock(cache=None, last_used_tool=None), + tools=[failing_tool], + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=mock_action, + ) + + started_events = [] + error_events = [] + finished_events = [] + error_received = threading.Event() + + @crewai_event_bus.on(ToolUsageStartedEvent) + def on_started(source, event): + if event.tool_name == "failing_tool": + started_events.append(event) + + @crewai_event_bus.on(ToolUsageErrorEvent) + def on_error(source, event): + if event.tool_name == "failing_tool": + error_events.append(event) + error_received.set() + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def on_finished(source, event): + if event.tool_name == "failing_tool": + finished_events.append(event) + + tool_calling = ToolCalling(tool_name="failing_tool", arguments={}) + tool_usage.use(calling=tool_calling, tool_string="Action: failing_tool") + + assert error_received.wait(timeout=5), "Timeout waiting for error event" + crewai_event_bus.flush() + + assert len(started_events) >= 1, "Expected at least one ToolUsageStartedEvent" + assert len(error_events) >= 1, "Expected at least one ToolUsageErrorEvent" + assert len(finished_events) == 0, ( + "ToolUsageFinishedEvent should NOT be emitted after ToolUsageErrorEvent" + )