1use a2a::*;
5use async_trait::async_trait;
6use std::collections::HashMap;
7use tokio::sync::RwLock;
8
9use super::apply_history_length;
10use super::store::{TaskStore, TaskVersion};
11use crate::pagination::resolve_page_size;
12
13struct StoredEntry {
14 task: Task,
15 version: TaskVersion,
16}
17
18pub struct InMemoryTaskStore {
20 tasks: RwLock<HashMap<TaskId, StoredEntry>>,
21}
22
23impl InMemoryTaskStore {
24 pub fn new() -> Self {
25 InMemoryTaskStore {
26 tasks: RwLock::new(HashMap::new()),
27 }
28 }
29}
30
31impl Default for InMemoryTaskStore {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37#[async_trait]
38impl TaskStore for InMemoryTaskStore {
39 async fn create(&self, task: Task) -> Result<TaskVersion, A2AError> {
40 let mut store = self.tasks.write().await;
41 if store.contains_key(&task.id) {
42 return Err(crate::sanitized_internal_error("task already exists"));
43 }
44 let id = task.id.clone();
45 store.insert(id, StoredEntry { task, version: 1 });
46 Ok(1)
47 }
48
49 async fn update(&self, task: Task) -> Result<TaskVersion, A2AError> {
50 let mut store = self.tasks.write().await;
51 let entry = store
52 .get_mut(&task.id)
53 .ok_or_else(|| A2AError::task_not_found(&task.id))?;
54 if entry.task.status.state.is_terminal() && entry.task.status.state != task.status.state {
59 return Err(A2AError::invalid_request(format!(
60 "task {} is in terminal state {:?} and cannot transition to {:?}",
61 task.id, entry.task.status.state, task.status.state
62 )));
63 }
64 entry.version += 1;
65 entry.task = task;
66 Ok(entry.version)
67 }
68
69 async fn begin_cancel(&self, task_id: &str) -> Result<Task, A2AError> {
70 let mut store = self.tasks.write().await;
74 let entry = store
75 .get_mut(task_id)
76 .ok_or_else(|| A2AError::task_not_found(task_id))?;
77 if entry.task.status.state.is_terminal() {
78 return Err(A2AError::task_not_cancelable(task_id));
79 }
80 entry.version += 1;
81 entry.task.status.state = TaskState::Canceled;
82 entry.task.status.timestamp = Some(chrono::Utc::now());
83 Ok(entry.task.clone())
84 }
85
86 async fn get(&self, task_id: &str) -> Result<Option<Task>, A2AError> {
87 let store = self.tasks.read().await;
88 Ok(store.get(task_id).map(|e| e.task.clone()))
89 }
90
91 async fn list(&self, req: &ListTasksRequest) -> Result<ListTasksResponse, A2AError> {
92 let store = self.tasks.read().await;
93 let mut tasks: Vec<Task> = store
94 .values()
95 .filter(|entry| {
96 if let Some(ref ctx_id) = req.context_id {
98 if entry.task.context_id != *ctx_id {
99 return false;
100 }
101 }
102 if let Some(ref status) = req.status {
104 if entry.task.status.state != *status {
105 return false;
106 }
107 }
108 if let Some(after) = req.status_timestamp_after {
112 if entry
113 .task
114 .status
115 .timestamp
116 .is_none_or(|timestamp| timestamp <= after)
117 {
118 return false;
119 }
120 }
121 true
122 })
123 .map(|e| e.task.clone())
124 .collect();
125
126 tasks.sort_by(|a, b| a.id.cmp(&b.id));
128
129 let page_size = resolve_page_size(req.page_size);
134 let start = if let Some(ref token) = req.page_token {
135 token
137 .parse::<usize>()
138 .map_err(|_| A2AError::invalid_params("invalid page token"))?
139 } else {
140 0
141 };
142
143 let total_size = tasks.len();
144 let start = start.min(total_size);
146 let end = start.saturating_add(page_size).min(total_size);
147 let page = tasks[start..end].to_vec();
148
149 let next_page_token = if end < total_size {
150 Some(end.to_string())
151 } else {
152 None
153 };
154
155 let page = page
156 .into_iter()
157 .map(|mut task| {
158 apply_history_length(&mut task, req.history_length);
159 task
160 })
161 .collect();
162
163 Ok(ListTasksResponse {
164 tasks: page,
165 next_page_token: next_page_token.unwrap_or_default(),
166 page_size: page_size as i32,
167 total_size: total_size as i32,
168 })
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn make_task(id: &str, ctx: &str, state: TaskState) -> Task {
177 Task {
178 id: id.to_string(),
179 context_id: ctx.to_string(),
180 status: TaskStatus {
181 state,
182 message: None,
183 timestamp: None,
184 },
185 artifacts: None,
186 history: None,
187 metadata: None,
188 }
189 }
190
191 #[tokio::test]
192 async fn test_create_and_get() {
193 let store = InMemoryTaskStore::new();
194 let task = make_task("t1", "c1", TaskState::Submitted);
195 let ver = store.create(task.clone()).await.unwrap();
196 assert_eq!(ver, 1);
197
198 let got = store.get("t1").await.unwrap().unwrap();
199 assert_eq!(got.id, "t1");
200 assert_eq!(got.context_id, "c1");
201 }
202
203 #[tokio::test]
204 async fn test_create_duplicate() {
205 let store = InMemoryTaskStore::new();
206 let task = make_task("t1", "c1", TaskState::Submitted);
207 store.create(task.clone()).await.unwrap();
208
209 let result = store.create(task).await;
210 assert!(result.is_err());
211 }
212
213 #[tokio::test]
214 async fn test_update() {
215 let store = InMemoryTaskStore::new();
216 let task = make_task("t1", "c1", TaskState::Submitted);
217 store.create(task).await.unwrap();
218
219 let updated = make_task("t1", "c1", TaskState::Working);
220 let ver = store.update(updated).await.unwrap();
221 assert_eq!(ver, 2);
222
223 let got = store.get("t1").await.unwrap().unwrap();
224 assert_eq!(got.status.state, TaskState::Working);
225 }
226
227 #[tokio::test]
228 async fn test_update_not_found() {
229 let store = InMemoryTaskStore::new();
230 let task = make_task("t1", "c1", TaskState::Working);
231 let result = store.update(task).await;
232 assert!(result.is_err());
233 }
234
235 #[tokio::test]
236 async fn test_update_terminal_state_rejected() {
237 let store = InMemoryTaskStore::new();
238 store
239 .create(make_task("t1", "c1", TaskState::Completed))
240 .await
241 .unwrap();
242
243 for state in [
245 TaskState::Submitted,
246 TaskState::Working,
247 TaskState::Canceled,
248 TaskState::Failed,
249 ] {
250 let result = store.update(make_task("t1", "c1", state.clone())).await;
251 assert!(result.is_err(), "expected {state:?} update to be rejected");
252 assert_eq!(
253 result.unwrap_err().code,
254 error_code::INVALID_REQUEST,
255 "unexpected code for {state:?}"
256 );
257 }
258 }
259
260 #[tokio::test]
261 async fn test_update_terminal_same_state_allowed() {
262 let store = InMemoryTaskStore::new();
263 let mut task = make_task("t1", "c1", TaskState::Canceled);
264 store.create(task.clone()).await.unwrap();
265
266 task.status.message = Some(Message::new(Role::Agent, vec![Part::text("after cancel")]));
268 let ver = store.update(task).await.unwrap();
269 assert_eq!(ver, 2);
270 let got = store.get("t1").await.unwrap().unwrap();
271 assert_eq!(got.status.state, TaskState::Canceled);
272 assert!(got.status.message.is_some());
273 }
274
275 #[tokio::test]
276 async fn test_begin_cancel_transitions_non_terminal() {
277 let store = InMemoryTaskStore::new();
278 store
279 .create(make_task("t1", "c1", TaskState::Working))
280 .await
281 .unwrap();
282
283 let task = store.begin_cancel("t1").await.unwrap();
284 assert_eq!(task.status.state, TaskState::Canceled);
285
286 let result = store.begin_cancel("t1").await;
288 assert!(result.is_err());
289 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_CANCELABLE);
290 }
291
292 #[tokio::test]
293 async fn test_begin_cancel_terminal_rejected() {
294 let store = InMemoryTaskStore::new();
295 store
296 .create(make_task("t1", "c1", TaskState::Completed))
297 .await
298 .unwrap();
299 let result = store.begin_cancel("t1").await;
300 assert!(result.is_err());
301 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_CANCELABLE);
302 }
303
304 #[tokio::test]
305 async fn test_begin_cancel_not_found() {
306 let store = InMemoryTaskStore::new();
307 let result = store.begin_cancel("nonexistent").await;
308 assert!(result.is_err());
309 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_FOUND);
310 }
311
312 #[tokio::test]
313 async fn test_get_not_found() {
314 let store = InMemoryTaskStore::new();
315 let got = store.get("nonexistent").await.unwrap();
316 assert!(got.is_none());
317 }
318
319 #[tokio::test]
320 async fn test_list_filter_by_context() {
321 let store = InMemoryTaskStore::new();
322 store
323 .create(make_task("t1", "c1", TaskState::Submitted))
324 .await
325 .unwrap();
326 store
327 .create(make_task("t2", "c2", TaskState::Working))
328 .await
329 .unwrap();
330 store
331 .create(make_task("t3", "c1", TaskState::Completed))
332 .await
333 .unwrap();
334
335 let req = ListTasksRequest {
336 context_id: Some("c1".to_string()),
337 status: None,
338 page_size: None,
339 page_token: None,
340 history_length: None,
341 status_timestamp_after: None,
342 include_artifacts: None,
343 tenant: None,
344 };
345 let resp = store.list(&req).await.unwrap();
346 assert_eq!(resp.tasks.len(), 2);
347 assert_eq!(resp.total_size, 2);
348 }
349
350 #[tokio::test]
351 async fn test_list_filter_by_status() {
352 let store = InMemoryTaskStore::new();
353 store
354 .create(make_task("t1", "c1", TaskState::Submitted))
355 .await
356 .unwrap();
357 store
358 .create(make_task("t2", "c1", TaskState::Working))
359 .await
360 .unwrap();
361
362 let req = ListTasksRequest {
363 context_id: None,
364 status: Some(TaskState::Working),
365 page_size: None,
366 page_token: None,
367 history_length: None,
368 status_timestamp_after: None,
369 include_artifacts: None,
370 tenant: None,
371 };
372 let resp = store.list(&req).await.unwrap();
373 assert_eq!(resp.tasks.len(), 1);
374 assert_eq!(resp.tasks[0].id, "t2");
375 }
376
377 #[tokio::test]
378 async fn test_list_filter_by_status_timestamp_after() {
379 let store = InMemoryTaskStore::new();
380 let cutoff = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
381 .unwrap()
382 .with_timezone(&chrono::Utc);
383
384 let mut old = make_task("old", "c1", TaskState::Completed);
385 old.status.timestamp = Some(cutoff - chrono::Duration::seconds(1));
386 store.create(old).await.unwrap();
387
388 let mut recent = make_task("recent", "c1", TaskState::Completed);
389 recent.status.timestamp = Some(cutoff + chrono::Duration::seconds(1));
390 store.create(recent).await.unwrap();
391
392 store
394 .create(make_task("missing", "c1", TaskState::Completed))
395 .await
396 .unwrap();
397
398 let resp = store
399 .list(&ListTasksRequest {
400 context_id: None,
401 status: None,
402 page_size: None,
403 page_token: None,
404 history_length: None,
405 status_timestamp_after: Some(cutoff),
406 include_artifacts: None,
407 tenant: None,
408 })
409 .await
410 .unwrap();
411
412 assert_eq!(resp.total_size, 1);
413 assert_eq!(
414 resp.tasks
415 .iter()
416 .map(|task| task.id.as_str())
417 .collect::<Vec<_>>(),
418 ["recent"]
419 );
420 }
421
422 #[tokio::test]
423 async fn test_list_pagination() {
424 let store = InMemoryTaskStore::new();
425 for i in 0..5 {
426 store
427 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
428 .await
429 .unwrap();
430 }
431
432 let req = ListTasksRequest {
433 context_id: None,
434 status: None,
435 page_size: Some(2),
436 page_token: None,
437 history_length: None,
438 status_timestamp_after: None,
439 include_artifacts: None,
440 tenant: None,
441 };
442 let resp = store.list(&req).await.unwrap();
443 assert_eq!(resp.tasks.len(), 2);
444 assert_eq!(resp.total_size, 5);
445 assert!(!resp.next_page_token.is_empty());
446
447 let req2 = ListTasksRequest {
449 page_token: Some(resp.next_page_token),
450 page_size: Some(2),
451 ..req
452 };
453 let resp2 = store.list(&req2).await.unwrap();
454 assert_eq!(resp2.tasks.len(), 2);
455 }
456
457 #[tokio::test]
458 async fn test_list_zero_page_size_uses_default_window() {
459 let store = InMemoryTaskStore::new();
460 for i in 0..3 {
461 store
462 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
463 .await
464 .unwrap();
465 }
466
467 let req = ListTasksRequest {
468 context_id: None,
469 status: None,
470 page_size: Some(0),
471 page_token: None,
472 history_length: None,
473 status_timestamp_after: None,
474 include_artifacts: None,
475 tenant: None,
476 };
477 let resp = store.list(&req).await.unwrap();
478 assert_eq!(resp.tasks.len(), 3);
479 assert_eq!(resp.page_size, 50);
480 }
481
482 #[tokio::test]
483 async fn test_list_history_truncation() {
484 let store = InMemoryTaskStore::new();
485 let mut task = make_task("t1", "c1", TaskState::Working);
486 task.history = Some(vec![
487 Message::new(Role::User, vec![Part::text("1")]),
488 Message::new(Role::Agent, vec![Part::text("2")]),
489 Message::new(Role::User, vec![Part::text("3")]),
490 ]);
491 store.create(task).await.unwrap();
492
493 let req = ListTasksRequest {
494 context_id: None,
495 status: None,
496 page_size: None,
497 page_token: None,
498 history_length: Some(1),
499 status_timestamp_after: None,
500 include_artifacts: None,
501 tenant: None,
502 };
503 let resp = store.list(&req).await.unwrap();
504 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 1);
505
506 let empty = store
507 .list(&ListTasksRequest {
508 history_length: Some(-1),
509 ..req
510 })
511 .await
512 .unwrap();
513 assert!(empty.tasks[0].history.as_ref().unwrap().is_empty());
514 }
515
516 fn make_task_with_history(id: &str, messages: Vec<&str>) -> Task {
517 let mut task = make_task(id, "c1", TaskState::Working);
518 task.history = Some(
519 messages
520 .into_iter()
521 .map(|m| Message::new(Role::User, vec![Part::text(m)]))
522 .collect(),
523 );
524 task
525 }
526
527 #[tokio::test]
528 async fn test_list_negative_history_length_returns_empty_history() {
529 let store = InMemoryTaskStore::new();
530 store
531 .create(make_task_with_history("t1", vec!["1", "2", "3"]))
532 .await
533 .unwrap();
534
535 let req = ListTasksRequest {
536 context_id: None,
537 status: None,
538 page_size: None,
539 page_token: None,
540 history_length: Some(-1),
541 status_timestamp_after: None,
542 include_artifacts: None,
543 tenant: None,
544 };
545 let resp = store.list(&req).await.unwrap();
546 assert_eq!(resp.tasks.len(), 1);
547 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 0);
548 }
549
550 #[tokio::test]
551 async fn test_list_page_size_is_capped() {
552 let store = InMemoryTaskStore::new();
553 for i in 0..150 {
554 store
555 .create(make_task(&format!("t{i:03}"), "c1", TaskState::Submitted))
556 .await
557 .unwrap();
558 }
559
560 let req = ListTasksRequest {
561 context_id: None,
562 status: None,
563 page_size: Some(1000),
564 page_token: None,
565 history_length: None,
566 status_timestamp_after: None,
567 include_artifacts: None,
568 tenant: None,
569 };
570 let resp = store.list(&req).await.unwrap();
571 assert_eq!(resp.tasks.len(), 100);
572 assert_eq!(resp.page_size, 100);
573 assert_eq!(resp.total_size, 150);
574 }
575
576 #[tokio::test]
577 async fn test_list_invalid_page_token_errors() {
578 let store = InMemoryTaskStore::new();
579 store
580 .create(make_task("t1", "c1", TaskState::Submitted))
581 .await
582 .unwrap();
583
584 let req = ListTasksRequest {
585 context_id: None,
586 status: None,
587 page_size: None,
588 page_token: Some("not-a-number".into()),
589 history_length: None,
590 status_timestamp_after: None,
591 include_artifacts: None,
592 tenant: None,
593 };
594 let result = store.list(&req).await;
595 assert!(result.is_err());
596 assert_eq!(result.unwrap_err().code, error_code::INVALID_PARAMS);
597 }
598
599 #[tokio::test]
600 async fn test_list_out_of_range_page_token_returns_empty_page() {
601 let store = InMemoryTaskStore::new();
602 for i in 0..3 {
603 store
604 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
605 .await
606 .unwrap();
607 }
608
609 let req = ListTasksRequest {
610 context_id: None,
611 status: None,
612 page_size: None,
613 page_token: Some("999".to_string()),
614 history_length: None,
615 status_timestamp_after: None,
616 include_artifacts: None,
617 tenant: None,
618 };
619 let resp = store.list(&req).await.unwrap();
620
621 assert!(resp.tasks.is_empty());
622 assert!(
623 resp.next_page_token.is_empty(),
624 "must not hand back another token"
625 );
626 assert_eq!(resp.total_size, 3);
627 }
628}