Fix #5534: sync Crew.agents with task agents so input_files propagate

When a Task has an agent assigned but the Crew is built without an
explicit agents=[...] list, setup_agents iterates an empty list and
never wires agent.crew = crew. Downstream paths that rely on the crew
reference (task.prompt() file injection, crew_agent_executor file
lookups, delegation tools) then silently no-op, so input_files attached
at the Task level are ignored.

Add a model_validator that auto-populates Crew.agents with any agent
referenced by a task so the crew behaves the same whether agents=[...]
is passed explicitly or inferred from tasks, and cover the regression
with unit tests.

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2026-04-19 16:25:25 +00:00
parent f879909526
commit 518c9afc8e
2 changed files with 145 additions and 1 deletions

View File

@@ -624,6 +624,42 @@ class Crew(FlowTrackable, BaseModel):
)
return self
@model_validator(mode="after")
def sync_agents_with_tasks(self) -> Self:
"""Ensure ``agents`` includes every agent assigned to a task.
When a ``Task`` has an agent assigned (``task.agent=agent``) but that
agent is not listed in ``Crew.agents``, downstream setup such as
``agent.crew = crew`` is skipped, which causes features that depend
on the crew reference (e.g. ``input_files`` injection, delegation
tools, crew memory lookups) to silently no-op.
This validator auto-populates ``self.agents`` with any task agent
that isn't already present so the crew behaves the same whether or
not ``agents=[...]`` was passed explicitly.
"""
if not self.tasks:
return self
existing: list[BaseAgent] = list(self.agents)
def _contains(agent: BaseAgent) -> bool:
return any(existing_agent is agent for existing_agent in existing)
for task in self.tasks:
task_agent = task.agent
if task_agent is None:
continue
if task_agent is self.manager_agent:
continue
if not _contains(task_agent):
existing.append(task_agent)
if len(existing) != len(self.agents):
self.agents = existing
return self
@model_validator(mode="after")
def check_manager_llm(self) -> Self:
"""Validates that the language model is set when using hierarchical process."""

View File

@@ -457,4 +457,112 @@ class TestCrewMultimodalFileUpload:
result = crew.kickoff(input_files={"document": pdf_file})
assert result.raw
assert len(result.raw) > 0
assert len(result.raw) > 0
class TestCrewMultimodalWithoutExplicitAgents:
"""Regression tests for issue #5534.
``Task.input_files`` must be propagated even when ``Crew`` is constructed
with only ``tasks=[...]`` and no explicit ``agents=[...]``. Prior to the
fix, ``crew.agents`` was empty in that case, ``setup_agents`` never wired
``task.agent.crew = crew``, and file injection silently no-op'd.
"""
@staticmethod
def _build_task_and_crew(
image_file: ImageFile,
) -> tuple[Agent, Task, Crew]:
llm = LLM(model="openai/gpt-4o-mini")
agent = Agent(
role="File Analyst",
goal="Analyze files",
backstory="Expert analyst.",
llm=llm,
multimodal=True,
verbose=False,
)
task = Task(
description="Describe the image.",
expected_output="A brief description.",
agent=agent,
input_files={"chart": image_file},
)
crew = Crew(tasks=[task], verbose=False)
return agent, task, crew
def test_crew_agents_auto_populated_from_tasks(
self, image_file: ImageFile
) -> None:
"""Crew.agents should include task.agent even if not passed explicitly."""
agent, _task, crew = self._build_task_and_crew(image_file)
assert agent in crew.agents
assert len(crew.agents) == 1
def test_crew_agents_not_duplicated_when_provided_explicitly(
self, image_file: ImageFile
) -> None:
"""When agents=[agent] is passed explicitly, no duplicates are added."""
llm = LLM(model="openai/gpt-4o-mini")
agent = Agent(
role="File Analyst",
goal="Analyze files",
backstory="Expert analyst.",
llm=llm,
multimodal=True,
verbose=False,
)
task = Task(
description="Describe the image.",
expected_output="A brief description.",
agent=agent,
input_files={"chart": image_file},
)
crew = Crew(agents=[agent], tasks=[task], verbose=False)
assert crew.agents.count(agent) == 1
def test_prepare_kickoff_wires_task_agent_to_crew(
self, image_file: ImageFile
) -> None:
"""Task agents not in ``agents=[...]`` should still get ``agent.crew``
set so downstream file/delegation code can find the crew."""
from crewai.crews.utils import prepare_kickoff
from crewai.utilities.file_store import clear_files, get_all_files
agent, task, crew = self._build_task_and_crew(image_file)
try:
prepare_kickoff(crew, inputs=None, input_files=None)
assert agent.crew is crew
# After prepare_kickoff + task._store_input_files, files stored
# at the task level must be retrievable via the crew id.
task._store_input_files()
files = get_all_files(crew.id, task.id)
assert files is not None
assert "chart" in files
finally:
clear_files(crew.id)
def test_task_prompt_includes_input_files_without_explicit_agents(
self, image_file: ImageFile
) -> None:
"""``task.prompt()`` must reference the input file even when the
crew is built without ``agents=[...]``."""
from crewai.crews.utils import prepare_kickoff
from crewai.utilities.file_store import clear_files
_agent, task, crew = self._build_task_and_crew(image_file)
try:
prepare_kickoff(crew, inputs=None, input_files=None)
task._store_input_files()
rendered = task.prompt()
assert "chart" in rendered
finally:
clear_files(crew.id)