Skip to content

Commit 76b5f72

Browse files
authored
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.
1 parent d86d43d commit 76b5f72

File tree

5 files changed

+152
-31
lines changed

5 files changed

+152
-31
lines changed

lib/crewai/src/crewai/agents/crew_agent_executor.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ def _handle_native_tool_calls(
814814
agent_key=agent_key,
815815
),
816816
)
817+
error_event_emitted = False
817818

818819
track_delegation_if_needed(func_name, args_dict, self.task)
819820

@@ -896,6 +897,7 @@ def _handle_native_tool_calls(
896897
error=e,
897898
),
898899
)
900+
error_event_emitted = True
899901
elif max_usage_reached and original_tool:
900902
# Return error message when max usage limit is reached
901903
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 @@ def _handle_native_tool_calls(
923925
color="red",
924926
)
925927

926-
# Emit tool usage finished event
927-
crewai_event_bus.emit(
928-
self,
929-
event=ToolUsageFinishedEvent(
930-
output=result,
931-
tool_name=func_name,
932-
tool_args=args_dict,
933-
from_agent=self.agent,
934-
from_task=self.task,
935-
agent_key=agent_key,
936-
started_at=started_at,
937-
finished_at=datetime.now(),
938-
),
939-
)
928+
if not error_event_emitted:
929+
crewai_event_bus.emit(
930+
self,
931+
event=ToolUsageFinishedEvent(
932+
output=result,
933+
tool_name=func_name,
934+
tool_args=args_dict,
935+
from_agent=self.agent,
936+
from_task=self.task,
937+
agent_key=agent_key,
938+
started_at=started_at,
939+
finished_at=datetime.now(),
940+
),
941+
)
940942

941943
# Append tool result message
942944
tool_message: LLMMessage = {

lib/crewai/src/crewai/experimental/agent_executor.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ def execute_native_tool(
689689
agent_key=agent_key,
690690
),
691691
)
692+
error_event_emitted = False
692693

693694
track_delegation_if_needed(func_name, args_dict, self.task)
694695

@@ -764,6 +765,7 @@ def execute_native_tool(
764765
error=e,
765766
),
766767
)
768+
error_event_emitted = True
767769
elif max_usage_reached and original_tool:
768770
# Return error message when max usage limit is reached
769771
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 @@ def execute_native_tool(
792794
color="red",
793795
)
794796

795-
# Emit tool usage finished event
796-
crewai_event_bus.emit(
797-
self,
798-
event=ToolUsageFinishedEvent(
799-
output=result,
800-
tool_name=func_name,
801-
tool_args=args_dict,
802-
from_agent=self.agent,
803-
from_task=self.task,
804-
agent_key=agent_key,
805-
started_at=started_at,
806-
finished_at=datetime.now(),
807-
),
808-
)
797+
if not error_event_emitted:
798+
crewai_event_bus.emit(
799+
self,
800+
event=ToolUsageFinishedEvent(
801+
output=result,
802+
tool_name=func_name,
803+
tool_args=args_dict,
804+
from_agent=self.agent,
805+
from_task=self.task,
806+
agent_key=agent_key,
807+
started_at=started_at,
808+
finished_at=datetime.now(),
809+
),
810+
)
809811

810812
# Append tool result message
811813
tool_message: LLMMessage = {

lib/crewai/src/crewai/tools/tool_usage.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ async def _ause(
270270
result = None # type: ignore
271271
should_retry = False
272272
available_tool = None
273+
error_event_emitted = False
273274

274275
try:
275276
if self.tools_handler and self.tools_handler.cache:
@@ -408,6 +409,7 @@ async def _ause(
408409

409410
except Exception as e:
410411
self.on_tool_error(tool=tool, tool_calling=calling, e=e)
412+
error_event_emitted = True
411413
self._run_attempts += 1
412414
if self._run_attempts > self._max_parsing_attempts:
413415
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
@@ -435,7 +437,7 @@ async def _ause(
435437
result = self._format_result(result=result)
436438

437439
finally:
438-
if started_event_emitted:
440+
if started_event_emitted and not error_event_emitted:
439441
self.on_tool_use_finished(
440442
tool=tool,
441443
tool_calling=calling,
@@ -500,6 +502,7 @@ def _use(
500502
result = None # type: ignore
501503
should_retry = False
502504
available_tool = None
505+
error_event_emitted = False
503506

504507
try:
505508
if self.tools_handler and self.tools_handler.cache:
@@ -638,6 +641,7 @@ def _use(
638641

639642
except Exception as e:
640643
self.on_tool_error(tool=tool, tool_calling=calling, e=e)
644+
error_event_emitted = True
641645
self._run_attempts += 1
642646
if self._run_attempts > self._max_parsing_attempts:
643647
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
@@ -665,7 +669,7 @@ def _use(
665669
result = self._format_result(result=result)
666670

667671
finally:
668-
if started_event_emitted:
672+
if started_event_emitted and not error_event_emitted:
669673
self.on_tool_use_finished(
670674
tool=tool,
671675
tool_calling=calling,

lib/crewai/tests/events/test_event_context.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,40 @@ def test_scope_restores_on_exception(self) -> None:
177177
raise ValueError("test error")
178178
except ValueError:
179179
pass
180-
assert get_triggering_event_id() is None
180+
assert get_triggering_event_id() is None
181+
182+
183+
def test_agent_scope_preserved_after_tool_error_event() -> None:
184+
from crewai.events import crewai_event_bus
185+
from crewai.events.types.tool_usage_events import (
186+
ToolUsageErrorEvent,
187+
ToolUsageStartedEvent,
188+
)
189+
190+
push_event_scope("crew-1", "crew_kickoff_started")
191+
push_event_scope("task-1", "task_started")
192+
push_event_scope("agent-1", "agent_execution_started")
193+
194+
crewai_event_bus.emit(
195+
None,
196+
ToolUsageStartedEvent(
197+
tool_name="test_tool",
198+
tool_args={},
199+
agent_key="test_agent",
200+
)
201+
)
202+
203+
crewai_event_bus.emit(
204+
None,
205+
ToolUsageErrorEvent(
206+
tool_name="test_tool",
207+
tool_args={},
208+
agent_key="test_agent",
209+
error=ValueError("test error"),
210+
)
211+
)
212+
213+
crewai_event_bus.flush()
214+
215+
assert get_current_parent_id() == "agent-1"
216+

lib/crewai/tests/tools/test_tool_usage.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from crewai.events.event_bus import crewai_event_bus
1111
from crewai.events.types.tool_usage_events import (
1212
ToolSelectionErrorEvent,
13+
ToolUsageErrorEvent,
1314
ToolUsageFinishedEvent,
15+
ToolUsageStartedEvent,
1416
ToolValidateInputErrorEvent,
1517
)
1618
from crewai.tools import BaseTool
@@ -744,3 +746,78 @@ def event_handler(source, event):
744746
assert isinstance(event.started_at, datetime.datetime)
745747
assert isinstance(event.finished_at, datetime.datetime)
746748
assert event.type == "tool_usage_finished"
749+
750+
751+
def test_tool_error_does_not_emit_finished_event():
752+
from crewai.tools.tool_calling import ToolCalling
753+
754+
class FailingTool(BaseTool):
755+
name: str = "Failing Tool"
756+
description: str = "A tool that always fails"
757+
758+
def _run(self, **kwargs) -> str:
759+
raise ValueError("Intentional failure")
760+
761+
failing_tool = FailingTool().to_structured_tool()
762+
763+
mock_agent = MagicMock()
764+
mock_agent.key = "test_agent_key"
765+
mock_agent.role = "test_agent_role"
766+
mock_agent._original_role = "test_agent_role"
767+
mock_agent.verbose = False
768+
mock_agent.fingerprint = None
769+
mock_agent.i18n.tools.return_value = {"name": "Add Image"}
770+
mock_agent.i18n.errors.return_value = "Error: {error}"
771+
mock_agent.i18n.slice.return_value = "Available tools: {tool_names}"
772+
773+
mock_task = MagicMock()
774+
mock_task.delegations = 0
775+
mock_task.name = "Test Task"
776+
mock_task.description = "A test task"
777+
mock_task.id = "test-task-id"
778+
779+
mock_action = MagicMock()
780+
mock_action.tool = "failing_tool"
781+
mock_action.tool_input = "{}"
782+
783+
tool_usage = ToolUsage(
784+
tools_handler=MagicMock(cache=None, last_used_tool=None),
785+
tools=[failing_tool],
786+
task=mock_task,
787+
function_calling_llm=None,
788+
agent=mock_agent,
789+
action=mock_action,
790+
)
791+
792+
started_events = []
793+
error_events = []
794+
finished_events = []
795+
error_received = threading.Event()
796+
797+
@crewai_event_bus.on(ToolUsageStartedEvent)
798+
def on_started(source, event):
799+
if event.tool_name == "failing_tool":
800+
started_events.append(event)
801+
802+
@crewai_event_bus.on(ToolUsageErrorEvent)
803+
def on_error(source, event):
804+
if event.tool_name == "failing_tool":
805+
error_events.append(event)
806+
error_received.set()
807+
808+
@crewai_event_bus.on(ToolUsageFinishedEvent)
809+
def on_finished(source, event):
810+
if event.tool_name == "failing_tool":
811+
finished_events.append(event)
812+
813+
tool_calling = ToolCalling(tool_name="failing_tool", arguments={})
814+
tool_usage.use(calling=tool_calling, tool_string="Action: failing_tool")
815+
816+
assert error_received.wait(timeout=5), "Timeout waiting for error event"
817+
crewai_event_bus.flush()
818+
819+
assert len(started_events) >= 1, "Expected at least one ToolUsageStartedEvent"
820+
assert len(error_events) >= 1, "Expected at least one ToolUsageErrorEvent"
821+
assert len(finished_events) == 0, (
822+
"ToolUsageFinishedEvent should NOT be emitted after ToolUsageErrorEvent"
823+
)

0 commit comments

Comments
 (0)