a2a_protocol_client/methods/
tasks.rs1use a2a_protocol_types::{
12 CancelTaskParams, ListTasksParams, Task, TaskIdParams, TaskListResponse, TaskQueryParams,
13};
14
15use crate::client::A2aClient;
16use crate::error::{ClientError, ClientResult};
17use crate::interceptor::{ClientRequest, ClientResponse};
18use crate::streaming::EventStream;
19
20impl A2aClient {
21 pub async fn get_task(&self, mut params: TaskQueryParams) -> ClientResult<Task> {
30 const METHOD: &str = "GetTask";
31
32 params.tenant = self.tenant_or_default(params.tenant.take());
33 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
34
35 let mut req = ClientRequest::new(METHOD, params_value);
36 self.interceptors.run_before(&mut req).await?;
37
38 let result = self
39 .transport
40 .send_request(METHOD, req.params, &req.extra_headers)
41 .await?;
42
43 let resp = ClientResponse {
44 method: METHOD.to_owned(),
45 result,
46 status_code: 200,
47 };
48 self.interceptors.run_after(&resp).await?;
49
50 serde_json::from_value::<Task>(resp.result).map_err(ClientError::Serialization)
51 }
52
53 pub async fn list_tasks(&self, mut params: ListTasksParams) -> ClientResult<TaskListResponse> {
62 const METHOD: &str = "ListTasks";
63
64 params.tenant = self.tenant_or_default(params.tenant.take());
65 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
66
67 let mut req = ClientRequest::new(METHOD, params_value);
68 self.interceptors.run_before(&mut req).await?;
69
70 let result = self
71 .transport
72 .send_request(METHOD, req.params, &req.extra_headers)
73 .await?;
74
75 let resp = ClientResponse {
76 method: METHOD.to_owned(),
77 result,
78 status_code: 200,
79 };
80 self.interceptors.run_after(&resp).await?;
81
82 serde_json::from_value::<TaskListResponse>(resp.result).map_err(ClientError::Serialization)
83 }
84
85 pub async fn cancel_task(&self, id: impl Into<String>) -> ClientResult<Task> {
96 const METHOD: &str = "CancelTask";
97
98 let params = CancelTaskParams {
99 tenant: self.tenant_or_default(None),
100 id: id.into(),
101 metadata: None,
102 };
103 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
104
105 let mut req = ClientRequest::new(METHOD, params_value);
106 self.interceptors.run_before(&mut req).await?;
107
108 let result = self
109 .transport
110 .send_request(METHOD, req.params, &req.extra_headers)
111 .await?;
112
113 let resp = ClientResponse {
114 method: METHOD.to_owned(),
115 result,
116 status_code: 200,
117 };
118 self.interceptors.run_after(&resp).await?;
119
120 serde_json::from_value::<Task>(resp.result).map_err(ClientError::Serialization)
121 }
122
123 pub async fn subscribe_to_task(&self, id: impl Into<String>) -> ClientResult<EventStream> {
136 const METHOD: &str = "SubscribeToTask";
137
138 let params = TaskIdParams {
139 tenant: self.tenant_or_default(None),
140 id: id.into(),
141 };
142 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
143
144 let mut req = ClientRequest::new(METHOD, params_value);
145 self.interceptors.run_before(&mut req).await?;
146
147 let stream = self
148 .transport
149 .send_streaming_request(METHOD, req.params, &req.extra_headers)
150 .await?;
151
152 let resp = ClientResponse {
155 method: METHOD.to_owned(),
156 result: serde_json::Value::Null,
157 status_code: 200,
158 };
159 self.interceptors.run_after(&resp).await?;
160
161 Ok(stream)
162 }
163}
164
165#[cfg(test)]
168mod tests {
169 use std::collections::HashMap;
170 use std::future::Future;
171 use std::pin::Pin;
172
173 use a2a_protocol_types::{ListTasksParams, TaskQueryParams};
174
175 use crate::ClientBuilder;
176 use crate::error::{ClientError, ClientResult};
177 use crate::streaming::EventStream;
178 use crate::transport::Transport;
179
180 struct MockTransport {
183 response: serde_json::Value,
184 }
185
186 impl MockTransport {
187 fn new(response: serde_json::Value) -> Self {
188 Self { response }
189 }
190 }
191
192 impl Transport for MockTransport {
193 fn send_request<'a>(
194 &'a self,
195 _method: &'a str,
196 _params: serde_json::Value,
197 _extra_headers: &'a HashMap<String, String>,
198 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>> {
199 let resp = self.response.clone();
200 Box::pin(async move { Ok(resp) })
201 }
202
203 fn send_streaming_request<'a>(
204 &'a self,
205 _method: &'a str,
206 _params: serde_json::Value,
207 _extra_headers: &'a HashMap<String, String>,
208 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
209 Box::pin(async move {
210 Err(ClientError::Transport(
211 "mock: streaming not supported".into(),
212 ))
213 })
214 }
215 }
216
217 fn make_client(transport: impl Transport) -> crate::A2aClient {
218 ClientBuilder::new("http://localhost:8080")
219 .with_custom_transport(transport)
220 .build()
221 .expect("build client")
222 }
223
224 fn task_json() -> serde_json::Value {
225 serde_json::json!({
226 "id": "task-1",
227 "contextId": "ctx-1",
228 "status": {
229 "state": "TASK_STATE_COMPLETED"
230 }
231 })
232 }
233
234 #[tokio::test]
235 async fn get_task_success() {
236 let transport = MockTransport::new(task_json());
237 let client = make_client(transport);
238
239 let params = TaskQueryParams {
240 tenant: None,
241 id: "task-1".into(),
242 history_length: None,
243 };
244 let task = client.get_task(params).await.unwrap();
245 assert_eq!(task.id.as_ref(), "task-1");
246 }
247
248 #[tokio::test]
249 async fn list_tasks_success() {
250 let response = serde_json::json!({
251 "tasks": [
252 {
253 "id": "task-1",
254 "contextId": "ctx-1",
255 "status": { "state": "TASK_STATE_COMPLETED" }
256 },
257 {
258 "id": "task-2",
259 "contextId": "ctx-2",
260 "status": { "state": "TASK_STATE_WORKING" }
261 }
262 ]
263 });
264 let transport = MockTransport::new(response);
265 let client = make_client(transport);
266
267 let params = ListTasksParams::default();
268 let result = client.list_tasks(params).await.unwrap();
269 assert_eq!(result.tasks.len(), 2);
270 assert_eq!(result.tasks[0].id.as_ref(), "task-1");
271 }
272
273 #[tokio::test]
274 async fn cancel_task_success() {
275 let transport = MockTransport::new(task_json());
276 let client = make_client(transport);
277
278 let task = client.cancel_task("task-1").await.unwrap();
279 assert_eq!(task.id.as_ref(), "task-1");
280 }
281
282 #[allow(clippy::unused_async_trait_impl)]
286 #[tokio::test]
287 async fn subscribe_to_task_calls_after_interceptor() {
288 use std::sync::Arc;
289 use std::sync::atomic::{AtomicUsize, Ordering};
290
291 use crate::interceptor::{CallInterceptor, ClientRequest, ClientResponse};
292
293 struct StreamingOkTransport;
294
295 impl Transport for StreamingOkTransport {
296 fn send_request<'a>(
297 &'a self,
298 _method: &'a str,
299 _params: serde_json::Value,
300 _extra_headers: &'a HashMap<String, String>,
301 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>>
302 {
303 Box::pin(async move { Ok(serde_json::Value::Null) })
304 }
305
306 fn send_streaming_request<'a>(
307 &'a self,
308 _method: &'a str,
309 _params: serde_json::Value,
310 _extra_headers: &'a HashMap<String, String>,
311 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
312 Box::pin(async move {
313 let (tx, rx) = tokio::sync::mpsc::channel(8);
314 drop(tx);
315 Ok(EventStream::new(rx))
316 })
317 }
318 }
319
320 struct CountingInterceptor {
321 before_count: Arc<AtomicUsize>,
322 after_count: Arc<AtomicUsize>,
323 }
324
325 impl CallInterceptor for CountingInterceptor {
326 async fn before<'a>(&'a self, _req: &'a mut ClientRequest) -> ClientResult<()> {
327 self.before_count.fetch_add(1, Ordering::SeqCst);
328 Ok(())
329 }
330 async fn after<'a>(&'a self, _resp: &'a ClientResponse) -> ClientResult<()> {
331 self.after_count.fetch_add(1, Ordering::SeqCst);
332 Ok(())
333 }
334 }
335
336 let before = Arc::new(AtomicUsize::new(0));
337 let after = Arc::new(AtomicUsize::new(0));
338 let interceptor = CountingInterceptor {
339 before_count: Arc::clone(&before),
340 after_count: Arc::clone(&after),
341 };
342
343 let client = ClientBuilder::new("http://localhost:8080")
344 .with_custom_transport(StreamingOkTransport)
345 .with_interceptor(interceptor)
346 .build()
347 .expect("build");
348
349 let result = client.subscribe_to_task("task-1").await;
350 assert!(result.is_ok(), "subscribe should succeed");
351 assert_eq!(before.load(Ordering::SeqCst), 1, "before should be called");
352 assert_eq!(
353 after.load(Ordering::SeqCst),
354 1,
355 "after should be called for subscribe streaming"
356 );
357 }
358
359 #[tokio::test]
360 async fn subscribe_to_task_returns_transport_error() {
361 let transport = MockTransport::new(serde_json::Value::Null);
365 let client = make_client(transport);
366
367 let err = client.subscribe_to_task("task-1").await.unwrap_err();
368 assert!(
369 matches!(err, ClientError::Transport(ref msg) if msg.contains("streaming not supported")),
370 "expected Transport error, got {err:?}"
371 );
372 }
373}