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