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
```
This commit is contained in:
Vinicius Brasil
2026-07-01 16:47:11 -07:00
parent 24901cd4f6
commit 570b100b33
7 changed files with 198 additions and 22 deletions

View File

@@ -258,9 +258,10 @@ Fields:
#### Crew Agent Definition (`methods.<name>.do[call=crew].with.agents.<name>`)
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.<name>.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}`

View File

@@ -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=[
{

View File

@@ -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.",

View File

@@ -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:

View File

@@ -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"))

View File

@@ -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"]

View File

@@ -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",