1use std::collections::HashMap;
8
9use super::config::MultiAgentConfig;
10use super::path::AgentPath;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum AgentStatus {
19 Idle,
21 Running,
23 Done,
25}
26
27#[derive(Clone, Debug)]
29pub struct AgentEntry {
30 pub path: AgentPath,
32 pub status: AgentStatus,
34 pub depth: i32,
36 pub tool_count: usize,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum SpawnError {
43 MaxAgentsReached { max: usize },
45 DepthLimitReached { max: i32, attempted: i32 },
47 AlreadyExists,
49}
50
51impl std::fmt::Display for SpawnError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::MaxAgentsReached { max } => {
55 write!(f, "max agents reached (limit: {})", max)
56 }
57 Self::DepthLimitReached { max, attempted } => {
58 write!(
59 f,
60 "agent depth limit reached (max: {}, attempted: {})",
61 max, attempted
62 )
63 }
64 Self::AlreadyExists => {
65 write!(f, "agent with this path already exists")
66 }
67 }
68 }
69}
70
71pub struct AgentRegistry {
89 config: MultiAgentConfig,
90 agents: HashMap<AgentPath, AgentEntry>,
91}
92
93impl AgentRegistry {
94 pub fn new(config: MultiAgentConfig) -> Self {
96 Self {
97 config,
98 agents: HashMap::new(),
99 }
100 }
101
102 pub fn can_spawn(&self, depth: i32) -> Result<(), SpawnError> {
107 if self.config.enabled && self.agents.len() >= self.config.max_sub_agents {
109 return Err(SpawnError::MaxAgentsReached {
110 max: self.config.max_sub_agents,
111 });
112 }
113
114 if self.config.enabled && depth > self.config.max_agent_depth {
116 return Err(SpawnError::DepthLimitReached {
117 max: self.config.max_agent_depth,
118 attempted: depth,
119 });
120 }
121
122 Ok(())
123 }
124
125 pub fn register(
130 &mut self,
131 path: &AgentPath,
132 depth: i32,
133 tool_count: usize,
134 ) -> Result<(), SpawnError> {
135 self.can_spawn(depth)?;
136
137 if self.agents.contains_key(path) {
138 return Err(SpawnError::AlreadyExists);
139 }
140
141 self.agents.insert(
142 path.clone(),
143 AgentEntry {
144 path: path.clone(),
145 status: AgentStatus::Idle,
146 depth,
147 tool_count,
148 },
149 );
150
151 Ok(())
152 }
153
154 pub fn close(&mut self, path: &AgentPath) -> Option<AgentEntry> {
159 self.agents.remove(path)
160 }
161
162 pub fn set_status(&mut self, path: &AgentPath, status: AgentStatus) -> bool {
166 match self.agents.get_mut(path) {
167 Some(entry) => {
168 entry.status = status;
169 true
170 }
171 None => false,
172 }
173 }
174
175 pub fn get(&self, path: &AgentPath) -> Option<&AgentEntry> {
177 self.agents.get(path)
178 }
179
180 pub fn list(&self) -> Vec<&AgentEntry> {
182 let mut entries: Vec<&AgentEntry> = self.agents.values().collect();
183 entries.sort_by(|a, b| a.path.cmp(&b.path));
184 entries
185 }
186
187 pub fn count(&self) -> usize {
189 self.agents.len()
190 }
191
192 pub fn count_by_status(&self, status: &AgentStatus) -> usize {
194 self.agents.values().filter(|e| e.status == *status).count()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.agents.is_empty()
200 }
201
202 pub fn contains(&self, path: &AgentPath) -> bool {
204 self.agents.contains_key(path)
205 }
206
207 pub fn config(&self) -> &MultiAgentConfig {
209 &self.config
210 }
211}
212
213#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn test_config() -> MultiAgentConfig {
222 MultiAgentConfig::enabled()
223 }
224
225 fn test_path(name: &str) -> AgentPath {
226 AgentPath::root().join(name)
227 }
228
229 #[test]
230 fn register_and_close() {
231 let mut reg = AgentRegistry::new(test_config());
232 let path = test_path("worker");
233
234 assert!(reg.register(&path, 1, 5).is_ok());
235 assert_eq!(reg.count(), 1);
236 assert!(reg.contains(&path));
237
238 let entry = reg.get(&path).unwrap();
239 assert_eq!(entry.status, AgentStatus::Idle);
240 assert_eq!(entry.depth, 1);
241 assert_eq!(entry.tool_count, 5);
242
243 let closed = reg.close(&path).unwrap();
244 assert_eq!(closed.path, path);
245 assert_eq!(reg.count(), 0);
246 assert!(!reg.contains(&path));
247 }
248
249 #[test]
250 fn duplicate_register_fails() {
251 let mut reg = AgentRegistry::new(test_config());
252 let path = test_path("worker");
253
254 assert!(reg.register(&path, 1, 3).is_ok());
255 assert_eq!(
256 reg.register(&path, 1, 3).unwrap_err(),
257 SpawnError::AlreadyExists
258 );
259 }
260
261 #[test]
262 fn max_agents_limit() {
263 let config = MultiAgentConfig::with_limits(2, 1);
264 let mut reg = AgentRegistry::new(config);
265
266 assert!(reg.register(&test_path("a"), 1, 1).is_ok());
267 assert!(reg.register(&test_path("b"), 1, 1).is_ok());
268 assert_eq!(
269 reg.register(&test_path("c"), 1, 1).unwrap_err(),
270 SpawnError::MaxAgentsReached { max: 2 }
271 );
272 }
273
274 #[test]
275 fn depth_limit() {
276 let config = MultiAgentConfig::with_limits(8, 1);
277 let reg = AgentRegistry::new(config);
278
279 assert!(reg.can_spawn(1).is_ok());
281
282 assert_eq!(
284 reg.can_spawn(2).unwrap_err(),
285 SpawnError::DepthLimitReached {
286 max: 1,
287 attempted: 2
288 }
289 );
290 }
291
292 #[test]
293 fn lifecycle_states() {
294 let mut reg = AgentRegistry::new(test_config());
295 let path = test_path("worker");
296
297 reg.register(&path, 1, 3).unwrap();
298 assert_eq!(reg.count_by_status(&AgentStatus::Idle), 1);
299
300 reg.set_status(&path, AgentStatus::Running);
301 assert_eq!(reg.count_by_status(&AgentStatus::Idle), 0);
302 assert_eq!(reg.count_by_status(&AgentStatus::Running), 1);
303 assert_eq!(reg.get(&path).unwrap().status, AgentStatus::Running);
304
305 reg.set_status(&path, AgentStatus::Done);
306 assert_eq!(reg.count_by_status(&AgentStatus::Running), 0);
307 assert_eq!(reg.count_by_status(&AgentStatus::Done), 1);
308
309 assert_eq!(reg.count(), 1);
311 }
312
313 #[test]
314 fn close_frees_quota() {
315 let config = MultiAgentConfig::with_limits(1, 1);
316 let mut reg = AgentRegistry::new(config);
317
318 let path = test_path("worker");
319 reg.register(&path, 1, 3).unwrap();
320 assert_eq!(reg.count(), 1);
321
322 assert!(reg.can_spawn(1).is_err());
324
325 reg.close(&path);
327 assert_eq!(reg.count(), 0);
328 assert!(reg.can_spawn(1).is_ok());
329 }
330
331 #[test]
332 fn list_sorted() {
333 let mut reg = AgentRegistry::new(test_config());
334 reg.register(&test_path("b"), 1, 1).unwrap();
335 reg.register(&test_path("a"), 1, 1).unwrap();
336
337 let list = reg.list();
338 assert_eq!(list.len(), 2);
339 assert_eq!(list[0].path.name(), "a");
340 assert_eq!(list[1].path.name(), "b");
341 }
342
343 #[test]
344 fn set_status_nonexistent() {
345 let mut reg = AgentRegistry::new(test_config());
346 assert!(!reg.set_status(&test_path("ghost"), AgentStatus::Running));
347 }
348
349 #[test]
350 fn disabled_config_allows_spawn() {
351 let config = MultiAgentConfig {
354 enabled: false,
355 ..MultiAgentConfig::default()
356 };
357 let reg = AgentRegistry::new(config);
358
359 assert!(reg.can_spawn(999).is_ok());
362 }
363
364 #[test]
365 fn spawn_error_display() {
366 assert_eq!(
367 SpawnError::MaxAgentsReached { max: 8 }.to_string(),
368 "max agents reached (limit: 8)"
369 );
370 assert_eq!(
371 SpawnError::DepthLimitReached {
372 max: 1,
373 attempted: 2
374 }
375 .to_string(),
376 "agent depth limit reached (max: 1, attempted: 2)"
377 );
378 assert_eq!(
379 SpawnError::AlreadyExists.to_string(),
380 "agent with this path already exists"
381 );
382 }
383}