// hippocampus/memory.rs — In-memory view of a graph node // // MemoryNode is a lightweight representation of a loaded node: // key, content, links, version, weight. Used by the agent for // context tracking and by tools for direct store access. use super::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 = super::query::engine::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) } }