gestura_core_a2a/
client.rs

1//! A2A HTTP Client
2//!
3//! HTTP client for communicating with A2A-compatible agents.
4
5use super::types::*;
6use gestura_core_foundation::error::AppError;
7use std::time::Duration;
8
9type Result<T> = std::result::Result<T, AppError>;
10
11/// A2A HTTP Client for agent-to-agent communication
12pub struct A2AClient {
13    client: reqwest::Client,
14    auth_token: Option<String>,
15}
16
17impl Default for A2AClient {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl A2AClient {
24    /// Create a new A2A client
25    pub fn new() -> Self {
26        let client = reqwest::Client::builder()
27            .timeout(Duration::from_secs(30))
28            .build()
29            .unwrap_or_default();
30
31        Self {
32            client,
33            auth_token: None,
34        }
35    }
36
37    /// Create a client with authentication token
38    pub fn with_auth(token: impl Into<String>) -> Self {
39        let mut client = Self::new();
40        client.auth_token = Some(token.into());
41        client
42    }
43
44    /// Set authentication token
45    pub fn set_auth_token(&mut self, token: impl Into<String>) {
46        self.auth_token = Some(token.into());
47    }
48
49    /// Send a JSON-RPC request to an A2A endpoint
50    async fn send_request(&self, url: &str, request: A2ARequest) -> Result<A2AResponse> {
51        let mut req_builder = self.client.post(url).json(&request);
52
53        if let Some(ref token) = self.auth_token {
54            req_builder = req_builder.header("Authorization", format!("Bearer {}", token));
55        }
56
57        let response = req_builder
58            .send()
59            .await
60            .map_err(|e| AppError::Io(std::io::Error::other(format!("A2A request failed: {e}"))))?;
61
62        if !response.status().is_success() {
63            let status = response.status();
64            let text = response.text().await.unwrap_or_default();
65            return Err(AppError::Io(std::io::Error::other(format!(
66                "A2A request failed with status {}: {}",
67                status, text
68            ))));
69        }
70
71        response.json::<A2AResponse>().await.map_err(|e| {
72            AppError::Io(std::io::Error::other(format!(
73                "Failed to parse A2A response: {e}"
74            )))
75        })
76    }
77
78    /// Discover an agent at the given URL
79    pub async fn discover(&self, url: &str) -> Result<AgentCard> {
80        let request = A2ARequest::new("agent/discover", serde_json::Value::Null);
81        let response = self.send_request(url, request).await?;
82
83        if let Some(error) = response.error {
84            return Err(AppError::Io(std::io::Error::other(format!(
85                "A2A error {}: {}",
86                error.code, error.message
87            ))));
88        }
89
90        let result = response
91            .result
92            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
93
94        serde_json::from_value(result).map_err(|e| {
95            AppError::Io(std::io::Error::other(format!(
96                "Failed to parse AgentCard: {e}"
97            )))
98        })
99    }
100
101    /// Create a task on a remote agent
102    pub async fn create_task(&self, url: &str, message: &str) -> Result<A2ATask> {
103        let request = CreateTaskRequest::from_message(A2AMessage {
104            role: "user".to_string(),
105            parts: vec![MessagePart::Text {
106                text: message.to_string(),
107            }],
108        });
109
110        self.create_task_with_request(url, request).await
111    }
112
113    /// Create a task with a structured remote contract.
114    pub async fn create_task_with_request(
115        &self,
116        url: &str,
117        task: CreateTaskRequest,
118    ) -> Result<A2ATask> {
119        let params = serde_json::to_value(task).map_err(|e| {
120            AppError::Io(std::io::Error::other(format!(
121                "Failed to serialize A2A task request: {e}"
122            )))
123        })?;
124
125        let request = A2ARequest::new("task/create", params);
126        let response = self.send_request(url, request).await?;
127
128        if let Some(error) = response.error {
129            return Err(AppError::Io(std::io::Error::other(format!(
130                "A2A error {}: {}",
131                error.code, error.message
132            ))));
133        }
134
135        let result = response
136            .result
137            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
138
139        serde_json::from_value(result).map_err(|e| {
140            AppError::Io(std::io::Error::other(format!(
141                "Failed to parse A2ATask: {e}"
142            )))
143        })
144    }
145
146    /// Retry an existing remote task.
147    pub async fn retry_task(&self, url: &str, task_id: &str) -> Result<A2ATask> {
148        let params = serde_json::json!({"taskId": task_id});
149        let request = A2ARequest::new("task/retry", params);
150        let response = self.send_request(url, request).await?;
151
152        if let Some(error) = response.error {
153            return Err(AppError::Io(std::io::Error::other(format!(
154                "A2A error {}: {}",
155                error.code, error.message
156            ))));
157        }
158
159        let result = response
160            .result
161            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
162
163        serde_json::from_value(result).map_err(|e| {
164            AppError::Io(std::io::Error::other(format!(
165                "Failed to parse A2ATask: {e}"
166            )))
167        })
168    }
169
170    /// Send a heartbeat/update for a leased remote task.
171    pub async fn heartbeat_task(
172        &self,
173        url: &str,
174        heartbeat: TaskHeartbeatRequest,
175    ) -> Result<A2ATask> {
176        let params = serde_json::to_value(heartbeat).map_err(|e| {
177            AppError::Io(std::io::Error::other(format!(
178                "Failed to serialize A2A heartbeat request: {e}"
179            )))
180        })?;
181        let request = A2ARequest::new("task/heartbeat", params);
182        let response = self.send_request(url, request).await?;
183
184        if let Some(error) = response.error {
185            return Err(AppError::Io(std::io::Error::other(format!(
186                "A2A error {}: {}",
187                error.code, error.message
188            ))));
189        }
190
191        let result = response
192            .result
193            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
194
195        serde_json::from_value(result).map_err(|e| {
196            AppError::Io(std::io::Error::other(format!(
197                "Failed to parse A2ATask: {e}"
198            )))
199        })
200    }
201
202    /// List artifact manifests for a task.
203    pub async fn list_task_artifacts(
204        &self,
205        url: &str,
206        task_id: &str,
207    ) -> Result<Vec<ArtifactManifestEntry>> {
208        let request = A2ARequest::new("task/artifacts", serde_json::json!({ "taskId": task_id }));
209        let response = self.send_request(url, request).await?;
210        if let Some(error) = response.error {
211            return Err(AppError::Io(std::io::Error::other(format!(
212                "A2A error {}: {}",
213                error.code, error.message
214            ))));
215        }
216        let result = response
217            .result
218            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
219        serde_json::from_value(result).map_err(|e| {
220            AppError::Io(std::io::Error::other(format!(
221                "Failed to parse artifact manifest: {e}"
222            )))
223        })
224    }
225
226    /// Fetch a specific artifact payload for a task.
227    pub async fn fetch_task_artifact(
228        &self,
229        url: &str,
230        task_id: &str,
231        artifact_name: &str,
232    ) -> Result<Artifact> {
233        let request = A2ARequest::new(
234            "task/artifact",
235            serde_json::json!({ "taskId": task_id, "artifactName": artifact_name }),
236        );
237        let response = self.send_request(url, request).await?;
238        if let Some(error) = response.error {
239            return Err(AppError::Io(std::io::Error::other(format!(
240                "A2A error {}: {}",
241                error.code, error.message
242            ))));
243        }
244        let result = response
245            .result
246            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
247        serde_json::from_value(result).map_err(|e| {
248            AppError::Io(std::io::Error::other(format!(
249                "Failed to parse artifact payload: {e}"
250            )))
251        })
252    }
253
254    /// Get task status
255    pub async fn get_task_status(&self, url: &str, task_id: &str) -> Result<A2ATask> {
256        let params = serde_json::json!({"taskId": task_id});
257        let request = A2ARequest::new("task/status", params);
258        let response = self.send_request(url, request).await?;
259
260        if let Some(error) = response.error {
261            return Err(AppError::Io(std::io::Error::other(format!(
262                "A2A error {}: {}",
263                error.code, error.message
264            ))));
265        }
266
267        let result = response
268            .result
269            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
270
271        serde_json::from_value(result).map_err(|e| {
272            AppError::Io(std::io::Error::other(format!(
273                "Failed to parse A2ATask: {e}"
274            )))
275        })
276    }
277
278    /// Cancel a task
279    pub async fn cancel_task(&self, url: &str, task_id: &str) -> Result<A2ATask> {
280        let params = serde_json::json!({"taskId": task_id});
281        let request = A2ARequest::new("task/cancel", params);
282        let response = self.send_request(url, request).await?;
283
284        if let Some(error) = response.error {
285            return Err(AppError::Io(std::io::Error::other(format!(
286                "A2A error {}: {}",
287                error.code, error.message
288            ))));
289        }
290
291        let result = response
292            .result
293            .ok_or_else(|| AppError::Io(std::io::Error::other("No result in A2A response")))?;
294
295        serde_json::from_value(result).map_err(|e| {
296            AppError::Io(std::io::Error::other(format!(
297                "Failed to parse A2ATask: {e}"
298            )))
299        })
300    }
301}