Skip to main content

deepstrike_sdk/
memory.rs

1use async_trait::async_trait;
2use deepstrike_core::memory::curator::CurationResult;
3use deepstrike_core::memory::durable::SessionData;
4use deepstrike_core::memory::semantic::MemoryEntry;
5
6/// Backing store for the idle dreaming pipeline.
7///
8/// Implementors bridge the kernel's pure-computation delta to whatever durable
9/// storage the application uses (Postgres, SQLite, a vector DB, etc.).
10///
11/// # Contract
12/// `commit` is always called with the `CurationResult` produced from the
13/// `existing` slice returned by `load_memories` in the same cycle.
14/// The `to_remove_indices` inside `CurationResult` index into that slice,
15/// so the implementor must correlate them itself.
16#[async_trait]
17pub trait DreamStore: Send + Sync {
18    /// Load recent sessions for the given agent. The kernel caps processing
19    /// at `IdlePolicy::max_sessions_per_run`; returning more is fine.
20    async fn load_sessions(&self, agent_id: &str) -> crate::Result<Vec<SessionData>>;
21
22    /// Load all current long-term memory entries for the agent.
23    async fn load_memories(&self, agent_id: &str) -> crate::Result<Vec<MemoryEntry>>;
24
25    /// Apply the curation delta — add new entries, remove stale ones.
26    ///
27    /// `existing` is the same slice returned by `load_memories` in this cycle;
28    /// it is needed to resolve `result.to_remove_indices` back to concrete entries.
29    async fn commit(
30        &self,
31        agent_id: &str,
32        result: CurationResult,
33        existing: &[MemoryEntry],
34    ) -> crate::Result<()>;
35
36    /// Semantic search over the agent's long-term memories.
37    /// Called on demand during a session when the LLM invokes the `memory` meta-tool.
38    async fn search(
39        &self,
40        agent_id: &str,
41        query: &str,
42        top_k: usize,
43    ) -> crate::Result<Vec<MemoryEntry>>;
44
45    /// Persist a completed session for future consolidation via `Agent::dream()`.
46    async fn save_session(
47        &self,
48        data: deepstrike_core::memory::durable::SessionData,
49    ) -> crate::Result<()>;
50}
51
52/// Summary of one dreaming cycle returned to the caller.
53#[derive(Debug, Default, Clone)]
54pub struct DreamResult {
55    pub sessions_processed: usize,
56    pub insights_extracted: usize,
57    pub entries_added: usize,
58    pub entries_removed: usize,
59}
60
61/// In-process scratch pad for within-run state.
62#[derive(Default)]
63pub struct WorkingMemory {
64    store: std::collections::HashMap<String, serde_json::Value>,
65}
66
67impl WorkingMemory {
68    pub fn set(&mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) {
69        self.store.insert(key.into(), value.into());
70    }
71    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
72        self.store.get(key)
73    }
74    pub fn clear(&mut self) {
75        self.store.clear();
76    }
77}
78
79/// `InMemoryDreamStore` — a lightweight `DreamStore` backed by per-agent in-memory maps.
80///
81/// Rust port of node/src/memory/in-memory-store.ts. Use for benchmarks, unit tests, and local
82/// development where persistent memory isn't needed. `search()` is a non-semantic slice — it
83/// returns the first `top_k` memories regardless of `query`. The kernel ranks by score, so
84/// caller insertion order is what surfaces.
85pub struct InMemoryDreamStore {
86    sessions: std::sync::Mutex<std::collections::HashMap<String, Vec<SessionData>>>,
87    memories: std::sync::Mutex<std::collections::HashMap<String, Vec<MemoryEntry>>>,
88    initial_memories: Vec<MemoryEntry>,
89    saved_sessions: std::sync::Mutex<Vec<SessionData>>,
90}
91
92impl InMemoryDreamStore {
93    pub fn new() -> Self {
94        Self::with_initial_memories(Vec::new())
95    }
96
97    pub fn with_initial_memories(initial: Vec<MemoryEntry>) -> Self {
98        Self {
99            sessions: std::sync::Mutex::new(std::collections::HashMap::new()),
100            memories: std::sync::Mutex::new(std::collections::HashMap::new()),
101            initial_memories: initial,
102            saved_sessions: std::sync::Mutex::new(Vec::new()),
103        }
104    }
105
106    pub fn add_session(&self, agent_id: impl Into<String>, session: SessionData) {
107        self.sessions
108            .lock()
109            .unwrap()
110            .entry(agent_id.into())
111            .or_default()
112            .push(session);
113    }
114
115    pub fn add_memories(&self, agent_id: impl Into<String>, entries: Vec<MemoryEntry>) {
116        self.memories
117            .lock()
118            .unwrap()
119            .entry(agent_id.into())
120            .or_default()
121            .extend(entries);
122    }
123
124    pub fn saved_sessions(&self) -> Vec<SessionData> {
125        self.saved_sessions.lock().unwrap().clone()
126    }
127}
128
129impl Default for InMemoryDreamStore {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[async_trait]
136impl DreamStore for InMemoryDreamStore {
137    async fn load_sessions(&self, agent_id: &str) -> crate::Result<Vec<SessionData>> {
138        Ok(self.sessions.lock().unwrap().get(agent_id).cloned().unwrap_or_default())
139    }
140
141    async fn load_memories(&self, agent_id: &str) -> crate::Result<Vec<MemoryEntry>> {
142        let mut memories = self.memories.lock().unwrap();
143        if let Some(existing) = memories.get(agent_id) {
144            return Ok(existing.clone());
145        }
146        if !self.initial_memories.is_empty() {
147            memories.insert(agent_id.to_string(), self.initial_memories.clone());
148            return Ok(self.initial_memories.clone());
149        }
150        Ok(Vec::new())
151    }
152
153    async fn commit(
154        &self,
155        agent_id: &str,
156        result: CurationResult,
157        existing: &[MemoryEntry],
158    ) -> crate::Result<()> {
159        let remove: std::collections::HashSet<usize> = result.to_remove_indices.iter().copied().collect();
160        let mut kept: Vec<MemoryEntry> = existing
161            .iter()
162            .enumerate()
163            .filter_map(|(i, m)| if remove.contains(&i) { None } else { Some(m.clone()) })
164            .collect();
165        kept.extend(result.to_add);
166        self.memories
167            .lock()
168            .unwrap()
169            .insert(agent_id.to_string(), kept);
170        Ok(())
171    }
172
173    async fn search(
174        &self,
175        agent_id: &str,
176        _query: &str,
177        top_k: usize,
178    ) -> crate::Result<Vec<MemoryEntry>> {
179        let all = self.load_memories(agent_id).await?;
180        Ok(all.into_iter().take(top_k).collect())
181    }
182
183    async fn save_session(&self, data: SessionData) -> crate::Result<()> {
184        self.saved_sessions.lock().unwrap().push(data.clone());
185        self.sessions
186            .lock()
187            .unwrap()
188            .entry(data.agent_id.clone())
189            .or_default()
190            .push(data);
191        Ok(())
192    }
193}