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;
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 = match (task.history, history_length) {
55 (Some(msgs), n) if n > 0 => {
56 let n = n as usize;
57 if msgs.len() > n {
58 Some(msgs[msgs.len() - n..].to_vec())
59 } else {
60 Some(msgs)
61 }
62 }
63 _ => None,
64 };
65 }
66
67 self.interceptors.run_after(&call_ctx).await?;
68 Ok(task)
69 })
70 .await;
71
72 let elapsed = start.elapsed();
73 match &result {
74 Ok(_) => {
75 self.metrics.on_response("GetTask");
76 self.metrics.on_latency("GetTask", elapsed);
77 }
78 Err(e) => {
79 self.metrics.on_error("GetTask", e.metric_label());
80 self.metrics.on_latency("GetTask", elapsed);
81 }
82 }
83 result
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use a2a_protocol_types::params::TaskQueryParams;
90 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
91
92 use crate::agent_executor;
93 use crate::builder::RequestHandlerBuilder;
94 use crate::error::ServerError;
95
96 struct DummyExecutor;
97 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
98
99 fn make_completed_task(id: &str) -> Task {
100 Task {
101 id: TaskId::new(id),
102 context_id: ContextId::new("ctx-1"),
103 status: TaskStatus::new(TaskState::Completed),
104 history: None,
105 artifacts: None,
106 metadata: None,
107 }
108 }
109
110 #[tokio::test]
111 async fn get_task_not_found_returns_error() {
112 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
113 let params = TaskQueryParams {
114 tenant: None,
115 id: "nonexistent-task".to_owned(),
116 history_length: None,
117 };
118 let result = handler.on_get_task(params, None).await;
119 assert!(
120 matches!(result, Err(ServerError::TaskNotFound(_))),
121 "expected TaskNotFound for missing task, got: {result:?}"
122 );
123 }
124
125 #[tokio::test]
126 async fn get_task_found_returns_task() {
127 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
128 let task = make_completed_task("t-get-1");
129 handler.task_store.save(&task).await.unwrap();
130
131 let params = TaskQueryParams {
132 tenant: None,
133 id: "t-get-1".to_owned(),
134 history_length: None,
135 };
136 let result = handler.on_get_task(params, None).await;
137 assert!(
138 result.is_ok(),
139 "expected Ok for existing task, got: {result:?}"
140 );
141 assert_eq!(result.unwrap().id, TaskId::new("t-get-1"));
142 }
143
144 #[tokio::test]
145 async fn get_task_error_path_records_metrics() {
146 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
148 let params = TaskQueryParams {
149 tenant: None,
150 id: "nonexistent-metrics".to_owned(),
151 history_length: None,
152 };
153 let result = handler.on_get_task(params, None).await;
154 assert!(
155 matches!(result, Err(ServerError::TaskNotFound(_))),
156 "expected TaskNotFound for error metrics path, got: {result:?}"
157 );
158 }
159
160 fn make_task_with_history(id: &str, num_messages: usize) -> Task {
163 use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
164 let history: Vec<Message> = (0..num_messages)
165 .map(|i| Message {
166 id: MessageId::new(format!("msg-{i}")),
167 role: MessageRole::User,
168 parts: vec![Part::text(format!("message {i}"))],
169 context_id: None,
170 task_id: None,
171 reference_task_ids: None,
172 extensions: None,
173 metadata: None,
174 })
175 .collect();
176 Task {
177 id: TaskId::new(id),
178 context_id: ContextId::new("ctx-hist"),
179 status: TaskStatus::new(TaskState::Completed),
180 history: if history.is_empty() {
181 None
182 } else {
183 Some(history)
184 },
185 artifacts: None,
186 metadata: None,
187 }
188 }
189
190 #[tokio::test]
191 async fn get_task_history_length_zero_returns_no_history() {
192 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
193 handler
194 .task_store
195 .save(&make_task_with_history("t-hl-0", 5))
196 .await
197 .unwrap();
198
199 let params = TaskQueryParams {
200 tenant: None,
201 id: "t-hl-0".to_owned(),
202 history_length: Some(0),
203 };
204 let task = handler.on_get_task(params, None).await.unwrap();
205 assert!(
206 task.history.is_none(),
207 "historyLength=0 should return no history, got: {:?}",
208 task.history
209 );
210 }
211
212 #[tokio::test]
213 async fn get_task_history_length_truncates_to_most_recent() {
214 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
215 handler
216 .task_store
217 .save(&make_task_with_history("t-hl-2", 5))
218 .await
219 .unwrap();
220
221 let params = TaskQueryParams {
222 tenant: None,
223 id: "t-hl-2".to_owned(),
224 history_length: Some(2),
225 };
226 let task = handler.on_get_task(params, None).await.unwrap();
227 let history = task.history.expect("should have history");
228 assert_eq!(history.len(), 2, "historyLength=2 should return 2 messages");
229 assert!(
231 history[0]
232 .parts
233 .iter()
234 .any(|p| p.text_content() == Some("message 3")),
235 "first message should be 'message 3', got: {:?}",
236 history[0].parts
237 );
238 assert!(
239 history[1]
240 .parts
241 .iter()
242 .any(|p| p.text_content() == Some("message 4")),
243 "second message should be 'message 4', got: {:?}",
244 history[1].parts
245 );
246 }
247
248 #[tokio::test]
249 async fn get_task_history_length_larger_than_history_returns_all() {
250 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
251 handler
252 .task_store
253 .save(&make_task_with_history("t-hl-big", 3))
254 .await
255 .unwrap();
256
257 let params = TaskQueryParams {
258 tenant: None,
259 id: "t-hl-big".to_owned(),
260 history_length: Some(100),
261 };
262 let task = handler.on_get_task(params, None).await.unwrap();
263 let history = task.history.expect("should have history");
264 assert_eq!(
265 history.len(),
266 3,
267 "historyLength > actual should return all messages"
268 );
269 }
270
271 #[tokio::test]
272 async fn get_task_no_history_length_returns_full_history() {
273 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
274 handler
275 .task_store
276 .save(&make_task_with_history("t-hl-none", 5))
277 .await
278 .unwrap();
279
280 let params = TaskQueryParams {
281 tenant: None,
282 id: "t-hl-none".to_owned(),
283 history_length: None,
284 };
285 let task = handler.on_get_task(params, None).await.unwrap();
286 let history = task.history.expect("should have history");
287 assert_eq!(
288 history.len(),
289 5,
290 "no historyLength should return all messages"
291 );
292 }
293}