Skip to content

Commit ffc0bb3

Browse files
committed
Require top-level keys in ConfigLoaders for schema validation
- AgentConfigLoader now requires 'agent:' top-level key - GraphConfigLoader now requires 'graph:' top-level key - SwarmConfigLoader now requires 'swarm:' top-level key - Updated serialize methods to include top-level keys - Enables proper YAML/schema validation and identification - Breaking change: all configurations must be wrapped in appropriate top-level key BREAKING CHANGE: Configuration structure changed - Agent configs: wrap in 'agent:' key - Graph configs: wrap in 'graph:' key - Swarm configs: wrap in 'swarm:' key
1 parent b669aff commit ffc0bb3

File tree

4 files changed

+89
-65
lines changed

4 files changed

+89
-65
lines changed

src/strands/experimental/config_loader/agent/agent_config_loader.py

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def load_agent(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
7676
"""Load an Agent from a dictionary configuration.
7777
7878
Args:
79-
config: Dictionary containing agent configuration.
79+
config: Dictionary containing agent configuration with top-level 'agent' key.
8080
cache_key: Optional key for caching the loaded agent.
8181
8282
Returns:
@@ -86,6 +86,14 @@ def load_agent(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
8686
ValueError: If required configuration is missing or invalid.
8787
ImportError: If specified models or tools cannot be imported.
8888
"""
89+
# Validate top-level structure
90+
if "agent" not in config:
91+
raise ValueError("Configuration must contain a top-level 'agent' key")
92+
93+
agent_config = config["agent"]
94+
if not isinstance(agent_config, dict):
95+
raise ValueError("The 'agent' configuration must be a dictionary")
96+
8997
# Check cache first
9098
if cache_key and cache_key in self._agent_cache:
9199
logger.debug("agent_cache_key=<%s> | found in cache", cache_key)
@@ -100,30 +108,30 @@ def load_agent(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
100108
if "structured_output_defaults" in config:
101109
self._structured_output_defaults = config["structured_output_defaults"]
102110

103-
# Extract configuration values
104-
model_config = config.get("model")
105-
system_prompt = config.get("system_prompt")
106-
tools_config = config.get("tools", [])
107-
messages_config = config.get("messages", [])
111+
# Extract configuration values from agent_config
112+
model_config = agent_config.get("model")
113+
system_prompt = agent_config.get("system_prompt")
114+
tools_config = agent_config.get("tools", [])
115+
messages_config = agent_config.get("messages", [])
108116

109117
# Note: 'prompt' field is handled by AgentAsToolWrapper, not by Agent itself
110118
# The Agent class doesn't have a prompt parameter - it uses system_prompt
111119
# The prompt field is used for tool invocation templates
112120

113121
# Agent metadata
114-
agent_id = config.get("agent_id")
115-
name = config.get("name")
116-
description = config.get("description")
122+
agent_id = agent_config.get("agent_id")
123+
name = agent_config.get("name")
124+
description = agent_config.get("description")
117125

118126
# Advanced configuration
119-
callback_handler_config = config.get("callback_handler")
120-
conversation_manager_config = config.get("conversation_manager")
121-
record_direct_tool_call = config.get("record_direct_tool_call", True)
122-
load_tools_from_directory = config.get("load_tools_from_directory", False)
123-
trace_attributes = config.get("trace_attributes")
124-
state_config = config.get("state")
125-
hooks_config = config.get("hooks", [])
126-
session_manager_config = config.get("session_manager")
127+
callback_handler_config = agent_config.get("callback_handler")
128+
conversation_manager_config = agent_config.get("conversation_manager")
129+
record_direct_tool_call = agent_config.get("record_direct_tool_call", True)
130+
load_tools_from_directory = agent_config.get("load_tools_from_directory", False)
131+
trace_attributes = agent_config.get("trace_attributes")
132+
state_config = agent_config.get("state")
133+
hooks_config = agent_config.get("hooks", [])
134+
session_manager_config = agent_config.get("session_manager")
127135

128136
# Load model
129137
model = self._load_model(model_config)
@@ -169,8 +177,8 @@ def load_agent(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
169177
)
170178

171179
# Configure structured output if specified
172-
if "structured_output" in config:
173-
self._configure_agent_structured_output(agent, config["structured_output"])
180+
if "structured_output" in agent_config:
181+
self._configure_agent_structured_output(agent, agent_config["structured_output"])
174182

175183
# Cache the agent if cache key provided
176184
if cache_key:
@@ -186,56 +194,56 @@ def serialize_agent(self, agent: Agent) -> Dict[str, Any]:
186194
agent: Agent instance to serialize.
187195
188196
Returns:
189-
Dictionary containing the agent's configuration.
197+
Dictionary containing the agent's configuration with top-level 'agent' key.
190198
191199
Note:
192200
The 'prompt' field is not serialized here as it's specific to AgentAsToolWrapper
193201
and not part of the core Agent configuration.
194202
"""
195-
config = {}
203+
agent_config = {}
196204

197205
# Basic configuration
198206
if hasattr(agent.model, "model_id"):
199-
config["model"] = agent.model.model_id
207+
agent_config["model"] = agent.model.model_id
200208
elif hasattr(agent.model, "config") and agent.model.config.get("model_id"):
201-
config["model"] = agent.model.config["model_id"]
209+
agent_config["model"] = agent.model.config["model_id"]
202210

203211
if agent.system_prompt:
204-
config["system_prompt"] = agent.system_prompt
212+
agent_config["system_prompt"] = agent.system_prompt
205213

206214
# Tools configuration
207215
if hasattr(agent, "tool_registry") and agent.tool_registry:
208216
tools_config = []
209217
for tool_name in agent.tool_names:
210218
tools_config.append({"name": tool_name})
211219
if tools_config:
212-
config["tools"] = tools_config
220+
agent_config["tools"] = tools_config
213221

214222
# Messages
215223
if agent.messages:
216-
config["messages"] = agent.messages
224+
agent_config["messages"] = agent.messages
217225

218226
# Agent metadata
219227
if agent.agent_id != "default":
220-
config["agent_id"] = agent.agent_id
228+
agent_config["agent_id"] = agent.agent_id
221229
if agent.name != "Strands Agents":
222-
config["name"] = agent.name
230+
agent_config["name"] = agent.name
223231
if agent.description:
224-
config["description"] = agent.description
232+
agent_config["description"] = agent.description
225233

226234
# Advanced configuration
227235
if agent.record_direct_tool_call is not True:
228-
config["record_direct_tool_call"] = agent.record_direct_tool_call
236+
agent_config["record_direct_tool_call"] = agent.record_direct_tool_call
229237
if agent.load_tools_from_directory is not False:
230-
config["load_tools_from_directory"] = agent.load_tools_from_directory
238+
agent_config["load_tools_from_directory"] = agent.load_tools_from_directory
231239
if agent.trace_attributes:
232-
config["trace_attributes"] = agent.trace_attributes
240+
agent_config["trace_attributes"] = agent.trace_attributes
233241

234242
# State
235243
if agent.state and agent.state.get():
236-
config["state"] = agent.state.get()
244+
agent_config["state"] = agent.state.get()
237245

238-
return config
246+
return {"agent": agent_config}
239247

240248
def _load_model(self, model_config: Optional[Union[str, Dict[str, Any]]]) -> Optional[Model]:
241249
"""Load a model from configuration.

src/strands/experimental/config_loader/graph/graph_config_loader.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def load_graph(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
8888
"""Load a Graph from YAML configuration (loaded as dictionary).
8989
9090
Args:
91-
config: Dictionary containing graph configuration.
91+
config: Dictionary containing graph configuration with top-level 'graph' key.
9292
cache_key: Optional key for caching the loaded graph.
9393
9494
Returns:
@@ -98,28 +98,36 @@ def load_graph(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
9898
ValueError: If required configuration is missing or invalid.
9999
ImportError: If specified models or tools cannot be imported.
100100
"""
101+
# Validate top-level structure
102+
if "graph" not in config:
103+
raise ValueError("Configuration must contain a top-level 'graph' key")
104+
105+
graph_config = config["graph"]
106+
if not isinstance(graph_config, dict):
107+
raise ValueError("The 'graph' configuration must be a dictionary")
108+
101109
# Check cache first
102110
if cache_key and cache_key in self._graph_cache:
103111
logger.debug("graph_cache_key=<%s> | found in cache", cache_key)
104112
return self._graph_cache[cache_key]
105113

106114
# Validate configuration structure
107-
self._validate_config(config)
115+
self._validate_config(graph_config)
108116

109117
# Load nodes
110-
nodes = self._load_nodes(config.get("nodes", []))
118+
nodes = self._load_nodes(graph_config.get("nodes", []))
111119

112120
# Load edges with conditions
113-
edges = self._load_edges(config.get("edges", []), nodes)
121+
edges = self._load_edges(graph_config.get("edges", []), nodes)
114122

115123
# Load entry points
116-
entry_points = self._load_entry_points(config.get("entry_points", []), nodes)
124+
entry_points = self._load_entry_points(graph_config.get("entry_points", []), nodes)
117125

118126
# Extract graph configuration
119-
graph_config = self._extract_graph_parameters(config)
127+
graph_params = self._extract_graph_parameters(graph_config)
120128

121129
# Create graph
122-
graph = Graph(nodes=nodes, edges=edges, entry_points=entry_points, **graph_config)
130+
graph = Graph(nodes=nodes, edges=edges, entry_points=entry_points, **graph_params)
123131

124132
# Cache the graph if cache key provided
125133
if cache_key:
@@ -135,23 +143,23 @@ def serialize_graph(self, graph: Graph) -> Dict[str, Any]:
135143
graph: Graph instance to serialize.
136144
137145
Returns:
138-
Dictionary containing the graph's configuration that can be saved as YAML.
146+
Dictionary containing the graph's configuration with top-level 'graph' key.
139147
"""
140-
config: Dict[str, Any] = {}
148+
graph_config: Dict[str, Any] = {}
141149

142150
# Serialize nodes
143151
nodes_config = []
144152
for node_id, node in graph.nodes.items():
145153
node_config = self._serialize_node(node_id, node)
146154
nodes_config.append(node_config)
147-
config["nodes"] = nodes_config
155+
graph_config["nodes"] = nodes_config
148156

149157
# Serialize edges
150158
edges_config = []
151159
for edge in graph.edges:
152160
edge_config = self._serialize_edge(edge)
153161
edges_config.append(edge_config)
154-
config["edges"] = edges_config
162+
graph_config["edges"] = edges_config
155163

156164
# Serialize entry points
157165
entry_points_config: List[str] = []
@@ -161,19 +169,19 @@ def serialize_graph(self, graph: Graph) -> Dict[str, Any]:
161169
if node == entry_point:
162170
entry_points_config.append(node_id)
163171
break
164-
config["entry_points"] = entry_points_config
172+
graph_config["entry_points"] = entry_points_config
165173

166174
# Serialize graph parameters (only include non-default values)
167175
if graph.max_node_executions is not None:
168-
config["max_node_executions"] = graph.max_node_executions
176+
graph_config["max_node_executions"] = graph.max_node_executions
169177
if graph.execution_timeout is not None:
170-
config["execution_timeout"] = graph.execution_timeout
178+
graph_config["execution_timeout"] = graph.execution_timeout
171179
if graph.node_timeout is not None:
172-
config["node_timeout"] = graph.node_timeout
180+
graph_config["node_timeout"] = graph.node_timeout
173181
if graph.reset_on_revisit is not False:
174-
config["reset_on_revisit"] = graph.reset_on_revisit
182+
graph_config["reset_on_revisit"] = graph.reset_on_revisit
175183

176-
return config
184+
return {"graph": graph_config}
177185

178186
def clear_cache(self) -> None:
179187
"""Clear the internal graph cache."""

src/strands/experimental/config_loader/schema/SCHEMA-PLAN.md

Whitespace-only changes.

src/strands/experimental/config_loader/swarm/swarm_config_loader.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def load_swarm(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
6161
"""Load a Swarm from YAML configuration (loaded as dictionary).
6262
6363
Args:
64-
config: Dictionary containing swarm configuration.
64+
config: Dictionary containing swarm configuration with top-level 'swarm' key.
6565
cache_key: Optional key for caching the loaded swarm.
6666
6767
Returns:
@@ -71,24 +71,32 @@ def load_swarm(self, config: Dict[str, Any], cache_key: Optional[str] = None) ->
7171
ValueError: If required configuration is missing or invalid.
7272
ImportError: If specified models or tools cannot be imported.
7373
"""
74+
# Validate top-level structure
75+
if "swarm" not in config:
76+
raise ValueError("Configuration must contain a top-level 'swarm' key")
77+
78+
swarm_config = config["swarm"]
79+
if not isinstance(swarm_config, dict):
80+
raise ValueError("The 'swarm' configuration must be a dictionary")
81+
7482
# Check cache first
7583
if cache_key and cache_key in self._swarm_cache:
7684
logger.debug("swarm_cache_key=<%s> | found in cache", cache_key)
7785
return self._swarm_cache[cache_key]
7886

7987
# Validate configuration structure
80-
self._validate_config(config)
88+
self._validate_config(swarm_config)
8189

8290
# Extract agents configuration
83-
agents_config = config.get("agents", [])
91+
agents_config = swarm_config.get("agents", [])
8492
if not agents_config:
8593
raise ValueError("Swarm configuration must include 'agents' field with at least one agent")
8694

8795
# Load agents using AgentConfigLoader
8896
agents = self.load_agents(agents_config)
8997

9098
# Extract swarm parameters
91-
swarm_params = self._extract_swarm_parameters(config)
99+
swarm_params = self._extract_swarm_parameters(swarm_config)
92100

93101
# Create swarm
94102
swarm = Swarm(nodes=agents, **swarm_params)
@@ -107,23 +115,23 @@ def serialize_swarm(self, swarm: Swarm) -> Dict[str, Any]:
107115
swarm: Swarm instance to serialize.
108116
109117
Returns:
110-
Dictionary containing the swarm's configuration that can be saved as YAML.
118+
Dictionary containing the swarm's configuration with top-level 'swarm' key.
111119
"""
112-
config = {}
120+
swarm_config = {}
113121

114122
# Serialize swarm parameters (only include non-default values)
115123
if swarm.max_handoffs != 20:
116-
config["max_handoffs"] = swarm.max_handoffs
124+
swarm_config["max_handoffs"] = swarm.max_handoffs
117125
if swarm.max_iterations != 20:
118-
config["max_iterations"] = swarm.max_iterations
126+
swarm_config["max_iterations"] = swarm.max_iterations
119127
if swarm.execution_timeout != 900.0:
120-
config["execution_timeout"] = swarm.execution_timeout
128+
swarm_config["execution_timeout"] = swarm.execution_timeout
121129
if swarm.node_timeout != 300.0:
122-
config["node_timeout"] = swarm.node_timeout
130+
swarm_config["node_timeout"] = swarm.node_timeout
123131
if swarm.repetitive_handoff_detection_window != 0:
124-
config["repetitive_handoff_detection_window"] = swarm.repetitive_handoff_detection_window
132+
swarm_config["repetitive_handoff_detection_window"] = swarm.repetitive_handoff_detection_window
125133
if swarm.repetitive_handoff_min_unique_agents != 0:
126-
config["repetitive_handoff_min_unique_agents"] = swarm.repetitive_handoff_min_unique_agents
134+
swarm_config["repetitive_handoff_min_unique_agents"] = swarm.repetitive_handoff_min_unique_agents
127135

128136
# Serialize agents
129137
agents_config = []
@@ -139,9 +147,9 @@ def serialize_swarm(self, swarm: Swarm) -> Dict[str, Any]:
139147
agent_config = agent_loader.serialize_agent(temp_agent)
140148
agents_config.append(agent_config)
141149

142-
config["agents"] = agents_config
150+
swarm_config["agents"] = agents_config
143151

144-
return config
152+
return {"swarm": swarm_config}
145153

146154
def _create_clean_agent_copy(self, agent: Agent) -> Agent:
147155
"""Create a copy of an agent without swarm coordination tools.

0 commit comments

Comments
 (0)