Skip to main content

agent_code_lib/services/
coordinator.rs

1//! Multi-agent coordinator.
2//!
3//! Routes tasks to specialized agents based on the task type.
4//! The coordinator acts as an orchestrator, spawning agents with
5//! appropriate configurations and aggregating their results.
6//!
7//! # Agent types
8//!
9//! - `general-purpose`: default agent with full tool access
10//! - `explore`: fast read-only agent for codebase exploration
11//! - `plan`: planning agent restricted to analysis tools
12//!
13//! Agents are defined as configurations that customize the tool
14//! set, system prompt, and permission mode.
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19/// Definition of a specialized agent type.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AgentDefinition {
22    /// Unique agent type name.
23    pub name: String,
24    /// Description of what this agent specializes in.
25    pub description: String,
26    /// System prompt additions for this agent type.
27    pub system_prompt: Option<String>,
28    /// Model override (if different from default).
29    pub model: Option<String>,
30    /// Tools to include (if empty, use all).
31    pub include_tools: Vec<String>,
32    /// Tools to exclude.
33    pub exclude_tools: Vec<String>,
34    /// Whether this agent runs in read-only mode.
35    pub read_only: bool,
36    /// Maximum turns for this agent type.
37    pub max_turns: Option<usize>,
38}
39
40/// Registry of available agent types.
41pub struct AgentRegistry {
42    agents: HashMap<String, AgentDefinition>,
43}
44
45impl AgentRegistry {
46    /// Create the registry with built-in agent types.
47    pub fn with_defaults() -> Self {
48        let mut agents = HashMap::new();
49
50        agents.insert(
51            "general-purpose".to_string(),
52            AgentDefinition {
53                name: "general-purpose".to_string(),
54                description: "General-purpose agent with full tool access.".to_string(),
55                system_prompt: None,
56                model: None,
57                include_tools: Vec::new(),
58                exclude_tools: Vec::new(),
59                read_only: false,
60                max_turns: None,
61            },
62        );
63
64        agents.insert(
65            "explore".to_string(),
66            AgentDefinition {
67                name: "explore".to_string(),
68                description: "Fast read-only agent for searching and understanding code."
69                    .to_string(),
70                system_prompt: Some(
71                    "You are a fast exploration agent. Focus on finding information \
72                     quickly. Use Grep, Glob, and FileRead to answer questions about \
73                     the codebase. Do not modify files."
74                        .to_string(),
75                ),
76                model: None,
77                include_tools: vec![
78                    "FileRead".into(),
79                    "Grep".into(),
80                    "Glob".into(),
81                    "Bash".into(),
82                    "WebFetch".into(),
83                ],
84                exclude_tools: Vec::new(),
85                read_only: true,
86                max_turns: Some(20),
87            },
88        );
89
90        agents.insert(
91            "plan".to_string(),
92            AgentDefinition {
93                name: "plan".to_string(),
94                description: "Planning agent that designs implementation strategies.".to_string(),
95                system_prompt: Some(
96                    "You are a software architect agent. Design implementation plans, \
97                     identify critical files, and consider architectural trade-offs. \
98                     Do not modify files directly."
99                        .to_string(),
100                ),
101                model: None,
102                include_tools: vec![
103                    "FileRead".into(),
104                    "Grep".into(),
105                    "Glob".into(),
106                    "Bash".into(),
107                ],
108                exclude_tools: Vec::new(),
109                read_only: true,
110                max_turns: Some(30),
111            },
112        );
113
114        Self { agents }
115    }
116
117    /// Look up an agent definition by type name.
118    pub fn get(&self, name: &str) -> Option<&AgentDefinition> {
119        self.agents.get(name)
120    }
121
122    /// Register a custom agent type.
123    pub fn register(&mut self, definition: AgentDefinition) {
124        self.agents.insert(definition.name.clone(), definition);
125    }
126
127    /// List all available agent types.
128    pub fn list(&self) -> Vec<&AgentDefinition> {
129        let mut agents: Vec<_> = self.agents.values().collect();
130        agents.sort_by_key(|a| &a.name);
131        agents
132    }
133
134    /// Load agent definitions from disk (`.agent/agents/` and `~/.config/agent-code/agents/`).
135    /// Each `.md` file is parsed for YAML frontmatter with agent configuration.
136    pub fn load_from_disk(&mut self, cwd: Option<&std::path::Path>) {
137        // Project-level agents.
138        if let Some(cwd) = cwd {
139            let project_dir = cwd.join(".agent").join("agents");
140            self.load_agents_from_dir(&project_dir);
141        }
142
143        // User-level agents.
144        if let Some(config_dir) = dirs::config_dir() {
145            let user_dir = config_dir.join("agent-code").join("agents");
146            self.load_agents_from_dir(&user_dir);
147        }
148    }
149
150    fn load_agents_from_dir(&mut self, dir: &std::path::Path) {
151        let entries = match std::fs::read_dir(dir) {
152            Ok(e) => e,
153            Err(_) => return,
154        };
155
156        for entry in entries.flatten() {
157            let path = entry.path();
158            if path.extension().is_some_and(|e| e == "md")
159                && let Some(def) = parse_agent_file(&path)
160            {
161                self.agents.insert(def.name.clone(), def);
162            }
163        }
164    }
165}
166
167/// Parse an agent definition from a markdown file with YAML frontmatter.
168///
169/// Expected format:
170/// ```markdown
171/// ---
172/// name: my-agent
173/// description: A specialized agent
174/// model: gpt-4.1-mini
175/// read_only: false
176/// max_turns: 20
177/// include_tools: [FileRead, Grep, Glob]
178/// exclude_tools: [Bash]
179/// ---
180///
181/// System prompt additions go here...
182/// ```
183fn parse_agent_file(path: &std::path::Path) -> Option<AgentDefinition> {
184    let content = std::fs::read_to_string(path).ok()?;
185
186    // Parse YAML frontmatter.
187    if !content.starts_with("---") {
188        return None;
189    }
190    let end = content[3..].find("---")?;
191    let frontmatter = &content[3..3 + end];
192    let body = content[3 + end + 3..].trim();
193
194    let mut name = path
195        .file_stem()
196        .and_then(|s| s.to_str())
197        .unwrap_or("custom")
198        .to_string();
199    let mut description = String::new();
200    let mut model = None;
201    let mut read_only = false;
202    let mut max_turns = None;
203    let mut include_tools = Vec::new();
204    let mut exclude_tools = Vec::new();
205
206    for line in frontmatter.lines() {
207        let line = line.trim();
208        if let Some((key, value)) = line.split_once(':') {
209            let key = key.trim();
210            let value = value.trim();
211            match key {
212                "name" => name = value.to_string(),
213                "description" => description = value.to_string(),
214                "model" => model = Some(value.to_string()),
215                "read_only" => read_only = value == "true",
216                "max_turns" => max_turns = value.parse().ok(),
217                "include_tools" => {
218                    include_tools = value
219                        .trim_matches(|c| c == '[' || c == ']')
220                        .split(',')
221                        .map(|s| s.trim().to_string())
222                        .filter(|s| !s.is_empty())
223                        .collect();
224                }
225                "exclude_tools" => {
226                    exclude_tools = value
227                        .trim_matches(|c| c == '[' || c == ']')
228                        .split(',')
229                        .map(|s| s.trim().to_string())
230                        .filter(|s| !s.is_empty())
231                        .collect();
232                }
233                _ => {}
234            }
235        }
236    }
237
238    let system_prompt = if body.is_empty() {
239        None
240    } else {
241        Some(body.to_string())
242    };
243
244    Some(AgentDefinition {
245        name,
246        description,
247        system_prompt,
248        model,
249        include_tools,
250        exclude_tools,
251        read_only,
252        max_turns,
253    })
254}