mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
fix: skip FlowFailedEvent when the run never opened a scope
The `kickoff_async` try block starts before `FlowStartedEvent` is emitted, so an abort in the execution-start hooks, in input handling or in state restore emitted a `flow_failed` with no opener, which pops an unrelated scope and warns about an empty scope stack. The failure event is now gated on the flow scope actually being open, either from this kickoff's `flow_started` or from a restored deferred session scope.
This commit is contained in:
@@ -2131,6 +2131,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
# EXECUTION_START/EXECUTION_END dispatch independently.
|
||||
execution_start_dispatched = False
|
||||
execution_end_dispatched = False
|
||||
# Guards the failure event: everything between here and the
|
||||
# ``flow_started`` emission below (hooks, input handling, state
|
||||
# restore) can raise, and a ``flow_failed`` with no opener would pop
|
||||
# an unrelated scope.
|
||||
flow_scope_open = False
|
||||
|
||||
try:
|
||||
from crewai.hooks.contexts import (
|
||||
@@ -2270,6 +2275,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
and get_current_parent_id() is None
|
||||
):
|
||||
restore_event_scope(((deferred_started_event_id, "flow_started"),))
|
||||
flow_scope_open = True
|
||||
elif get_current_parent_id() is None:
|
||||
reset_emission_counter()
|
||||
reset_last_event_id()
|
||||
@@ -2284,6 +2290,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
inputs=inputs,
|
||||
)
|
||||
future = crewai_event_bus.emit(self, started_event)
|
||||
flow_scope_open = True
|
||||
if future:
|
||||
try:
|
||||
await asyncio.wrap_future(future)
|
||||
@@ -2475,7 +2482,8 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
# not (exactly-once per invocation).
|
||||
if execution_start_dispatched and not execution_end_dispatched:
|
||||
self._dispatch_execution_end_failure(e)
|
||||
await self._emit_flow_failed(e)
|
||||
if flow_scope_open:
|
||||
await self._emit_flow_failed(e)
|
||||
raise
|
||||
finally:
|
||||
# Safety net for the exception path; the success path already
|
||||
|
||||
@@ -51,6 +51,7 @@ from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.human_feedback import human_feedback
|
||||
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
||||
from crewai.hooks.dispatch import HookAborted, InterceptionPoint, clear_all, on
|
||||
from crewai.llm import LLM
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
@@ -627,6 +628,42 @@ def test_suppressed_flow_failure_matches_finished_event_emission():
|
||||
assert len(failed) == 1
|
||||
|
||||
|
||||
def test_abort_before_flow_started_emits_no_failed_event():
|
||||
started: list[FlowStartedEvent] = []
|
||||
failed: list[FlowFailedEvent] = []
|
||||
|
||||
class BlockedFlow(Flow):
|
||||
@start()
|
||||
def begin(self) -> str:
|
||||
return "never runs"
|
||||
|
||||
clear_all()
|
||||
try:
|
||||
|
||||
@on(InterceptionPoint.EXECUTION_START)
|
||||
def block(_ctx):
|
||||
raise HookAborted(reason="blocked by policy")
|
||||
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@crewai_event_bus.on(FlowStartedEvent)
|
||||
def handle_flow_started(source, event):
|
||||
started.append(event)
|
||||
|
||||
@crewai_event_bus.on(FlowFailedEvent)
|
||||
def handle_flow_failed(source, event):
|
||||
failed.append(event)
|
||||
|
||||
with pytest.raises(HookAborted):
|
||||
BlockedFlow().kickoff()
|
||||
wait_for_event_handlers()
|
||||
finally:
|
||||
clear_all()
|
||||
|
||||
assert started == []
|
||||
assert failed == []
|
||||
|
||||
|
||||
def test_resume_emits_failed_event_paired_with_resume_started_event(tmp_path):
|
||||
started: list[FlowStartedEvent] = []
|
||||
failed: list[FlowFailedEvent] = []
|
||||
|
||||
Reference in New Issue
Block a user