1use crate::a2a::types::{AgentCapabilities, AgentCard, AgentSkill};
8use dashmap::DashMap;
9use uuid::Uuid;
10
11pub struct AgentRegistry {
13 cards: DashMap<String, AgentCard>,
14}
15
16impl AgentRegistry {
17 pub fn new() -> Self {
19 Self {
20 cards: DashMap::new(),
21 }
22 }
23
24 pub fn register(&self, card: AgentCard) {
26 let id = card.name.clone();
27 tracing::info!(agent_id = %id, "Agent registered on bus");
28 self.cards.insert(id, card);
29 }
30
31 pub fn register_ready(&self, agent_id: &str, capabilities: &[String]) -> AgentCard {
36 let skills = capabilities
37 .iter()
38 .map(|capability| {
39 let id = sanitize_skill_id(capability);
40 AgentSkill {
41 id,
42 name: capability.clone(),
43 description: format!("Capability: {capability}"),
44 tags: vec!["protocol".to_string(), "bus".to_string()],
45 examples: vec![],
46 input_modes: vec!["text".to_string()],
47 output_modes: vec!["text".to_string()],
48 }
49 })
50 .collect::<Vec<_>>();
51
52 let card = AgentCard {
53 name: agent_id.to_string(),
54 description: format!("In-process agent {agent_id} (registered via AgentReady)"),
55 url: format!("bus://local/{agent_id}"),
56 version: "ephemeral".to_string(),
57 protocol_version: "0.3.0".to_string(),
58 preferred_transport: Some("BUS".to_string()),
59 additional_interfaces: vec![],
60 capabilities: AgentCapabilities {
61 streaming: true,
62 push_notifications: false,
63 state_transition_history: false,
64 extensions: vec![],
65 },
66 skills,
67 default_input_modes: vec!["text".to_string()],
68 default_output_modes: vec!["text".to_string()],
69 provider: None,
70 icon_url: None,
71 documentation_url: None,
72 security_schemes: Default::default(),
73 security: vec![],
74 supports_authenticated_extended_card: false,
75 signatures: vec![],
76 };
77
78 self.register(card.clone());
79 card
80 }
81
82 pub fn deregister(&self, agent_id: &str) -> Option<AgentCard> {
84 tracing::info!(agent_id = %agent_id, "Agent deregistered from bus");
85 self.cards.remove(agent_id).map(|(_, card)| card)
86 }
87
88 pub fn get(&self, agent_id: &str) -> Option<AgentCard> {
90 self.cards.get(agent_id).map(|r| r.value().clone())
91 }
92
93 pub fn agent_ids(&self) -> Vec<String> {
95 self.cards.iter().map(|r| r.key().clone()).collect()
96 }
97
98 pub fn len(&self) -> usize {
100 self.cards.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
105 self.cards.is_empty()
106 }
107
108 pub fn create_ephemeral(
115 &self,
116 name: impl Into<String>,
117 description: impl Into<String>,
118 skills: Vec<AgentSkill>,
119 ) -> AgentCard {
120 let name = name.into();
121 let card = AgentCard {
122 name: name.clone(),
123 description: description.into(),
124 url: format!("bus://local/{name}"),
125 version: "ephemeral".to_string(),
126 protocol_version: "0.3.0".to_string(),
127 preferred_transport: Some("BUS".to_string()),
128 additional_interfaces: vec![],
129 capabilities: AgentCapabilities {
130 streaming: true,
131 push_notifications: false,
132 state_transition_history: false,
133 extensions: vec![],
134 },
135 skills,
136 default_input_modes: vec!["text".to_string()],
137 default_output_modes: vec!["text".to_string()],
138 provider: None,
139 icon_url: None,
140 documentation_url: None,
141 security_schemes: Default::default(),
142 security: vec![],
143 supports_authenticated_extended_card: false,
144 signatures: vec![],
145 };
146 self.register(card.clone());
147 card
148 }
149
150 pub fn ephemeral_name(prefix: &str) -> String {
152 let short_id = &Uuid::new_v4().to_string()[..8];
153 format!("{prefix}-{short_id}")
154 }
155}
156
157fn sanitize_skill_id(raw: &str) -> String {
158 let mut out = String::with_capacity(raw.len());
159 for ch in raw.chars() {
160 if ch.is_ascii_alphanumeric() {
161 out.push(ch.to_ascii_lowercase());
162 } else if !out.ends_with('-') {
163 out.push('-');
164 }
165 }
166
167 let trimmed = out.trim_matches('-');
168 if trimmed.is_empty() {
169 "capability".to_string()
170 } else {
171 trimmed.to_string()
172 }
173}
174
175impl Default for AgentRegistry {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_register_and_lookup() {
187 let reg = AgentRegistry::new();
188 let card = reg.create_ephemeral("agent-1", "Test agent", vec![]);
189 assert_eq!(reg.len(), 1);
190
191 let found = reg.get("agent-1").unwrap();
192 assert_eq!(found.url, card.url);
193 }
194
195 #[test]
196 fn test_deregister() {
197 let reg = AgentRegistry::new();
198 reg.create_ephemeral("agent-2", "temp", vec![]);
199 assert_eq!(reg.len(), 1);
200
201 let removed = reg.deregister("agent-2");
202 assert!(removed.is_some());
203 assert_eq!(reg.len(), 0);
204 }
205
206 #[test]
207 fn test_ephemeral_name_unique() {
208 let a = AgentRegistry::ephemeral_name("sub");
209 let b = AgentRegistry::ephemeral_name("sub");
210 assert_ne!(a, b);
211 }
212
213 #[test]
214 fn test_agent_ids() {
215 let reg = AgentRegistry::new();
216 reg.create_ephemeral("alpha", "a", vec![]);
217 reg.create_ephemeral("beta", "b", vec![]);
218 let mut ids = reg.agent_ids();
219 ids.sort();
220 assert_eq!(ids, vec!["alpha", "beta"]);
221 }
222
223 #[test]
224 fn test_register_ready_creates_card_with_skills() {
225 let reg = AgentRegistry::new();
226 let caps = vec!["plan.tasks".to_string(), "tool.call".to_string()];
227 let card = reg.register_ready("agent-ready-1", &caps);
228
229 assert_eq!(card.name, "agent-ready-1");
230 assert_eq!(card.url, "bus://local/agent-ready-1");
231 assert_eq!(card.skills.len(), 2);
232 assert!(reg.get("agent-ready-1").is_some());
233 }
234}