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