vtcode_acp_client/
discovery.rs1use crate::error::{AcpError, AcpResult};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentInfo {
13 pub id: String,
15
16 pub name: String,
18
19 pub base_url: String,
21
22 pub description: Option<String>,
24
25 pub capabilities: Vec<String>,
27
28 #[serde(default)]
30 pub metadata: HashMap<String, Value>,
31
32 #[serde(default = "default_online")]
34 pub online: bool,
35
36 pub last_seen: Option<String>,
38}
39
40fn default_online() -> bool {
41 true
42}
43
44#[derive(Clone)]
46pub struct AgentRegistry {
47 agents: Arc<RwLock<HashMap<String, AgentInfo>>>,
48}
49
50impl AgentRegistry {
51 pub fn new() -> Self {
53 Self {
54 agents: Arc::new(RwLock::new(HashMap::new())),
55 }
56 }
57
58 pub async fn register(&self, agent: AgentInfo) -> AcpResult<()> {
60 let mut agents = self.agents.write().await;
61 agents.insert(agent.id.clone(), agent);
62 Ok(())
63 }
64
65 pub async fn unregister(&self, agent_id: &str) -> AcpResult<()> {
67 let mut agents = self.agents.write().await;
68 agents.remove(agent_id);
69 Ok(())
70 }
71
72 pub async fn find(&self, agent_id: &str) -> AcpResult<AgentInfo> {
74 let agents = self.agents.read().await;
75 agents
76 .get(agent_id)
77 .cloned()
78 .ok_or_else(|| AcpError::AgentNotFound(agent_id.to_string()))
79 }
80
81 pub async fn find_by_capability(&self, capability: &str) -> AcpResult<Vec<AgentInfo>> {
83 let agents = self.agents.read().await;
84 let matching = agents
85 .values()
86 .filter(|a| a.online && a.capabilities.contains(&capability.to_string()))
87 .cloned()
88 .collect();
89 Ok(matching)
90 }
91
92 pub async fn list_all(&self) -> AcpResult<Vec<AgentInfo>> {
94 let agents = self.agents.read().await;
95 Ok(agents.values().cloned().collect())
96 }
97
98 pub async fn list_online(&self) -> AcpResult<Vec<AgentInfo>> {
100 let agents = self.agents.read().await;
101 Ok(agents.values().filter(|a| a.online).cloned().collect())
102 }
103
104 pub async fn update_status(&self, agent_id: &str, online: bool) -> AcpResult<()> {
106 let mut agents = self.agents.write().await;
107 if let Some(agent) = agents.get_mut(agent_id) {
108 agent.online = online;
109 agent.last_seen = Some(chrono::Utc::now().to_rfc3339());
110 Ok(())
111 } else {
112 Err(AcpError::AgentNotFound(agent_id.to_string()))
113 }
114 }
115
116 pub async fn count(&self) -> usize {
118 self.agents.read().await.len()
119 }
120
121 pub async fn clear(&self) {
123 self.agents.write().await.clear();
124 }
125}
126
127impl Default for AgentRegistry {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[tokio::test]
138 async fn test_agent_registry() {
139 let registry = AgentRegistry::new();
140
141 let agent = AgentInfo {
142 id: "test-agent".to_string(),
143 name: "Test Agent".to_string(),
144 base_url: "http://localhost:8080".to_string(),
145 description: Some("A test agent".to_string()),
146 capabilities: vec!["bash".to_string(), "python".to_string()],
147 metadata: HashMap::new(),
148 online: true,
149 last_seen: None,
150 };
151
152 registry.register(agent.clone()).await.unwrap();
153
154 let found = registry.find("test-agent").await.unwrap();
155 assert_eq!(found.id, "test-agent");
156
157 assert_eq!(registry.count().await, 1);
158
159 registry.unregister("test-agent").await.unwrap();
160 assert_eq!(registry.count().await, 0);
161 }
162
163 #[tokio::test]
164 async fn test_find_by_capability() {
165 let registry = AgentRegistry::new();
166
167 let agent1 = AgentInfo {
168 id: "agent-1".to_string(),
169 name: "Agent 1".to_string(),
170 base_url: "http://localhost:8080".to_string(),
171 description: None,
172 capabilities: vec!["bash".to_string()],
173 metadata: HashMap::new(),
174 online: true,
175 last_seen: None,
176 };
177
178 let agent2 = AgentInfo {
179 id: "agent-2".to_string(),
180 name: "Agent 2".to_string(),
181 base_url: "http://localhost:8081".to_string(),
182 description: None,
183 capabilities: vec!["bash".to_string(), "python".to_string()],
184 metadata: HashMap::new(),
185 online: true,
186 last_seen: None,
187 };
188
189 registry.register(agent1).await.unwrap();
190 registry.register(agent2).await.unwrap();
191
192 let bash_agents = registry.find_by_capability("bash").await.unwrap();
193 assert_eq!(bash_agents.len(), 2);
194
195 let python_agents = registry.find_by_capability("python").await.unwrap();
196 assert_eq!(python_agents.len(), 1);
197 }
198}