From 570b100b331d78529f433a525c41ba1c3398fddc Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Wed, 1 Jul 2026 16:47:11 -0700 Subject: [PATCH] Allow repository-backed flow agents Flow declarations can now reference full repository-backed Agent config without duplicating it inline. The repository response is merged the same way as `Agent(from_repository=...)`, so values like `role`, `goal`, `backstory`, `tools`, `planning_config`, deprecated `reasoning`, `memory`, and other Agent fields can come from the repository. Local YAML fields still override repository values. Standalone agent action: ```yaml do: call: agent with: from_repository: support_specialist input: "${state.question}" ``` Inline crew agent: ```yaml do: call: crew with: name: inline_research agents: researcher: from_repository: researcher tasks: - description: Research {topic} expected_output: Findings about {topic} agent: researcher ``` --- .../templates/declarative_flow/AGENTS.md | 16 +-- lib/crewai/src/crewai/flow/flow_definition.py | 7 +- .../src/crewai/project/crew_definition.py | 58 +++++++-- lib/crewai/src/crewai/project/json_loader.py | 7 +- lib/crewai/tests/project/test_json_loader.py | 12 ++ lib/crewai/tests/test_flow_definition.py | 2 +- lib/crewai/tests/test_flow_from_definition.py | 118 ++++++++++++++++++ 7 files changed, 198 insertions(+), 22 deletions(-) diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md index 9f96d5efa..fa3eb3c25 100644 --- a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md @@ -258,9 +258,10 @@ Fields: #### Crew Agent Definition (`methods..do[call=crew].with.agents.`) Fields: -- `role` (required): string. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst` -- `goal` (required): string. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}` -- `backstory` (required): string. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.` +- `role` (optional): string | null; default `null`. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst` +- `goal` (optional): string | null; default `null`. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}` +- `backstory` (optional): string | null; default `null`. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.` +- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `researcher` - `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}` - `llm` (optional): string or inline LLM config; default `null`. Language model that runs this crew agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}` - `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}` @@ -292,15 +293,16 @@ Shape: - `call: agent` Fields: -- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline Agent definition outside of a crew. Example: `agent` +- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline or repository-backed Agent definition outside of a crew. Example: `agent` - `with` (required): any. Individual Agent definition to load and execute outside of a crew for this action. Put the agent input in `with.input`; agent actions do not support action-level `inputs`. Example: `{"backstory": "Precise and concise.", "goal": "Answer user questions", "input": "${state.question}", "role": "Analyst", "settings": {"llm": "openai/gpt-4o-mini"}}` #### Agent Definition (`methods..do[call=agent].with`) Fields: -- `role` (required): string. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist` -- `goal` (required): string. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply` -- `backstory` (required): string. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.` +- `role` (optional): string | null; default `null`. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist` +- `goal` (optional): string | null; default `null`. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply` +- `backstory` (optional): string | null; default `null`. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.` +- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `support_specialist` - `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}` - `llm` (optional): string or inline LLM config; default `null`. Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}` - `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}` diff --git a/lib/crewai/src/crewai/flow/flow_definition.py b/lib/crewai/src/crewai/flow/flow_definition.py index 3ac661efd..2e6a63f4a 100644 --- a/lib/crewai/src/crewai/flow/flow_definition.py +++ b/lib/crewai/src/crewai/flow/flow_definition.py @@ -472,8 +472,8 @@ class FlowAgentActionDefinition(BaseModel): call: Literal["agent"] = Field( description=( - "Action discriminator. Use agent to run an individual inline Agent " - "definition outside of a crew." + "Action discriminator. Use agent to run an individual inline or " + "repository-backed Agent definition outside of a crew." ), examples=["agent"], ) @@ -481,7 +481,8 @@ class FlowAgentActionDefinition(BaseModel): alias="with", description=( "Individual Agent definition to load and execute outside of a crew " - "for this action." + "for this action. Set from_repository to load agent configuration " + "from the agent repository." ), examples=[ { diff --git a/lib/crewai/src/crewai/project/crew_definition.py b/lib/crewai/src/crewai/project/crew_definition.py index ebfb55e05..234ba7b86 100644 --- a/lib/crewai/src/crewai/project/crew_definition.py +++ b/lib/crewai/src/crewai/project/crew_definition.py @@ -62,25 +62,28 @@ class LLMDefinition(BaseModel): class CrewAgentDefinition(BaseModel): - """Inline agent definition used by a crew definition.""" + """Agent definition used by a crew definition.""" model_config = ConfigDict(extra="allow") - role: str = Field( + role: str | None = Field( + default=None, description=( "Crew agent role. Crew inputs are interpolated with `{name}` " "placeholders such as `{topic}`; this is not CEL." ), examples=["Research analyst"], ) - goal: str = Field( + goal: str | None = Field( + default=None, description=( "Crew agent goal. Crew inputs are interpolated with `{name}` " "placeholders such as `{topic}`; this is not CEL." ), examples=["Research {topic}"], ) - backstory: str = Field( + backstory: str | None = Field( + default=None, description=( "Crew agent backstory. Crew inputs are interpolated with `{name}` " "placeholders such as `{topic}`; this is not CEL." @@ -92,6 +95,15 @@ class CrewAgentDefinition(BaseModel): description="Optional built-in type or Python reference used to load the agent.", examples=["agent", {"python": "my_project.agents.ResearchAgent"}], ) + from_repository: str | None = Field( + default=None, + description=( + "Agent repository name to load. Repository values supply missing " + "agent configuration; explicitly provided local fields override the " + "repository values." + ), + examples=["researcher"], + ) settings: dict[str, Any] = Field( default_factory=dict, description="Additional agent settings passed to the loader.", @@ -179,19 +191,40 @@ class CrewAgentDefinition(BaseModel): raise ValueError("agent.settings must be a mapping") return value or {} + @model_validator(mode="after") + def _validate_agent_source(self) -> CrewAgentDefinition: + if self.from_repository: + return self + + missing = [ + field + for field in ("role", "goal", "backstory") + if getattr(self, field) is None + ] + if missing: + missing_fields = ", ".join(f"'{field}'" for field in missing) + raise ValueError( + f"agent definition requires {missing_fields} unless " + "from_repository is set" + ) + return self + class AgentDefinition(CrewAgentDefinition): - """Inline individual agent definition used outside of a crew.""" + """Individual agent definition used outside of a crew.""" - role: str = Field( + role: str | None = Field( + default=None, description="Individual agent role used by a Flow agent action outside of a crew.", examples=["Support specialist"], ) - goal: str = Field( + goal: str | None = Field( + default=None, description="Individual agent goal for the Flow agent action outside of a crew.", examples=["Draft a concise customer reply"], ) - backstory: str = Field( + backstory: str | None = Field( + default=None, description=( "Individual agent backstory used to shape behavior outside of a crew." ), @@ -202,6 +235,15 @@ class AgentDefinition(CrewAgentDefinition): description="Optional built-in type or Python reference used to load the agent.", examples=["agent", {"python": "my_project.agents.SupportAgent"}], ) + from_repository: str | None = Field( + default=None, + description=( + "Agent repository name to load. Repository values supply missing " + "agent configuration; explicitly provided local fields override the " + "repository values." + ), + examples=["support_specialist"], + ) settings: dict[str, Any] = Field( default_factory=dict, description="Additional agent settings passed to the loader.", diff --git a/lib/crewai/src/crewai/project/json_loader.py b/lib/crewai/src/crewai/project/json_loader.py index 2c2a229fb..c28d42cc3 100644 --- a/lib/crewai/src/crewai/project/json_loader.py +++ b/lib/crewai/src/crewai/project/json_loader.py @@ -978,9 +978,10 @@ def _agent_kwargs_from_definition( extra_allowed, skip_unknown=skip_unknown, ) - for required in ("role", "goal", "backstory"): - if required not in defn: - errors.append(f"{path}: missing required field '{required}'") + if not defn.get("from_repository"): + for required in ("role", "goal", "backstory"): + if defn.get(required) is None: + errors.append(f"{path}: missing required field '{required}'") settings = defn.get("settings", {}) if settings is None: diff --git a/lib/crewai/tests/project/test_json_loader.py b/lib/crewai/tests/project/test_json_loader.py index ff2d5d48c..6e1f68640 100644 --- a/lib/crewai/tests/project/test_json_loader.py +++ b/lib/crewai/tests/project/test_json_loader.py @@ -355,6 +355,18 @@ class TestLoadAgent: with pytest.raises(Exception): load_agent(agent_file) + def test_load_agent_rejects_null_required_fields(self, tmp_path: Path): + agent_def = { + "role": None, + "goal": "Find information", + "backstory": "Expert researcher.", + } + agent_file = tmp_path / "agent.json" + agent_file.write_text(json.dumps(agent_def)) + + with pytest.raises(JSONProjectValidationError, match="missing required field 'role'"): + load_agent(agent_file) + def test_load_agent_file_not_found(self): with pytest.raises(FileNotFoundError): load_agent(Path("/nonexistent/agent.json")) diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index c225dc955..8830214c0 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -88,7 +88,7 @@ def test_flow_definition_json_schema_carries_reference_descriptions(): agent_properties = defs["FlowAgentActionDefinition"]["properties"] assert "Individual Agent definition" in agent_properties["with"]["description"] assert "outside of a crew" in agent_properties["with"]["description"] - assert "individual inline Agent" in agent_properties["call"]["description"] + assert "repository-backed Agent" in agent_properties["call"]["description"] expression_rule = FLOW_TEMPLATE_EXPRESSION_RULES[0] code_properties = defs["FlowCodeActionDefinition"]["properties"] diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index c42d24358..26a0e977c 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -1163,6 +1163,55 @@ methods: } +def test_agent_action_runs_repository_yaml_definition( + monkeypatch: pytest.MonkeyPatch, +): + import crewai.agent.core as agent_core + from crewai import Agent + + def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]: + assert from_repository == "support_specialist" + return { + "role": "Repository specialist", + "goal": "Answer support questions", + "backstory": "Loaded from the agent repository.", + "max_iter": 3, + } + + async def fake_kickoff_async( + self: Agent, messages: str, **_kwargs: Any + ) -> dict[str, Any]: + return {"agent": self.role, "input": messages, "max_iter": self.max_iter} + + monkeypatch.setattr( + agent_core, + "load_agent_from_repository", + fake_load_agent_from_repository, + ) + monkeypatch.setattr(Agent, "kickoff_async", fake_kickoff_async) + + yaml_str = """ +schema: crewai.flow/v1 +name: AgentFlow +methods: + answer: + do: + call: agent + with: + from_repository: support_specialist + input: "${state.question}" + start: true +""" + + flow = Flow.from_declaration(contents=yaml_str) + + assert flow.kickoff(inputs={"question": "What is CrewAI?"}) == { + "agent": "Repository specialist", + "input": "What is CrewAI?", + "max_iter": 3, + } + + def test_agent_action_renders_text_custom_expression_input( monkeypatch: pytest.MonkeyPatch, ): @@ -1281,6 +1330,7 @@ def test_agent_action_json_schema_describes_inline_agent_definitions(): "role", "goal", "backstory", + "from_repository", "settings", "llm", "input", @@ -1437,6 +1487,73 @@ methods: } +def test_crew_action_runs_repository_agent_yaml_definition( + monkeypatch: pytest.MonkeyPatch, +): + import crewai.agent.core as agent_core + from crewai import Crew + + def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]: + assert from_repository == "researcher" + return { + "role": "Repository researcher", + "goal": "Research {topic}", + "backstory": "Loaded from the agent repository.", + "max_iter": 5, + } + + async def fake_kickoff_async( + self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any + ) -> dict[str, Any]: + return { + "crew": self.name, + "agents": [ + {"role": agent.role, "max_iter": agent.max_iter} + for agent in self.agents + ], + "tasks": [task.description for task in self.tasks], + "inputs": inputs, + } + + monkeypatch.setattr( + agent_core, + "load_agent_from_repository", + fake_load_agent_from_repository, + ) + monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async) + + yaml_str = """ +schema: crewai.flow/v1 +name: CrewFlow +methods: + research: + do: + call: crew + with: + name: inline_research + agents: + researcher: + from_repository: researcher + tasks: + - name: research_task + description: Research {topic} + expected_output: Findings about {topic} + agent: researcher + inputs: + topic: "${state.topic}" + start: true +""" + + flow = Flow.from_declaration(contents=yaml_str) + + assert flow.kickoff(inputs={"topic": "AI"}) == { + "crew": "inline_research", + "agents": [{"role": "Repository researcher", "max_iter": 5}], + "tasks": ["Research {topic}"], + "inputs": {"topic": "AI"}, + } + + def test_crew_action_runs_crew_from_declaration( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ): @@ -1709,6 +1826,7 @@ def test_crew_action_json_schema_describes_inline_crew_definitions(): "role", "goal", "backstory", + "from_repository", "settings", "llm", "tools",