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;
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 = match (task.history.take(), hl) {
71 (Some(msgs), n) if n > 0 => {
72 let n = n as usize;
73 if msgs.len() > n {
74 Some(msgs[msgs.len() - n..].to_vec())
75 } else {
76 Some(msgs)
77 }
78 }
79 _ => None,
80 };
81 }
82 }
83
84 if !include_artifacts.unwrap_or(false) {
87 for task in &mut result.tasks {
88 task.artifacts = None;
89 }
90 }
91
92 self.interceptors.run_after(&call_ctx).await?;
93 Ok(result)
94 })
95 .await;
96
97 let elapsed = start.elapsed();
98 match &result {
99 Ok(_) => {
100 self.metrics.on_response("ListTasks");
101 self.metrics.on_latency("ListTasks", elapsed);
102 }
103 Err(e) => {
104 self.metrics.on_error("ListTasks", e.metric_label());
105 self.metrics.on_latency("ListTasks", elapsed);
106 }
107 }
108 result
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use a2a_protocol_types::params::ListTasksParams;
115 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
116
117 use crate::agent_executor;
118 use crate::builder::RequestHandlerBuilder;
119
120 struct DummyExecutor;
121 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
122
123 fn make_completed_task(id: &str) -> Task {
124 Task {
125 id: TaskId::new(id),
126 context_id: ContextId::new("ctx-1"),
127 status: TaskStatus::new(TaskState::Completed),
128 history: None,
129 artifacts: None,
130 metadata: None,
131 }
132 }
133
134 #[tokio::test]
135 async fn list_tasks_empty_store_returns_empty() {
136 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
137 let params = ListTasksParams::default();
138 let result = handler
139 .on_list_tasks(params, None)
140 .await
141 .expect("list_tasks should succeed on empty store");
142 assert!(
143 result.tasks.is_empty(),
144 "listing tasks on an empty store should return an empty list"
145 );
146 }
147
148 #[tokio::test]
149 async fn list_tasks_returns_saved_task() {
150 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
151 let task = make_completed_task("t-list-1");
152 handler.task_store.save(&task).await.unwrap();
153
154 let params = ListTasksParams::default();
155 let result = handler
156 .on_list_tasks(params, None)
157 .await
158 .expect("list_tasks should succeed");
159 assert_eq!(result.tasks.len(), 1, "should return the one saved task");
160 }
161
162 #[tokio::test]
163 async fn list_tasks_invalid_status_timestamp_after_is_invalid_params() {
164 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
165 let params = ListTasksParams {
166 status_timestamp_after: Some("not-a-timestamp".into()),
167 ..Default::default()
168 };
169 let err = handler
170 .on_list_tasks(params, None)
171 .await
172 .expect_err("malformed statusTimestampAfter must be rejected");
173 assert!(
174 matches!(err, crate::error::ServerError::InvalidParams(ref m) if m.contains("statusTimestampAfter")),
175 "expected InvalidParams naming the field, got {err:?}"
176 );
177 }
178
179 #[tokio::test]
180 async fn list_tasks_status_timestamp_after_filters_results() {
181 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
182 let mut old_task = make_completed_task("t-old");
183 old_task.status.timestamp = Some("2026-01-01T00:00:00.000Z".into());
184 let mut new_task = make_completed_task("t-new");
185 new_task.status.timestamp = Some("2026-01-03T00:00:00.000Z".into());
186 handler.task_store.save(&old_task).await.unwrap();
187 handler.task_store.save(&new_task).await.unwrap();
188
189 let params = ListTasksParams {
190 status_timestamp_after: Some("2026-01-02T00:00:00.000Z".into()),
191 ..Default::default()
192 };
193 let result = handler
194 .on_list_tasks(params, None)
195 .await
196 .expect("filtered list must succeed");
197 let ids: Vec<&str> = result.tasks.iter().map(|t| t.id.0.as_str()).collect();
198 assert_eq!(ids, vec!["t-new"], "only strictly-after tasks are returned");
199 }
200
201 #[tokio::test]
202 async fn list_tasks_with_tenant() {
203 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
205 let params = ListTasksParams {
206 tenant: Some("test-tenant".to_string()),
207 ..Default::default()
208 };
209 let result = handler
210 .on_list_tasks(params, None)
211 .await
212 .expect("list_tasks with tenant should succeed");
213 assert!(result.tasks.is_empty());
214 }
215
216 #[tokio::test]
217 async fn list_tasks_with_headers() {
218 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
220 let params = ListTasksParams::default();
221 let mut headers = std::collections::HashMap::new();
222 headers.insert("authorization".to_string(), "Bearer tok".to_string());
223 let result = handler
224 .on_list_tasks(params, Some(&headers))
225 .await
226 .expect("list_tasks with headers should succeed");
227 assert!(result.tasks.is_empty());
228 }
229
230 #[tokio::test]
231 async fn list_tasks_error_path_records_metrics() {
232 use crate::call_context::CallContext;
234 use crate::interceptor::ServerInterceptor;
235 use std::future::Future;
236 use std::pin::Pin;
237
238 struct FailInterceptor;
239 impl ServerInterceptor for FailInterceptor {
240 fn before<'a>(
241 &'a self,
242 _ctx: &'a CallContext,
243 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
244 {
245 Box::pin(async {
246 Err(a2a_protocol_types::error::A2aError::internal(
247 "forced failure",
248 ))
249 })
250 }
251 fn after<'a>(
252 &'a self,
253 _ctx: &'a CallContext,
254 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
255 {
256 Box::pin(async { Ok(()) })
257 }
258 }
259
260 let handler = RequestHandlerBuilder::new(DummyExecutor)
261 .with_interceptor(FailInterceptor)
262 .build()
263 .unwrap();
264
265 let params = ListTasksParams::default();
266 let result = handler.on_list_tasks(params, None).await;
267 assert!(
268 result.is_err(),
269 "list_tasks should fail when interceptor rejects, got: {result:?}"
270 );
271 }
272}