Skip to content

Commit 1c59ff8

Browse files
devin-ai-integration[bot]João
andcommitted
Fix output file writes outdated task result after guardrail execution
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 <joao@crewai.com>
1 parent b9dd166 commit 1c59ff8

File tree

2 files changed

+169
-6
lines changed

2 files changed

+169
-6
lines changed

lib/crewai/src/crewai/task.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -584,10 +584,12 @@ async def _aexecute_core(
584584

585585
if self.output_file:
586586
content = (
587-
json_output
588-
if json_output
587+
task_output.json_dict
588+
if task_output.json_dict
589589
else (
590-
pydantic_output.model_dump_json() if pydantic_output else result
590+
task_output.pydantic.model_dump_json()
591+
if task_output.pydantic
592+
else task_output.raw
591593
)
592594
)
593595
self._save_file(content)
@@ -677,10 +679,12 @@ def _execute_core(
677679

678680
if self.output_file:
679681
content = (
680-
json_output
681-
if json_output
682+
task_output.json_dict
683+
if task_output.json_dict
682684
else (
683-
pydantic_output.model_dump_json() if pydantic_output else result
685+
task_output.pydantic.model_dump_json()
686+
if task_output.pydantic
687+
else task_output.raw
684688
)
685689
)
686690
self._save_file(content)

lib/crewai/tests/test_task_guardrails.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,162 @@ def guardrail_3(result: TaskOutput) -> tuple[bool, str]:
752752
assert call_counts["g3"] == 1
753753

754754
assert "G3(1)" in result.raw
755+
756+
757+
def test_output_file_contains_guardrail_modified_raw_result(tmp_path, monkeypatch):
758+
"""Test that output file contains the result after guardrail modification for raw output."""
759+
monkeypatch.chdir(tmp_path)
760+
output_file = tmp_path / "output.txt"
761+
762+
def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
763+
return (True, "MODIFIED BY GUARDRAIL")
764+
765+
agent = Mock()
766+
agent.role = "test_agent"
767+
agent.execute_task.return_value = "original result"
768+
agent.crew = None
769+
agent.last_messages = []
770+
771+
task = create_smart_task(
772+
description="Test task",
773+
expected_output="Output",
774+
guardrails=[modify_guardrail],
775+
output_file="output.txt",
776+
)
777+
778+
result = task.execute_sync(agent=agent)
779+
780+
assert result.raw == "MODIFIED BY GUARDRAIL"
781+
assert output_file.read_text() == "MODIFIED BY GUARDRAIL"
782+
783+
784+
def test_output_file_contains_guardrail_modified_json_result(tmp_path, monkeypatch):
785+
"""Test that output file contains the result after guardrail modification for JSON output."""
786+
import json
787+
788+
from pydantic import BaseModel
789+
790+
monkeypatch.chdir(tmp_path)
791+
792+
class TestModel(BaseModel):
793+
message: str
794+
795+
output_file = tmp_path / "output.json"
796+
797+
def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
798+
return (True, '{"message": "modified by guardrail"}')
799+
800+
agent = Mock()
801+
agent.role = "test_agent"
802+
agent.execute_task.return_value = '{"message": "original"}'
803+
agent.crew = None
804+
agent.last_messages = []
805+
806+
task = create_smart_task(
807+
description="Test task",
808+
expected_output="Output",
809+
guardrails=[modify_guardrail],
810+
output_json=TestModel,
811+
output_file="output.json",
812+
)
813+
814+
result = task.execute_sync(agent=agent)
815+
816+
assert result.json_dict == {"message": "modified by guardrail"}
817+
file_content = json.loads(output_file.read_text())
818+
assert file_content == {"message": "modified by guardrail"}
819+
820+
821+
def test_output_file_contains_guardrail_modified_pydantic_result(tmp_path, monkeypatch):
822+
"""Test that output file contains the result after guardrail modification for pydantic output."""
823+
import json
824+
825+
from pydantic import BaseModel
826+
827+
monkeypatch.chdir(tmp_path)
828+
829+
class TestModel(BaseModel):
830+
message: str
831+
832+
output_file = tmp_path / "output.json"
833+
834+
def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
835+
return (True, '{"message": "modified by guardrail"}')
836+
837+
agent = Mock()
838+
agent.role = "test_agent"
839+
agent.execute_task.return_value = '{"message": "original"}'
840+
agent.crew = None
841+
agent.last_messages = []
842+
843+
task = create_smart_task(
844+
description="Test task",
845+
expected_output="Output",
846+
guardrails=[modify_guardrail],
847+
output_pydantic=TestModel,
848+
output_file="output.json",
849+
)
850+
851+
result = task.execute_sync(agent=agent)
852+
853+
assert result.pydantic is not None
854+
assert result.pydantic.message == "modified by guardrail"
855+
file_content = json.loads(output_file.read_text())
856+
assert file_content == {"message": "modified by guardrail"}
857+
858+
859+
def test_output_file_with_single_guardrail_modification(tmp_path, monkeypatch):
860+
"""Test that output file contains the result after single guardrail modification."""
861+
monkeypatch.chdir(tmp_path)
862+
output_file = tmp_path / "output.txt"
863+
864+
def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
865+
return (True, result.raw.upper())
866+
867+
agent = Mock()
868+
agent.role = "test_agent"
869+
agent.execute_task.return_value = "hello world"
870+
agent.crew = None
871+
agent.last_messages = []
872+
873+
task = create_smart_task(
874+
description="Test task",
875+
expected_output="Output",
876+
guardrail=modify_guardrail,
877+
output_file="output.txt",
878+
)
879+
880+
result = task.execute_sync(agent=agent)
881+
882+
assert result.raw == "HELLO WORLD"
883+
assert output_file.read_text() == "HELLO WORLD"
884+
885+
886+
def test_output_file_with_multiple_guardrails_chained_modifications(tmp_path, monkeypatch):
887+
"""Test that output file contains the final result after multiple guardrail modifications."""
888+
monkeypatch.chdir(tmp_path)
889+
output_file = tmp_path / "output.txt"
890+
891+
def first_guardrail(result: TaskOutput) -> tuple[bool, str]:
892+
return (True, f"[FIRST] {result.raw}")
893+
894+
def second_guardrail(result: TaskOutput) -> tuple[bool, str]:
895+
return (True, f"{result.raw} [SECOND]")
896+
897+
agent = Mock()
898+
agent.role = "test_agent"
899+
agent.execute_task.return_value = "original"
900+
agent.crew = None
901+
agent.last_messages = []
902+
903+
task = create_smart_task(
904+
description="Test task",
905+
expected_output="Output",
906+
guardrails=[first_guardrail, second_guardrail],
907+
output_file="output.txt",
908+
)
909+
910+
result = task.execute_sync(agent=agent)
911+
912+
assert result.raw == "[FIRST] original [SECOND]"
913+
assert output_file.read_text() == "[FIRST] original [SECOND]"

0 commit comments

Comments
 (0)