From f630c471cf9e2c7fd91154aaa1daef1d19e601dd Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Wed, 1 Jul 2026 14:08:44 -0700 Subject: [PATCH] Document flow agent options (#6420) * Document flow agent options Document and type inline Flow agent options so authored flows can set: * `llm.model`, `llm.max_tokens`, and `llm.max_completion_tokens` * `planning_config.max_attempts` * `allow_delegation` * `max_iter` * `max_rpm` * `max_execution_time` in seconds Also tell the flow skill to omit optional fields unless needed. * Potential fix for pull request finding 'Unused import' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../templates/declarative_flow/AGENTS.md | 50 +++++++++++-- lib/crewai/src/crewai/agent/core.py | 2 +- lib/crewai/src/crewai/flow/skill.py | 17 +++++ .../templates/flow_definition_skill.md.j2 | 2 + .../src/crewai/project/crew_definition.py | 59 +++++++++++++++ lib/crewai/tests/test_flow_definition.py | 11 +++ lib/crewai/tests/test_flow_from_definition.py | 74 ++++++++++++++++++- 7 files changed, 206 insertions(+), 9 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 1d7886bc4..21e33b600 100644 --- a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md @@ -1,8 +1,3 @@ ---- -name: flow-definition -description: Create or edit CrewAI Flow declarations. Use when the user needs a YAML or JSON flow with methods, state, agents, crews, tools, outputs, or conditional branches. ---- - # Flow Definition You are writing a CrewAI Flow declaration for the user. @@ -27,6 +22,8 @@ Do not include explanatory prose unless the user asks for it. 6. Pass data with `${...}` mappings from `state` and completed `outputs`. 7. Before final output, check every `listen`, `emit`, and `outputs.some_method` reference. +Set optional fields only when you are confident they are needed. Otherwise, trust CrewAI defaults and omit them. + Method names must match `^[A-Za-z_][A-Za-z0-9_]*$`. ## Choose One Action Per Method @@ -42,6 +39,7 @@ Pick the simplest action that does the job. - `state` is the initial shared data shape. Action results do not automatically merge into `state`. - Read method results with `outputs.method_name` after that method can run. - `listen` targets a method name or a router-emitted event name. +- Methods must not listen to their own method name. - Method names and emitted event names share one namespace. Avoid reusing the same string for both unless the user explicitly wants that. - Use `router: true` plus `emit` when one method chooses between named branches. - A router action must return exactly one emitted event string. It must not return JSON, a list, or an explanation. @@ -93,6 +91,7 @@ Dynamic value rules: - Do not put more than one action under a method's `do`. - Do not make `do` a list. - Do not reference `outputs.some_method` before `some_method` can run. +- Do not set a method's `listen` to its own method name. - Do not use the same string for an emitted event and a method name unless the user asks for it. - Do not use `emit` without `router: true`. - Do not rely on crew action-level `inputs` alone to ground agent behavior. Inputs that do not match placeholders are effectively unused by the prompt. @@ -163,7 +162,7 @@ methods: role: Follow-up router goal: 'Return exactly one bare value: followup or done. Do not include explanation.' backstory: Skilled at routing reviewed research briefs. - input: "${outputs.research_brief.raw}" + input: "${'Reviewed research: ' + text(outputs, 'research_brief.raw')}" write_followup: listen: followup do: @@ -186,6 +185,7 @@ Fields: - `name` (required): string. Unique flow name used in logs, events, and traces. - `description` (optional): string | null; default `null`. Human-readable summary of the flow. - `state` (required): [State](#json-schema-state-statetypejson_schema). State contract for the initial state and updates during execution. +- `config` (optional): [Config (`config`)](#config-config); default generated default. Serializable flow-level execution configuration. - `methods` (required): map of string to [Method](#method-methods). Mapping of method names to method definitions. ### JSON Schema State (`state[type=json_schema]`) @@ -241,10 +241,22 @@ Fields: - `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.` - `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}` +- `allow_delegation` (optional): boolean | null; default `null`. Enable agent to delegate and ask questions among each other. Example: `false` +- `max_iter` (optional): integer | null; default `null`. Maximum iterations for an agent to execute a task Example: `25` +- `max_rpm` (optional): integer | null; default `null`. Maximum number of requests per minute for the agent execution to be respected. Example: `10` +- `max_execution_time` (optional): integer | null; default `null`. Maximum execution time in seconds for an agent to execute a task Example: `300` - `tools` (optional): list[string | map of string to any] | null; default `null`. Tool refs or serialized tool definitions available to this agent. String refs can use CrewAI tool names, `custom:`, or fully qualified `module:Class` references. Example: `["crewai_tools:SerperDevTool", "custom:file_read"]` - `apps` (optional): list[string] | null; default `null`. Platform apps available to this agent. Can contain app names such as `gmail` or app/action refs such as `gmail/send_email`. Example: `["gmail", "slack/send_message"]` - `mcps` (optional): list[string | map of string to any] | null; default `null`. MCP server refs or serialized MCP server configs available to this agent. String refs can use HTTPS URLs, connected MCP integration slugs, or refs with a `#tool_name` suffix for specific tools. Example: `["https://api.weather.com/mcp#get_current_weather", "snowflake", "stripe#list_invoices", {"cache_tools_list": true, "headers": {"Authorization": "Bearer your_token"}, "streamable": true, "url": "https://api.example.com/mcp"}]` +#### LLM Definition + +Fields: +- `model` (required): string. Model identifier used to instantiate the LLM. Example: `openai/gpt-4o-mini` +- `max_tokens` (optional): integer | null; default `null`. Maximum number of tokens the LLM can generate. If null, CrewAI does not set an explicit output token cap and the provider's default applies. Example: `4096` + #### Crew Task Definition (`methods..do[call=crew].with.tasks[]`) Fields: @@ -269,11 +281,23 @@ Fields: - `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.` - `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}` +- `allow_delegation` (optional): boolean | null; default `null`. Enable agent to delegate and ask questions among each other. Example: `false` +- `max_iter` (optional): integer | null; default `null`. Maximum iterations for an agent to execute a task Example: `25` +- `max_rpm` (optional): integer | null; default `null`. Maximum number of requests per minute for the agent execution to be respected. Example: `10` +- `max_execution_time` (optional): integer | null; default `null`. Maximum execution time in seconds for an agent to execute a task Example: `300` - `tools` (optional): list[string | map of string to any] | null; default `null`. Tool refs or serialized tool definitions available to this agent. String refs can use CrewAI tool names, `custom:`, or fully qualified `module:Class` references. Example: `["crewai_tools:SerperDevTool", "custom:file_read"]` - `apps` (optional): list[string] | null; default `null`. Platform apps available to this agent. Can contain app names such as `gmail` or app/action refs such as `gmail/send_email`. Example: `["gmail", "slack/send_message"]` - `mcps` (optional): list[string | map of string to any] | null; default `null`. MCP server refs or serialized MCP server configs available to this agent. String refs can use HTTPS URLs, connected MCP integration slugs, or refs with a `#tool_name` suffix for specific tools. Example: `["https://api.weather.com/mcp#get_current_weather", "snowflake", "stripe#list_invoices", {"cache_tools_list": true, "headers": {"Authorization": "Bearer your_token"}, "streamable": true, "url": "https://api.example.com/mcp"}]` - `input` (required): string. Input passed to the individual agent kickoff outside of a crew. Use a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, using `text(root, 'path')` for values that may be missing or null, for example `${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}`. In YAML, avoid `\n` escapes inside `${...}` strings. Example: `${state.ticket.body}` +#### LLM Definition + +Fields: +- `model` (required): string. Model identifier used to instantiate the LLM. Example: `openai/gpt-4o-mini` +- `max_tokens` (optional): integer | null; default `null`. Maximum number of tokens the LLM can generate. If null, CrewAI does not set an explicit output token cap and the provider's default applies. Example: `4096` + ### Expression Action (`methods..do[call=expression]`) Shape: @@ -283,11 +307,25 @@ Fields: - `call` (required): must be `expression`. Action discriminator. Use expression to evaluate a CEL expression. - `expr` (required): string. CEL expression evaluated against state, outputs, and local context. +### Config (`config`) + +Fields: +- `tracing` (optional): boolean | null; default `null`. Override for flow tracing; when omitted, execution defaults apply. +- `stream` (optional): boolean; default `false`. Whether the flow should emit streaming events when supported. +- `memory` (optional): map of string to any | null; default `null`. Serializable memory configuration passed to flow execution. +- `input_provider` (optional): string | null; default `null`. Provider key used to supply initial state. +- `suppress_flow_events` (optional): boolean; default `false`. Disable flow event emission for this definition. +- `max_method_calls` (optional): integer; default `100`. Maximum number of method executions allowed during one kickoff. +- `defer_trace_finalization` (optional): boolean; default `false`. Defer trace finalization so callers can complete tracing later. +- `checkpoint` (optional): boolean | map of string to any | null; default `null`. Checkpointing configuration, or true to use default checkpointing. + ### Cross-Field Rules - A method has exactly one `do` action object with one `call` discriminator. - `listen` targets method names and router-emitted event names in one shared namespace. +- Methods cannot listen to their own method name. - A router method result must match one declared `emit` value. - Crew action-level `inputs` are the Crew kickoff inputs; use CEL-wrapped strings there for runtime values. - Crew agent/task interpolation uses `{name}` placeholders from evaluated crew inputs. - Agent `with.input` must be text. Use `${outputs.method_name.raw}` or a text field like `${outputs.method_name.json_dict.summary}`. + diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 19baaabb8..5751f3a9a 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -201,7 +201,7 @@ class Agent(BaseAgent): _last_messages: list[LLMMessage] = PrivateAttr(default_factory=list) max_execution_time: int | None = Field( default=None, - description="Maximum execution time for an agent to execute a task", + description="Maximum execution time in seconds for an agent to execute a task", ) step_callback: SerializableCallable | None = Field( default=None, diff --git a/lib/crewai/src/crewai/flow/skill.py b/lib/crewai/src/crewai/flow/skill.py index 766c93e57..0d5af2921 100644 --- a/lib/crewai/src/crewai/flow/skill.py +++ b/lib/crewai/src/crewai/flow/skill.py @@ -29,6 +29,8 @@ FIELD_TYPE_OVERRIDES: dict[tuple[str, str], str] = { ("FlowDefinition", "methods"): "map of string to [Method](#method-methods)", ("FlowMethodDefinition", "do"): "[Action](#action)", ("FlowCrewActionDefinition", "with"): "inline crew definition", + ("CrewAgentDefinition", "llm"): "string or inline LLM config", + ("AgentDefinition", "llm"): "string or inline LLM config", } _TEMPLATES_DIR = Path(__file__).parent / "templates" @@ -124,6 +126,7 @@ MODEL_TITLES = { "CrewAgentDefinition": "Crew Agent Definition", "CrewTaskDefinition": "Crew Task Definition", "AgentDefinition": "Agent Definition", + "LLMDefinition": "LLM Definition", "FlowConfigDefinition": "Config", "FlowPersistenceDefinition": "Persistence", "FlowHumanFeedbackDefinition": "Human Feedback", @@ -222,6 +225,16 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( "methods..do[call=crew].with.agents.", hidden=True, examples=True, + descriptions={ + "llm": "Language model that runs this crew agent. Use an object when setting LLM options such as `max_tokens`.", + "planning_config": "Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution.", + }, + ), + ModelSpec( + "LLMDefinition", + "LLM Definition", + hidden=True, + examples=True, ), ModelSpec( "CrewTaskDefinition", @@ -241,6 +254,8 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( examples=True, descriptions={ "input": "Input passed to the individual agent kickoff outside of a crew. Use a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, using `text(root, 'path')` for values that may be missing or null, for example `${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}`. In YAML, avoid `\\n` escapes inside `${...}` strings.", + "llm": "Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`.", + "planning_config": "Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution.", }, ), ModelSpec("FlowConfigDefinition", "Config", "config"), @@ -342,7 +357,9 @@ class FlowSkillReferenceExtractor: "CrewAgentDefinition", "CrewTaskDefinition", ), + "CrewAgentDefinition": ("LLMDefinition",), "FlowAgentActionDefinition": ("AgentDefinition",), + "AgentDefinition": ("LLMDefinition",), } return [ self.extract_model(_SPECS_BY_NAME[name]) diff --git a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 index bd049dd6a..bd205b1d7 100644 --- a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 +++ b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 @@ -31,6 +31,8 @@ Do not include explanatory prose unless the user asks for it. 6. Pass data with `${...}` mappings from `state` and completed `outputs`. 7. Before final output, check every `listen`, `emit`, and `outputs.some_method` reference. +Set optional fields only when you are confident they are needed. Otherwise, trust CrewAI defaults and omit them. + Method names must match `^[A-Za-z_][A-Za-z0-9_]*$`. ## Choose One Action Per Method diff --git a/lib/crewai/src/crewai/project/crew_definition.py b/lib/crewai/src/crewai/project/crew_definition.py index f69787766..ebfb55e05 100644 --- a/lib/crewai/src/crewai/project/crew_definition.py +++ b/lib/crewai/src/crewai/project/crew_definition.py @@ -6,12 +6,15 @@ from typing import Any, TypeAlias from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from crewai.agent.planning_config import PlanningConfig + __all__ = [ "AgentDefinition", "CrewAgentDefinition", "CrewDefinition", "CrewTaskDefinition", + "LLMDefinition", "PythonReferenceDefinition", ] @@ -38,6 +41,26 @@ class PythonReferenceDefinition(BaseModel): return path +class LLMDefinition(BaseModel): + """LLM configuration used by inline agent definitions.""" + + model_config = ConfigDict(extra="allow") + + model: str = Field( + description="Model identifier used to instantiate the LLM.", + examples=["openai/gpt-4o-mini"], + ) + max_tokens: int | None = Field( + default=None, + description=( + "Maximum number of tokens the LLM can generate. If null, CrewAI " + "does not set an explicit output token cap and the provider's " + "default applies." + ), + examples=[4096], + ) + + class CrewAgentDefinition(BaseModel): """Inline agent definition used by a crew definition.""" @@ -74,6 +97,42 @@ class CrewAgentDefinition(BaseModel): description="Additional agent settings passed to the loader.", examples=[{"llm": "openai/gpt-4o-mini"}], ) + llm: str | LLMDefinition | None = Field( + default=None, + description=( + "Language model that runs the agent. Use a string model name or an " + "object with model settings such as max_tokens." + ), + examples=[{"model": "openai/gpt-4o-mini", "max_tokens": 4096}], + ) + planning_config: PlanningConfig | None = Field( + default=None, + description="Configuration for agent planning before task execution.", + examples=[{"max_attempts": 3}], + ) + allow_delegation: bool | None = Field( + default=None, + description="Enable agent to delegate and ask questions among each other.", + examples=[False], + ) + max_iter: int | None = Field( + default=None, + description="Maximum iterations for an agent to execute a task", + examples=[25], + ) + max_rpm: int | None = Field( + default=None, + description=( + "Maximum number of requests per minute for the agent execution to be " + "respected." + ), + examples=[10], + ) + max_execution_time: int | None = Field( + default=None, + description="Maximum execution time in seconds for an agent to execute a task", + examples=[300], + ) tools: list[str | dict[str, Any]] | None = Field( default=None, description=( diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index 81c0fc174..5feb7025f 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -1335,6 +1335,17 @@ def test_skill_documents_flow_wiring(): assert "[Method](#method-methods)" in skill assert "input: \"${'Reviewed research: ' + text(outputs, 'research_brief.raw')}\"" in skill assert 'text(root, "path", "default")' in skill + assert "trust CrewAI defaults and omit them" in skill + assert "#### LLM Definition" in skill + assert "`max_tokens` (optional): integer | null; default `null`" in skill + assert "CrewAI does not set an explicit output token cap" in skill + assert "`planning_config` (optional): object | null; default `null`" in skill + assert "Set `max_attempts` to limit planning refinement attempts" in skill + assert "`allow_delegation` (optional): boolean | null; default `null`" in skill + assert "`max_iter` (optional): integer | null; default `null`" in skill + assert "`max_rpm` (optional): integer | null; default `null`" in skill + assert "`max_execution_time` (optional): integer | null; default `null`" in skill + assert "Maximum execution time in seconds for an agent" in skill def test_skill_can_render_json_examples(): diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index 7c2ba90df..c913e8b8a 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -11,6 +11,7 @@ from unittest.mock import patch import pytest from pydantic import BaseModel, ValidationError +from crewai.agent.planning_config import PlanningConfig from crewai.events.event_bus import crewai_event_bus from crewai.events.types.flow_events import ( FlowCreatedEvent, @@ -27,11 +28,62 @@ from crewai.flow.flow_definition import FlowConfigDefinition, FlowDefinition from crewai.flow.persistence import persist from crewai.flow.persistence.base import FlowPersistence from crewai.flow.runtime._actions import FlowScriptExecutionDisabledError +from crewai.project.crew_definition import AgentDefinition from crewai.state.checkpoint_config import CheckpointConfig from crewai.tools import BaseTool from crewai.types.streaming import StreamSession +AGENT_RUNTIME_CONTROL_FIELDS = ( + "planning_config", + "allow_delegation", + "max_iter", + "max_rpm", + "max_execution_time", +) + + +def assert_agent_runtime_field_schema(properties: dict[str, Any]) -> None: + for field_name in AGENT_RUNTIME_CONTROL_FIELDS: + assert "default" in properties[field_name] + assert properties[field_name]["default"] is None + assert properties[field_name]["description"] + + +def assert_planning_config_schema(schema_defs: dict[str, Any]) -> None: + properties = schema_defs["PlanningConfig"]["properties"] + max_attempts = properties["max_attempts"] + planning_config_field = PlanningConfig.model_fields["max_attempts"] + + assert max_attempts["default"] == planning_config_field.default + assert max_attempts["description"] == planning_config_field.description + + +def assert_llm_definition_schema(schema_defs: dict[str, Any]) -> None: + properties = schema_defs["LLMDefinition"]["properties"] + + assert set(properties) >= { + "model", + "max_tokens", + } + assert properties["model"]["type"] == "string" + assert properties["max_tokens"]["default"] is None + + +def test_inline_agent_definition_omits_unspecified_runtime_controls(): + definition = AgentDefinition( + role="Analyst", + goal="Answer questions", + backstory="Knows things.", + input="${state.question}", + ) + + dumped = definition.model_dump(mode="python", exclude_none=True) + + for field_name in AGENT_RUNTIME_CONTROL_FIELDS: + assert field_name not in dumped + + class StaticSearchTool(BaseTool): name: str = "StaticSearchTool" description: str = "Returns a deterministic search result." @@ -996,6 +1048,10 @@ def test_agent_action_round_trips_with_inline_definition(): "role": "Analyst", "goal": "Answer questions", "backstory": "Knows things.", + "llm": { + "model": "openai/gpt-4o-mini", + "max_tokens": 4096, + }, "settings": {"verbose": True}, "input": "${state.question}", }, @@ -1010,20 +1066,28 @@ def test_agent_action_round_trips_with_inline_definition(): assert action.call == "agent" assert action.with_.role == "Analyst" assert action.with_.input == "${state.question}" + assert action.with_.llm is not None + assert action.with_.llm.max_tokens == 4096 assert action.with_.settings == {"verbose": True} def test_agent_action_json_schema_describes_inline_agent_definitions(): schema_defs = FlowDefinition.model_json_schema(by_alias=True)["$defs"] + properties = schema_defs["AgentDefinition"]["properties"] - assert set(schema_defs["AgentDefinition"]["properties"]) >= { + assert set(properties) >= { "role", "goal", "backstory", "settings", + "llm", "input", "response_format", + *AGENT_RUNTIME_CONTROL_FIELDS, } + assert_agent_runtime_field_schema(properties) + assert_planning_config_schema(schema_defs) + assert_llm_definition_schema(schema_defs) def test_agent_action_rejects_non_string_input_in_definition(): @@ -1379,6 +1443,7 @@ def test_crew_action_normalizes_named_agent_list_definition(): def test_crew_action_json_schema_describes_inline_crew_definitions(): schema_defs = FlowDefinition.model_json_schema(by_alias=True)["$defs"] agents_schema = schema_defs["CrewDefinition"]["properties"]["agents"] + agent_properties = schema_defs["CrewAgentDefinition"]["properties"] assert set(schema_defs["CrewDefinition"]["properties"]) >= { "agents", @@ -1386,15 +1451,20 @@ def test_crew_action_json_schema_describes_inline_crew_definitions(): "inputs", } assert {option["type"] for option in agents_schema["anyOf"]} == {"array", "object"} - assert set(schema_defs["CrewAgentDefinition"]["properties"]) >= { + assert set(agent_properties) >= { "role", "goal", "backstory", "settings", + "llm", "tools", "apps", "mcps", + *AGENT_RUNTIME_CONTROL_FIELDS, } + assert_agent_runtime_field_schema(agent_properties) + assert_planning_config_schema(schema_defs) + assert_llm_definition_schema(schema_defs) assert set(schema_defs["CrewTaskDefinition"]["properties"]) >= { "description", "expected_output",