From 1c59ff8b96268fb16b5e3d54893c145cd17b9325 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 17:37:07 +0000 Subject: [PATCH] Fix output file writes outdated task result after guardrail execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The output file was being written with pre-guardrail output instead of post-guardrail output. This was because the file save logic used the original variables (json_output, pydantic_output, result) instead of the updated task_output object after guardrails executed. Fixed by using task_output.json_dict, task_output.pydantic, and task_output.raw in the file save logic for both sync (_execute_core) and async (_aexecute_core) execution paths. Added 5 tests to verify output file contains post-guardrail results: - test_output_file_contains_guardrail_modified_raw_result - test_output_file_contains_guardrail_modified_json_result - test_output_file_contains_guardrail_modified_pydantic_result - test_output_file_with_single_guardrail_modification - test_output_file_with_multiple_guardrails_chained_modifications Fixes #4156 Co-Authored-By: João --- lib/crewai/src/crewai/task.py | 16 ++- lib/crewai/tests/test_task_guardrails.py | 159 +++++++++++++++++++++++ 2 files changed, 169 insertions(+), 6 deletions(-) diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index 13d30b5648..58f52db32a 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -584,10 +584,12 @@ async def _aexecute_core( if self.output_file: content = ( - json_output - if json_output + task_output.json_dict + if task_output.json_dict else ( - pydantic_output.model_dump_json() if pydantic_output else result + task_output.pydantic.model_dump_json() + if task_output.pydantic + else task_output.raw ) ) self._save_file(content) @@ -677,10 +679,12 @@ def _execute_core( if self.output_file: content = ( - json_output - if json_output + task_output.json_dict + if task_output.json_dict else ( - pydantic_output.model_dump_json() if pydantic_output else result + task_output.pydantic.model_dump_json() + if task_output.pydantic + else task_output.raw ) ) self._save_file(content) diff --git a/lib/crewai/tests/test_task_guardrails.py b/lib/crewai/tests/test_task_guardrails.py index 7ceaf847b2..82381612a0 100644 --- a/lib/crewai/tests/test_task_guardrails.py +++ b/lib/crewai/tests/test_task_guardrails.py @@ -752,3 +752,162 @@ def guardrail_3(result: TaskOutput) -> tuple[bool, str]: assert call_counts["g3"] == 1 assert "G3(1)" in result.raw + + +def test_output_file_contains_guardrail_modified_raw_result(tmp_path, monkeypatch): + """Test that output file contains the result after guardrail modification for raw output.""" + monkeypatch.chdir(tmp_path) + output_file = tmp_path / "output.txt" + + def modify_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, "MODIFIED BY GUARDRAIL") + + agent = Mock() + agent.role = "test_agent" + agent.execute_task.return_value = "original result" + agent.crew = None + agent.last_messages = [] + + task = create_smart_task( + description="Test task", + expected_output="Output", + guardrails=[modify_guardrail], + output_file="output.txt", + ) + + result = task.execute_sync(agent=agent) + + assert result.raw == "MODIFIED BY GUARDRAIL" + assert output_file.read_text() == "MODIFIED BY GUARDRAIL" + + +def test_output_file_contains_guardrail_modified_json_result(tmp_path, monkeypatch): + """Test that output file contains the result after guardrail modification for JSON output.""" + import json + + from pydantic import BaseModel + + monkeypatch.chdir(tmp_path) + + class TestModel(BaseModel): + message: str + + output_file = tmp_path / "output.json" + + def modify_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, '{"message": "modified by guardrail"}') + + agent = Mock() + agent.role = "test_agent" + agent.execute_task.return_value = '{"message": "original"}' + agent.crew = None + agent.last_messages = [] + + task = create_smart_task( + description="Test task", + expected_output="Output", + guardrails=[modify_guardrail], + output_json=TestModel, + output_file="output.json", + ) + + result = task.execute_sync(agent=agent) + + assert result.json_dict == {"message": "modified by guardrail"} + file_content = json.loads(output_file.read_text()) + assert file_content == {"message": "modified by guardrail"} + + +def test_output_file_contains_guardrail_modified_pydantic_result(tmp_path, monkeypatch): + """Test that output file contains the result after guardrail modification for pydantic output.""" + import json + + from pydantic import BaseModel + + monkeypatch.chdir(tmp_path) + + class TestModel(BaseModel): + message: str + + output_file = tmp_path / "output.json" + + def modify_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, '{"message": "modified by guardrail"}') + + agent = Mock() + agent.role = "test_agent" + agent.execute_task.return_value = '{"message": "original"}' + agent.crew = None + agent.last_messages = [] + + task = create_smart_task( + description="Test task", + expected_output="Output", + guardrails=[modify_guardrail], + output_pydantic=TestModel, + output_file="output.json", + ) + + result = task.execute_sync(agent=agent) + + assert result.pydantic is not None + assert result.pydantic.message == "modified by guardrail" + file_content = json.loads(output_file.read_text()) + assert file_content == {"message": "modified by guardrail"} + + +def test_output_file_with_single_guardrail_modification(tmp_path, monkeypatch): + """Test that output file contains the result after single guardrail modification.""" + monkeypatch.chdir(tmp_path) + output_file = tmp_path / "output.txt" + + def modify_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, result.raw.upper()) + + agent = Mock() + agent.role = "test_agent" + agent.execute_task.return_value = "hello world" + agent.crew = None + agent.last_messages = [] + + task = create_smart_task( + description="Test task", + expected_output="Output", + guardrail=modify_guardrail, + output_file="output.txt", + ) + + result = task.execute_sync(agent=agent) + + assert result.raw == "HELLO WORLD" + assert output_file.read_text() == "HELLO WORLD" + + +def test_output_file_with_multiple_guardrails_chained_modifications(tmp_path, monkeypatch): + """Test that output file contains the final result after multiple guardrail modifications.""" + monkeypatch.chdir(tmp_path) + output_file = tmp_path / "output.txt" + + def first_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, f"[FIRST] {result.raw}") + + def second_guardrail(result: TaskOutput) -> tuple[bool, str]: + return (True, f"{result.raw} [SECOND]") + + agent = Mock() + agent.role = "test_agent" + agent.execute_task.return_value = "original" + agent.crew = None + agent.last_messages = [] + + task = create_smart_task( + description="Test task", + expected_output="Output", + guardrails=[first_guardrail, second_guardrail], + output_file="output.txt", + ) + + result = task.execute_sync(agent=agent) + + assert result.raw == "[FIRST] original [SECOND]" + assert output_file.read_text() == "[FIRST] original [SECOND]"