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 true
109 })
110 .map(|e| e.task.clone())
111 .collect();
112
113 tasks.sort_by(|a, b| a.id.cmp(&b.id));
115
116 let page_size = resolve_page_size(req.page_size);
121 let start = if let Some(ref token) = req.page_token {
122 token
124 .parse::<usize>()
125 .map_err(|_| A2AError::invalid_params("invalid page token"))?
126 } else {
127 0
128 };
129
130 let total_size = tasks.len();
131 let start = start.min(total_size);
133 let end = start.saturating_add(page_size).min(total_size);
134 let page = tasks[start..end].to_vec();
135
136 let next_page_token = if end < total_size {
137 Some(end.to_string())
138 } else {
139 None
140 };
141
142 let page = page
143 .into_iter()
144 .map(|mut task| {
145 apply_history_length(&mut task, req.history_length);
146 task
147 })
148 .collect();
149
150 Ok(ListTasksResponse {
151 tasks: page,
152 next_page_token: next_page_token.unwrap_or_default(),
153 page_size: page_size as i32,
154 total_size: total_size as i32,
155 })
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn make_task(id: &str, ctx: &str, state: TaskState) -> Task {
164 Task {
165 id: id.to_string(),
166 context_id: ctx.to_string(),
167 status: TaskStatus {
168 state,
169 message: None,
170 timestamp: None,
171 },
172 artifacts: None,
173 history: None,
174 metadata: None,
175 }
176 }
177
178 #[tokio::test]
179 async fn test_create_and_get() {
180 let store = InMemoryTaskStore::new();
181 let task = make_task("t1", "c1", TaskState::Submitted);
182 let ver = store.create(task.clone()).await.unwrap();
183 assert_eq!(ver, 1);
184
185 let got = store.get("t1").await.unwrap().unwrap();
186 assert_eq!(got.id, "t1");
187 assert_eq!(got.context_id, "c1");
188 }
189
190 #[tokio::test]
191 async fn test_create_duplicate() {
192 let store = InMemoryTaskStore::new();
193 let task = make_task("t1", "c1", TaskState::Submitted);
194 store.create(task.clone()).await.unwrap();
195
196 let result = store.create(task).await;
197 assert!(result.is_err());
198 }
199
200 #[tokio::test]
201 async fn test_update() {
202 let store = InMemoryTaskStore::new();
203 let task = make_task("t1", "c1", TaskState::Submitted);
204 store.create(task).await.unwrap();
205
206 let updated = make_task("t1", "c1", TaskState::Working);
207 let ver = store.update(updated).await.unwrap();
208 assert_eq!(ver, 2);
209
210 let got = store.get("t1").await.unwrap().unwrap();
211 assert_eq!(got.status.state, TaskState::Working);
212 }
213
214 #[tokio::test]
215 async fn test_update_not_found() {
216 let store = InMemoryTaskStore::new();
217 let task = make_task("t1", "c1", TaskState::Working);
218 let result = store.update(task).await;
219 assert!(result.is_err());
220 }
221
222 #[tokio::test]
223 async fn test_update_terminal_state_rejected() {
224 let store = InMemoryTaskStore::new();
225 store
226 .create(make_task("t1", "c1", TaskState::Completed))
227 .await
228 .unwrap();
229
230 for state in [
232 TaskState::Submitted,
233 TaskState::Working,
234 TaskState::Canceled,
235 TaskState::Failed,
236 ] {
237 let result = store.update(make_task("t1", "c1", state.clone())).await;
238 assert!(result.is_err(), "expected {state:?} update to be rejected");
239 assert_eq!(
240 result.unwrap_err().code,
241 error_code::INVALID_REQUEST,
242 "unexpected code for {state:?}"
243 );
244 }
245 }
246
247 #[tokio::test]
248 async fn test_update_terminal_same_state_allowed() {
249 let store = InMemoryTaskStore::new();
250 let mut task = make_task("t1", "c1", TaskState::Canceled);
251 store.create(task.clone()).await.unwrap();
252
253 task.status.message = Some(Message::new(Role::Agent, vec![Part::text("after cancel")]));
255 let ver = store.update(task).await.unwrap();
256 assert_eq!(ver, 2);
257 let got = store.get("t1").await.unwrap().unwrap();
258 assert_eq!(got.status.state, TaskState::Canceled);
259 assert!(got.status.message.is_some());
260 }
261
262 #[tokio::test]
263 async fn test_begin_cancel_transitions_non_terminal() {
264 let store = InMemoryTaskStore::new();
265 store
266 .create(make_task("t1", "c1", TaskState::Working))
267 .await
268 .unwrap();
269
270 let task = store.begin_cancel("t1").await.unwrap();
271 assert_eq!(task.status.state, TaskState::Canceled);
272
273 let result = store.begin_cancel("t1").await;
275 assert!(result.is_err());
276 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_CANCELABLE);
277 }
278
279 #[tokio::test]
280 async fn test_begin_cancel_terminal_rejected() {
281 let store = InMemoryTaskStore::new();
282 store
283 .create(make_task("t1", "c1", TaskState::Completed))
284 .await
285 .unwrap();
286 let result = store.begin_cancel("t1").await;
287 assert!(result.is_err());
288 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_CANCELABLE);
289 }
290
291 #[tokio::test]
292 async fn test_begin_cancel_not_found() {
293 let store = InMemoryTaskStore::new();
294 let result = store.begin_cancel("nonexistent").await;
295 assert!(result.is_err());
296 assert_eq!(result.unwrap_err().code, error_code::TASK_NOT_FOUND);
297 }
298
299 #[tokio::test]
300 async fn test_get_not_found() {
301 let store = InMemoryTaskStore::new();
302 let got = store.get("nonexistent").await.unwrap();
303 assert!(got.is_none());
304 }
305
306 #[tokio::test]
307 async fn test_list_filter_by_context() {
308 let store = InMemoryTaskStore::new();
309 store
310 .create(make_task("t1", "c1", TaskState::Submitted))
311 .await
312 .unwrap();
313 store
314 .create(make_task("t2", "c2", TaskState::Working))
315 .await
316 .unwrap();
317 store
318 .create(make_task("t3", "c1", TaskState::Completed))
319 .await
320 .unwrap();
321
322 let req = ListTasksRequest {
323 context_id: Some("c1".to_string()),
324 status: None,
325 page_size: None,
326 page_token: None,
327 history_length: None,
328 status_timestamp_after: None,
329 include_artifacts: None,
330 tenant: None,
331 };
332 let resp = store.list(&req).await.unwrap();
333 assert_eq!(resp.tasks.len(), 2);
334 assert_eq!(resp.total_size, 2);
335 }
336
337 #[tokio::test]
338 async fn test_list_filter_by_status() {
339 let store = InMemoryTaskStore::new();
340 store
341 .create(make_task("t1", "c1", TaskState::Submitted))
342 .await
343 .unwrap();
344 store
345 .create(make_task("t2", "c1", TaskState::Working))
346 .await
347 .unwrap();
348
349 let req = ListTasksRequest {
350 context_id: None,
351 status: Some(TaskState::Working),
352 page_size: None,
353 page_token: None,
354 history_length: None,
355 status_timestamp_after: None,
356 include_artifacts: None,
357 tenant: None,
358 };
359 let resp = store.list(&req).await.unwrap();
360 assert_eq!(resp.tasks.len(), 1);
361 assert_eq!(resp.tasks[0].id, "t2");
362 }
363
364 #[tokio::test]
365 async fn test_list_pagination() {
366 let store = InMemoryTaskStore::new();
367 for i in 0..5 {
368 store
369 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
370 .await
371 .unwrap();
372 }
373
374 let req = ListTasksRequest {
375 context_id: None,
376 status: None,
377 page_size: Some(2),
378 page_token: None,
379 history_length: None,
380 status_timestamp_after: None,
381 include_artifacts: None,
382 tenant: None,
383 };
384 let resp = store.list(&req).await.unwrap();
385 assert_eq!(resp.tasks.len(), 2);
386 assert_eq!(resp.total_size, 5);
387 assert!(!resp.next_page_token.is_empty());
388
389 let req2 = ListTasksRequest {
391 page_token: Some(resp.next_page_token),
392 page_size: Some(2),
393 ..req
394 };
395 let resp2 = store.list(&req2).await.unwrap();
396 assert_eq!(resp2.tasks.len(), 2);
397 }
398
399 #[tokio::test]
400 async fn test_list_zero_page_size_uses_default_window() {
401 let store = InMemoryTaskStore::new();
402 for i in 0..3 {
403 store
404 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
405 .await
406 .unwrap();
407 }
408
409 let req = ListTasksRequest {
410 context_id: None,
411 status: None,
412 page_size: Some(0),
413 page_token: None,
414 history_length: None,
415 status_timestamp_after: None,
416 include_artifacts: None,
417 tenant: None,
418 };
419 let resp = store.list(&req).await.unwrap();
420 assert_eq!(resp.tasks.len(), 3);
421 assert_eq!(resp.page_size, 50);
422 }
423
424 #[tokio::test]
425 async fn test_list_history_truncation() {
426 let store = InMemoryTaskStore::new();
427 let mut task = make_task("t1", "c1", TaskState::Working);
428 task.history = Some(vec![
429 Message::new(Role::User, vec![Part::text("1")]),
430 Message::new(Role::Agent, vec![Part::text("2")]),
431 Message::new(Role::User, vec![Part::text("3")]),
432 ]);
433 store.create(task).await.unwrap();
434
435 let req = ListTasksRequest {
436 context_id: None,
437 status: None,
438 page_size: None,
439 page_token: None,
440 history_length: Some(1),
441 status_timestamp_after: None,
442 include_artifacts: None,
443 tenant: None,
444 };
445 let resp = store.list(&req).await.unwrap();
446 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 1);
447
448 let empty = store
449 .list(&ListTasksRequest {
450 history_length: Some(-1),
451 ..req
452 })
453 .await
454 .unwrap();
455 assert!(empty.tasks[0].history.as_ref().unwrap().is_empty());
456 }
457
458 fn make_task_with_history(id: &str, messages: Vec<&str>) -> Task {
459 let mut task = make_task(id, "c1", TaskState::Working);
460 task.history = Some(
461 messages
462 .into_iter()
463 .map(|m| Message::new(Role::User, vec![Part::text(m)]))
464 .collect(),
465 );
466 task
467 }
468
469 #[tokio::test]
470 async fn test_list_negative_history_length_returns_empty_history() {
471 let store = InMemoryTaskStore::new();
472 store
473 .create(make_task_with_history("t1", vec!["1", "2", "3"]))
474 .await
475 .unwrap();
476
477 let req = ListTasksRequest {
478 context_id: None,
479 status: None,
480 page_size: None,
481 page_token: None,
482 history_length: Some(-1),
483 status_timestamp_after: None,
484 include_artifacts: None,
485 tenant: None,
486 };
487 let resp = store.list(&req).await.unwrap();
488 assert_eq!(resp.tasks.len(), 1);
489 assert_eq!(resp.tasks[0].history.as_ref().unwrap().len(), 0);
490 }
491
492 #[tokio::test]
493 async fn test_list_page_size_is_capped() {
494 let store = InMemoryTaskStore::new();
495 for i in 0..150 {
496 store
497 .create(make_task(&format!("t{i:03}"), "c1", TaskState::Submitted))
498 .await
499 .unwrap();
500 }
501
502 let req = ListTasksRequest {
503 context_id: None,
504 status: None,
505 page_size: Some(1000),
506 page_token: None,
507 history_length: None,
508 status_timestamp_after: None,
509 include_artifacts: None,
510 tenant: None,
511 };
512 let resp = store.list(&req).await.unwrap();
513 assert_eq!(resp.tasks.len(), 100);
514 assert_eq!(resp.page_size, 100);
515 assert_eq!(resp.total_size, 150);
516 }
517
518 #[tokio::test]
519 async fn test_list_invalid_page_token_errors() {
520 let store = InMemoryTaskStore::new();
521 store
522 .create(make_task("t1", "c1", TaskState::Submitted))
523 .await
524 .unwrap();
525
526 let req = ListTasksRequest {
527 context_id: None,
528 status: None,
529 page_size: None,
530 page_token: Some("not-a-number".into()),
531 history_length: None,
532 status_timestamp_after: None,
533 include_artifacts: None,
534 tenant: None,
535 };
536 let result = store.list(&req).await;
537 assert!(result.is_err());
538 assert_eq!(result.unwrap_err().code, error_code::INVALID_PARAMS);
539 }
540
541 #[tokio::test]
542 async fn test_list_out_of_range_page_token_returns_empty_page() {
543 let store = InMemoryTaskStore::new();
544 for i in 0..3 {
545 store
546 .create(make_task(&format!("t{i}"), "c1", TaskState::Submitted))
547 .await
548 .unwrap();
549 }
550
551 let req = ListTasksRequest {
552 context_id: None,
553 status: None,
554 page_size: None,
555 page_token: Some("999".to_string()),
556 history_length: None,
557 status_timestamp_after: None,
558 include_artifacts: None,
559 tenant: None,
560 };
561 let resp = store.list(&req).await.unwrap();
562
563 assert!(resp.tasks.is_empty());
564 assert!(
565 resp.next_page_token.is_empty(),
566 "must not hand back another token"
567 );
568 assert_eq!(resp.total_size, 3);
569 }
570}