use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub name: String,
pub description: String,
pub system_prompt: Option<String>,
pub model: Option<String>,
pub include_tools: Vec<String>,
pub exclude_tools: Vec<String>,
pub read_only: bool,
pub max_turns: Option<usize>,
}
pub struct AgentRegistry {
agents: HashMap<String, AgentDefinition>,
}
impl AgentRegistry {
pub fn with_defaults() -> Self {
let mut agents = HashMap::new();
agents.insert(
"general-purpose".to_string(),
AgentDefinition {
name: "general-purpose".to_string(),
description: "General-purpose agent with full tool access.".to_string(),
system_prompt: None,
model: None,
include_tools: Vec::new(),
exclude_tools: Vec::new(),
read_only: false,
max_turns: None,
},
);
agents.insert(
"explore".to_string(),
AgentDefinition {
name: "explore".to_string(),
description: "Fast read-only agent for searching and understanding code."
.to_string(),
system_prompt: Some(
"You are a fast exploration agent. Focus on finding information \
quickly. Use Grep, Glob, and FileRead to answer questions about \
the codebase. Do not modify files."
.to_string(),
),
model: None,
include_tools: vec![
"FileRead".into(),
"Grep".into(),
"Glob".into(),
"Bash".into(),
"WebFetch".into(),
],
exclude_tools: Vec::new(),
read_only: true,
max_turns: Some(20),
},
);
agents.insert(
"plan".to_string(),
AgentDefinition {
name: "plan".to_string(),
description: "Planning agent that designs implementation strategies.".to_string(),
system_prompt: Some(
"You are a software architect agent. Design implementation plans, \
identify critical files, and consider architectural trade-offs. \
Do not modify files directly."
.to_string(),
),
model: None,
include_tools: vec![
"FileRead".into(),
"Grep".into(),
"Glob".into(),
"Bash".into(),
],
exclude_tools: Vec::new(),
read_only: true,
max_turns: Some(30),
},
);
Self { agents }
}
pub fn get(&self, name: &str) -> Option<&AgentDefinition> {
self.agents.get(name)
}
pub fn register(&mut self, definition: AgentDefinition) {
self.agents.insert(definition.name.clone(), definition);
}
pub fn list(&self) -> Vec<&AgentDefinition> {
let mut agents: Vec<_> = self.agents.values().collect();
agents.sort_by_key(|a| &a.name);
agents
}
pub fn load_from_disk(&mut self, cwd: Option<&std::path::Path>) {
if let Some(cwd) = cwd {
let project_dir = cwd.join(".agent").join("agents");
self.load_agents_from_dir(&project_dir);
}
if let Some(config_dir) = dirs::config_dir() {
let user_dir = config_dir.join("agent-code").join("agents");
self.load_agents_from_dir(&user_dir);
}
}
fn load_agents_from_dir(&mut self, dir: &std::path::Path) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "md")
&& let Some(def) = parse_agent_file(&path)
{
self.agents.insert(def.name.clone(), def);
}
}
}
}
fn parse_agent_file(path: &std::path::Path) -> Option<AgentDefinition> {
let content = std::fs::read_to_string(path).ok()?;
if !content.starts_with("---") {
return None;
}
let end = content[3..].find("---")?;
let frontmatter = &content[3..3 + end];
let body = content[3 + end + 3..].trim();
let mut name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("custom")
.to_string();
let mut description = String::new();
let mut model = None;
let mut read_only = false;
let mut max_turns = None;
let mut include_tools = Vec::new();
let mut exclude_tools = Vec::new();
for line in frontmatter.lines() {
let line = line.trim();
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
"name" => name = value.to_string(),
"description" => description = value.to_string(),
"model" => model = Some(value.to_string()),
"read_only" => read_only = value == "true",
"max_turns" => max_turns = value.parse().ok(),
"include_tools" => {
include_tools = value
.trim_matches(|c| c == '[' || c == ']')
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
"exclude_tools" => {
exclude_tools = value
.trim_matches(|c| c == '[' || c == ']')
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
_ => {}
}
}
}
let system_prompt = if body.is_empty() {
None
} else {
Some(body.to_string())
};
Some(AgentDefinition {
name,
description,
system_prompt,
model,
include_tools,
exclude_tools,
read_only,
max_turns,
})
}