use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub name: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub instruction: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tools: Vec<ToolConfig>,
#[serde(default)]
pub sub_agents: Vec<AgentConfig>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub thinking_budget: Option<u32>,
#[serde(default)]
pub output_key: Option<String>,
#[serde(default)]
pub output_schema: Option<serde_json::Value>,
#[serde(default)]
pub max_llm_calls: Option<u32>,
#[serde(default = "default_agent_type")]
pub agent_type: String,
#[serde(default)]
pub max_iterations: Option<u32>,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default)]
pub voice: Option<String>,
#[serde(default)]
pub greeting: Option<String>,
#[serde(default)]
pub transcription: Option<bool>,
#[serde(default)]
pub a2a: Option<bool>,
#[serde(default)]
pub env: HashMap<String, String>,
}
fn default_agent_type() -> String {
"llm".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolConfig {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub builtin: Option<String>,
#[serde(default)]
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, thiserror::Error)]
pub enum AgentConfigError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML parse error: {0}")]
Yaml(String),
#[error("TOML parse error: {0}")]
Toml(String),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid config: {0}")]
Invalid(String),
}
impl AgentConfig {
pub fn from_yaml_file(path: &Path) -> Result<Self, AgentConfigError> {
let content = std::fs::read_to_string(path)?;
Self::from_yaml(&content)
}
pub fn from_yaml(yaml: &str) -> Result<Self, AgentConfigError> {
serde_json::from_value(
serde_json::to_value(
serde_json::from_str::<serde_json::Value>(yaml)
.map_err(|e| AgentConfigError::Yaml(e.to_string()))?,
)
.map_err(|e| AgentConfigError::Yaml(e.to_string()))?,
)
.map_err(|e| AgentConfigError::Yaml(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, AgentConfigError> {
Ok(serde_json::from_str(json)?)
}
pub fn from_value(value: serde_json::Value) -> Result<Self, AgentConfigError> {
Ok(serde_json::from_value(value)?)
}
pub fn validate(&self) -> Result<(), AgentConfigError> {
if self.name.is_empty() {
return Err(AgentConfigError::Invalid("Agent name is required".into()));
}
if let Some(temp) = self.temperature {
if !(0.0..=2.0).contains(&temp) {
return Err(AgentConfigError::Invalid(format!(
"Temperature must be 0.0-2.0, got {}",
temp
)));
}
}
for sub in &self.sub_agents {
sub.validate()?;
}
Ok(())
}
pub fn builtin_tools(&self) -> Vec<&str> {
self.tools
.iter()
.filter_map(|t| t.builtin.as_deref())
.collect()
}
pub fn is_workflow(&self) -> bool {
matches!(self.agent_type.as_str(), "sequential" | "parallel" | "loop")
}
}
pub fn discover_agent_configs(dir: &Path) -> Result<Vec<AgentConfig>, AgentConfigError> {
let candidates = ["agent.json", "root_agent.json", "agent.toml"];
let mut configs = Vec::new();
for candidate in &candidates {
let path = dir.join(candidate);
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let config: AgentConfig = if candidate.ends_with(".json") {
serde_json::from_str(&content)?
} else if candidate.ends_with(".toml") {
return Err(AgentConfigError::Toml(
"TOML parsing requires the adk-cli crate".into(),
));
} else {
return Err(AgentConfigError::Yaml(
"YAML parsing requires the adk-cli crate".into(),
));
};
config.validate()?;
configs.push(config);
}
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Ok(sub_configs) = discover_agent_configs(&path) {
configs.extend(sub_configs);
}
}
}
}
Ok(configs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_json_config() {
let json = r#"{"name": "test_agent"}"#;
let config = AgentConfig::from_json(json).unwrap();
assert_eq!(config.name, "test_agent");
assert_eq!(config.agent_type, "llm");
assert!(config.model.is_none());
assert!(config.tools.is_empty());
}
#[test]
fn parse_full_json_config() {
let json = r#"{
"name": "weather_agent",
"model": "gemini-2.0-flash",
"instruction": "You are a weather assistant.",
"description": "Gets weather info",
"temperature": 0.3,
"max_output_tokens": 1024,
"output_key": "weather_result",
"max_llm_calls": 10,
"tools": [
{"name": "get_weather", "description": "Get weather for a city"},
{"builtin": "google_search"}
],
"sub_agents": [
{"name": "forecast", "instruction": "Give forecasts"}
]
}"#;
let config = AgentConfig::from_json(json).unwrap();
assert_eq!(config.name, "weather_agent");
assert_eq!(config.model.as_deref(), Some("gemini-2.0-flash"));
assert_eq!(config.temperature, Some(0.3));
assert_eq!(config.output_key.as_deref(), Some("weather_result"));
assert_eq!(config.max_llm_calls, Some(10));
assert_eq!(config.tools.len(), 2);
assert_eq!(config.sub_agents.len(), 1);
assert_eq!(config.builtin_tools(), vec!["google_search"]);
}
#[test]
fn validate_empty_name_fails() {
let config = AgentConfig::from_json(r#"{"name": ""}"#).unwrap();
assert!(config.validate().is_err());
}
#[test]
fn validate_bad_temperature_fails() {
let config = AgentConfig::from_json(r#"{"name": "test", "temperature": 3.0}"#).unwrap();
assert!(config.validate().is_err());
}
#[test]
fn validate_good_config_passes() {
let config = AgentConfig::from_json(r#"{"name": "test", "temperature": 0.7}"#).unwrap();
assert!(config.validate().is_ok());
}
#[test]
fn is_workflow_detection() {
let sequential =
AgentConfig::from_json(r#"{"name": "seq", "agent_type": "sequential"}"#).unwrap();
assert!(sequential.is_workflow());
let llm = AgentConfig::from_json(r#"{"name": "llm"}"#).unwrap();
assert!(!llm.is_workflow());
}
#[test]
fn tool_config_variants() {
let custom = ToolConfig {
name: Some("my_tool".into()),
description: Some("Does stuff".into()),
builtin: None,
parameters: Some(serde_json::json!({"type": "object"})),
};
assert!(custom.name.is_some());
assert!(custom.builtin.is_none());
let builtin = ToolConfig {
name: None,
description: None,
builtin: Some("google_search".into()),
parameters: None,
};
assert!(builtin.builtin.is_some());
}
}