1use std::collections::HashMap;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use tokio::sync::Mutex; use crate::adapter::business::push_notification::{
12 PushNotificationRegistry, PushNotificationSender,
13};
14
15#[cfg(feature = "http-client")]
16use crate::adapter::business::push_notification::HttpPushNotificationSender;
17#[cfg(not(feature = "http-client"))]
18use crate::adapter::business::push_notification::NoopPushNotificationSender;
19use crate::domain::{
20 A2AError, ContextId, Message, Task, TaskId, TaskPushNotificationConfig, TaskState,
21 TaskStateExt, VersionedTask,
22};
23use crate::port::{
24 AsyncNotificationManager, AsyncPushNotifier, AsyncTaskLifecycle, AsyncTaskQuery,
25 AsyncTaskVersioning,
26};
27
28pub struct InMemoryTaskStorage {
37 pub(crate) tasks: Arc<Mutex<HashMap<String, Task>>>,
39 pub(crate) versions: Arc<Mutex<HashMap<String, u64>>>,
45 pub(crate) push_notification_registry: Arc<PushNotificationRegistry>,
47}
48
49impl InMemoryTaskStorage {
50 pub fn new() -> Self {
52 #[cfg(feature = "http-client")]
54 let push_sender = HttpPushNotificationSender::new();
55 #[cfg(not(feature = "http-client"))]
56 let push_sender = NoopPushNotificationSender;
57
58 let push_registry = PushNotificationRegistry::new(push_sender);
59
60 Self {
61 tasks: Arc::new(Mutex::new(HashMap::new())),
62 versions: Arc::new(Mutex::new(HashMap::new())),
63 push_notification_registry: Arc::new(push_registry),
64 }
65 }
66
67 pub fn with_push_sender(push_sender: impl PushNotificationSender + 'static) -> Self {
69 let push_registry = PushNotificationRegistry::new(push_sender);
70
71 Self {
72 tasks: Arc::new(Mutex::new(HashMap::new())),
73 versions: Arc::new(Mutex::new(HashMap::new())),
74 push_notification_registry: Arc::new(push_registry),
75 }
76 }
77
78 async fn bump_version(&self, task_id: &str) -> u64 {
82 let mut versions = self.versions.lock().await;
83 let v = versions.entry(task_id.to_string()).or_insert(0);
84 *v += 1;
85 *v
86 }
87
88 pub fn push_notifier(&self) -> Arc<dyn AsyncPushNotifier> {
95 self.push_notification_registry.clone()
96 }
97}
98
99impl Default for InMemoryTaskStorage {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105#[async_trait]
106impl AsyncTaskLifecycle for InMemoryTaskStorage {
107 async fn create(&self, id: &TaskId, context_id: &ContextId) -> Result<Task, A2AError> {
108 let task_id = id.as_str();
109 let context_id = context_id.as_str();
110 let mut tasks_guard = self.tasks.lock().await;
111
112 if tasks_guard.contains_key(task_id) {
113 return Err(A2AError::TaskNotFound(format!(
114 "Task {} already exists",
115 task_id
116 )));
117 }
118
119 let task = Task::new(task_id.to_string(), context_id.to_string());
120 tasks_guard.insert(task_id.to_string(), task.clone());
121 self.bump_version(task_id).await; Ok(task)
124 }
125
126 async fn update_status(
127 &self,
128 id: &TaskId,
129 state: TaskState,
130 message: Option<Message>,
131 ) -> Result<Task, A2AError> {
132 let task_id = id.as_str();
133 let mut tasks_guard = self.tasks.lock().await;
134
135 let task = tasks_guard
136 .get_mut(task_id)
137 .ok_or_else(|| A2AError::TaskNotFound(task_id.to_string()))?;
138
139 task.update_status(state, message);
141 let updated = task.clone();
142 self.bump_version(task_id).await;
143
144 Ok(updated)
148 }
149
150 async fn exists(&self, id: &TaskId) -> Result<bool, A2AError> {
151 let task_id = id.as_str();
152 let tasks_guard = self.tasks.lock().await;
153 Ok(tasks_guard.contains_key(task_id))
154 }
155
156 async fn get(&self, id: &TaskId, history_length: Option<u32>) -> Result<Task, A2AError> {
157 let task_id = id.as_str();
158 let task = {
160 let tasks_guard = self.tasks.lock().await;
161
162 let Some(task) = tasks_guard.get(task_id) else {
163 return Err(A2AError::TaskNotFound(task_id.to_string()));
164 };
165
166 task.with_limited_history(history_length)
168 }; Ok(task)
171 }
172
173 async fn cancel(&self, id: &TaskId) -> Result<Task, A2AError> {
174 let task_id = id.as_str();
175 let mut tasks_guard = self.tasks.lock().await;
176
177 let Some(task) = tasks_guard.get(task_id) else {
178 return Err(A2AError::TaskNotFound(task_id.to_string()));
179 };
180
181 let mut updated_task = task.clone();
182
183 if !updated_task.status.state.is_cancelable() {
188 return Err(A2AError::TaskNotCancelable(format!(
189 "Task {} has already finished in state {:?} and cannot be canceled",
190 task_id, updated_task.status.state
191 )));
192 }
193
194 let cancel_message = Message {
196 role: ::buffa::EnumValue::from(crate::domain::Role::Agent),
197 parts: vec![crate::domain::Part::text(format!(
198 "Task {} canceled.",
199 task_id
200 ))],
201 message_id: uuid::Uuid::new_v4().to_string(),
202 task_id: task_id.to_string(),
203 context_id: updated_task.context_id.clone(),
204 ..Default::default()
205 };
206
207 updated_task.update_status(TaskState::Canceled, Some(cancel_message));
209 tasks_guard.insert(task_id.to_string(), updated_task.clone());
210 self.bump_version(task_id).await;
211
212 Ok(updated_task)
215 }
216}
217
218#[async_trait]
219impl AsyncTaskVersioning for InMemoryTaskStorage {
220 async fn version(&self, id: &TaskId) -> Result<u64, A2AError> {
221 let task_id = id.as_str();
222 let tasks_guard = self.tasks.lock().await;
223 if !tasks_guard.contains_key(task_id) {
224 return Err(A2AError::TaskNotFound(task_id.to_string()));
225 }
226 let versions = self.versions.lock().await;
227 Ok(versions.get(task_id).copied().unwrap_or(0))
228 }
229
230 async fn get_versioned(
231 &self,
232 id: &TaskId,
233 history_length: Option<u32>,
234 ) -> Result<VersionedTask, A2AError> {
235 let task_id = id.as_str();
236 let tasks_guard = self.tasks.lock().await;
237 let Some(task) = tasks_guard.get(task_id) else {
238 return Err(A2AError::TaskNotFound(task_id.to_string()));
239 };
240 let task = task.with_limited_history(history_length);
241 let versions = self.versions.lock().await;
242 let version = versions.get(task_id).copied().unwrap_or(0);
243 Ok(VersionedTask::new(task, version))
244 }
245
246 async fn update_status_checked(
247 &self,
248 id: &TaskId,
249 expected: u64,
250 state: TaskState,
251 message: Option<Message>,
252 ) -> Result<VersionedTask, A2AError> {
253 let task_id = id.as_str();
254 let mut tasks_guard = self.tasks.lock().await;
257 let task = tasks_guard
258 .get_mut(task_id)
259 .ok_or_else(|| A2AError::TaskNotFound(task_id.to_string()))?;
260 let mut versions = self.versions.lock().await;
261 let current = versions.get(task_id).copied().unwrap_or(0);
262 if current != expected {
263 return Err(A2AError::VersionConflict {
264 id: task_id.to_string(),
265 expected,
266 actual: current,
267 });
268 }
269 task.update_status(state, message);
270 let new_version = current + 1;
271 versions.insert(task_id.to_string(), new_version);
272 Ok(VersionedTask::new(task.clone(), new_version))
273 }
274}
275
276#[async_trait]
277impl AsyncTaskQuery for InMemoryTaskStorage {
278 async fn list(
279 &self,
280 params: &crate::domain::ListTasksParams,
281 ) -> Result<crate::domain::ListTasksResult, A2AError> {
282 use crate::domain::ListTasksResult;
283
284 let tasks_guard = self.tasks.lock().await;
285
286 let mut filtered_tasks: Vec<_> = tasks_guard
288 .values()
289 .filter(|task| {
290 if let Some(ref context_id) = params.context_id
292 && &task.context_id != context_id
293 {
294 return false;
295 }
296
297 if let Some(ref status) = params.status
299 && &task.status.state != status
300 {
301 return false;
302 }
303
304 if let Some(status_timestamp_after) = ¶ms.status_timestamp_after
306 && let Ok(after_dt) =
307 chrono::DateTime::parse_from_rfc3339(status_timestamp_after)
308 && let Some(timestamp) = task.status.timestamp_utc()
309 && timestamp <= after_dt.with_timezone(&chrono::Utc)
310 {
311 return false;
312 }
313
314 true
315 })
316 .cloned()
317 .collect();
318
319 filtered_tasks.sort_by(|a, b| {
321 let a_time = a
322 .status
323 .timestamp_utc()
324 .map(|t| t.timestamp_millis())
325 .unwrap_or(0);
326 let b_time = b
327 .status
328 .timestamp_utc()
329 .map(|t| t.timestamp_millis())
330 .unwrap_or(0);
331 b_time.cmp(&a_time)
332 });
333
334 let total_size = filtered_tasks.len() as i32;
335
336 let page_size = params.page_size.unwrap_or(50).clamp(1, 100) as usize;
338 let page_start = if let Some(ref token) = params.page_token {
339 token.parse::<usize>().unwrap_or(0)
341 } else {
342 0
343 };
344
345 let page_end = (page_start + page_size).min(filtered_tasks.len());
346 let has_more = page_end < filtered_tasks.len();
347
348 let mut page_tasks: Vec<_> = filtered_tasks[page_start..page_end].to_vec();
350
351 let history_length = params.history_length.unwrap_or(0);
353 for task in &mut page_tasks {
354 *task = task.with_limited_history(Some(history_length as u32));
355
356 if !params.include_artifacts.unwrap_or(false) {
358 task.artifacts.clear();
359 }
360 }
361
362 let next_page_token = if has_more {
364 page_end.to_string()
365 } else {
366 String::new()
367 };
368
369 Ok(ListTasksResult {
370 tasks: page_tasks,
371 total_size,
372 page_size: page_size as i32,
373 next_page_token,
374 })
375 }
376}
377
378#[async_trait]
383impl AsyncNotificationManager for InMemoryTaskStorage {
384 async fn set_config(
385 &self,
386 config: &TaskPushNotificationConfig,
387 ) -> Result<TaskPushNotificationConfig, A2AError> {
388 #[cfg(feature = "tracing")]
389 tracing::info!(
390 task_id = %config.task_id,
391 url = %config.url,
392 "🚀 Registering push notification config for task"
393 );
394
395 self.push_notification_registry
397 .register(&config.task_id, config.clone())
398 .await?;
399
400 #[cfg(feature = "tracing")]
401 tracing::info!(
402 task_id = %config.task_id,
403 "✅ Push notification config registered successfully"
404 );
405
406 Ok(config.clone())
407 }
408
409 async fn get_config(
410 &self,
411 params: &crate::domain::GetTaskPushNotificationConfigParams,
412 ) -> Result<TaskPushNotificationConfig, A2AError> {
413 match self
414 .push_notification_registry
415 .get_config(¶ms.id)
416 .await?
417 {
418 Some(config) => Ok(config),
419 None => Err(A2AError::PushNotificationNotSupported),
420 }
421 }
422
423 async fn list_configs(
424 &self,
425 params: &crate::domain::ListTaskPushNotificationConfigsParams,
426 ) -> Result<Vec<TaskPushNotificationConfig>, A2AError> {
427 match self
430 .push_notification_registry
431 .get_config(¶ms.id)
432 .await?
433 {
434 Some(config) => Ok(vec![config]),
435 None => Ok(vec![]),
436 }
437 }
438
439 async fn delete_config(
440 &self,
441 params: &crate::domain::DeleteTaskPushNotificationConfigParams,
442 ) -> Result<(), A2AError> {
443 self.push_notification_registry
446 .unregister(¶ms.id)
447 .await?;
448 Ok(())
449 }
450}
451
452impl Clone for InMemoryTaskStorage {
453 fn clone(&self) -> Self {
454 Self {
455 tasks: self.tasks.clone(),
456 versions: self.versions.clone(),
457 push_notification_registry: self.push_notification_registry.clone(),
458 }
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::domain::ContextId;
466
467 fn tid(s: &str) -> TaskId {
468 s.parse().unwrap()
469 }
470 fn cid(s: &str) -> ContextId {
471 s.parse().unwrap()
472 }
473
474 #[tokio::test]
475 async fn versioning_tracks_and_guards_mutations() {
476 let store = InMemoryTaskStorage::new();
477 store.create(&tid("t1"), &cid("c1")).await.unwrap();
478 assert_eq!(store.version(&tid("t1")).await.unwrap(), 1);
479
480 store
482 .update_status(&tid("t1"), TaskState::Working, None)
483 .await
484 .unwrap();
485 let snap = store.get_versioned(&tid("t1"), None).await.unwrap();
486 assert_eq!(snap.version, 2);
487
488 let err = store
490 .update_status_checked(&tid("t1"), 1, TaskState::Completed, None)
491 .await
492 .unwrap_err();
493 assert!(matches!(
494 err,
495 A2AError::VersionConflict {
496 expected: 1,
497 actual: 2,
498 ..
499 }
500 ));
501 assert_eq!(
502 store.get(&tid("t1"), None).await.unwrap().status.state,
503 TaskState::Working
504 );
505
506 let ok = store
508 .update_status_checked(&tid("t1"), 2, TaskState::Completed, None)
509 .await
510 .unwrap();
511 assert_eq!(ok.version, 3);
512 assert_eq!(ok.task.status.state, TaskState::Completed);
513 }
514}