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