vtcode_acp_client/
client.rs1use crate::discovery::AgentRegistry;
4use crate::error::{AcpError, AcpResult};
5use crate::messages::AcpMessage;
6use reqwest::{Client as HttpClient, StatusCode};
7use serde_json::Value;
8use std::time::Duration;
9use tracing::{debug, trace};
10
11pub struct AcpClient {
13 http_client: HttpClient,
15
16 local_agent_id: String,
18
19 registry: AgentRegistry,
21
22 #[allow(dead_code)]
24 timeout: Duration,
25}
26
27pub struct AcpClientBuilder {
29 local_agent_id: String,
30 timeout: Duration,
31}
32
33impl AcpClientBuilder {
34 pub fn new(local_agent_id: String) -> Self {
36 Self {
37 local_agent_id,
38 timeout: Duration::from_secs(30),
39 }
40 }
41
42 pub fn with_timeout(mut self, timeout: Duration) -> Self {
44 self.timeout = timeout;
45 self
46 }
47
48 pub fn build(self) -> AcpResult<AcpClient> {
50 let http_client = HttpClient::builder().timeout(self.timeout).build()?;
51
52 Ok(AcpClient {
53 http_client,
54 local_agent_id: self.local_agent_id,
55 registry: AgentRegistry::new(),
56 timeout: self.timeout,
57 })
58 }
59}
60
61impl AcpClient {
62 pub fn new(local_agent_id: String) -> AcpResult<Self> {
64 AcpClientBuilder::new(local_agent_id).build()
65 }
66
67 pub fn registry(&self) -> &AgentRegistry {
69 &self.registry
70 }
71
72 pub async fn call_sync(
74 &self,
75 remote_agent_id: &str,
76 action: String,
77 args: Value,
78 ) -> AcpResult<Value> {
79 debug!(
80 remote_agent = remote_agent_id,
81 action = %action,
82 "Sending synchronous request to remote agent"
83 );
84
85 let agent_info = self
86 .registry
87 .find(remote_agent_id)
88 .await
89 .map_err(|_| AcpError::AgentNotFound(remote_agent_id.to_string()))?;
90
91 let message = AcpMessage::request(
92 self.local_agent_id.clone(),
93 remote_agent_id.to_string(),
94 action,
95 args,
96 );
97
98 let response = self.send_request(&agent_info.base_url, &message).await?;
99
100 trace!(
101 remote_agent = remote_agent_id,
102 "Response received from remote agent"
103 );
104
105 Ok(response)
106 }
107
108 pub async fn call_async(
110 &self,
111 remote_agent_id: &str,
112 action: String,
113 args: Value,
114 ) -> AcpResult<String> {
115 debug!(
116 remote_agent = remote_agent_id,
117 action = %action,
118 "Sending asynchronous request to remote agent"
119 );
120
121 let agent_info = self
122 .registry
123 .find(remote_agent_id)
124 .await
125 .map_err(|_| AcpError::AgentNotFound(remote_agent_id.to_string()))?;
126
127 let mut message = AcpMessage::request(
128 self.local_agent_id.clone(),
129 remote_agent_id.to_string(),
130 action,
131 args,
132 );
133
134 if let crate::messages::MessageContent::Request(ref mut req) = message.content {
136 req.sync = false;
137 }
138
139 let _ = self.send_request(&agent_info.base_url, &message).await;
141
142 trace!(
143 remote_agent = remote_agent_id,
144 message_id = %message.id,
145 "Asynchronous request sent"
146 );
147
148 Ok(message.id)
149 }
150
151 async fn send_request(&self, base_url: &str, message: &AcpMessage) -> AcpResult<Value> {
153 let url = format!("{}/messages", base_url.trim_end_matches('/'));
154
155 trace!(url = %url, message_id = %message.id, "Sending ACP message");
156
157 let response = self.http_client.post(&url).json(message).send().await?;
158
159 let status = response.status();
160
161 match status {
162 StatusCode::OK | StatusCode::ACCEPTED => {
163 let body = response.text().await?;
164 trace!(
165 status = %status,
166 body_len = body.len(),
167 "Received ACP response"
168 );
169
170 if body.is_empty() {
171 return Ok(Value::Null);
172 }
173
174 serde_json::from_str(&body).map_err(|e| {
175 AcpError::SerializationError(format!(
176 "Failed to parse response: {}: {}",
177 e, body
178 ))
179 })
180 }
181
182 StatusCode::REQUEST_TIMEOUT => Err(AcpError::Timeout(
183 "Request to remote agent timed out".to_string(),
184 )),
185
186 StatusCode::NOT_FOUND => Err(AcpError::AgentNotFound(
187 "Remote agent endpoint not found".to_string(),
188 )),
189
190 status => {
191 let body = response.text().await.unwrap_or_default();
192 Err(AcpError::RemoteError {
193 agent_id: base_url.to_string(),
194 message: format!("HTTP {}: {}", status.as_u16(), body),
195 code: Some(status.as_u16() as i32),
196 })
197 }
198 }
199 }
200
201 pub async fn discover_agent(&self, base_url: &str) -> AcpResult<crate::discovery::AgentInfo> {
203 let url = format!("{}/metadata", base_url.trim_end_matches('/'));
204
205 trace!(url = %url, "Discovering agent metadata");
206
207 let response = self
208 .http_client
209 .get(&url)
210 .send()
211 .await
212 .map_err(|e| AcpError::NetworkError(format!("Discovery failed: {}", e)))?;
213
214 if !response.status().is_success() {
215 return Err(AcpError::NetworkError(format!(
216 "Discovery failed with status {}",
217 response.status()
218 )));
219 }
220
221 let agent_info = response.json().await?;
222
223 trace!("Agent metadata discovered successfully");
224
225 Ok(agent_info)
226 }
227
228 pub async fn ping(&self, remote_agent_id: &str) -> AcpResult<bool> {
230 let agent_info = self
231 .registry
232 .find(remote_agent_id)
233 .await
234 .map_err(|_| AcpError::AgentNotFound(remote_agent_id.to_string()))?;
235
236 let url = format!("{}/health", agent_info.base_url.trim_end_matches('/'));
237
238 match self.http_client.get(&url).send().await {
239 Ok(response) => {
240 let is_healthy = response.status().is_success();
241 if is_healthy {
242 self.registry
243 .update_status(remote_agent_id, true)
244 .await
245 .ok();
246 }
247 Ok(is_healthy)
248 }
249 Err(_) => {
250 self.registry
251 .update_status(remote_agent_id, false)
252 .await
253 .ok();
254 Ok(false)
255 }
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[tokio::test]
265 async fn test_client_creation() {
266 let client = AcpClient::new("test-agent".to_string()).unwrap();
267 assert_eq!(client.local_agent_id, "test-agent");
268 }
269
270 #[tokio::test]
271 async fn test_client_builder() {
272 let client = AcpClientBuilder::new("test-agent".to_string())
273 .with_timeout(Duration::from_secs(60))
274 .build()
275 .unwrap();
276
277 assert_eq!(client.local_agent_id, "test-agent");
278 assert_eq!(client.timeout, Duration::from_secs(60));
279 }
280}