mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-03 08:12:39 +00:00
fix: preserve task outputs when mixing sync and async tasks (#4137)
This fixes issue #4137 where task outputs were lost when a Crew executed a mix of synchronous and asynchronous tasks. The bug was caused by _process_async_tasks and _aprocess_async_tasks returning a new list, which then replaced the existing task_outputs list instead of extending it. Changes: - Changed task_outputs = self._process_async_tasks(...) to task_outputs.extend(self._process_async_tasks(...)) in _execute_tasks - Changed task_outputs = await self._aprocess_async_tasks(...) to task_outputs.extend(await self._aprocess_async_tasks(...)) in _aexecute_tasks - Applied the same fix to _handle_conditional_task and _ahandle_conditional_task Added tests: - test_sync_task_outputs_preserved_when_mixing_sync_async_tasks - test_sync_task_outputs_preserved_when_crew_ends_with_async_task - test_sync_multiple_sync_tasks_before_async_all_preserved - TestMixedSyncAsyncTaskOutputs class with async variants Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -1251,6 +1251,200 @@ async def test_async_task_execution_call_count(researcher, writer):
|
||||
assert mock_execute_sync.call_count == 1
|
||||
|
||||
|
||||
def test_sync_task_outputs_preserved_when_mixing_sync_async_tasks():
|
||||
"""Test for issue #4137: Task outputs lost when mixing sync and async tasks.
|
||||
|
||||
Scenario: sync -> async -> sync
|
||||
Expected: All 3 task outputs should be in the result.
|
||||
"""
|
||||
researcher_agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
allow_delegation=False,
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Sync task 1",
|
||||
expected_output="Output 1",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Async task 2",
|
||||
expected_output="Output 2",
|
||||
agent=researcher_agent,
|
||||
async_execution=True,
|
||||
)
|
||||
task3 = Task(
|
||||
description="Sync task 3",
|
||||
expected_output="Output 3",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher_agent],
|
||||
tasks=[task1, task2, task3],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="Sync task 1",
|
||||
raw="Result 1",
|
||||
agent="Researcher",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Async task 2",
|
||||
raw="Result 2",
|
||||
agent="Researcher",
|
||||
)
|
||||
mock_output3 = TaskOutput(
|
||||
description="Sync task 3",
|
||||
raw="Result 3",
|
||||
agent="Researcher",
|
||||
)
|
||||
|
||||
mock_future = MagicMock(spec=Future)
|
||||
mock_future.result.return_value = mock_output2
|
||||
|
||||
with (
|
||||
patch.object(Task, "execute_sync", side_effect=[mock_output1, mock_output3]),
|
||||
patch.object(Task, "execute_async", return_value=mock_future),
|
||||
):
|
||||
result = crew.kickoff()
|
||||
|
||||
assert result is not None
|
||||
assert len(result.tasks_output) == 3
|
||||
assert result.tasks_output[0].raw == "Result 1"
|
||||
assert result.tasks_output[1].raw == "Result 2"
|
||||
assert result.tasks_output[2].raw == "Result 3"
|
||||
|
||||
|
||||
def test_sync_task_outputs_preserved_when_crew_ends_with_async_task():
|
||||
"""Test for issue #4137: Task outputs preserved when crew ends with async task.
|
||||
|
||||
Scenario: sync -> async (final)
|
||||
Expected: Both task outputs should be in the result.
|
||||
"""
|
||||
researcher_agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
allow_delegation=False,
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Sync task 1",
|
||||
expected_output="Output 1",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Async task 2",
|
||||
expected_output="Output 2",
|
||||
agent=researcher_agent,
|
||||
async_execution=True,
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher_agent],
|
||||
tasks=[task1, task2],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="Sync task 1",
|
||||
raw="Result 1",
|
||||
agent="Researcher",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Async task 2",
|
||||
raw="Result 2",
|
||||
agent="Researcher",
|
||||
)
|
||||
|
||||
mock_future = MagicMock(spec=Future)
|
||||
mock_future.result.return_value = mock_output2
|
||||
|
||||
with (
|
||||
patch.object(Task, "execute_sync", return_value=mock_output1),
|
||||
patch.object(Task, "execute_async", return_value=mock_future),
|
||||
):
|
||||
result = crew.kickoff()
|
||||
|
||||
assert result is not None
|
||||
assert len(result.tasks_output) == 2
|
||||
assert result.tasks_output[0].raw == "Result 1"
|
||||
assert result.tasks_output[1].raw == "Result 2"
|
||||
|
||||
|
||||
def test_sync_multiple_sync_tasks_before_async_all_preserved():
|
||||
"""Test for issue #4137: Multiple sync task outputs before async are preserved.
|
||||
|
||||
Scenario: sync -> sync -> async -> sync
|
||||
Expected: All 4 task outputs should be in the result.
|
||||
"""
|
||||
researcher_agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
allow_delegation=False,
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Sync task 1",
|
||||
expected_output="Output 1",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Sync task 2",
|
||||
expected_output="Output 2",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
task3 = Task(
|
||||
description="Async task 3",
|
||||
expected_output="Output 3",
|
||||
agent=researcher_agent,
|
||||
async_execution=True,
|
||||
)
|
||||
task4 = Task(
|
||||
description="Sync task 4",
|
||||
expected_output="Output 4",
|
||||
agent=researcher_agent,
|
||||
async_execution=False,
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher_agent],
|
||||
tasks=[task1, task2, task3, task4],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_outputs = [
|
||||
TaskOutput(description=f"Task {i}", raw=f"Result {i}", agent="Researcher")
|
||||
for i in range(1, 5)
|
||||
]
|
||||
|
||||
mock_future = MagicMock(spec=Future)
|
||||
mock_future.result.return_value = mock_outputs[2]
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
Task, "execute_sync", side_effect=[mock_outputs[0], mock_outputs[1], mock_outputs[3]]
|
||||
),
|
||||
patch.object(Task, "execute_async", return_value=mock_future),
|
||||
):
|
||||
result = crew.kickoff()
|
||||
|
||||
assert result is not None
|
||||
assert len(result.tasks_output) == 4
|
||||
for i in range(4):
|
||||
assert result.tasks_output[i].raw == f"Result {i + 1}"
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_kickoff_for_each_single_input():
|
||||
"""Tests if kickoff_for_each works with a single input."""
|
||||
|
||||
Reference in New Issue
Block a user