1use std::collections::HashMap;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AgentConfig {
32 pub name: String,
34
35 #[serde(default)]
37 pub model: Option<String>,
38
39 #[serde(default)]
41 pub instruction: Option<String>,
42
43 #[serde(default)]
45 pub description: Option<String>,
46
47 #[serde(default)]
49 pub tools: Vec<ToolConfig>,
50
51 #[serde(default)]
53 pub sub_agents: Vec<AgentConfig>,
54
55 #[serde(default)]
57 pub temperature: Option<f32>,
58
59 #[serde(default)]
61 pub max_output_tokens: Option<u32>,
62
63 #[serde(default)]
65 pub thinking_budget: Option<u32>,
66
67 #[serde(default)]
69 pub output_key: Option<String>,
70
71 #[serde(default)]
73 pub output_schema: Option<serde_json::Value>,
74
75 #[serde(default)]
77 pub max_llm_calls: Option<u32>,
78
79 #[serde(default = "default_agent_type")]
81 pub agent_type: String,
82
83 #[serde(default)]
85 pub max_iterations: Option<u32>,
86
87 #[serde(default)]
89 pub metadata: HashMap<String, serde_json::Value>,
90
91 #[serde(default)]
93 pub voice: Option<String>,
94
95 #[serde(default)]
97 pub greeting: Option<String>,
98
99 #[serde(default)]
101 pub transcription: Option<bool>,
102
103 #[serde(default)]
105 pub a2a: Option<bool>,
106
107 #[serde(default)]
109 pub env: HashMap<String, String>,
110}
111
112fn default_agent_type() -> String {
113 "llm".to_string()
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ToolConfig {
119 #[serde(default)]
121 pub name: Option<String>,
122
123 #[serde(default)]
125 pub description: Option<String>,
126
127 #[serde(default)]
129 pub builtin: Option<String>,
130
131 #[serde(default)]
133 pub parameters: Option<serde_json::Value>,
134}
135
136#[derive(Debug, thiserror::Error)]
138pub enum AgentConfigError {
139 #[error("IO error: {0}")]
141 Io(#[from] std::io::Error),
142
143 #[error("YAML parse error: {0}")]
145 Yaml(String),
146
147 #[error("TOML parse error: {0}")]
149 Toml(String),
150
151 #[error("JSON parse error: {0}")]
153 Json(#[from] serde_json::Error),
154
155 #[error("Invalid config: {0}")]
157 Invalid(String),
158}
159
160impl AgentConfig {
161 pub fn from_yaml_file(path: &Path) -> Result<Self, AgentConfigError> {
163 let content = std::fs::read_to_string(path)?;
164 Self::from_yaml(&content)
165 }
166
167 pub fn from_yaml(yaml: &str) -> Result<Self, AgentConfigError> {
169 serde_json::from_value(
170 serde_json::to_value(
171 serde_json::from_str::<serde_json::Value>(yaml)
174 .map_err(|e| AgentConfigError::Yaml(e.to_string()))?,
175 )
176 .map_err(|e| AgentConfigError::Yaml(e.to_string()))?,
177 )
178 .map_err(|e| AgentConfigError::Yaml(e.to_string()))
179 }
180
181 pub fn from_json(json: &str) -> Result<Self, AgentConfigError> {
183 Ok(serde_json::from_str(json)?)
184 }
185
186 pub fn from_value(value: serde_json::Value) -> Result<Self, AgentConfigError> {
188 Ok(serde_json::from_value(value)?)
189 }
190
191 pub fn validate(&self) -> Result<(), AgentConfigError> {
193 if self.name.is_empty() {
194 return Err(AgentConfigError::Invalid("Agent name is required".into()));
195 }
196 if let Some(temp) = self.temperature {
197 if !(0.0..=2.0).contains(&temp) {
198 return Err(AgentConfigError::Invalid(format!(
199 "Temperature must be 0.0-2.0, got {}",
200 temp
201 )));
202 }
203 }
204 for sub in &self.sub_agents {
206 sub.validate()?;
207 }
208 Ok(())
209 }
210
211 pub fn builtin_tools(&self) -> Vec<&str> {
213 self.tools
214 .iter()
215 .filter_map(|t| t.builtin.as_deref())
216 .collect()
217 }
218
219 pub fn is_workflow(&self) -> bool {
221 matches!(self.agent_type.as_str(), "sequential" | "parallel" | "loop")
222 }
223}
224
225pub fn discover_agent_configs(dir: &Path) -> Result<Vec<AgentConfig>, AgentConfigError> {
230 let candidates = ["agent.json", "root_agent.json", "agent.toml"];
231
232 let mut configs = Vec::new();
233 for candidate in &candidates {
234 let path = dir.join(candidate);
235 if path.exists() {
236 let content = std::fs::read_to_string(&path)?;
237 let config: AgentConfig = if candidate.ends_with(".json") {
238 serde_json::from_str(&content)?
239 } else if candidate.ends_with(".toml") {
240 return Err(AgentConfigError::Toml(
242 "TOML parsing requires the adk-cli crate".into(),
243 ));
244 } else {
245 return Err(AgentConfigError::Yaml(
246 "YAML parsing requires the adk-cli crate".into(),
247 ));
248 };
249 config.validate()?;
250 configs.push(config);
251 }
252 }
253
254 if let Ok(entries) = std::fs::read_dir(dir) {
256 for entry in entries.flatten() {
257 let path = entry.path();
258 if path.is_dir() {
259 if let Ok(sub_configs) = discover_agent_configs(&path) {
260 configs.extend(sub_configs);
261 }
262 }
263 }
264 }
265
266 Ok(configs)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn parse_minimal_json_config() {
275 let json = r#"{"name": "test_agent"}"#;
276 let config = AgentConfig::from_json(json).unwrap();
277 assert_eq!(config.name, "test_agent");
278 assert_eq!(config.agent_type, "llm");
279 assert!(config.model.is_none());
280 assert!(config.tools.is_empty());
281 }
282
283 #[test]
284 fn parse_full_json_config() {
285 let json = r#"{
286 "name": "weather_agent",
287 "model": "gemini-2.0-flash",
288 "instruction": "You are a weather assistant.",
289 "description": "Gets weather info",
290 "temperature": 0.3,
291 "max_output_tokens": 1024,
292 "output_key": "weather_result",
293 "max_llm_calls": 10,
294 "tools": [
295 {"name": "get_weather", "description": "Get weather for a city"},
296 {"builtin": "google_search"}
297 ],
298 "sub_agents": [
299 {"name": "forecast", "instruction": "Give forecasts"}
300 ]
301 }"#;
302 let config = AgentConfig::from_json(json).unwrap();
303 assert_eq!(config.name, "weather_agent");
304 assert_eq!(config.model.as_deref(), Some("gemini-2.0-flash"));
305 assert_eq!(config.temperature, Some(0.3));
306 assert_eq!(config.output_key.as_deref(), Some("weather_result"));
307 assert_eq!(config.max_llm_calls, Some(10));
308 assert_eq!(config.tools.len(), 2);
309 assert_eq!(config.sub_agents.len(), 1);
310 assert_eq!(config.builtin_tools(), vec!["google_search"]);
311 }
312
313 #[test]
314 fn validate_empty_name_fails() {
315 let config = AgentConfig::from_json(r#"{"name": ""}"#).unwrap();
316 assert!(config.validate().is_err());
317 }
318
319 #[test]
320 fn validate_bad_temperature_fails() {
321 let config = AgentConfig::from_json(r#"{"name": "test", "temperature": 3.0}"#).unwrap();
322 assert!(config.validate().is_err());
323 }
324
325 #[test]
326 fn validate_good_config_passes() {
327 let config = AgentConfig::from_json(r#"{"name": "test", "temperature": 0.7}"#).unwrap();
328 assert!(config.validate().is_ok());
329 }
330
331 #[test]
332 fn is_workflow_detection() {
333 let sequential =
334 AgentConfig::from_json(r#"{"name": "seq", "agent_type": "sequential"}"#).unwrap();
335 assert!(sequential.is_workflow());
336
337 let llm = AgentConfig::from_json(r#"{"name": "llm"}"#).unwrap();
338 assert!(!llm.is_workflow());
339 }
340
341 #[test]
342 fn tool_config_variants() {
343 let custom = ToolConfig {
344 name: Some("my_tool".into()),
345 description: Some("Does stuff".into()),
346 builtin: None,
347 parameters: Some(serde_json::json!({"type": "object"})),
348 };
349 assert!(custom.name.is_some());
350 assert!(custom.builtin.is_none());
351
352 let builtin = ToolConfig {
353 name: None,
354 description: None,
355 builtin: Some("google_search".into()),
356 parameters: None,
357 };
358 assert!(builtin.builtin.is_some());
359 }
360}