Skip to main content

a2a_server/task_store/
inmemory.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use 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
15/// In-memory task store. Contents do not survive restarts.
16pub 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                // Filter by context_id if provided
67                if let Some(ref ctx_id) = req.context_id {
68                    if entry.task.context_id != *ctx_id {
69                        return false;
70                    }
71                }
72                // Filter by status if provided
73                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        // Sort by ID for deterministic output
84        tasks.sort_by(|a, b| a.id.cmp(&b.id));
85
86        // Apply pagination
87        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            // Simple offset-based pagination
93            token.parse::<usize>().unwrap_or(0)
94        } else {
95            0
96        };
97
98        let total_size = tasks.len();
99        // Make sure start is within bounds
100        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        // Apply history length truncation
111        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        // Get second page
292        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}