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