From 9a49af098beb9ac7b94b127a7b3d831c70e2b065 Mon Sep 17 00:00:00 2001 From: Lucas Gomide Date: Thu, 16 Jul 2026 15:37:56 -0300 Subject: [PATCH] fix: sync kickoff-completed event with OUTPUT hook result (#6571) * fix: sync kickoff-completed event with OUTPUT hook result `CrewKickoffCompletedEvent` still carried the pre-hook `TaskOutput`, so AMP/OTEL consumers never saw `OUTPUT` mutations even though the returned `CrewOutput` was updated. Sync `final_task_output.raw` from the post-hook payload before emit, matching `FlowFinishedEvent`. * style: drop OUTPUT sync comment and rename crew output test --- lib/crewai/src/crewai/crew.py | 3 ++ .../hooks/test_interception_conformance.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 686b08cc4..ce3822387 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -1928,6 +1928,9 @@ class Crew(FlowTrackable, BaseModel): dispatch(InterceptionPoint.EXECUTION_END, end_ctx) crew_output = cast(CrewOutput, end_ctx.payload) + if isinstance(crew_output, CrewOutput): + final_task_output.raw = crew_output.raw + # Ensure background memory saves finish (and emit their # completed/failed events) before the kickoff-completed event below # triggers listener teardown/finalization. diff --git a/lib/crewai/tests/hooks/test_interception_conformance.py b/lib/crewai/tests/hooks/test_interception_conformance.py index dc63db644..c9371bcc6 100644 --- a/lib/crewai/tests/hooks/test_interception_conformance.py +++ b/lib/crewai/tests/hooks/test_interception_conformance.py @@ -10,6 +10,9 @@ from __future__ import annotations from unittest.mock import patch from crewai.agent import Agent +from crewai.crew import Crew +from crewai.events.event_bus import crewai_event_bus +from crewai.events.types.crew_events import CrewKickoffCompletedEvent from crewai.flow.flow import Flow, listen, start from crewai.hooks.dispatch import ( HookAborted, @@ -147,3 +150,34 @@ class TestTaskStepPoints: assert result.raw == "sanitized output" assert (tmp_path / "output.txt").read_text() == "sanitized output" + + +class TestCrewOutput: + def test_output_modification_reaches_kickoff_completed_event(self): + @on(InterceptionPoint.OUTPUT) + def append_notice(ctx): + if hasattr(ctx.payload, "raw") and isinstance(ctx.payload.raw, str): + ctx.payload.raw += "\nchanged by hook" + return None + + completed_raw: list[str] = [] + + @crewai_event_bus.on(CrewKickoffCompletedEvent) + def capture_completed(_source, event: CrewKickoffCompletedEvent): + completed_raw.append(event.output.raw) + + agent = Agent(role="Writer", goal="Write", backstory="Writes things.") + task = Task( + description="Write something", + expected_output="Some text", + agent=agent, + ) + crew = Crew(agents=[agent], tasks=[task], verbose=False) + + with patch.object(Agent, "execute_task", return_value="original output"): + result = crew.kickoff() + crewai_event_bus.flush() + + assert result.raw.endswith("changed by hook") + assert completed_raw + assert completed_raw[-1].endswith("changed by hook")