a2a_protocol_server/handler/lifecycle/
get_task.rs1use std::collections::HashMap;
9use std::time::Instant;
10
11use a2a_protocol_types::params::TaskQueryParams;
12use a2a_protocol_types::task::{Task, TaskId};
13
14use crate::error::{ServerError, ServerResult};
15
16use super::super::helpers::{build_call_context, truncate_history};
17use super::super::RequestHandler;
18
19impl RequestHandler {
20 pub async fn on_get_task(
26 &self,
27 params: TaskQueryParams,
28 headers: Option<&HashMap<String, String>>,
29 ) -> ServerResult<Task> {
30 let start = Instant::now();
31 trace_info!(method = "GetTask", task_id = %params.id, "handling get task");
32 self.metrics.on_request("GetTask");
33
34 let tenant = self
35 .resolve_tenant("GetTask", headers, params.tenant.as_deref())
36 .await?;
37 let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
38 let call_ctx = build_call_context("GetTask", headers);
39 self.interceptors.run_before(&call_ctx).await?;
40 self.ensure_required_extensions(&call_ctx)?;
43
44 let task_id = TaskId::new(¶ms.id);
45 let mut task = self
46 .task_store
47 .get(&task_id)
48 .await?
49 .ok_or_else(|| ServerError::TaskNotFound(task_id))?;
50
51 if let Some(history_length) = params.history_length {
54 task.history = truncate_history(task.history, history_length);
55 }
56
57 self.interceptors.run_after(&call_ctx).await?;
58 Ok(task)
59 })
60 .await;
61
62 let elapsed = start.elapsed();
63 match &result {
64 Ok(_) => {
65 self.metrics.on_response("GetTask");
66 self.metrics.on_latency("GetTask", elapsed);
67 }
68 Err(e) => {
69 self.metrics.on_error("GetTask", e.metric_label());
70 self.metrics.on_latency("GetTask", elapsed);
71 }
72 }
73 result
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use a2a_protocol_types::params::TaskQueryParams;
80 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
81
82 use crate::agent_executor;
83 use crate::builder::RequestHandlerBuilder;
84 use crate::error::ServerError;
85
86 struct DummyExecutor;
87 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
88
89 fn make_completed_task(id: &str) -> Task {
90 Task {
91 id: TaskId::new(id),
92 context_id: ContextId::new("ctx-1"),
93 status: TaskStatus::new(TaskState::Completed),
94 history: None,
95 artifacts: None,
96 metadata: None,
97 }
98 }
99
100 #[tokio::test]
101 async fn get_task_not_found_returns_error() {
102 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
103 let params = TaskQueryParams {
104 tenant: None,
105 id: "nonexistent-task".to_owned(),
106 history_length: None,
107 };
108 let result = handler.on_get_task(params, None).await;
109 assert!(
110 matches!(result, Err(ServerError::TaskNotFound(_))),
111 "expected TaskNotFound for missing task, got: {result:?}"
112 );
113 }
114
115 #[tokio::test]
116 async fn get_task_found_returns_task() {
117 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
118 let task = make_completed_task("t-get-1");
119 handler.task_store.save(&task).await.unwrap();
120
121 let params = TaskQueryParams {
122 tenant: None,
123 id: "t-get-1".to_owned(),
124 history_length: None,
125 };
126 let result = handler.on_get_task(params, None).await;
127 assert!(
128 result.is_ok(),
129 "expected Ok for existing task, got: {result:?}"
130 );
131 assert_eq!(result.unwrap().id, TaskId::new("t-get-1"));
132 }
133
134 #[tokio::test]
135 async fn get_task_error_path_records_metrics() {
136 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
138 let params = TaskQueryParams {
139 tenant: None,
140 id: "nonexistent-metrics".to_owned(),
141 history_length: None,
142 };
143 let result = handler.on_get_task(params, None).await;
144 assert!(
145 matches!(result, Err(ServerError::TaskNotFound(_))),
146 "expected TaskNotFound for error metrics path, got: {result:?}"
147 );
148 }
149
150 fn make_task_with_history(id: &str, num_messages: usize) -> Task {
153 use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
154 let history: Vec<Message> = (0..num_messages)
155 .map(|i| Message {
156 id: MessageId::new(format!("msg-{i}")),
157 role: MessageRole::User,
158 parts: vec![Part::text(format!("message {i}"))],
159 context_id: None,
160 task_id: None,
161 reference_task_ids: None,
162 extensions: None,
163 metadata: None,
164 })
165 .collect();
166 Task {
167 id: TaskId::new(id),
168 context_id: ContextId::new("ctx-hist"),
169 status: TaskStatus::new(TaskState::Completed),
170 history: if history.is_empty() {
171 None
172 } else {
173 Some(history)
174 },
175 artifacts: None,
176 metadata: None,
177 }
178 }
179
180 #[tokio::test]
181 async fn get_task_history_length_zero_returns_no_history() {
182 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
183 handler
184 .task_store
185 .save(&make_task_with_history("t-hl-0", 5))
186 .await
187 .unwrap();
188
189 let params = TaskQueryParams {
190 tenant: None,
191 id: "t-hl-0".to_owned(),
192 history_length: Some(0),
193 };
194 let task = handler.on_get_task(params, None).await.unwrap();
195 assert!(
196 task.history.is_none(),
197 "historyLength=0 should return no history, got: {:?}",
198 task.history
199 );
200 }
201
202 #[tokio::test]
203 async fn get_task_history_length_truncates_to_most_recent() {
204 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
205 handler
206 .task_store
207 .save(&make_task_with_history("t-hl-2", 5))
208 .await
209 .unwrap();
210
211 let params = TaskQueryParams {
212 tenant: None,
213 id: "t-hl-2".to_owned(),
214 history_length: Some(2),
215 };
216 let task = handler.on_get_task(params, None).await.unwrap();
217 let history = task.history.expect("should have history");
218 assert_eq!(history.len(), 2, "historyLength=2 should return 2 messages");
219 assert!(
221 history[0]
222 .parts
223 .iter()
224 .any(|p| p.text_content() == Some("message 3")),
225 "first message should be 'message 3', got: {:?}",
226 history[0].parts
227 );
228 assert!(
229 history[1]
230 .parts
231 .iter()
232 .any(|p| p.text_content() == Some("message 4")),
233 "second message should be 'message 4', got: {:?}",
234 history[1].parts
235 );
236 }
237
238 #[tokio::test]
239 async fn get_task_history_length_larger_than_history_returns_all() {
240 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
241 handler
242 .task_store
243 .save(&make_task_with_history("t-hl-big", 3))
244 .await
245 .unwrap();
246
247 let params = TaskQueryParams {
248 tenant: None,
249 id: "t-hl-big".to_owned(),
250 history_length: Some(100),
251 };
252 let task = handler.on_get_task(params, None).await.unwrap();
253 let history = task.history.expect("should have history");
254 assert_eq!(
255 history.len(),
256 3,
257 "historyLength > actual should return all messages"
258 );
259 }
260
261 #[tokio::test]
262 async fn get_task_no_history_length_returns_full_history() {
263 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
264 handler
265 .task_store
266 .save(&make_task_with_history("t-hl-none", 5))
267 .await
268 .unwrap();
269
270 let params = TaskQueryParams {
271 tenant: None,
272 id: "t-hl-none".to_owned(),
273 history_length: None,
274 };
275 let task = handler.on_get_task(params, None).await.unwrap();
276 let history = task.history.expect("should have history");
277 assert_eq!(
278 history.len(),
279 5,
280 "no historyLength should return all messages"
281 );
282 }
283}