a2a_protocol_server/handler/lifecycle/
list_tasks.rs1use std::collections::HashMap;
9use std::time::Instant;
10
11use a2a_protocol_types::params::ListTasksParams;
12use a2a_protocol_types::responses::TaskListResponse;
13
14use crate::error::ServerResult;
15
16use super::super::RequestHandler;
17use super::super::helpers::{build_call_context, truncate_history};
18
19impl RequestHandler {
20 #[allow(clippy::too_many_lines)]
26 pub async fn on_list_tasks(
27 &self,
28 params: ListTasksParams,
29 headers: Option<&HashMap<String, String>>,
30 ) -> ServerResult<TaskListResponse> {
31 let start = Instant::now();
32 trace_info!(method = "ListTasks", "handling list tasks");
33 self.metrics.on_request("ListTasks");
34
35 let tenant = self
36 .resolve_tenant("ListTasks", headers, params.tenant.as_deref())
37 .await?;
38 let mut params = params;
40 if let Some(ps) = params.page_size {
41 params.page_size = Some(ps.min(1000));
42 }
43 if let Some(ref after) = params.status_timestamp_after
47 && a2a_protocol_types::parse_iso8601_to_unix_millis(after).is_none()
48 {
49 let err = crate::error::ServerError::InvalidParams(format!(
50 "statusTimestampAfter is not a valid ISO 8601 timestamp: {after:?}"
51 ));
52 self.metrics.on_error("ListTasks", err.metric_label());
53 self.metrics.on_latency("ListTasks", start.elapsed());
54 return Err(err);
55 }
56 let history_length = params.history_length;
57 let include_artifacts = params.include_artifacts;
58 let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
59 let call_ctx = build_call_context("ListTasks", headers, self.inbound_trace_policy);
60 let mut call = self.interceptors.begin(&call_ctx);
61 let result = async {
62 call.before().await?;
63 self.ensure_required_extensions(&call_ctx)?;
66 let mut result = self.task_store.list(¶ms).await?;
67
68 if let Some(hl) = history_length {
71 for task in &mut result.tasks {
72 task.history = truncate_history(task.history.take(), hl);
73 }
74 }
75
76 if !include_artifacts.unwrap_or(false) {
79 for task in &mut result.tasks {
80 task.artifacts = None;
81 }
82 }
83
84 Ok(result)
85 }
86 .await;
87 call.finish(result).await
88 })
89 .await;
90
91 let elapsed = start.elapsed();
92 match &result {
93 Ok(_) => {
94 self.metrics.on_response("ListTasks");
95 self.metrics.on_latency("ListTasks", elapsed);
96 }
97 Err(e) => {
98 self.metrics.on_error("ListTasks", e.metric_label());
99 self.metrics.on_latency("ListTasks", elapsed);
100 }
101 }
102 result
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use a2a_protocol_types::params::ListTasksParams;
109 use a2a_protocol_types::responses::TaskListResponse;
110 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
111
112 use crate::agent_executor;
113 use crate::builder::RequestHandlerBuilder;
114
115 struct DummyExecutor;
116 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
117
118 fn make_completed_task(id: &str) -> Task {
119 Task {
120 id: TaskId::new(id),
121 context_id: ContextId::new("ctx-1"),
122 status: TaskStatus::new(TaskState::Completed),
123 history: None,
124 artifacts: None,
125 metadata: None,
126 }
127 }
128
129 fn make_task_with_history(id: &str, len: usize) -> Task {
132 use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
133 let mut task = make_completed_task(id);
134 task.history = Some(
135 (0..len)
136 .map(|i| Message {
137 id: MessageId::new(format!("{id}-m{i}")),
138 role: MessageRole::User,
139 parts: vec![Part::text(format!("message {i}"))],
140 context_id: None,
141 task_id: None,
142 reference_task_ids: None,
143 extensions: None,
144 metadata: None,
145 })
146 .collect(),
147 );
148 task
149 }
150
151 fn history_texts(resp: &TaskListResponse, id: &str) -> Option<Vec<String>> {
157 let task = resp
158 .tasks
159 .iter()
160 .find(|t| t.id.0 == id)
161 .unwrap_or_else(|| panic!("task {id} missing from the listing"));
162 task.history.as_ref().map(|msgs: &Vec<_>| {
163 msgs.iter()
164 .map(|m| {
165 m.parts
166 .iter()
167 .find_map(a2a_protocol_types::message::Part::text_content)
168 .unwrap_or_default()
169 .to_owned()
170 })
171 .collect()
172 })
173 }
174
175 #[tokio::test]
176 async fn list_tasks_empty_store_returns_empty() {
177 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
178 let params = ListTasksParams::default();
179 let result = handler
180 .on_list_tasks(params, None)
181 .await
182 .expect("list_tasks should succeed on empty store");
183 assert!(
184 result.tasks.is_empty(),
185 "listing tasks on an empty store should return an empty list"
186 );
187 }
188
189 #[tokio::test]
190 async fn list_tasks_returns_saved_task() {
191 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
192 let task = make_completed_task("t-list-1");
193 handler.task_store.save(&task).await.unwrap();
194
195 let params = ListTasksParams::default();
196 let result = handler
197 .on_list_tasks(params, None)
198 .await
199 .expect("list_tasks should succeed");
200 assert_eq!(result.tasks.len(), 1, "should return the one saved task");
201 }
202
203 #[tokio::test]
204 async fn list_tasks_invalid_status_timestamp_after_is_invalid_params() {
205 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
206 let params = ListTasksParams {
207 status_timestamp_after: Some("not-a-timestamp".into()),
208 ..Default::default()
209 };
210 let err = handler
211 .on_list_tasks(params, None)
212 .await
213 .expect_err("malformed statusTimestampAfter must be rejected");
214 assert!(
215 matches!(err, crate::error::ServerError::InvalidParams(ref m) if m.contains("statusTimestampAfter")),
216 "expected InvalidParams naming the field, got {err:?}"
217 );
218 }
219
220 #[tokio::test]
221 async fn list_tasks_status_timestamp_after_filters_results() {
222 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
223 let mut old_task = make_completed_task("t-old");
224 old_task.status.timestamp = Some("2026-01-01T00:00:00.000Z".into());
225 let mut new_task = make_completed_task("t-new");
226 new_task.status.timestamp = Some("2026-01-03T00:00:00.000Z".into());
227 handler.task_store.save(&old_task).await.unwrap();
228 handler.task_store.save(&new_task).await.unwrap();
229
230 let params = ListTasksParams {
231 status_timestamp_after: Some("2026-01-02T00:00:00.000Z".into()),
232 ..Default::default()
233 };
234 let result = handler
235 .on_list_tasks(params, None)
236 .await
237 .expect("filtered list must succeed");
238 let ids: Vec<&str> = result.tasks.iter().map(|t| t.id.0.as_str()).collect();
239 assert_eq!(ids, vec!["t-new"], "only strictly-after tasks are returned");
240 }
241
242 #[tokio::test]
243 async fn list_tasks_with_tenant() {
244 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
246 let params = ListTasksParams {
247 tenant: Some("test-tenant".to_string()),
248 ..Default::default()
249 };
250 let result = handler
251 .on_list_tasks(params, None)
252 .await
253 .expect("list_tasks with tenant should succeed");
254 assert_eq!(result.tasks, [] as [a2a_protocol_types::Task; 0]);
255 }
256
257 #[tokio::test]
258 async fn list_tasks_with_headers() {
259 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
261 let params = ListTasksParams::default();
262 let mut headers = std::collections::HashMap::new();
263 headers.insert("authorization".to_string(), "Bearer tok".to_string());
264 let result = handler
265 .on_list_tasks(params, Some(&headers))
266 .await
267 .expect("list_tasks with headers should succeed");
268 assert_eq!(result.tasks, [] as [a2a_protocol_types::Task; 0]);
269 }
270
271 #[tokio::test]
287 async fn list_tasks_history_length_zero_omits_history_from_every_task() {
288 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
289 handler
290 .task_store
291 .save(&make_task_with_history("t-hl0-a", 5))
292 .await
293 .unwrap();
294 handler
295 .task_store
296 .save(&make_task_with_history("t-hl0-b", 5))
297 .await
298 .unwrap();
299
300 let params = ListTasksParams {
301 history_length: Some(0),
302 ..Default::default()
303 };
304 let resp = handler.on_list_tasks(params, None).await.unwrap();
305
306 assert_eq!(history_texts(&resp, "t-hl0-a"), None);
308 assert_eq!(history_texts(&resp, "t-hl0-b"), None);
309 }
310
311 #[tokio::test]
312 async fn list_tasks_history_length_keeps_the_most_recent_for_every_task() {
313 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
314 handler
315 .task_store
316 .save(&make_task_with_history("t-hl2-a", 5))
317 .await
318 .unwrap();
319 handler
320 .task_store
321 .save(&make_task_with_history("t-hl2-b", 5))
322 .await
323 .unwrap();
324
325 let params = ListTasksParams {
326 history_length: Some(2),
327 ..Default::default()
328 };
329 let resp = handler.on_list_tasks(params, None).await.unwrap();
330
331 let expected = Some(vec!["message 3".to_owned(), "message 4".to_owned()]);
334 assert_eq!(history_texts(&resp, "t-hl2-a"), expected);
335 assert_eq!(history_texts(&resp, "t-hl2-b"), expected);
336 }
337
338 #[tokio::test]
339 async fn list_tasks_history_length_above_the_length_returns_all() {
340 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
341 handler
342 .task_store
343 .save(&make_task_with_history("t-hlbig-a", 3))
344 .await
345 .unwrap();
346 handler
347 .task_store
348 .save(&make_task_with_history("t-hlbig-b", 3))
349 .await
350 .unwrap();
351
352 let params = ListTasksParams {
353 history_length: Some(100),
354 ..Default::default()
355 };
356 let resp = handler.on_list_tasks(params, None).await.unwrap();
357
358 let all = Some(vec![
359 "message 0".to_owned(),
360 "message 1".to_owned(),
361 "message 2".to_owned(),
362 ]);
363 assert_eq!(history_texts(&resp, "t-hlbig-a"), all);
364 assert_eq!(history_texts(&resp, "t-hlbig-b"), all);
365 }
366
367 #[tokio::test]
375 async fn list_tasks_without_history_length_leaves_history_intact() {
376 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
377 handler
378 .task_store
379 .save(&make_task_with_history("t-hlnone", 3))
380 .await
381 .unwrap();
382
383 let params = ListTasksParams::default();
384 let resp = handler.on_list_tasks(params, None).await.unwrap();
385
386 assert_eq!(
387 history_texts(&resp, "t-hlnone"),
388 Some(vec![
389 "message 0".to_owned(),
390 "message 1".to_owned(),
391 "message 2".to_owned(),
392 ]),
393 "absent historyLength must not be treated as 0"
394 );
395 }
396
397 #[tokio::test]
398 async fn list_tasks_error_path_records_metrics() {
399 use crate::call_context::CallContext;
401 use crate::interceptor::ServerInterceptor;
402 use std::future::Future;
403 use std::pin::Pin;
404
405 struct FailInterceptor;
406 impl ServerInterceptor for FailInterceptor {
407 fn before<'a>(
408 &'a self,
409 _ctx: &'a CallContext,
410 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
411 {
412 Box::pin(async {
413 Err(a2a_protocol_types::error::A2aError::internal(
414 "forced failure",
415 ))
416 })
417 }
418 fn after<'a>(
419 &'a self,
420 _ctx: &'a CallContext,
421 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
422 {
423 Box::pin(async { Ok(()) })
424 }
425 }
426
427 let handler = RequestHandlerBuilder::new(DummyExecutor)
428 .with_interceptor(FailInterceptor)
429 .build()
430 .unwrap();
431
432 let params = ListTasksParams::default();
433 let result = handler.on_list_tasks(params, None).await;
434 assert!(
435 result.is_err(),
436 "list_tasks should fail when interceptor rejects, got: {result:?}"
437 );
438 }
439}