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