1use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14
15use async_trait::async_trait;
16use tokio::sync::RwLock;
17
18use crate::protocol::{A2ATask, A2ATaskResult, TaskFilter};
19
20#[derive(Debug, Clone)]
26pub struct StoredTask {
27 pub task: A2ATask,
29 pub result: Option<A2ATaskResult>,
31 pub error: Option<String>,
33 pub trace_id: Option<String>,
38 pub created_at: Instant,
40 pub updated_at: Instant,
42}
43
44impl StoredTask {
45 pub fn new(task: A2ATask) -> Self {
47 let now = Instant::now();
48 Self {
49 task,
50 result: None,
51 error: None,
52 trace_id: None,
53 created_at: now,
54 updated_at: now,
55 }
56 }
57
58 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
60 self.trace_id = Some(trace_id.into());
61 self
62 }
63
64 pub fn touch(&mut self) {
66 self.updated_at = Instant::now();
67 }
68
69 pub fn age(&self) -> Duration {
71 self.updated_at.elapsed()
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
77#[non_exhaustive]
78pub enum StoreError {
79 #[error("task store unavailable: {0}")]
81 Unavailable(String),
82 #[error("task store capacity exceeded: {0}")]
84 CapacityExceeded(String),
85}
86
87#[async_trait]
96pub trait TaskStore: Send + Sync {
97 async fn upsert(&self, stored: StoredTask) -> Result<(), StoreError>;
99
100 async fn get(&self, task_id: &str) -> Result<Option<StoredTask>, StoreError>;
102
103 async fn list(&self, filter: &TaskFilter) -> Result<Vec<StoredTask>, StoreError>;
105
106 async fn delete(&self, task_id: &str) -> Result<bool, StoreError>;
108
109 async fn compare_and_update(
121 &self,
122 task_id: &str,
123 update: StoredTask,
124 ) -> Result<bool, StoreError> {
125 match self.get(task_id).await? {
126 Some(current) if current.task.status.can_transition_to(&update.task.status) => {
127 self.upsert(update).await?;
128 Ok(true)
129 }
130 _ => Ok(false),
131 }
132 }
133}
134
135pub const DEFAULT_MAX_TASKS: usize = 10_000;
137
138#[derive(Debug, Clone)]
144pub struct InMemoryTaskStore {
145 inner: Arc<RwLock<HashMap<String, StoredTask>>>,
146 max_tasks: usize,
147}
148
149impl InMemoryTaskStore {
150 pub fn new() -> Self {
152 Self::with_max_tasks(DEFAULT_MAX_TASKS)
153 }
154
155 pub fn with_max_tasks(max_tasks: usize) -> Self {
157 Self {
158 inner: Arc::new(RwLock::new(HashMap::new())),
159 max_tasks,
160 }
161 }
162}
163
164impl Default for InMemoryTaskStore {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[async_trait]
171impl TaskStore for InMemoryTaskStore {
172 async fn upsert(&self, stored: StoredTask) -> Result<(), StoreError> {
173 let mut guard = self.inner.write().await;
177 let inserting_new = !guard.contains_key(&stored.task.id);
178 if inserting_new && self.max_tasks > 0 && guard.len() >= self.max_tasks {
179 let oldest_key = guard
181 .iter()
182 .min_by_key(|(_, t)| t.updated_at)
183 .map(|(k, _)| k.clone());
184 if let Some(key) = oldest_key {
185 guard.remove(&key);
186 }
187 }
188 guard.insert(stored.task.id.clone(), stored);
189 Ok(())
190 }
191
192 async fn get(&self, task_id: &str) -> Result<Option<StoredTask>, StoreError> {
193 Ok(self.inner.read().await.get(task_id).cloned())
194 }
195
196 async fn list(&self, filter: &TaskFilter) -> Result<Vec<StoredTask>, StoreError> {
197 let guard = self.inner.read().await;
198 let mut out: Vec<StoredTask> = guard
199 .values()
200 .filter(|t| filter.matches(&t.task))
201 .cloned()
202 .collect();
203 out.sort_by_key(|t| t.created_at);
205 Ok(out)
206 }
207
208 async fn delete(&self, task_id: &str) -> Result<bool, StoreError> {
209 Ok(self.inner.write().await.remove(task_id).is_some())
210 }
211
212 async fn compare_and_update(
213 &self,
214 task_id: &str,
215 update: StoredTask,
216 ) -> Result<bool, StoreError> {
217 let mut guard = self.inner.write().await;
222 match guard.get(task_id) {
223 Some(current) if current.task.status.can_transition_to(&update.task.status) => {
224 guard.insert(task_id.to_string(), update);
225 Ok(true)
226 }
227 _ => Ok(false),
228 }
229 }
230}
231
232pub fn in_memory_store() -> Arc<dyn TaskStore> {
234 Arc::new(InMemoryTaskStore::new())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::protocol::{A2AMessage, TaskStatus};
241
242 fn sample_task(id: &str, status: TaskStatus) -> A2ATask {
243 A2ATask::new(id, A2AMessage::user("hi")).with_status(status)
244 }
245
246 #[tokio::test]
247 async fn upsert_get_roundtrip() {
248 let store = InMemoryTaskStore::new();
249 let mut stored = StoredTask::new(sample_task("t1", TaskStatus::Working));
250 stored.result = Some(A2ATaskResult::new("done"));
251 store.upsert(stored).await.unwrap();
252
253 let got = store.get("t1").await.unwrap().expect("task present");
254 assert_eq!(got.task.id, "t1");
255 assert_eq!(got.result.as_ref().unwrap().output, "done");
256 assert_eq!(got.task.status, TaskStatus::Working);
257 assert_eq!(got.created_at, got.updated_at);
258 }
259
260 #[tokio::test]
261 async fn upsert_updates_existing_in_place() {
262 let store = InMemoryTaskStore::new();
263 store
264 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Submitted)))
265 .await
266 .unwrap();
267 store
268 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Completed)))
269 .await
270 .unwrap();
271
272 let got = store.get("t1").await.unwrap().unwrap();
273 assert_eq!(got.task.status, TaskStatus::Completed);
274 }
275
276 #[tokio::test]
277 async fn get_missing_returns_none() {
278 let store = InMemoryTaskStore::new();
279 assert!(store.get("nope").await.unwrap().is_none());
280 }
281
282 #[tokio::test]
283 async fn list_filters_by_owner_and_status() {
284 let store = InMemoryTaskStore::new();
285 store
286 .upsert(StoredTask::new(
287 sample_task("t1", TaskStatus::Working).with_owner("a"),
288 ))
289 .await
290 .unwrap();
291 store
292 .upsert(StoredTask::new(
293 sample_task("t2", TaskStatus::Completed).with_owner("a"),
294 ))
295 .await
296 .unwrap();
297 store
298 .upsert(StoredTask::new(
299 sample_task("t3", TaskStatus::Working).with_owner("b"),
300 ))
301 .await
302 .unwrap();
303
304 let all = store.list(&TaskFilter::new()).await.unwrap();
305 assert_eq!(all.len(), 3);
306
307 let only_a = store
308 .list(&TaskFilter::new().with_owner("a"))
309 .await
310 .unwrap();
311 assert_eq!(only_a.len(), 2);
312
313 let a_working = store
314 .list(
315 &TaskFilter::new()
316 .with_owner("a")
317 .with_statuses(vec![TaskStatus::Working]),
318 )
319 .await
320 .unwrap();
321 assert_eq!(a_working.len(), 1);
322 assert_eq!(a_working[0].task.id, "t1");
323 }
324
325 #[tokio::test]
326 async fn delete_removes_and_reports() {
327 let store = InMemoryTaskStore::new();
328 store
329 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Submitted)))
330 .await
331 .unwrap();
332
333 assert!(store.delete("t1").await.unwrap());
334 assert!(!store.delete("t1").await.unwrap());
335 assert!(store.get("t1").await.unwrap().is_none());
336 }
337
338 #[tokio::test]
339 async fn evicts_oldest_when_full() {
340 let store = InMemoryTaskStore::with_max_tasks(2);
341 store
342 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Submitted)))
343 .await
344 .unwrap();
345 store
346 .upsert(StoredTask::new(sample_task("t2", TaskStatus::Submitted)))
347 .await
348 .unwrap();
349 store
351 .upsert(StoredTask::new(sample_task("t3", TaskStatus::Submitted)))
352 .await
353 .unwrap();
354
355 assert!(store.get("t1").await.unwrap().is_none());
356 assert!(store.get("t2").await.unwrap().is_some());
357 assert!(store.get("t3").await.unwrap().is_some());
358 }
359
360 #[tokio::test]
361 async fn touch_bumps_updated_at() {
362 let store = InMemoryTaskStore::new();
363 store
364 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Submitted)))
365 .await
366 .unwrap();
367 let mut stored = store.get("t1").await.unwrap().unwrap();
368 stored.touch();
369 assert!(stored.updated_at >= stored.created_at);
370 }
371
372 #[tokio::test]
373 async fn store_is_clone_shareable() {
374 let store = InMemoryTaskStore::new();
375 let clone = store.clone();
376 store
377 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Submitted)))
378 .await
379 .unwrap();
380 assert!(clone.get("t1").await.unwrap().is_some());
381 }
382
383 #[tokio::test]
384 async fn compare_and_update_rejects_stale_status() {
385 let store = InMemoryTaskStore::new();
388 store
389 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Working)))
390 .await
391 .unwrap();
392 let mut stale = store.get("t1").await.unwrap().unwrap();
393
394 let mut cancelled = store.get("t1").await.unwrap().unwrap();
396 cancelled.task.status = TaskStatus::Cancelled;
397 cancelled.touch();
398 store.upsert(cancelled).await.unwrap();
399
400 stale.task.status = TaskStatus::Completed;
401 stale.touch();
402 assert!(!store.compare_and_update("t1", stale).await.unwrap());
403 assert_eq!(
404 store.get("t1").await.unwrap().unwrap().task.status,
405 TaskStatus::Cancelled
406 );
407 }
408
409 #[tokio::test]
410 async fn compare_and_update_applies_valid_transition() {
411 let store = InMemoryTaskStore::new();
412 store
413 .upsert(StoredTask::new(sample_task("t1", TaskStatus::Working)))
414 .await
415 .unwrap();
416 let mut done = store.get("t1").await.unwrap().unwrap();
417 done.task.status = TaskStatus::Completed;
418 done.touch();
419 assert!(store.compare_and_update("t1", done).await.unwrap());
420 assert_eq!(
421 store.get("t1").await.unwrap().unwrap().task.status,
422 TaskStatus::Completed
423 );
424 }
425
426 #[tokio::test]
427 async fn compare_and_update_missing_task_is_noop() {
428 let store = InMemoryTaskStore::new();
429 let update = StoredTask::new(sample_task("ghost", TaskStatus::Completed));
430 assert!(!store.compare_and_update("ghost", update).await.unwrap());
431 assert!(store.get("ghost").await.unwrap().is_none());
432 }
433}