vtcode_acp_client/
client.rs

1//! HTTP-based ACP client for agent communication
2
3use 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
11/// ACP Client for communicating with remote agents
12pub struct AcpClient {
13    /// HTTP client for requests
14    http_client: HttpClient,
15
16    /// Local agent identifier
17    local_agent_id: String,
18
19    /// Agent discovery registry
20    registry: AgentRegistry,
21
22    /// Request timeout
23    #[allow(dead_code)]
24    timeout: Duration,
25}
26
27/// Builder for ACP client
28pub struct AcpClientBuilder {
29    local_agent_id: String,
30    timeout: Duration,
31}
32
33impl AcpClientBuilder {
34    /// Create a new builder
35    pub fn new(local_agent_id: String) -> Self {
36        Self {
37            local_agent_id,
38            timeout: Duration::from_secs(30),
39        }
40    }
41
42    /// Set request timeout
43    pub fn with_timeout(mut self, timeout: Duration) -> Self {
44        self.timeout = timeout;
45        self
46    }
47
48    /// Build the client
49    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    /// Create a new ACP client with default settings
63    pub fn new(local_agent_id: String) -> AcpResult<Self> {
64        AcpClientBuilder::new(local_agent_id).build()
65    }
66
67    /// Get the agent registry
68    pub fn registry(&self) -> &AgentRegistry {
69        &self.registry
70    }
71
72    /// Send a request to a remote agent synchronously
73    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    /// Send a request to a remote agent asynchronously
109    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        // Set async flag in request
135        if let crate::messages::MessageContent::Request(ref mut req) = message.content {
136            req.sync = false;
137        }
138
139        // Async calls may not wait for response
140        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    /// Send raw ACP message and get response
152    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    /// Discover agent metadata from base URL (offline discovery)
202    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    /// Check if a remote agent is reachable
229    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}