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>
This commit is contained in:
Devin AI
2025-12-29 17:37:07 +00:00
parent b9dd166a6b
commit 1c59ff8b96
2 changed files with 169 additions and 6 deletions

View File

@@ -584,10 +584,12 @@ class Task(BaseModel):
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 @@ class Task(BaseModel):
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)

View File

@@ -752,3 +752,162 @@ def test_per_guardrail_independent_retry_tracking():
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]"