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.read().expect("registry poisoned").len()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn register_and_list() {
169 let r = AgentCapabilityRegistry::new();
170 r.register("summarize", "alpha");
171 r.register("summarize", "beta");
172 r.register("transcribe-audio", "alpha");
173 assert_eq!(r.agents_for("summarize"), vec!["alpha", "beta"]);
174 assert_eq!(r.agents_for("transcribe-audio"), vec!["alpha"]);
175 assert!(r.agents_for("nonexistent").is_empty());
176 assert_eq!(r.all_agents(), vec!["alpha", "beta"]);
177 assert_eq!(r.capability_count(), 2);
178 }
179
180 #[test]
181 fn register_is_idempotent() {
182 let r = AgentCapabilityRegistry::new();
183 r.register("summarize", "alpha");
184 r.register("summarize", "alpha");
185 r.register("summarize", "alpha");
186 assert_eq!(r.agents_for("summarize"), vec!["alpha"]);
187 }
188
189 #[test]
190 fn select_honors_hint() {
191 let r = AgentCapabilityRegistry::new();
192 r.register("summarize", "alpha");
193 r.register("summarize", "beta");
194 r.register("summarize", "gamma");
195 assert_eq!(r.select("summarize", Some("beta")), Some("beta".into()));
196 assert_eq!(
198 r.select("summarize", Some("nonexistent")),
199 Some("alpha".into())
200 );
201 }
202
203 #[test]
204 fn select_uses_mru_when_no_hint() {
205 let r = AgentCapabilityRegistry::new();
206 r.register("summarize", "alpha");
207 r.register("summarize", "beta");
208 assert_eq!(r.select("summarize", None), Some("alpha".into()));
210 r.note_used("summarize", "beta");
212 assert_eq!(r.select("summarize", None), Some("beta".into()));
213 std::thread::sleep(std::time::Duration::from_millis(2));
215 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 std::thread::sleep(std::time::Duration::from_millis(2));
259 r.note_used("summarize", "alpha");
260 assert_eq!(r.select("summarize", None), Some("alpha".into()));
261
262 assert_eq!(r.select("summarize", Some("beta")), Some("beta".into()));
264 }
265}