1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9use crate::discovery::{DiscoveredBinary, DiscoveryStatus, InvocationTemplate};
10use crate::subprocess::{SubprocessAgentConfig, SubprocessAgentDelegate};
11use crate::traits::{AgentCapabilities, AgentDelegate, AgentError};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct AgentOverride {
17 pub binary: Option<PathBuf>,
19 #[serde(default)]
21 pub disabled: bool,
22 pub capabilities: Option<AgentCapabilities>,
24 pub args: Option<Vec<String>>,
27 pub prompt_via_stdin: Option<bool>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CustomAgentSpec {
35 pub id: String,
36 pub binary: PathBuf,
37 #[serde(default)]
38 pub args: Vec<String>,
39 #[serde(default = "default_true")]
40 pub prompt_via_stdin: bool,
41 #[serde(default)]
42 pub capabilities: AgentCapabilities,
43}
44
45fn default_true() -> bool {
46 true
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct DelegateOverrides {
53 #[serde(default = "default_true")]
55 pub auto_discovery: bool,
56 #[serde(default)]
58 pub overrides: HashMap<String, AgentOverride>,
59 #[serde(default)]
60 pub custom: Vec<CustomAgentSpec>,
61}
62
63impl DelegateOverrides {
64 pub fn new() -> Self {
65 Self {
66 auto_discovery: true,
67 ..Self::default()
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
76pub enum RegistryAgentStatus {
77 Registered {
78 binary: PathBuf,
79 version: Option<String>,
80 source: AgentSource,
81 },
82 DisabledByConfig,
83 Unavailable {
84 binary: PathBuf,
85 reason: String,
86 },
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum AgentSource {
94 Discovered,
95 Custom,
96 Manual,
97}
98
99#[derive(Default)]
102pub struct AgentRegistry {
103 delegates: HashMap<String, Arc<dyn AgentDelegate>>,
104 aliases: HashMap<String, String>,
105 agent_status: HashMap<String, RegistryAgentStatus>,
106}
107
108impl AgentRegistry {
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn register(&mut self, delegate: Arc<dyn AgentDelegate>) {
115 let name = delegate.name().to_string();
116 self.delegates.insert(name, delegate);
117 }
118
119 pub fn register_manual(
124 &mut self,
125 delegate: Arc<dyn AgentDelegate>,
126 binary: PathBuf,
127 version: Option<String>,
128 ) {
129 let name = delegate.name().to_string();
130 self.agent_status.insert(
131 name.clone(),
132 RegistryAgentStatus::Registered {
133 binary,
134 version,
135 source: AgentSource::Manual,
136 },
137 );
138 self.delegates.insert(name, delegate);
139 }
140
141 pub fn alias(&mut self, alias: impl Into<String>, canonical: impl Into<String>) {
144 self.aliases.insert(alias.into(), canonical.into());
145 }
146
147 pub fn get(&self, name: &str) -> Result<Arc<dyn AgentDelegate>, AgentError> {
148 let resolved = self.aliases.get(name).map(String::as_str).unwrap_or(name);
149 self.delegates
150 .get(resolved)
151 .cloned()
152 .ok_or_else(|| AgentError::NotFound(name.to_string()))
153 }
154
155 pub fn contains(&self, name: &str) -> bool {
156 let resolved = self.aliases.get(name).map(String::as_str).unwrap_or(name);
157 self.delegates.contains_key(resolved)
158 }
159
160 pub fn list(&self) -> Vec<String> {
162 let mut names: Vec<String> = self.delegates.keys().cloned().collect();
163 names.sort();
164 names
165 }
166
167 pub fn is_empty(&self) -> bool {
168 self.delegates.is_empty()
169 }
170
171 pub fn agent_status(&self, id: &str) -> Option<&RegistryAgentStatus> {
175 self.agent_status.get(id)
176 }
177
178 pub fn known_agents(&self) -> Vec<(&str, &RegistryAgentStatus)> {
180 let mut v: Vec<_> = self
181 .agent_status
182 .iter()
183 .map(|(k, v)| (k.as_str(), v))
184 .collect();
185 v.sort_by_key(|(k, _)| *k);
186 v
187 }
188
189 pub fn populate_from_discovery(
197 &mut self,
198 discovered: Vec<DiscoveredBinary>,
199 overrides: &DelegateOverrides,
200 ) {
201 if overrides.auto_discovery {
202 for d in discovered {
203 let ov = overrides
204 .overrides
205 .get(&d.agent_id)
206 .cloned()
207 .unwrap_or_default();
208 if ov.disabled {
209 self.agent_status
210 .insert(d.agent_id.clone(), RegistryAgentStatus::DisabledByConfig);
211 continue;
212 }
213 match &d.status {
214 DiscoveryStatus::Available => {
215 let binary = ov.binary.clone().unwrap_or_else(|| d.path.clone());
216 let caps = ov
217 .capabilities
218 .clone()
219 .unwrap_or_else(|| d.capabilities.clone());
220 let delegate = build_from_template(
221 &d.agent_id,
222 &binary,
223 &d.invocation,
224 &d.version_args,
225 &ov,
226 caps,
227 );
228 self.agent_status.insert(
229 d.agent_id.clone(),
230 RegistryAgentStatus::Registered {
231 binary,
232 version: d.version.clone(),
233 source: AgentSource::Discovered,
234 },
235 );
236 self.register(delegate);
237 }
238 DiscoveryStatus::Unavailable(reason) => {
239 self.agent_status.insert(
240 d.agent_id.clone(),
241 RegistryAgentStatus::Unavailable {
242 binary: d.path.clone(),
243 reason: reason.clone(),
244 },
245 );
246 }
247 }
248 }
249 }
250
251 for spec in &overrides.custom {
252 let caps = spec.capabilities.clone();
253 let cfg = SubprocessAgentConfig {
254 name: spec.id.clone(),
255 binary: spec.binary.to_string_lossy().into_owned(),
256 args: spec.args.clone(),
257 workdir: None,
258 capabilities: caps,
259 prompt_via_stdin: spec.prompt_via_stdin,
260 version_args: vec!["--version".to_string()],
261 };
262 let delegate: Arc<dyn AgentDelegate> = Arc::new(SubprocessAgentDelegate::new(cfg));
263 self.agent_status.insert(
264 spec.id.clone(),
265 RegistryAgentStatus::Registered {
266 binary: spec.binary.clone(),
267 version: None,
268 source: AgentSource::Custom,
269 },
270 );
271 self.register(delegate);
272 }
273 }
274}
275
276fn build_from_template(
277 agent_id: &str,
278 binary: &Path,
279 default_invocation: &InvocationTemplate,
280 version_args: &[String],
281 ov: &AgentOverride,
282 capabilities: AgentCapabilities,
283) -> Arc<dyn AgentDelegate> {
284 let args = ov.args.clone().unwrap_or_else(|| {
285 default_invocation
286 .args
287 .iter()
288 .map(|s| (*s).to_string())
289 .collect::<Vec<_>>()
290 });
291 let prompt_via_stdin = ov
292 .prompt_via_stdin
293 .unwrap_or(default_invocation.prompt_via_stdin);
294 let cfg = SubprocessAgentConfig {
295 name: agent_id.to_string(),
296 binary: binary.to_string_lossy().into_owned(),
297 args,
298 workdir: None,
299 capabilities,
300 prompt_via_stdin,
301 version_args: version_args.to_vec(),
302 };
303 Arc::new(SubprocessAgentDelegate::new(cfg))
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::traits::{AgentCapabilities, AgentResult, AgentTask, AgentTaskStatus};
310 use async_trait::async_trait;
311 use chrono::Utc;
312
313 struct MockAgent {
314 name: String,
315 }
316
317 #[async_trait]
318 impl AgentDelegate for MockAgent {
319 fn name(&self) -> &str {
320 &self.name
321 }
322 fn capabilities(&self) -> AgentCapabilities {
323 AgentCapabilities::default()
324 }
325 async fn delegate(&self, task: AgentTask) -> Result<AgentResult, AgentError> {
326 let now = Utc::now();
327 Ok(AgentResult {
328 task_id: task.id,
329 status: AgentTaskStatus::Succeeded,
330 summary: format!("{} ran: {}", self.name, task.description),
331 artifacts: vec![],
332 stdout: String::new(),
333 stderr: String::new(),
334 exit_code: Some(0),
335 started_at: now,
336 completed_at: now,
337 })
338 }
339 }
340
341 #[test]
342 fn register_and_get() {
343 let mut reg = AgentRegistry::new();
344 reg.register(Arc::new(MockAgent {
345 name: "mock".to_string(),
346 }));
347 let d = reg.get("mock").unwrap();
348 assert_eq!(d.name(), "mock");
349 }
350
351 #[test]
352 fn alias_resolves() {
353 let mut reg = AgentRegistry::new();
354 reg.register(Arc::new(MockAgent {
355 name: "claude-code".to_string(),
356 }));
357 reg.alias("claude", "claude-code");
358 assert!(reg.contains("claude"));
359 assert_eq!(reg.get("claude").unwrap().name(), "claude-code");
360 }
361
362 #[test]
363 fn missing_delegate_errors() {
364 let reg = AgentRegistry::new();
365 assert!(matches!(reg.get("nope"), Err(AgentError::NotFound(_))));
366 }
367
368 #[test]
369 fn list_returns_sorted_names() {
370 let mut reg = AgentRegistry::new();
371 reg.register(Arc::new(MockAgent {
372 name: "b".to_string(),
373 }));
374 reg.register(Arc::new(MockAgent {
375 name: "a".to_string(),
376 }));
377 assert_eq!(reg.list(), vec!["a".to_string(), "b".to_string()]);
378 }
379
380 fn discovered(agent_id: &str, status: DiscoveryStatus) -> DiscoveredBinary {
381 DiscoveredBinary {
382 agent_id: agent_id.to_string(),
383 binary_name: agent_id.to_string(),
384 path: PathBuf::from(format!("/usr/local/bin/{agent_id}")),
385 version: Some(format!("{agent_id} 1.0")),
386 status,
387 capabilities: AgentCapabilities::default(),
388 invocation: InvocationTemplate {
389 args: &[],
390 prompt_via_stdin: true,
391 },
392 version_args: vec!["--version".to_string()],
393 }
394 }
395
396 #[test]
397 fn populate_registers_available_agents() {
398 let mut reg = AgentRegistry::new();
399 let discovered = vec![
400 discovered("codex", DiscoveryStatus::Available),
401 discovered("aider", DiscoveryStatus::Available),
402 ];
403 reg.populate_from_discovery(discovered, &DelegateOverrides::new());
404 assert!(reg.contains("codex"));
405 assert!(reg.contains("aider"));
406 assert!(matches!(
407 reg.agent_status("codex"),
408 Some(RegistryAgentStatus::Registered { .. })
409 ));
410 }
411
412 #[test]
413 fn populate_skips_unavailable_but_records_reason() {
414 let mut reg = AgentRegistry::new();
415 let discovered = vec![discovered(
416 "broken",
417 DiscoveryStatus::Unavailable("missing API key".into()),
418 )];
419 reg.populate_from_discovery(discovered, &DelegateOverrides::new());
420 assert!(!reg.contains("broken"));
421 match reg.agent_status("broken") {
422 Some(RegistryAgentStatus::Unavailable { reason, .. }) => {
423 assert!(reason.contains("missing API key"));
424 }
425 other => panic!("expected Unavailable, got {other:?}"),
426 }
427 }
428
429 #[test]
430 fn disabled_override_blocks_registration() {
431 let mut reg = AgentRegistry::new();
432 let mut overrides = DelegateOverrides::new();
433 overrides.overrides.insert(
434 "aider".to_string(),
435 AgentOverride {
436 disabled: true,
437 ..Default::default()
438 },
439 );
440 reg.populate_from_discovery(
441 vec![discovered("aider", DiscoveryStatus::Available)],
442 &overrides,
443 );
444 assert!(!reg.contains("aider"));
445 assert!(matches!(
446 reg.agent_status("aider"),
447 Some(RegistryAgentStatus::DisabledByConfig)
448 ));
449 }
450
451 #[test]
452 fn auto_discovery_off_still_registers_custom() {
453 let mut reg = AgentRegistry::new();
454 let overrides = DelegateOverrides {
455 auto_discovery: false,
456 overrides: HashMap::new(),
457 custom: vec![CustomAgentSpec {
458 id: "mine".to_string(),
459 binary: PathBuf::from("/usr/bin/true"),
460 args: vec![],
461 prompt_via_stdin: true,
462 capabilities: AgentCapabilities::default(),
463 }],
464 };
465 reg.populate_from_discovery(
466 vec![discovered("aider", DiscoveryStatus::Available)],
467 &overrides,
468 );
469 assert!(!reg.contains("aider"));
470 assert!(reg.contains("mine"));
471 match reg.agent_status("mine") {
472 Some(RegistryAgentStatus::Registered { source, .. }) => {
473 assert_eq!(*source, AgentSource::Custom);
474 }
475 other => panic!("expected Registered, got {other:?}"),
476 }
477 }
478
479 #[test]
480 fn override_binary_path_takes_precedence() {
481 let mut reg = AgentRegistry::new();
482 let mut overrides = DelegateOverrides::new();
483 overrides.overrides.insert(
484 "aider".to_string(),
485 AgentOverride {
486 binary: Some(PathBuf::from("/opt/custom/aider")),
487 ..Default::default()
488 },
489 );
490 reg.populate_from_discovery(
491 vec![discovered("aider", DiscoveryStatus::Available)],
492 &overrides,
493 );
494 match reg.agent_status("aider") {
495 Some(RegistryAgentStatus::Registered { binary, .. }) => {
496 assert_eq!(binary, &PathBuf::from("/opt/custom/aider"));
497 }
498 other => panic!("expected Registered, got {other:?}"),
499 }
500 }
501
502 #[test]
503 fn register_manual_records_status_with_manual_source() {
504 let mut reg = AgentRegistry::new();
505 reg.register_manual(
506 Arc::new(MockAgent {
507 name: "hand-wired".to_string(),
508 }),
509 PathBuf::from("/opt/local/bin/hand-wired"),
510 Some("1.2.3".to_string()),
511 );
512 assert!(reg.contains("hand-wired"));
513 match reg.agent_status("hand-wired") {
514 Some(RegistryAgentStatus::Registered {
515 source,
516 binary,
517 version,
518 }) => {
519 assert_eq!(*source, AgentSource::Manual);
520 assert_eq!(binary, &PathBuf::from("/opt/local/bin/hand-wired"));
521 assert_eq!(version.as_deref(), Some("1.2.3"));
522 }
523 other => panic!("expected Registered, got {other:?}"),
524 }
525 }
526}