Skip to main content

bob_adapters/
artifact_memory.rs

1//! In-memory artifact store adapter.
2
3use bob_core::{
4    error::StoreError,
5    ports::ArtifactStorePort,
6    types::{ArtifactRecord, SessionId},
7};
8
9/// In-memory artifact store grouped by session id.
10#[derive(Debug, Default)]
11pub struct InMemoryArtifactStore {
12    inner: scc::HashMap<SessionId, Vec<ArtifactRecord>>,
13}
14
15impl InMemoryArtifactStore {
16    #[must_use]
17    pub fn new() -> Self {
18        Self::default()
19    }
20}
21
22#[async_trait::async_trait]
23impl ArtifactStorePort for InMemoryArtifactStore {
24    async fn put(&self, artifact: ArtifactRecord) -> Result<(), StoreError> {
25        let entry = self.inner.entry_async(artifact.session_id.clone()).await;
26        match entry {
27            scc::hash_map::Entry::Occupied(mut occ) => {
28                occ.get_mut().push(artifact);
29            }
30            scc::hash_map::Entry::Vacant(vac) => {
31                let _ = vac.insert_entry(vec![artifact]);
32            }
33        }
34        Ok(())
35    }
36
37    async fn list_by_session(
38        &self,
39        session_id: &SessionId,
40    ) -> Result<Vec<ArtifactRecord>, StoreError> {
41        Ok(self.inner.read_async(session_id, |_k, v| v.clone()).await.unwrap_or_default())
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[tokio::test]
50    async fn stores_and_lists_artifacts() {
51        let store = InMemoryArtifactStore::new();
52        let put = store
53            .put(ArtifactRecord {
54                session_id: "s1".to_string(),
55                kind: "tool_result".to_string(),
56                name: "search".to_string(),
57                content: serde_json::json!({"hits": 3}),
58            })
59            .await;
60        assert!(put.is_ok());
61
62        let listed = store.list_by_session(&"s1".to_string()).await;
63        assert!(listed.is_ok());
64        assert_eq!(listed.ok().map(|records| records.len()), Some(1));
65    }
66}