car_engine/
agent_capability.rs1use std::collections::HashMap;
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::RwLock;
35
36#[derive(Debug, Clone)]
38struct AgentEntry {
39 id: String,
40 last_used: Option<u64>,
43}
44
45#[derive(Debug, Default)]
51pub struct AgentCapabilityRegistry {
52 by_capability: RwLock<HashMap<String, Vec<AgentEntry>>>,
53 usage_sequence: AtomicU64,
54}
55
56impl AgentCapabilityRegistry {
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn register(&self, capability: impl Into<String>, agent_id: impl Into<String>) {
65 let capability = capability.into();
66 let agent_id = agent_id.into();
67 let mut g = self.by_capability.write().expect("registry poisoned");
68 let entries = g.entry(capability).or_default();
69 if !entries.iter().any(|e| e.id == agent_id) {
70 entries.push(AgentEntry {
71 id: agent_id,
72 last_used: None,
73 });
74 }
75 }
76
77 pub fn unregister(&self, capability: &str, agent_id: &str) {
80 let mut g = self.by_capability.write().expect("registry poisoned");
81 if let Some(entries) = g.get_mut(capability) {
82 entries.retain(|e| e.id != agent_id);
83 if entries.is_empty() {
84 g.remove(capability);
85 }
86 }
87 }
88
89 pub fn agents_for(&self, capability: &str) -> Vec<String> {
92 let g = self.by_capability.read().expect("registry poisoned");
93 g.get(capability)
94 .map(|v| v.iter().map(|e| e.id.clone()).collect())
95 .unwrap_or_default()
96 }
97
98 pub fn all_agents(&self) -> Vec<String> {
102 let g = self.by_capability.read().expect("registry poisoned");
103 let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
104 for entries in g.values() {
105 for e in entries {
106 seen.insert(e.id.clone());
107 }
108 }
109 seen.into_iter().collect()
110 }
111
112 pub fn select(&self, capability: &str, hint: Option<&str>) -> Option<String> {
116 let g = self.by_capability.read().expect("registry poisoned");
117 let entries = g.get(capability)?;
118 if entries.is_empty() {
119 return None;
120 }
121
122 if let Some(h) = hint {
124 if let Some(e) = entries.iter().find(|e| e.id == h) {
125 return Some(e.id.clone());
126 }
127 }
128
129 let mut best: Option<&AgentEntry> = None;
136 for e in entries.iter() {
137 best = match best {
138 None => Some(e),
139 Some(b) if e.last_used > b.last_used => Some(e),
140 Some(b) => Some(b),
141 };
142 }
143 best.map(|e| e.id.clone())
144 }
145
146 pub fn note_used(&self, capability: &str, agent_id: &str) {
150 let mut g = self.by_capability.write().expect("registry poisoned");
151 if let Some(entries) = g.get_mut(capability) {
152 if let Some(e) = entries.iter_mut().find(|e| e.id == agent_id) {
153 e.last_used = Some(self.usage_sequence.fetch_add(1, Ordering::Relaxed) + 1);
154 }
155 }
156 }
157
158 pub fn capability_count(&self) -> usize {
160 self.by_capability.read().expect("registry poisoned").len()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn register_and_list() {
170 let r = AgentCapabilityRegistry::new();
171 r.register("summarize", "alpha");
172 r.register("summarize", "beta");
173 r.register("transcribe-audio", "alpha");
174 assert_eq!(r.agents_for("summarize"), vec!["alpha", "beta"]);
175 assert_eq!(r.agents_for("transcribe-audio"), vec!["alpha"]);
176 assert!(r.agents_for("nonexistent").is_empty());
177 assert_eq!(r.all_agents(), vec!["alpha", "beta"]);
178 assert_eq!(r.capability_count(), 2);
179 }
180
181 #[test]
182 fn register_is_idempotent() {
183 let r = AgentCapabilityRegistry::new();
184 r.register("summarize", "alpha");
185 r.register("summarize", "alpha");
186 r.register("summarize", "alpha");
187 assert_eq!(r.agents_for("summarize"), vec!["alpha"]);
188 }
189
190 #[test]
191 fn select_honors_hint() {
192 let r = AgentCapabilityRegistry::new();
193 r.register("summarize", "alpha");
194 r.register("summarize", "beta");
195 r.register("summarize", "gamma");
196 assert_eq!(r.select("summarize", Some("beta")), Some("beta".into()));
197 assert_eq!(
199 r.select("summarize", Some("nonexistent")),
200 Some("alpha".into())
201 );
202 }
203
204 #[test]
205 fn select_uses_mru_when_no_hint() {
206 let r = AgentCapabilityRegistry::new();
207 r.register("summarize", "alpha");
208 r.register("summarize", "beta");
209 assert_eq!(r.select("summarize", None), Some("alpha".into()));
211 r.note_used("summarize", "beta");
213 assert_eq!(r.select("summarize", None), Some("beta".into()));
214 r.note_used("summarize", "alpha");
216 assert_eq!(r.select("summarize", None), Some("alpha".into()));
217 }
218
219 #[test]
220 fn unregister_removes() {
221 let r = AgentCapabilityRegistry::new();
222 r.register("summarize", "alpha");
223 r.register("summarize", "beta");
224 r.unregister("summarize", "alpha");
225 assert_eq!(r.agents_for("summarize"), vec!["beta"]);
226 r.unregister("summarize", "beta");
227 assert!(r.agents_for("summarize").is_empty());
228 assert_eq!(r.capability_count(), 0);
229 }
230
231 #[test]
232 fn select_returns_none_for_unknown_capability() {
233 let r = AgentCapabilityRegistry::new();
234 assert_eq!(r.select("nope", None), None);
235 }
236
237 #[test]
244 fn select_three_agent_tiebreak() {
245 let r = AgentCapabilityRegistry::new();
246 r.register("summarize", "alpha");
247 r.register("summarize", "beta");
248 r.register("summarize", "gamma");
249
250 assert_eq!(r.select("summarize", None), Some("alpha".into()));
252
253 r.note_used("summarize", "gamma");
255 assert_eq!(r.select("summarize", None), Some("gamma".into()));
256
257 r.note_used("summarize", "alpha");
259 assert_eq!(r.select("summarize", None), Some("alpha".into()));
260
261 assert_eq!(r.select("summarize", Some("beta")), Some("beta".into()));
263 }
264}