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.send_intercepted(METHOD, &mut req).await?;
39
40 let resp = ClientResponse {
41 method: METHOD.to_owned(),
42 result,
43 status_code: 200,
44 };
45 self.interceptors.run_after(&resp).await?;
46
47 serde_json::from_value::<Task>(resp.result).map_err(ClientError::Serialization)
48 }
49
50 pub async fn list_tasks(&self, mut params: ListTasksParams) -> ClientResult<TaskListResponse> {
59 const METHOD: &str = "ListTasks";
60
61 params.tenant = self.tenant_or_default(params.tenant.take());
62 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
63
64 let mut req = ClientRequest::new(METHOD, params_value);
65 self.interceptors.run_before(&mut req).await?;
66
67 let result = self.send_intercepted(METHOD, &mut req).await?;
68
69 let resp = ClientResponse {
70 method: METHOD.to_owned(),
71 result,
72 status_code: 200,
73 };
74 self.interceptors.run_after(&resp).await?;
75
76 serde_json::from_value::<TaskListResponse>(resp.result).map_err(ClientError::Serialization)
77 }
78
79 pub async fn cancel_task(&self, id: impl Into<String>) -> ClientResult<Task> {
90 const METHOD: &str = "CancelTask";
91
92 let params = CancelTaskParams {
93 tenant: self.tenant_or_default(None),
94 id: id.into(),
95 metadata: None,
96 };
97 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
98
99 let mut req = ClientRequest::new(METHOD, params_value);
100 self.interceptors.run_before(&mut req).await?;
101
102 let result = self.send_intercepted(METHOD, &mut req).await?;
103
104 let resp = ClientResponse {
105 method: METHOD.to_owned(),
106 result,
107 status_code: 200,
108 };
109 self.interceptors.run_after(&resp).await?;
110
111 serde_json::from_value::<Task>(resp.result).map_err(ClientError::Serialization)
112 }
113
114 pub async fn subscribe_to_task(&self, id: impl Into<String>) -> ClientResult<EventStream> {
129 self.subscribe(id.into(), None).await
130 }
131
132 pub async fn subscribe_to_task_from(
167 &self,
168 id: impl Into<String>,
169 last_event_id: impl Into<String>,
170 ) -> ClientResult<EventStream> {
171 self.subscribe(id.into(), Some(last_event_id.into())).await
172 }
173
174 async fn subscribe(
175 &self,
176 id: String,
177 last_event_id: Option<String>,
178 ) -> ClientResult<EventStream> {
179 const METHOD: &str = "SubscribeToTask";
180
181 let params = TaskIdParams {
182 tenant: self.tenant_or_default(None),
183 id,
184 };
185 let params_value = serde_json::to_value(¶ms).map_err(ClientError::Serialization)?;
186
187 let mut req = ClientRequest::new(METHOD, params_value);
188 if let Some(last_event_id) = last_event_id {
189 req.extra_headers
192 .insert("last-event-id".to_owned(), last_event_id);
193 }
194 self.interceptors.run_before(&mut req).await?;
195
196 let stream = self.send_streaming_intercepted(METHOD, &mut req).await?;
197 let stream = self.configure_stream(stream);
198
199 let resp = ClientResponse {
202 method: METHOD.to_owned(),
203 result: serde_json::Value::Null,
204 status_code: 200,
205 };
206 self.interceptors.run_after(&resp).await?;
207
208 Ok(stream)
209 }
210}
211
212#[cfg(test)]
215mod tests {
216 use std::collections::HashMap;
217 use std::future::Future;
218 use std::pin::Pin;
219
220 use a2a_protocol_types::{ListTasksParams, TaskQueryParams};
221
222 use crate::ClientBuilder;
223 use crate::error::{ClientError, ClientResult};
224 use crate::streaming::EventStream;
225 use crate::transport::Transport;
226
227 struct MockTransport {
230 response: serde_json::Value,
231 }
232
233 impl MockTransport {
234 fn new(response: serde_json::Value) -> Self {
235 Self { response }
236 }
237 }
238
239 impl Transport for MockTransport {
240 fn send_request<'a>(
241 &'a self,
242 _method: &'a str,
243 _params: serde_json::Value,
244 _extra_headers: &'a HashMap<String, String>,
245 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>> {
246 let resp = self.response.clone();
247 Box::pin(async move { Ok(resp) })
248 }
249
250 fn send_streaming_request<'a>(
251 &'a self,
252 _method: &'a str,
253 _params: serde_json::Value,
254 _extra_headers: &'a HashMap<String, String>,
255 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
256 Box::pin(async move {
257 Err(ClientError::Transport(
258 "mock: streaming not supported".into(),
259 ))
260 })
261 }
262 }
263
264 fn make_client(transport: impl Transport) -> crate::A2aClient {
265 ClientBuilder::new("http://localhost:8080")
266 .with_custom_transport(transport)
267 .build()
268 .expect("build client")
269 }
270
271 fn task_json() -> serde_json::Value {
272 serde_json::json!({
273 "id": "task-1",
274 "contextId": "ctx-1",
275 "status": {
276 "state": "TASK_STATE_COMPLETED"
277 }
278 })
279 }
280
281 #[tokio::test]
282 async fn get_task_success() {
283 let transport = MockTransport::new(task_json());
284 let client = make_client(transport);
285
286 let params = TaskQueryParams {
287 tenant: None,
288 id: "task-1".into(),
289 history_length: None,
290 };
291 let task = client.get_task(params).await.unwrap();
292 assert_eq!(task.id.as_ref(), "task-1");
293 }
294
295 #[tokio::test]
296 async fn list_tasks_success() {
297 let response = serde_json::json!({
298 "tasks": [
299 {
300 "id": "task-1",
301 "contextId": "ctx-1",
302 "status": { "state": "TASK_STATE_COMPLETED" }
303 },
304 {
305 "id": "task-2",
306 "contextId": "ctx-2",
307 "status": { "state": "TASK_STATE_WORKING" }
308 }
309 ]
310 });
311 let transport = MockTransport::new(response);
312 let client = make_client(transport);
313
314 let params = ListTasksParams::default();
315 let result = client.list_tasks(params).await.unwrap();
316 assert_eq!(result.tasks.len(), 2);
317 assert_eq!(result.tasks[0].id.as_ref(), "task-1");
318 }
319
320 #[tokio::test]
321 async fn cancel_task_success() {
322 let transport = MockTransport::new(task_json());
323 let client = make_client(transport);
324
325 let task = client.cancel_task("task-1").await.unwrap();
326 assert_eq!(task.id.as_ref(), "task-1");
327 }
328
329 #[allow(clippy::unused_async_trait_impl)]
333 #[tokio::test]
334 async fn subscribe_to_task_calls_after_interceptor() {
335 use std::sync::Arc;
336 use std::sync::atomic::{AtomicUsize, Ordering};
337
338 use crate::interceptor::{CallInterceptor, ClientRequest, ClientResponse};
339
340 struct StreamingOkTransport;
341
342 impl Transport for StreamingOkTransport {
343 fn send_request<'a>(
344 &'a self,
345 _method: &'a str,
346 _params: serde_json::Value,
347 _extra_headers: &'a HashMap<String, String>,
348 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>>
349 {
350 Box::pin(async move { Ok(serde_json::Value::Null) })
351 }
352
353 fn send_streaming_request<'a>(
354 &'a self,
355 _method: &'a str,
356 _params: serde_json::Value,
357 _extra_headers: &'a HashMap<String, String>,
358 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
359 Box::pin(async move {
360 let (tx, rx) = tokio::sync::mpsc::channel(8);
361 drop(tx);
362 Ok(EventStream::new(rx))
363 })
364 }
365 }
366
367 struct CountingInterceptor {
368 before_count: Arc<AtomicUsize>,
369 after_count: Arc<AtomicUsize>,
370 }
371
372 impl CallInterceptor for CountingInterceptor {
373 async fn before<'a>(&'a self, _req: &'a mut ClientRequest) -> ClientResult<()> {
374 self.before_count.fetch_add(1, Ordering::SeqCst);
375 Ok(())
376 }
377 async fn after<'a>(&'a self, _resp: &'a ClientResponse) -> ClientResult<()> {
378 self.after_count.fetch_add(1, Ordering::SeqCst);
379 Ok(())
380 }
381 }
382
383 let before = Arc::new(AtomicUsize::new(0));
384 let after = Arc::new(AtomicUsize::new(0));
385 let interceptor = CountingInterceptor {
386 before_count: Arc::clone(&before),
387 after_count: Arc::clone(&after),
388 };
389
390 let client = ClientBuilder::new("http://localhost:8080")
391 .with_custom_transport(StreamingOkTransport)
392 .with_interceptor(interceptor)
393 .build()
394 .expect("build");
395
396 let result = client.subscribe_to_task("task-1").await;
397 assert!(result.is_ok(), "subscribe should succeed");
398 assert_eq!(before.load(Ordering::SeqCst), 1, "before should be called");
399 assert_eq!(
400 after.load(Ordering::SeqCst),
401 1,
402 "after should be called for subscribe streaming"
403 );
404 }
405
406 #[tokio::test]
407 async fn subscribe_to_task_returns_transport_error() {
408 let transport = MockTransport::new(serde_json::Value::Null);
412 let client = make_client(transport);
413
414 let err = client.subscribe_to_task("task-1").await.unwrap_err();
415 assert!(
416 matches!(err, ClientError::Transport(ref msg) if msg.contains("streaming not supported")),
417 "expected Transport error, got {err:?}"
418 );
419 }
420}