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 pub async fn on_list_tasks(
26 &self,
27 params: ListTasksParams,
28 headers: Option<&HashMap<String, String>>,
29 ) -> ServerResult<TaskListResponse> {
30 let start = Instant::now();
31 trace_info!(method = "ListTasks", "handling list tasks");
32 self.metrics.on_request("ListTasks");
33
34 let tenant = params.tenant.clone().unwrap_or_default();
35 let mut params = params;
37 if let Some(ps) = params.page_size {
38 params.page_size = Some(ps.min(1000));
39 }
40 let history_length = params.history_length;
41 let include_artifacts = params.include_artifacts;
42 let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
43 let call_ctx = build_call_context("ListTasks", headers);
44 self.interceptors.run_before(&call_ctx).await?;
45 let mut result = self.task_store.list(¶ms).await?;
46
47 if let Some(hl) = history_length {
50 for task in &mut result.tasks {
51 task.history = match (task.history.take(), hl) {
52 (Some(msgs), n) if n > 0 => {
53 let n = n as usize;
54 if msgs.len() > n {
55 Some(msgs[msgs.len() - n..].to_vec())
56 } else {
57 Some(msgs)
58 }
59 }
60 _ => None,
61 };
62 }
63 }
64
65 if !include_artifacts.unwrap_or(false) {
68 for task in &mut result.tasks {
69 task.artifacts = None;
70 }
71 }
72
73 self.interceptors.run_after(&call_ctx).await?;
74 Ok(result)
75 })
76 .await;
77
78 let elapsed = start.elapsed();
79 match &result {
80 Ok(_) => {
81 self.metrics.on_response("ListTasks");
82 self.metrics.on_latency("ListTasks", elapsed);
83 }
84 Err(e) => {
85 self.metrics.on_error("ListTasks", &e.to_string());
86 self.metrics.on_latency("ListTasks", elapsed);
87 }
88 }
89 result
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use a2a_protocol_types::params::ListTasksParams;
96 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
97
98 use crate::agent_executor;
99 use crate::builder::RequestHandlerBuilder;
100
101 struct DummyExecutor;
102 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
103
104 fn make_completed_task(id: &str) -> Task {
105 Task {
106 id: TaskId::new(id),
107 context_id: ContextId::new("ctx-1"),
108 status: TaskStatus::new(TaskState::Completed),
109 history: None,
110 artifacts: None,
111 metadata: None,
112 }
113 }
114
115 #[tokio::test]
116 async fn list_tasks_empty_store_returns_empty() {
117 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
118 let params = ListTasksParams::default();
119 let result = handler
120 .on_list_tasks(params, None)
121 .await
122 .expect("list_tasks should succeed on empty store");
123 assert!(
124 result.tasks.is_empty(),
125 "listing tasks on an empty store should return an empty list"
126 );
127 }
128
129 #[tokio::test]
130 async fn list_tasks_returns_saved_task() {
131 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
132 let task = make_completed_task("t-list-1");
133 handler.task_store.save(&task).await.unwrap();
134
135 let params = ListTasksParams::default();
136 let result = handler
137 .on_list_tasks(params, None)
138 .await
139 .expect("list_tasks should succeed");
140 assert_eq!(result.tasks.len(), 1, "should return the one saved task");
141 }
142
143 #[tokio::test]
144 async fn list_tasks_with_tenant() {
145 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
147 let params = ListTasksParams {
148 tenant: Some("test-tenant".to_string()),
149 ..Default::default()
150 };
151 let result = handler
152 .on_list_tasks(params, None)
153 .await
154 .expect("list_tasks with tenant should succeed");
155 assert!(result.tasks.is_empty());
156 }
157
158 #[tokio::test]
159 async fn list_tasks_with_headers() {
160 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
162 let params = ListTasksParams::default();
163 let mut headers = std::collections::HashMap::new();
164 headers.insert("authorization".to_string(), "Bearer tok".to_string());
165 let result = handler
166 .on_list_tasks(params, Some(&headers))
167 .await
168 .expect("list_tasks with headers should succeed");
169 assert!(result.tasks.is_empty());
170 }
171
172 #[tokio::test]
173 async fn list_tasks_error_path_records_metrics() {
174 use crate::call_context::CallContext;
176 use crate::interceptor::ServerInterceptor;
177 use std::future::Future;
178 use std::pin::Pin;
179
180 struct FailInterceptor;
181 impl ServerInterceptor for FailInterceptor {
182 fn before<'a>(
183 &'a self,
184 _ctx: &'a CallContext,
185 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
186 {
187 Box::pin(async {
188 Err(a2a_protocol_types::error::A2aError::internal(
189 "forced failure",
190 ))
191 })
192 }
193 fn after<'a>(
194 &'a self,
195 _ctx: &'a CallContext,
196 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
197 {
198 Box::pin(async { Ok(()) })
199 }
200 }
201
202 let handler = RequestHandlerBuilder::new(DummyExecutor)
203 .with_interceptor(FailInterceptor)
204 .build()
205 .unwrap();
206
207 let params = ListTasksParams::default();
208 let result = handler.on_list_tasks(params, None).await;
209 assert!(
210 result.is_err(),
211 "list_tasks should fail when interceptor rejects, got: {result:?}"
212 );
213 }
214}