Skip to content

Commit 9f711f4

Browse files
committed
feat(memory): implement FileMemoryStore with file-based persistence
- Add FileMemoryStore: JSON file per item + compact index for fast search - Atomic writes via temp file + rename (same pattern as FileSessionStore) - Path traversal prevention on memory IDs - Index rebuild from item files for corruption recovery - Wire AgentMemory into SessionOptions (with_memory, with_file_memory) - Wire AgentMemory into AgentSession with accessor - 15 new tests (1148 total, all passing)
1 parent aac7a1b commit 9f711f4

2 files changed

Lines changed: 626 additions & 1 deletion

File tree

core/src/agent_api.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ pub struct SessionOptions {
7676
pub goal_tracking: bool,
7777
/// Optional skill registry for instruction injection
7878
pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
79+
/// Optional memory store for long-term memory persistence
80+
pub memory_store: Option<Arc<dyn crate::memory::MemoryStore>>,
81+
/// Deferred file memory directory — constructed async in `build_session()`
82+
pub(crate) file_memory_dir: Option<PathBuf>,
7983
}
8084

8185
impl std::fmt::Debug for SessionOptions {
@@ -97,6 +101,7 @@ impl std::fmt::Debug for SessionOptions {
97101
.as_ref()
98102
.map(|r| format!("{} skills", r.len())),
99103
)
104+
.field("memory_store", &self.memory_store.is_some())
100105
.finish()
101106
}
102107
}
@@ -206,6 +211,24 @@ impl SessionOptions {
206211
self.skill_registry = Some(registry);
207212
self
208213
}
214+
215+
/// Set a custom memory store
216+
pub fn with_memory(mut self, store: Arc<dyn crate::memory::MemoryStore>) -> Self {
217+
self.memory_store = Some(store);
218+
self
219+
}
220+
221+
/// Use a file-based memory store at the given directory.
222+
///
223+
/// The store is created lazily when the session is built (requires async).
224+
/// This stores the directory path; `FileMemoryStore::new()` is called during
225+
/// session construction.
226+
pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
227+
// Store as a deferred path — we'll construct the FileMemoryStore in build_session
228+
// since FileMemoryStore::new() is async. Use a marker via the _file_memory_dir field.
229+
self.file_memory_dir = Some(dir.into());
230+
self
231+
}
209232
}
210233

211234
// ============================================================================
@@ -393,6 +416,39 @@ impl Agent {
393416
tool_context = tool_context.with_search_config(search_config.clone());
394417
}
395418

419+
// Resolve memory store: explicit store takes priority, then file_memory_dir
420+
let memory = {
421+
let store = if let Some(ref store) = opts.memory_store {
422+
Some(Arc::clone(store))
423+
} else if let Some(ref dir) = opts.file_memory_dir {
424+
match tokio::runtime::Handle::try_current() {
425+
Ok(handle) => {
426+
let dir = dir.clone();
427+
match tokio::task::block_in_place(|| {
428+
handle.block_on(crate::memory::FileMemoryStore::new(dir))
429+
}) {
430+
Ok(store) => {
431+
Some(Arc::new(store) as Arc<dyn crate::memory::MemoryStore>)
432+
}
433+
Err(e) => {
434+
tracing::warn!("Failed to create file memory store: {}", e);
435+
None
436+
}
437+
}
438+
}
439+
Err(_) => {
440+
tracing::warn!(
441+
"No async runtime available for file memory store — memory disabled"
442+
);
443+
None
444+
}
445+
}
446+
} else {
447+
None
448+
};
449+
store.map(crate::memory::AgentMemory::new)
450+
};
451+
396452
Ok(AgentSession {
397453
llm_client,
398454
tool_executor,
@@ -401,6 +457,7 @@ impl Agent {
401457
workspace: canonical,
402458
history: RwLock::new(Vec::new()),
403459
command_queue,
460+
memory,
404461
})
405462
}
406463
}
@@ -423,6 +480,8 @@ pub struct AgentSession {
423480
history: RwLock<Vec<Message>>,
424481
/// Optional lane queue for priority-based tool execution.
425482
command_queue: Option<Arc<SessionLaneQueue>>,
483+
/// Optional long-term memory.
484+
memory: Option<crate::memory::AgentMemory>,
426485
}
427486

428487
impl std::fmt::Debug for AgentSession {
@@ -508,6 +567,11 @@ impl AgentSession {
508567
self.history.read().unwrap().clone()
509568
}
510569

570+
/// Return a reference to the session's memory, if configured.
571+
pub fn memory(&self) -> Option<&crate::memory::AgentMemory> {
572+
self.memory.as_ref()
573+
}
574+
511575
/// Read a file from the workspace.
512576
pub async fn read_file(&self, path: &str) -> Result<String> {
513577
let args = serde_json::json!({ "file_path": path });

0 commit comments

Comments
 (0)