diff --git a/src/agent/memory.rs b/src/agent/memory.rs new file mode 100644 index 0000000..57ab4a0 --- /dev/null +++ b/src/agent/memory.rs @@ -0,0 +1,146 @@ +// agent/memory.rs — Agent's live view of memory nodes +// +// MemoryNode is the agent's in-memory representation of a loaded +// graph node. Unlike the store's Node (which has all the metadata), +// this holds what the agent needs: the key, rendered content, and +// links for navigation. The agent's context window tracks which +// MemoryNodes are currently loaded. + +use crate::store::Store; + +/// A memory node loaded into the agent's working memory. +#[derive(Debug, Clone)] +pub struct MemoryNode { + pub key: String, + pub content: String, + pub links: Vec, + /// Version from the store — used for change detection. + pub version: u32, + /// Weight in the graph. + pub weight: f32, +} + +/// A link to a neighbor node. +#[derive(Debug, Clone)] +pub struct Link { + pub target: String, + pub strength: f32, + /// Whether this link target is already referenced inline in the content. + pub inline: bool, +} + +impl MemoryNode { + /// Load a node from the store by key. Returns None if not found. + pub fn load(key: &str) -> Option { + let store = Store::load().ok()?; + Self::from_store(&store, key) + } + + /// Load from an already-open store. + pub fn from_store(store: &Store, key: &str) -> Option { + let node = store.nodes.get(key)?; + + // Collect neighbor strengths + let mut neighbors: std::collections::HashMap<&str, f32> = std::collections::HashMap::new(); + for r in &store.relations { + if r.deleted { continue; } + if r.source_key == key { + let e = neighbors.entry(&r.target_key).or_insert(0.0); + *e = e.max(r.strength); + } else if r.target_key == key { + let e = neighbors.entry(&r.source_key).or_insert(0.0); + *e = e.max(r.strength); + } + } + + let mut links: Vec = neighbors.into_iter() + .map(|(target, strength)| Link { + inline: node.content.contains(target), + target: target.to_string(), + strength, + }) + .collect(); + links.sort_by(|a, b| b.strength.total_cmp(&a.strength)); + + Some(MemoryNode { + key: key.to_string(), + content: node.content.clone(), + links, + version: node.version, + weight: node.weight, + }) + } + + /// Render for inclusion in the context window. + pub fn render(&self) -> String { + let mut out = self.content.clone(); + + // Footer: links not already referenced inline + let footer_links: Vec<&Link> = self.links.iter() + .filter(|l| !l.inline) + .collect(); + + if !footer_links.is_empty() { + let total = footer_links.len(); + out.push_str("\n\n---\nLinks:"); + for link in footer_links.iter().take(15) { + out.push_str(&format!("\n ({:.2}) `poc-memory render {}`", + link.strength, link.target)); + } + if total > 15 { + out.push_str(&format!("\n ... and {} more (`poc-memory graph link {}`)", + total - 15, self.key)); + } + } + out + } + + /// Write content to the store and return an updated MemoryNode. + pub fn write(key: &str, content: &str, provenance: Option<&str>) -> Result { + let prov = provenance.unwrap_or("manual"); + let mut store = Store::load()?; + store.upsert_provenance(key, content, prov)?; + store.save()?; + + Self::from_store(&store, key) + .ok_or_else(|| format!("wrote {} but failed to load back", key)) + } + + /// Search for nodes matching a query. Returns lightweight results. + pub fn search(query: &str) -> Result, String> { + let store = Store::load()?; + let results = crate::search::search(query, &store); + + Ok(results.into_iter().take(20).map(|hit| SearchResult { + key: hit.key.clone(), + score: hit.activation as f32, + snippet: hit.snippet.unwrap_or_default(), + }).collect()) + } + + /// Mark a node as used (boosts weight). + pub fn mark_used(key: &str) -> Result { + let mut store = Store::load()?; + if !store.nodes.contains_key(key) { + return Err(format!("node not found: {}", key)); + } + store.mark_used(key); + store.save()?; + Ok(format!("marked {} as used", key)) + } +} + +/// A search result — lightweight, not a full node load. +#[derive(Debug, Clone)] +pub struct SearchResult { + pub key: String, + pub score: f32, + pub snippet: String, +} + +impl SearchResult { + /// Format for display. + pub fn render(&self) -> String { + format!("({:.2}) {} — {}", self.score, self.key, self.snippet) + } +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index fee97de..c5b4960 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -28,6 +28,7 @@ pub mod tools; pub mod ui_channel; pub mod journal; +pub mod memory; pub mod runner; pub mod cli; pub mod context;