use std::collections::HashMap;
use std::time::Instant;
use a2a_protocol_types::params::ListTasksParams;
use a2a_protocol_types::responses::TaskListResponse;
use crate::error::ServerResult;
use super::super::helpers::{build_call_context, truncate_history};
use super::super::RequestHandler;
impl RequestHandler {
#[allow(clippy::too_many_lines)]
pub async fn on_list_tasks(
&self,
params: ListTasksParams,
headers: Option<&HashMap<String, String>>,
) -> ServerResult<TaskListResponse> {
let start = Instant::now();
trace_info!(method = "ListTasks", "handling list tasks");
self.metrics.on_request("ListTasks");
let tenant = self
.resolve_tenant("ListTasks", headers, params.tenant.as_deref())
.await?;
let mut params = params;
if let Some(ps) = params.page_size {
params.page_size = Some(ps.min(1000));
}
if let Some(ref after) = params.status_timestamp_after {
if a2a_protocol_types::parse_iso8601_to_unix_millis(after).is_none() {
let err = crate::error::ServerError::InvalidParams(format!(
"statusTimestampAfter is not a valid ISO 8601 timestamp: {after:?}"
));
self.metrics.on_error("ListTasks", err.metric_label());
self.metrics.on_latency("ListTasks", start.elapsed());
return Err(err);
}
}
let history_length = params.history_length;
let include_artifacts = params.include_artifacts;
let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
let call_ctx = build_call_context("ListTasks", headers);
self.interceptors.run_before(&call_ctx).await?;
self.ensure_required_extensions(&call_ctx)?;
let mut result = self.task_store.list(¶ms).await?;
if let Some(hl) = history_length {
for task in &mut result.tasks {
task.history = truncate_history(task.history.take(), hl);
}
}
if !include_artifacts.unwrap_or(false) {
for task in &mut result.tasks {
task.artifacts = None;
}
}
self.interceptors.run_after(&call_ctx).await?;
Ok(result)
})
.await;
let elapsed = start.elapsed();
match &result {
Ok(_) => {
self.metrics.on_response("ListTasks");
self.metrics.on_latency("ListTasks", elapsed);
}
Err(e) => {
self.metrics.on_error("ListTasks", e.metric_label());
self.metrics.on_latency("ListTasks", elapsed);
}
}
result
}
}
#[cfg(test)]
mod tests {
use a2a_protocol_types::params::ListTasksParams;
use a2a_protocol_types::responses::TaskListResponse;
use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
use crate::agent_executor;
use crate::builder::RequestHandlerBuilder;
struct DummyExecutor;
agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
fn make_completed_task(id: &str) -> Task {
Task {
id: TaskId::new(id),
context_id: ContextId::new("ctx-1"),
status: TaskStatus::new(TaskState::Completed),
history: None,
artifacts: None,
metadata: None,
}
}
fn make_task_with_history(id: &str, len: usize) -> Task {
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
let mut task = make_completed_task(id);
task.history = Some(
(0..len)
.map(|i| Message {
id: MessageId::new(format!("{id}-m{i}")),
role: MessageRole::User,
parts: vec![Part::text(format!("message {i}"))],
context_id: None,
task_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
})
.collect(),
);
task
}
fn history_texts(resp: &TaskListResponse, id: &str) -> Option<Vec<String>> {
let task = resp
.tasks
.iter()
.find(|t| t.id.0 == id)
.unwrap_or_else(|| panic!("task {id} missing from the listing"));
task.history.as_ref().map(|msgs: &Vec<_>| {
msgs.iter()
.map(|m| {
m.parts
.iter()
.find_map(a2a_protocol_types::message::Part::text_content)
.unwrap_or_default()
.to_owned()
})
.collect()
})
}
#[tokio::test]
async fn list_tasks_empty_store_returns_empty() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let params = ListTasksParams::default();
let result = handler
.on_list_tasks(params, None)
.await
.expect("list_tasks should succeed on empty store");
assert!(
result.tasks.is_empty(),
"listing tasks on an empty store should return an empty list"
);
}
#[tokio::test]
async fn list_tasks_returns_saved_task() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let task = make_completed_task("t-list-1");
handler.task_store.save(&task).await.unwrap();
let params = ListTasksParams::default();
let result = handler
.on_list_tasks(params, None)
.await
.expect("list_tasks should succeed");
assert_eq!(result.tasks.len(), 1, "should return the one saved task");
}
#[tokio::test]
async fn list_tasks_invalid_status_timestamp_after_is_invalid_params() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let params = ListTasksParams {
status_timestamp_after: Some("not-a-timestamp".into()),
..Default::default()
};
let err = handler
.on_list_tasks(params, None)
.await
.expect_err("malformed statusTimestampAfter must be rejected");
assert!(
matches!(err, crate::error::ServerError::InvalidParams(ref m) if m.contains("statusTimestampAfter")),
"expected InvalidParams naming the field, got {err:?}"
);
}
#[tokio::test]
async fn list_tasks_status_timestamp_after_filters_results() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let mut old_task = make_completed_task("t-old");
old_task.status.timestamp = Some("2026-01-01T00:00:00.000Z".into());
let mut new_task = make_completed_task("t-new");
new_task.status.timestamp = Some("2026-01-03T00:00:00.000Z".into());
handler.task_store.save(&old_task).await.unwrap();
handler.task_store.save(&new_task).await.unwrap();
let params = ListTasksParams {
status_timestamp_after: Some("2026-01-02T00:00:00.000Z".into()),
..Default::default()
};
let result = handler
.on_list_tasks(params, None)
.await
.expect("filtered list must succeed");
let ids: Vec<&str> = result.tasks.iter().map(|t| t.id.0.as_str()).collect();
assert_eq!(ids, vec!["t-new"], "only strictly-after tasks are returned");
}
#[tokio::test]
async fn list_tasks_with_tenant() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let params = ListTasksParams {
tenant: Some("test-tenant".to_string()),
..Default::default()
};
let result = handler
.on_list_tasks(params, None)
.await
.expect("list_tasks with tenant should succeed");
assert!(result.tasks.is_empty());
}
#[tokio::test]
async fn list_tasks_with_headers() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let params = ListTasksParams::default();
let mut headers = std::collections::HashMap::new();
headers.insert("authorization".to_string(), "Bearer tok".to_string());
let result = handler
.on_list_tasks(params, Some(&headers))
.await
.expect("list_tasks with headers should succeed");
assert!(result.tasks.is_empty());
}
#[tokio::test]
async fn list_tasks_history_length_zero_omits_history_from_every_task() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
handler
.task_store
.save(&make_task_with_history("t-hl0-a", 5))
.await
.unwrap();
handler
.task_store
.save(&make_task_with_history("t-hl0-b", 5))
.await
.unwrap();
let params = ListTasksParams {
history_length: Some(0),
..Default::default()
};
let resp = handler.on_list_tasks(params, None).await.unwrap();
assert_eq!(history_texts(&resp, "t-hl0-a"), None);
assert_eq!(history_texts(&resp, "t-hl0-b"), None);
}
#[tokio::test]
async fn list_tasks_history_length_keeps_the_most_recent_for_every_task() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
handler
.task_store
.save(&make_task_with_history("t-hl2-a", 5))
.await
.unwrap();
handler
.task_store
.save(&make_task_with_history("t-hl2-b", 5))
.await
.unwrap();
let params = ListTasksParams {
history_length: Some(2),
..Default::default()
};
let resp = handler.on_list_tasks(params, None).await.unwrap();
let expected = Some(vec!["message 3".to_owned(), "message 4".to_owned()]);
assert_eq!(history_texts(&resp, "t-hl2-a"), expected);
assert_eq!(history_texts(&resp, "t-hl2-b"), expected);
}
#[tokio::test]
async fn list_tasks_history_length_above_the_length_returns_all() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
handler
.task_store
.save(&make_task_with_history("t-hlbig-a", 3))
.await
.unwrap();
handler
.task_store
.save(&make_task_with_history("t-hlbig-b", 3))
.await
.unwrap();
let params = ListTasksParams {
history_length: Some(100),
..Default::default()
};
let resp = handler.on_list_tasks(params, None).await.unwrap();
let all = Some(vec![
"message 0".to_owned(),
"message 1".to_owned(),
"message 2".to_owned(),
]);
assert_eq!(history_texts(&resp, "t-hlbig-a"), all);
assert_eq!(history_texts(&resp, "t-hlbig-b"), all);
}
#[tokio::test]
async fn list_tasks_without_history_length_leaves_history_intact() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
handler
.task_store
.save(&make_task_with_history("t-hlnone", 3))
.await
.unwrap();
let params = ListTasksParams::default();
let resp = handler.on_list_tasks(params, None).await.unwrap();
assert_eq!(
history_texts(&resp, "t-hlnone"),
Some(vec![
"message 0".to_owned(),
"message 1".to_owned(),
"message 2".to_owned(),
]),
"absent historyLength must not be treated as 0"
);
}
#[tokio::test]
async fn list_tasks_error_path_records_metrics() {
use crate::call_context::CallContext;
use crate::interceptor::ServerInterceptor;
use std::future::Future;
use std::pin::Pin;
struct FailInterceptor;
impl ServerInterceptor for FailInterceptor {
fn before<'a>(
&'a self,
_ctx: &'a CallContext,
) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
{
Box::pin(async {
Err(a2a_protocol_types::error::A2aError::internal(
"forced failure",
))
})
}
fn after<'a>(
&'a self,
_ctx: &'a CallContext,
) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
{
Box::pin(async { Ok(()) })
}
}
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_interceptor(FailInterceptor)
.build()
.unwrap();
let params = ListTasksParams::default();
let result = handler.on_list_tasks(params, None).await;
assert!(
result.is_err(),
"list_tasks should fail when interceptor rejects, got: {result:?}"
);
}
}