use std::collections::HashMap;
use crate::domain::agent::{AgentConfig, AgentDefinition, builtin_agents};
const AGENT_TIMEOUT_ENV: &str = "CLAUDY_AGENT_TIMEOUT";
fn effective_timeout(builtin: u64, config_timeout: Option<u64>) -> u64 {
if let Some(t) = config_timeout {
return t;
}
if let Ok(val) = std::env::var(AGENT_TIMEOUT_ENV)
&& let Ok(secs) = val.parse::<u64>()
{
return secs;
}
builtin
}
pub fn discover_agents(overrides: &HashMap<String, AgentConfig>) -> Vec<AgentDefinition> {
let builtins = builtin_agents();
let mut result = Vec::new();
for mut def in builtins {
if let Some(config) = overrides.get(&def.name) {
if let Some(b) = &config.binary {
def.binary = b.clone();
}
if !config.args.is_empty() {
def.args = config.args.clone();
}
if let Some(desc) = &config.description {
def.description = desc.clone();
}
}
def.timeout = effective_timeout(
def.timeout,
overrides.get(&def.name).and_then(|c| c.timeout),
);
if which::which(&def.binary).is_ok() {
result.push(def);
}
}
let builtin_names: std::collections::HashSet<String> =
result.iter().map(|a| a.name.clone()).collect();
for (name, config) in overrides {
if builtin_names.contains(name) {
continue; }
let Some(binary) = &config.binary else {
continue; };
if which::which(binary).is_ok() {
result.push(AgentDefinition {
name: name.clone(),
binary: binary.clone(),
args: if config.args.is_empty() {
vec!["{prompt}".to_string()]
} else {
config.args.clone()
},
description: config
.description
.clone()
.unwrap_or_else(|| format!("Custom agent: {name}")),
timeout: effective_timeout(120, config.timeout),
});
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_agents_no_overrides_finds_available_builtins() {
let overrides = HashMap::new();
let agents = discover_agents(&overrides);
for agent in &agents {
assert!(!agent.name.is_empty());
assert!(!agent.binary.is_empty());
assert!(!agent.args.is_empty());
assert!(!agent.description.is_empty());
assert!(agent.timeout > 0);
}
}
#[test]
fn discover_agents_custom_agent_not_on_path_is_excluded() {
let mut overrides = HashMap::new();
overrides.insert(
"my-custom-agent".to_string(),
AgentConfig {
binary: Some("nonexistent_binary_xyz_12345".to_string()),
args: vec![],
description: None,
timeout: None,
},
);
let agents = discover_agents(&overrides);
assert!(!agents.iter().any(|a| a.name == "my-custom-agent"));
}
#[test]
fn discover_agents_override_description_applied() {
let mut overrides = HashMap::new();
overrides.insert(
"codex".to_string(),
AgentConfig {
binary: Some("cargo".to_string()),
args: vec![],
description: Some("Overridden description".to_string()),
timeout: Some(42),
},
);
let agents = discover_agents(&overrides);
let codex = agents.iter().find(|a| a.name == "codex");
if let Some(agent) = codex {
assert_eq!(agent.binary, "cargo");
assert_eq!(agent.description, "Overridden description");
assert_eq!(agent.timeout, 42);
}
}
#[test]
fn effective_timeout_env_var_overrides_builtin() {
assert_eq!(effective_timeout(120, Some(42)), 42);
unsafe { std::env::set_var("CLAUDY_AGENT_TIMEOUT", "600") };
assert_eq!(effective_timeout(120, None), 600);
unsafe { std::env::set_var("CLAUDY_AGENT_TIMEOUT", "not-a-number") };
assert_eq!(effective_timeout(120, None), 120);
unsafe { std::env::remove_var("CLAUDY_AGENT_TIMEOUT") };
}
#[test]
fn effective_timeout_config_beats_env_var() {
unsafe { std::env::set_var("CLAUDY_AGENT_TIMEOUT", "999") };
assert_eq!(effective_timeout(120, Some(42)), 42);
unsafe { std::env::remove_var("CLAUDY_AGENT_TIMEOUT") };
}
}