From 10932cb67e219fe75bd13f825ec194f37d4e110d Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Wed, 25 Mar 2026 01:55:21 -0400 Subject: [PATCH] hippocampus: move MemoryNode + store ops to where they belong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemoryNode moved from agent/memory.rs to hippocampus/memory.rs — it's a view over hippocampus data, not agent-specific. Store operations (set_weight, set_link_strength, add_link) moved into store/ops.rs. CLI code (cli/graph.rs, cli/node.rs) and agent tools both call the same store methods now. render_node() delegates to MemoryNode::from_store().render() — 3 lines instead of 40. Co-Authored-By: Proof of Concept --- src/agent/mod.rs | 1 - src/agent/runner.rs | 6 +-- src/agent/tools/memory.rs | 76 ++++------------------------ src/agent/types.rs | 2 +- src/cli/graph.rs | 67 ++++-------------------- src/cli/node.rs | 59 ++------------------- src/{agent => hippocampus}/memory.rs | 14 +++-- src/hippocampus/mod.rs | 1 + src/hippocampus/store/ops.rs | 71 ++++++++++++++++++++++++++ src/lib.rs | 2 +- 10 files changed, 108 insertions(+), 191 deletions(-) rename src/{agent => hippocampus}/memory.rs (90%) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index c5b4960..fee97de 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -28,7 +28,6 @@ pub mod tools; pub mod ui_channel; pub mod journal; -pub mod memory; pub mod runner; pub mod cli; pub mod context; diff --git a/src/agent/runner.rs b/src/agent/runner.rs index d3964d6..65fe083 100644 --- a/src/agent/runner.rs +++ b/src/agent/runner.rs @@ -16,8 +16,6 @@ use anyhow::Result; use tiktoken_rs::CoreBPE; -use std::io::Write; - use crate::agent::api::ApiClient; use crate::agent::journal; use crate::agent::log::ConversationLog; @@ -445,7 +443,7 @@ impl Agent { match call.function.name.as_str() { "memory_render" | "memory_links" => { if let Some(key) = args.get("key").and_then(|v| v.as_str()) { - if let Some(node) = crate::agent::memory::MemoryNode::load(key) { + if let Some(node) = crate::hippocampus::memory::MemoryNode::load(key) { // Replace if already tracked, otherwise add if let Some(existing) = self.context.loaded_nodes.iter_mut() .find(|n| n.key == node.key) { @@ -458,7 +456,7 @@ impl Agent { } "memory_write" => { if let Some(key) = args.get("key").and_then(|v| v.as_str()) { - if let Some(node) = crate::agent::memory::MemoryNode::load(key) { + if let Some(node) = crate::hippocampus::memory::MemoryNode::load(key) { // Refresh if already tracked if let Some(existing) = self.context.loaded_nodes.iter_mut() .find(|n| n.key == node.key) { diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 7479366..649b672 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -7,9 +7,9 @@ use anyhow::{Context, Result}; use serde_json::json; -use crate::agent::memory::MemoryNode; +use crate::hippocampus::memory::MemoryNode; use crate::agent::types::ToolDef; -use crate::store::{self, Store}; +use crate::store::Store; pub fn definitions() -> Vec { vec![ @@ -246,70 +246,18 @@ fn link_set(source: &str, target: &str, strength: f32) -> Result { let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let source = store.resolve_key(source).map_err(|e| anyhow::anyhow!("{}", e))?; let target = store.resolve_key(target).map_err(|e| anyhow::anyhow!("{}", e))?; - let strength = strength.clamp(0.01, 1.0); - - let mut found = false; - let mut first = true; - for rel in &mut store.relations { - if rel.deleted { continue; } - if (rel.source_key == source && rel.target_key == target) - || (rel.source_key == target && rel.target_key == source) - { - if first { - let old = rel.strength; - rel.strength = strength; - first = false; - found = true; - if (old - strength).abs() < 0.001 { - return Ok(format!("{} ↔ {} already at {:.2}", source, target, strength)); - } - } else { - rel.deleted = true; // deduplicate - } - } - } - - if !found { - anyhow::bail!("no link found between {} and {}", source, target); - } - + let old = store.set_link_strength(&source, &target, strength) + .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; - Ok(format!("set {} ↔ {} strength to {:.2}", source, target, strength)) + Ok(format!("set {} ↔ {} strength {:.2} → {:.2}", source, target, old, strength)) } fn link_add(source: &str, target: &str, prov: &str) -> Result { let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let source = store.resolve_key(source).map_err(|e| anyhow::anyhow!("{}", e))?; let target = store.resolve_key(target).map_err(|e| anyhow::anyhow!("{}", e))?; - - // Check for existing link - let exists = store.relations.iter().any(|r| - !r.deleted && - ((r.source_key == source && r.target_key == target) || - (r.source_key == target && r.target_key == source))); - if exists { - return Ok(format!("link already exists: {} ↔ {}", source, target)); - } - - let source_uuid = store.nodes.get(&source) - .map(|n| n.uuid) - .ok_or_else(|| anyhow::anyhow!("source not found: {}", source))?; - let target_uuid = store.nodes.get(&target) - .map(|n| n.uuid) - .ok_or_else(|| anyhow::anyhow!("target not found: {}", target))?; - - // Compute initial strength from Jaccard similarity - let graph = store.build_graph(); - let jaccard = graph.jaccard(&source, &target); - let strength = (jaccard * 3.0).clamp(0.1, 1.0) as f32; - - let mut rel = store::new_relation( - source_uuid, target_uuid, - store::RelationType::Link, strength, - &source, &target, - ); - rel.provenance = prov.to_string(); - store.add_relation(rel).map_err(|e| anyhow::anyhow!("{}", e))?; + let strength = store.add_link(&source, &target, prov) + .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("linked {} → {} (strength={:.2})", source, target, strength)) } @@ -317,14 +265,10 @@ fn link_add(source: &str, target: &str, prov: &str) -> Result { fn weight_set(key: &str, weight: f32) -> Result { let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let resolved = store.resolve_key(key).map_err(|e| anyhow::anyhow!("{}", e))?; - let weight = weight.clamp(0.01, 1.0); - - let node = store.nodes.get_mut(&resolved) - .ok_or_else(|| anyhow::anyhow!("node not found: {}", resolved))?; - let old = node.weight; - node.weight = weight; + let (old, new) = store.set_weight(&resolved, weight) + .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; - Ok(format!("weight {} {:.2} → {:.2}", resolved, old, weight)) + Ok(format!("weight {} {:.2} → {:.2}", resolved, old, new)) } fn supersede(args: &serde_json::Value, prov: &str) -> Result { diff --git a/src/agent/types.rs b/src/agent/types.rs index ed83a98..eef6c9d 100644 --- a/src/agent/types.rs +++ b/src/agent/types.rs @@ -327,7 +327,7 @@ pub struct ContextState { /// Memory nodes currently loaded in the context window. /// Tracked so the agent knows what it's "seeing" and can /// refresh nodes after writes. - pub loaded_nodes: Vec, + pub loaded_nodes: Vec, } pub const WORKING_STACK_INSTRUCTIONS: &str = "/home/kent/.config/poc-agent/working-stack.md"; diff --git a/src/cli/graph.rs b/src/cli/graph.rs index df60704..35c3411 100644 --- a/src/cli/graph.rs +++ b/src/cli/graph.rs @@ -144,37 +144,16 @@ pub fn cmd_link_add(source: &str, target: &str, reason: &[String]) -> Result<(), .map(|n| n.content.as_str()).unwrap_or(""); let target = neuro::refine_target(&store, source_content, &target); - // Find UUIDs - let source_uuid = store.nodes.get(&source) - .map(|n| n.uuid) - .ok_or_else(|| format!("source not found: {}", source))?; - let target_uuid = store.nodes.get(&target) - .map(|n| n.uuid) - .ok_or_else(|| format!("target not found: {}", target))?; - - // Check for existing link - let exists = store.relations.iter().any(|r| - !r.deleted && - ((r.source_key == source && r.target_key == target) || - (r.source_key == target && r.target_key == source))); - if exists { - println!("Link already exists: {} ↔ {}", source, target); - return Ok(()); + match store.add_link(&source, &target, "manual") { + Ok(strength) => { + store.save()?; + println!("Linked: {} → {} (strength={:.2}, {})", source, target, strength, reason); + } + Err(msg) if msg.contains("already exists") => { + println!("Link already exists: {} ↔ {}", source, target); + } + Err(e) => return Err(e), } - - // Compute initial strength from Jaccard neighborhood similarity - let graph = store.build_graph(); - let jaccard = graph.jaccard(&source, &target); - let strength = (jaccard * 3.0).clamp(0.1, 1.0); - - let rel = store::new_relation( - source_uuid, target_uuid, - store::RelationType::Link, strength, - &source, &target, - ); - store.add_relation(rel)?; - store.save()?; - println!("Linked: {} → {} (strength={:.2}, {})", source, target, strength, reason); Ok(()) } @@ -183,33 +162,9 @@ pub fn cmd_link_set(source: &str, target: &str, strength: f32) -> Result<(), Str let mut store = store::Store::load()?; let source = store.resolve_key(source)?; let target = store.resolve_key(target)?; - let strength = strength.clamp(0.01, 1.0); - - let mut found = false; - let mut first = true; - for rel in &mut store.relations { - if rel.deleted { continue; } - if (rel.source_key == source && rel.target_key == target) - || (rel.source_key == target && rel.target_key == source) - { - if first { - let old = rel.strength; - rel.strength = strength; - println!("Set: {} ↔ {} strength {:.2} → {:.2}", source, target, old, strength); - first = false; - } else { - // Duplicate — mark deleted - rel.deleted = true; - println!(" (removed duplicate link)"); - } - found = true; - } - } - - if !found { - return Err(format!("No link found between {} and {}", source, target)); - } + let old = store.set_link_strength(&source, &target, strength)?; + println!("Set: {} ↔ {} strength {:.2} → {:.2}", source, target, old, strength); store.save()?; Ok(()) } diff --git a/src/cli/node.rs b/src/cli/node.rs index 9a762cf..3965f89 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -87,15 +87,9 @@ pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> { super::check_dry_run(); let mut store = store::Store::load()?; let resolved = store.resolve_key(key)?; - let weight = weight.clamp(0.01, 1.0); - if let Some(node) = store.nodes.get_mut(&resolved) { - let old = node.weight; - node.weight = weight; - println!("Weight: {} {:.2} → {:.2}", resolved, old, weight); - store.save()?; - } else { - return Err(format!("Node not found: {}", resolved)); - } + let (old, new) = store.set_weight(&resolved, weight)?; + println!("Weight: {} {:.2} → {:.2}", resolved, old, new); + store.save()?; Ok(()) } @@ -190,51 +184,8 @@ pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> { /// Render a node to a string: content + deduped footer links. /// Used by both the CLI command and agent placeholders. pub fn render_node(store: &store::Store, key: &str) -> Option { - let node = store.nodes.get(key)?; - let mut out = node.content.clone(); - - // Build neighbor lookup: key → strength - let mut neighbor_strengths: 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 = neighbor_strengths.entry(&r.target_key).or_insert(0.0); - *e = e.max(r.strength); - } else if r.target_key == key { - let e = neighbor_strengths.entry(&r.source_key).or_insert(0.0); - *e = e.max(r.strength); - } - } - - // Detect which neighbors are already referenced inline in the content. - let mut inline_keys: std::collections::HashSet = std::collections::HashSet::new(); - for nbr_key in neighbor_strengths.keys() { - if node.content.contains(nbr_key) { - inline_keys.insert(nbr_key.to_string()); - } - } - - // Footer: only show links NOT already referenced inline - let mut footer_neighbors: Vec<(&str, f32)> = neighbor_strengths.iter() - .filter(|(k, _)| !inline_keys.contains(**k)) - .map(|(k, s)| (*k, *s)) - .collect(); - - if !footer_neighbors.is_empty() { - footer_neighbors.sort_by(|a, b| b.1.total_cmp(&a.1)); - let total = footer_neighbors.len(); - let shown: Vec = footer_neighbors.iter().take(15) - .map(|(k, s)| format!("({:.2}) `poc-memory render {}`", s, k)) - .collect(); - out.push_str("\n\n---\nLinks:"); - for link in &shown { - out.push_str(&format!("\n {}", link)); - } - if total > 15 { - out.push_str(&format!("\n ... and {} more (`poc-memory graph link {}`)", total - 15, key)); - } - } - Some(out) + crate::hippocampus::memory::MemoryNode::from_store(store, key) + .map(|node| node.render()) } pub fn cmd_render(key: &[String]) -> Result<(), String> { diff --git a/src/agent/memory.rs b/src/hippocampus/memory.rs similarity index 90% rename from src/agent/memory.rs rename to src/hippocampus/memory.rs index 57ab4a0..7ef5f8d 100644 --- a/src/agent/memory.rs +++ b/src/hippocampus/memory.rs @@ -1,12 +1,10 @@ -// agent/memory.rs — Agent's live view of memory nodes +// hippocampus/memory.rs — In-memory view of a graph node // -// 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. +// 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 crate::store::Store; +use super::store::Store; /// A memory node loaded into the agent's working memory. #[derive(Debug, Clone)] @@ -109,7 +107,7 @@ impl MemoryNode { /// 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); + let results = super::query::engine::search(query, &store); Ok(results.into_iter().take(20).map(|hit| SearchResult { key: hit.key.clone(), diff --git a/src/hippocampus/mod.rs b/src/hippocampus/mod.rs index 9e36ce6..e7ca92a 100644 --- a/src/hippocampus/mod.rs +++ b/src/hippocampus/mod.rs @@ -5,6 +5,7 @@ // consolidation (spaced repetition, interference detection, schema // assimilation). +pub mod memory; pub mod store; pub mod graph; pub mod lookups; diff --git a/src/hippocampus/store/ops.rs b/src/hippocampus/store/ops.rs index feaf26d..67d5a7f 100644 --- a/src/hippocampus/store/ops.rs +++ b/src/hippocampus/store/ops.rs @@ -325,4 +325,75 @@ impl Store { node.degree = Some(g.degree(key) as u32); } } + + /// Set a node's weight directly. Returns (old, new). + pub fn set_weight(&mut self, key: &str, weight: f32) -> Result<(f32, f32), String> { + let weight = weight.clamp(0.01, 1.0); + let node = self.nodes.get_mut(key) + .ok_or_else(|| format!("node not found: {}", key))?; + let old = node.weight; + node.weight = weight; + Ok((old, weight)) + } + + /// Set the strength of a link between two nodes. Deduplicates if + /// multiple links exist. Returns the old strength, or error if no link. + pub fn set_link_strength(&mut self, source: &str, target: &str, strength: f32) -> Result { + let strength = strength.clamp(0.01, 1.0); + let mut old = 0.0f32; + let mut found = false; + let mut first = true; + for rel in &mut self.relations { + if rel.deleted { continue; } + if (rel.source_key == source && rel.target_key == target) + || (rel.source_key == target && rel.target_key == source) + { + if first { + old = rel.strength; + rel.strength = strength; + first = false; + } else { + rel.deleted = true; // deduplicate + } + found = true; + } + } + if !found { + return Err(format!("no link between {} and {}", source, target)); + } + Ok(old) + } + + /// Add a link between two nodes with Jaccard-based initial strength. + /// Returns the strength, or a message if the link already exists. + pub fn add_link(&mut self, source: &str, target: &str, provenance: &str) -> Result { + // Check for existing + let exists = self.relations.iter().any(|r| + !r.deleted && + ((r.source_key == source && r.target_key == target) || + (r.source_key == target && r.target_key == source))); + if exists { + return Err(format!("link already exists: {} ↔ {}", source, target)); + } + + let source_uuid = self.nodes.get(source) + .map(|n| n.uuid) + .ok_or_else(|| format!("source not found: {}", source))?; + let target_uuid = self.nodes.get(target) + .map(|n| n.uuid) + .ok_or_else(|| format!("target not found: {}", target))?; + + let graph = self.build_graph(); + let jaccard = graph.jaccard(source, target); + let strength = (jaccard * 3.0).clamp(0.1, 1.0) as f32; + + let mut rel = new_relation( + source_uuid, target_uuid, + RelationType::Link, strength, + source, target, + ); + rel.provenance = provenance.to_string(); + self.add_relation(rel)?; + Ok(strength) + } } diff --git a/src/lib.rs b/src/lib.rs index 236fdfb..0cd22fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub mod memory_capnp { pub use hippocampus::{ store, graph, lookups, cursor, query, similarity, spectral, neuro, counters, - transcript, memory_search, migrate, + transcript, memory_search, migrate, memory, }; pub use hippocampus::query::engine as search; pub use hippocampus::query::parser as query_parser;