hippocampus: move MemoryNode + store ops to where they belong
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 <poc@bcachefs.org>
This commit is contained in:
parent
4b97bb2f2e
commit
10932cb67e
10 changed files with 108 additions and 191 deletions
144
src/hippocampus/memory.rs
Normal file
144
src/hippocampus/memory.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// 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<Link>,
|
||||
/// 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<Self> {
|
||||
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<Self> {
|
||||
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<Link> = 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<Self, String> {
|
||||
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<Vec<SearchResult>, 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<String, String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
// consolidation (spaced repetition, interference detection, schema
|
||||
// assimilation).
|
||||
|
||||
pub mod memory;
|
||||
pub mod store;
|
||||
pub mod graph;
|
||||
pub mod lookups;
|
||||
|
|
|
|||
|
|
@ -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<f32, String> {
|
||||
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<f32, String> {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue