diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index fda456be40..779c189ce4 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -835,6 +835,47 @@ def validate_async_task_cannot_include_sequential_async_tasks_in_context( break return self + @model_validator(mode="after") + def validate_context_no_circular_dependencies(self) -> Self: + """Validates that task context dependencies do not contain cycles.""" + task_ids = {id(task) for task in self.tasks} + visiting: set[int] = set() + visited: set[int] = set() + path: list[Task] = [] + + def visit(task: Task) -> None: + task_id = id(task) + if task_id in visited: + return + if task_id in visiting: + cycle_start = next( + index + for index, path_task in enumerate(path) + if id(path_task) == task_id + ) + cycle_tasks = [*path[cycle_start:], task] + cycle_descriptions = " -> ".join( + cycle_task.description for cycle_task in cycle_tasks + ) + raise ValueError( + "Task context dependencies contain a circular dependency: " + f"{cycle_descriptions}." + ) + + visiting.add(task_id) + path.append(task) + if isinstance(task.context, list): + for context_task in task.context: + if id(context_task) in task_ids: + visit(context_task) + path.pop() + visiting.remove(task_id) + visited.add(task_id) + + for task in self.tasks: + visit(task) + return self + @model_validator(mode="after") def validate_context_no_future_tasks(self) -> Self: """Validates that a task's context does not include future tasks.""" diff --git a/lib/crewai/tests/test_crew.py b/lib/crewai/tests/test_crew.py index 82f3207dd6..f5aecd5cd2 100644 --- a/lib/crewai/tests/test_crew.py +++ b/lib/crewai/tests/test_crew.py @@ -253,6 +253,37 @@ def test_context_no_future_tasks(researcher, writer): Crew(tasks=[task1, task2, task3, task4], agents=[researcher, writer]) +def test_context_no_circular_dependencies(researcher, writer): + task1 = Task( + description="Task 1", + expected_output="output", + agent=researcher, + ) + task2 = Task( + description="Task 2", + expected_output="output", + agent=researcher, + ) + task3 = Task( + description="Task 3", + expected_output="output", + agent=researcher, + ) + + task1.context = [task2] + task2.context = [task3] + task3.context = [task1] + + with pytest.raises( + ValueError, + match=re.escape( + "Task context dependencies contain a circular dependency: " + "Task 1 -> Task 2 -> Task 3 -> Task 1." + ), + ): + Crew(tasks=[task1, task2, task3], agents=[researcher, writer]) + + def test_crew_config_with_wrong_keys(): no_tasks_config = json.dumps( {