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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<ToolDef> {
|
||||
vec![
|
||||
|
|
@ -246,70 +246,18 @@ fn link_set(source: &str, target: &str, strength: f32) -> Result<String> {
|
|||
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<String> {
|
||||
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<String> {
|
|||
fn weight_set(key: &str, weight: f32) -> Result<String> {
|
||||
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<String> {
|
||||
|
|
|
|||
|
|
@ -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<crate::agent::memory::MemoryNode>,
|
||||
pub loaded_nodes: Vec<crate::hippocampus::memory::MemoryNode>,
|
||||
}
|
||||
|
||||
pub const WORKING_STACK_INSTRUCTIONS: &str = "/home/kent/.config/poc-agent/working-stack.md";
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
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),
|
||||
}
|
||||
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;
|
||||
let old = store.set_link_strength(&source, &target, 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));
|
||||
}
|
||||
|
||||
store.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
let (old, new) = store.set_weight(&resolved, weight)?;
|
||||
println!("Weight: {} {:.2} → {:.2}", resolved, old, new);
|
||||
store.save()?;
|
||||
} else {
|
||||
return Err(format!("Node not found: {}", resolved));
|
||||
}
|
||||
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<String> {
|
||||
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<String> = 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<String> = 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> {
|
||||
|
|
|
|||
|
|
@ -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<Vec<SearchResult>, 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(),
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue