1use a2a::*;
4use async_trait::async_trait;
5use std::collections::HashMap;
6use tokio::sync::RwLock;
7
8use super::apply_history_length;
9use super::store::{TaskStore, TaskVersion};
10use crate::pagination::resolve_page_size;
11
12struct StoredEntry {
13 task: Task,
14 version: TaskVersion,
15}
16
17pub struct InMemoryTaskStore {
19 tasks: RwLock<HashMap<TaskId, StoredEntry>>,
20}
21
22impl InMemoryTaskStore {
23 pub fn new() -> Self {
24 InMemoryTaskStore {
25 tasks: RwLock::new(HashMap::new()),
26 }
27 }
28}
29
30impl Default for InMemoryTaskStore {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36#[async_trait]
37impl TaskStore for InMemoryTaskStore {
38 async fn create(&self, task: Task) -> Result<TaskVersion, A2AError> {
39 let mut store = self.tasks.write().await;
40 if store.contains_key(&task.id) {
41 return Err(A2AError::internal("task already exists"));
42 }
43 let id = task.id.clone();
44 store.insert(id, StoredEntry { task, version: 1 });
45 Ok(1)
46 }
47
48 async fn update(&self, task: Task) -> Result<TaskVersion, A2AError> {
49 let mut store = self.tasks.write().await;
50 let entry = store
51 .get_mut(&task.id)
52 .ok_or_else(|| A2AError::task_not_found(&task.id))?;
53 entry.version += 1;
54 entry.task = task;
55 Ok(entry.version)
56 }
57
58 async fn get(&self, task_id: &str) -> Result<Option<Task>, A2AError> {
59 let store = self.tasks.read().await;
60 Ok(store.get(task_id).map(|e| e.task.clone()))
61 }
62
63 async fn list(&self, req: &ListTasksRequest) -> Result<ListTasksResponse, A2AError> {
64 let store = self.tasks.read().await;
65 let mut tasks: Vec<Task> = store
66 .values()
67 .filter(|entry| {
68 if let Some(ref ctx_id) = req.context_id {
70 if entry.task.context_id != *ctx_id {
71 return false;
72 }
73 }
74 if let Some(ref status) = req.status {
76 if entry.task.status.state != *status {
77 return false;
78 }
79 }
80 true
81 })
82 .map(|e| e.task.clone())
83 .collect();
84
85 tasks.sort_by(|a, b| a.id.cmp(&b.id));
87
88 let page_size = resolve_page_size(req.page_size);
93 let start = if let Some(ref token) = req.page_token {
94 token
96 .parse::<usize>()
97 .map_err(|_| A2AError::invalid_params("invalid page token"))?
98 } else {
99 0
100 };
101
102 let total_size = tasks.len();
103 let start = start.min(total_size);
105 let end = start.saturating_add(page_size).min(total_size);
106 let page = tasks[start..end].to_vec();
107
108 let next_page_token = if end < total_size {
109 Some(end.to_string())
110 } else {
111 None
112 };
113
114 let page = page
115 .into_iter()
116 .map(|mut task| {
117 apply_history_length(&mut task, req.history_length);
118 task
119 })
120 .collect();
121
122 Ok(ListTasksResponse {
123 tasks: page,
124 next_page_token: next_page_token.unwrap_or_default(),
125 page_size: page_size as i32,
126 total_size: total_size as i32,
127 })
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn make_task(id: &str, ctx: &str, state: TaskState) -> Task {
136 Task {
137 id: id.to_string(),
138 context_id: ctx.to_string(),
139 status: TaskStatus {
140 state,
141 message: None,
142 timestamp: None,
143 },
144 artifacts: None,
145 history: None,
146 metadata: None,
147 }
148 }
149
150 #[tokio::test]
151 async fn test_create_and_get() {
152 let store = InMemoryTaskStore::new();
153 let task = make_task("t1", "c1", TaskState::Submitted);
154 let ver = store.create(task.clone()).await.unwrap();
155 assert_eq!(ver, 1);
156
157 let got = store.get("t1").await.unwrap().unwrap();
158 assert_eq!(got.id, "t1");
159 assert_eq!(got.context_id, "c1");
160 }
161
162 #[tokio::test]
163 async fn test_create_duplicate() {
164 let store = InMemoryTaskStore::new();
165 let task = make_task("t1", "c1", TaskState::Submitted);
166 store.create(task.clone()).await.unwrap();
167
168 let result = store.create(task).await;
169 assert!(result.is_err());
170 }
171
172 #[tokio::test]
173 async fn test_update() {
174 let store = InMemoryTaskStore::new();
175 let task = make_task("t1", "c1", TaskState::Submitted);
176 store.create(task).await.unwrap();
177
178 let updated = make_task("t1", "c1", TaskState::Working);
179 let ver = store.update(updated).await.unwrap();
180 assert_eq!(ver, 2);
181
182 let got = store.get("t1").await.unwrap().unwrap();
183 assert_eq!(got.status.state, TaskState::Working);
184 }
185
186 #[tokio::test]
187 async fn test_update_not_found() {
188 let store = InMemoryTaskStore::new();
189 let task = make_task("t1", "c1", TaskState::Working);
190 let result = store.update(task).await;
191 assert!(result.is_err());
192 }
193
194 #[tokio::test]
195 async fn test_get_not_found() {
196 let store = InMemoryTaskStore::new();
197 let got = store.get("nonexistent").await.unwrap();
198 assert!(got.is_none());
199 }
200
201 #[tokio::test]
202 async fn test_list_filter_by_context() {
203 let store = InMemoryTaskStore::new();
204 store
205 .create(make_task("t1", "c1", TaskState::Submitted))
206 .await
207 .unwrap();
208 store
209 .create(make_task("t2", "c2", TaskState::Working))
210 .await
211 .unwrap();
212 store
213 .create(make_task("t3", "c1", TaskState::Completed))
214 .await
215 .unwrap();
216
217 let req = ListTasksRequest {
218 context_id: Some("c1".to_string()),
219 status: None,
220 page_size: None,
221 page_token: None,
222 history_length: None,
223 status_timestamp_after: None,
224 include_artifacts: None,
225 tenant: None,
226 };
227 let resp = store.list(&req).await.unwrap();
228 assert_eq!(resp.tasks.len(), 2);
229 assert_eq!(resp.total_size, 2);
230 }
231
232 #[tokio::test]
233 async fn test_list_filter_by_status() {
234 let store = InMemoryTaskStore::new();
235 store
236 .create(make_task("t1", "c1", TaskState::Submitted))
237 .await
238 .unwrap();
239 store
240 .create(make_task("t2", "c1", TaskState::Working))
241 .await
242 .unwrap();
243
244 let req = ListTasksRequest {
245 context_id: None,
246 status: Some(TaskState::Working),
247 page_size: None,
248 page_token: None,
249 history_length: None,
250 status_timestamp_after: None,
251 include_artifacts: None,
252 tenant: None,
253 };
254 let resp = store.list(&req).await.unwrap();
255 assert_eq!(resp.tasks.len(), 1);
256 assert_eq!(resp.tasks[0].id, "t2");
257 }
258
259 #[tokio::test]
260 async fn test_list_pagination() {
261 let store = InMemoryTaskStore::new();
262 for i in 0..5 {
263 store
264 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
265 .await
266 .unwrap();
267 }
268
269 let req = ListTasksRequest {
270 context_id: None,
271 status: None,
272 page_size: Some(2),
273 page_token: None,
274 history_length: None,
275 status_timestamp_after: None,
276 include_artifacts: None,
277 tenant: None,
278 };
279 let resp = store.list(&req).await.unwrap();
280 assert_eq!(resp.tasks.len(), 2);
281 assert_eq!(resp.total_size, 5);
282 assert!(!resp.next_page_token.is_empty());
283
284 let req2 = ListTasksRequest {
286 page_token: Some(resp.next_page_token),
287 page_size: Some(2),
288 ..req
289 };
290 let resp2 = store.list(&req2).await.unwrap();
291 assert_eq!(resp2.tasks.len(), 2);
292 }
293
294 #[tokio::test]
295 async fn test_list_zero_page_size_uses_default_window() {
296 let store = InMemoryTaskStore::new();
297 for i in 0..3 {
298 store
299 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
300 .await
301 .unwrap();
302 }
303
304 let req = ListTasksRequest {
305 context_id: None,
306 status: None,
307 page_size: Some(0),
308 page_token: None,
309 history_length: None,
310 status_timestamp_after: None,
311 include_artifacts: None,
312 tenant: None,
313 };
314 let resp = store.list(&req).await.unwrap();
315 assert_eq!(resp.tasks.len(), 3);
316 assert_eq!(resp.page_size, 50);
317 }
318
319 #[tokio::test]
320 async fn test_list_history_truncation() {
321 let store = InMemoryTaskStore::new();
322 let mut task = make_task("t1", "c1", TaskState::Working);
323 task.history = Some(vec![
324 Message::new(Role::User, vec![Part::text("1")]),
325 Message::new(Role::Agent, vec![Part::text("2")]),
326 Message::new(Role::User, vec![Part::text("3")]),
327 ]);
328 store.create(task).await.unwrap();
329
330 let req = ListTasksRequest {
331 context_id: None,
332 status: None,
333 page_size: None,
334 page_token: None,
335 history_length: Some(1),
336 status_timestamp_after: None,
337 include_artifacts: None,
338 tenant: None,
339 };
340 let resp = store.list(&req).await.unwrap();
341 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 1);
342
343 let empty = store
344 .list(&ListTasksRequest {
345 history_length: Some(-1),
346 ..req
347 })
348 .await
349 .unwrap();
350 assert!(empty.tasks[0].history.as_ref().unwrap().is_empty());
351 }
352
353 fn make_task_with_history(id: &str, messages: Vec<&str>) -> Task {
354 let mut task = make_task(id, "c1", TaskState::Working);
355 task.history = Some(
356 messages
357 .into_iter()
358 .map(|m| Message::new(Role::User, vec![Part::text(m)]))
359 .collect(),
360 );
361 task
362 }
363
364 #[tokio::test]
365 async fn test_list_negative_history_length_returns_empty_history() {
366 let store = InMemoryTaskStore::new();
367 store
368 .create(make_task_with_history("t1", vec!["1", "2", "3"]))
369 .await
370 .unwrap();
371
372 let req = ListTasksRequest {
373 context_id: None,
374 status: None,
375 page_size: None,
376 page_token: None,
377 history_length: Some(-1),
378 status_timestamp_after: None,
379 include_artifacts: None,
380 tenant: None,
381 };
382 let resp = store.list(&req).await.unwrap();
383 assert_eq!(resp.tasks.len(), 1);
384 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 0);
385 }
386
387 #[tokio::test]
388 async fn test_list_page_size_is_capped() {
389 let store = InMemoryTaskStore::new();
390 for i in 0..150 {
391 store
392 .create(make_task(&format!("t{i:03}"), "c1", TaskState::Submitted))
393 .await
394 .unwrap();
395 }
396
397 let req = ListTasksRequest {
398 context_id: None,
399 status: None,
400 page_size: Some(1000),
401 page_token: None,
402 history_length: None,
403 status_timestamp_after: None,
404 include_artifacts: None,
405 tenant: None,
406 };
407 let resp = store.list(&req).await.unwrap();
408 assert_eq!(resp.tasks.len(), 100);
409 assert_eq!(resp.page_size, 100);
410 assert_eq!(resp.total_size, 150);
411 }
412
413 #[tokio::test]
414 async fn test_list_invalid_page_token_errors() {
415 let store = InMemoryTaskStore::new();
416 store
417 .create(make_task("t1", "c1", TaskState::Submitted))
418 .await
419 .unwrap();
420
421 let req = ListTasksRequest {
422 context_id: None,
423 status: None,
424 page_size: None,
425 page_token: Some("not-a-number".into()),
426 history_length: None,
427 status_timestamp_after: None,
428 include_artifacts: None,
429 tenant: None,
430 };
431 let result = store.list(&req).await;
432 assert!(result.is_err());
433 assert_eq!(result.unwrap_err().code, error_code::INVALID_PARAMS);
434 }
435
436 #[tokio::test]
437 async fn test_list_out_of_range_page_token_returns_empty_page() {
438 let store = InMemoryTaskStore::new();
439 for i in 0..3 {
440 store
441 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
442 .await
443 .unwrap();
444 }
445
446 let req = ListTasksRequest {
447 context_id: None,
448 status: None,
449 page_size: None,
450 page_token: Some("999".to_string()),
451 history_length: None,
452 status_timestamp_after: None,
453 include_artifacts: None,
454 tenant: None,
455 };
456 let resp = store.list(&req).await.unwrap();
457
458 assert!(resp.tasks.is_empty());
459 assert!(
460 resp.next_page_token.is_empty(),
461 "must not hand back another token"
462 );
463 assert_eq!(resp.total_size, 3);
464 }
465}