agent_code_lib/services/
coordinator.rs1use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AgentDefinition {
22 pub name: String,
24 pub description: String,
26 pub system_prompt: Option<String>,
28 pub model: Option<String>,
30 pub include_tools: Vec<String>,
32 pub exclude_tools: Vec<String>,
34 pub read_only: bool,
36 pub max_turns: Option<usize>,
38}
39
40pub struct AgentRegistry {
42 agents: HashMap<String, AgentDefinition>,
43}
44
45impl AgentRegistry {
46 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 pub fn get(&self, name: &str) -> Option<&AgentDefinition> {
119 self.agents.get(name)
120 }
121
122 pub fn register(&mut self, definition: AgentDefinition) {
124 self.agents.insert(definition.name.clone(), definition);
125 }
126
127 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 pub fn load_from_disk(&mut self, cwd: Option<&std::path::Path>) {
137 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 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
167fn parse_agent_file(path: &std::path::Path) -> Option<AgentDefinition> {
184 let content = std::fs::read_to_string(path).ok()?;
185
186 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}