2026-03-25 01:55:21 -04:00
|
|
|
// hippocampus/memory.rs — In-memory view of a graph node
|
2026-03-25 01:39:48 -04:00
|
|
|
//
|
2026-03-25 01:55:21 -04:00
|
|
|
// MemoryNode is a lightweight representation of a loaded node:
|
|
|
|
|
// key, content, links, version, weight. Used by the agent for
|
2026-03-25 01:59:13 -04:00
|
|
|
// context tracking and by the CLI for rendering.
|
2026-03-25 01:39:48 -04:00
|
|
|
|
2026-03-25 01:55:21 -04:00
|
|
|
use super::store::Store;
|
2026-03-25 01:39:48 -04:00
|
|
|
|
|
|
|
|
/// A memory node loaded into the agent's working memory.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct MemoryNode {
|
|
|
|
|
pub key: String,
|
|
|
|
|
pub content: String,
|
2026-03-26 21:19:19 -04:00
|
|
|
pub links: Vec<(String, f32, bool)>, // (target_key, strength, is_new)
|
2026-03-25 01:39:48 -04:00
|
|
|
pub version: u32,
|
|
|
|
|
pub weight: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MemoryNode {
|
2026-03-25 01:59:13 -04:00
|
|
|
/// Load a node from the store by key.
|
2026-03-25 01:39:48 -04:00
|
|
|
pub fn load(key: &str) -> Option<Self> {
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
let arc = super::access_local().ok()?;
|
|
|
|
|
let store = arc.try_lock().ok()?;
|
2026-03-25 01:39:48 -04:00
|
|
|
Self::from_store(&store, key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load from an already-open store.
|
|
|
|
|
pub fn from_store(store: &Store, key: &str) -> Option<Self> {
|
2026-04-13 19:34:45 -04:00
|
|
|
let node = store.get_node(key).ok()??;
|
2026-03-25 01:39:48 -04:00
|
|
|
|
2026-03-26 21:19:19 -04:00
|
|
|
// If set, tag links to nodes created after this timestamp as (new)
|
|
|
|
|
let older_than: i64 = std::env::var("POC_MEMORIES_OLDER_THAN")
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|s| s.parse().ok())
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let mut neighbors: std::collections::HashMap<&str, (f32, bool)> = std::collections::HashMap::new();
|
2026-03-25 01:39:48 -04:00
|
|
|
for r in &store.relations {
|
|
|
|
|
if r.deleted { continue; }
|
2026-03-26 21:19:19 -04:00
|
|
|
let neighbor_key = if r.source_key == key {
|
|
|
|
|
&r.target_key
|
2026-03-25 01:39:48 -04:00
|
|
|
} else if r.target_key == key {
|
2026-03-26 21:19:19 -04:00
|
|
|
&r.source_key
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-13 19:34:45 -04:00
|
|
|
let is_new = older_than > 0 && store.get_node(neighbor_key)
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
2026-03-26 21:19:19 -04:00
|
|
|
.map(|n| n.created_at > older_than)
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
let e = neighbors.entry(neighbor_key.as_str()).or_insert((0.0, false));
|
|
|
|
|
e.0 = e.0.max(r.strength);
|
|
|
|
|
e.1 = e.1 || is_new;
|
2026-03-25 01:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:19:19 -04:00
|
|
|
let mut links: Vec<(String, f32, bool)> = neighbors.into_iter()
|
|
|
|
|
.map(|(k, (s, new))| (k.to_string(), s, new))
|
2026-03-25 01:39:48 -04:00
|
|
|
.collect();
|
2026-03-25 01:59:13 -04:00
|
|
|
links.sort_by(|a, b| b.1.total_cmp(&a.1));
|
2026-03-25 01:39:48 -04:00
|
|
|
|
|
|
|
|
Some(MemoryNode {
|
|
|
|
|
key: key.to_string(),
|
2026-04-13 19:34:45 -04:00
|
|
|
content: node.content,
|
2026-03-25 01:39:48 -04:00
|
|
|
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
|
2026-03-26 21:19:19 -04:00
|
|
|
let footer: Vec<&(String, f32, bool)> = self.links.iter()
|
|
|
|
|
.filter(|(target, _, _)| !self.content.contains(target.as_str()))
|
2026-03-25 01:39:48 -04:00
|
|
|
.collect();
|
|
|
|
|
|
2026-03-25 01:59:13 -04:00
|
|
|
if !footer.is_empty() {
|
|
|
|
|
let total = footer.len();
|
2026-03-25 01:39:48 -04:00
|
|
|
out.push_str("\n\n---\nLinks:");
|
2026-03-26 21:19:19 -04:00
|
|
|
for (target, strength, is_new) in footer.iter().take(15) {
|
|
|
|
|
let tag = if *is_new { " (new)" } else { "" };
|
|
|
|
|
out.push_str(&format!("\n ({:.2}) `{}`{}", strength, target, tag));
|
2026-03-25 01:39:48 -04:00
|
|
|
}
|
|
|
|
|
if total > 15 {
|
2026-03-26 14:22:21 -04:00
|
|
|
out.push_str(&format!("\n ... and {} more (memory_links({{\"{}\"}}))",
|
2026-03-25 01:39:48 -04:00
|
|
|
total - 15, self.key));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-12 22:20:22 -04:00
|
|
|
|
|
|
|
|
/// 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, key: &str) -> Option<String> {
|
|
|
|
|
crate::hippocampus::memory::MemoryNode::from_store(store, key)
|
|
|
|
|
.map(|node| node.render())
|
|
|
|
|
}
|