mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 15:25:09 +00:00
* feat: wire execution-boundary interception points Adds the typed interception contexts (`crewai/hooks/contexts.py`) and wires the `execution_start`, `input`, `output`, and `execution_end` points for both crews and flows through the dispatcher. `prepare_kickoff` and `Flow.kickoff_async` fire `execution_start`/`input` so a hook can rewrite resolved inputs before the run, while `Crew._create_crew_output` and the flow tail fire `output`/`execution_end` so the final result can be observed or replaced. Closes the eight critical-path points without touching the legacy hooks. * fix: correct execution-boundary hook ordering and input aliasing Reworks the crew and flow boundary seams flagged in review. `OUTPUT` and `EXECUTION_END` now run before the completion event (`CrewKickoffCompletedEvent` and `FlowFinishedEvent`) so a `HookAborted` no longer leaves a spurious completed signal and a returned payload replacement is honored on the emitted and returned result. Boundary contexts alias `inputs` to the same object as `payload` instead of a fresh dict from `or`, so in-place edits survive read-back. Flows re-publish the resolved inputs into `flow_inputs` baggage after the `INPUT` hook so trigger-payload injection observes hook rewrites, and a resumed flow now dispatches `OUTPUT`/`EXECUTION_END` on its completion path. * chore: drop redundant seam comments from execution-boundary wiring Removes two inline comments narrating the OUTPUT/EXECUTION_END dispatch ordering in `crew.py` and the flow runtime, plus a stray sentence about enterprise adapters in the conformance-suite docstring. Comment-only cleanup, no behavior change. * fix: keep crew output typed across boundary hook dispatch `_create_crew_output` reassigned `crew_output` from the hook contexts' `payload`, which is typed `Any`, so mypy flagged `no-any-return` at the function's return. Cast the payload back to `CrewOutput` after each dispatch and split the `ExecutionEndContext` construction to satisfy `ruff format`'s line-length limit.
89 lines
2.2 KiB
Python
89 lines
2.2 KiB
Python
"""Conformance suite for the framework-native interception points.
|
|
|
|
For each wired point this suite asserts the shared contract: the probe hook
|
|
sees a well-shaped payload, an in-place/returned modification is honored, and a
|
|
:class:`HookAborted` interrupts the step.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from crewai.flow.flow import Flow, listen, start
|
|
from crewai.hooks.dispatch import (
|
|
HookAborted,
|
|
InterceptionPoint,
|
|
clear_all,
|
|
on,
|
|
)
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_dispatch_registry():
|
|
clear_all()
|
|
yield
|
|
clear_all()
|
|
|
|
|
|
class _SimpleFlow(Flow):
|
|
@start()
|
|
def begin(self):
|
|
return "begin"
|
|
|
|
@listen(begin)
|
|
def finish(self, _):
|
|
return "flow-result"
|
|
|
|
|
|
class TestFlowExecutionBoundaries:
|
|
"""execution_start / input / output / execution_end on a flow."""
|
|
|
|
def test_all_boundary_points_fire_once(self):
|
|
fired: list[str] = []
|
|
|
|
for point in (
|
|
InterceptionPoint.EXECUTION_START,
|
|
InterceptionPoint.INPUT,
|
|
InterceptionPoint.OUTPUT,
|
|
InterceptionPoint.EXECUTION_END,
|
|
):
|
|
|
|
@on(point)
|
|
def _probe(ctx, _point=point):
|
|
fired.append(_point.value)
|
|
|
|
_SimpleFlow().kickoff(inputs={"seed": 1})
|
|
|
|
assert fired == [
|
|
"execution_start",
|
|
"input",
|
|
"output",
|
|
"execution_end",
|
|
]
|
|
|
|
def test_output_modification_is_honored(self):
|
|
@on(InterceptionPoint.OUTPUT)
|
|
def rewrite(ctx):
|
|
return "intercepted"
|
|
|
|
result = _SimpleFlow().kickoff()
|
|
assert result == "intercepted"
|
|
|
|
def test_input_payload_carries_inputs(self):
|
|
seen: dict = {}
|
|
|
|
@on(InterceptionPoint.INPUT)
|
|
def capture(ctx):
|
|
seen.update(ctx.payload or {})
|
|
|
|
_SimpleFlow().kickoff(inputs={"seed": 42})
|
|
assert seen == {"seed": 42}
|
|
|
|
def test_abort_at_execution_start_interrupts(self):
|
|
@on(InterceptionPoint.EXECUTION_START)
|
|
def block(ctx):
|
|
raise HookAborted(reason="not allowed", source="policy")
|
|
|
|
with pytest.raises(HookAborted) as exc:
|
|
_SimpleFlow().kickoff()
|
|
assert exc.value.reason == "not allowed"
|