// tools/memory.rs — Native memory graph operations // // Direct library calls into the store — no subprocess spawning. use anyhow::{Context, Result}; use serde_json::json; use crate::hippocampus::memory::MemoryNode; use crate::agent::types::ToolDef; use crate::store::Store; pub fn definitions() -> Vec { vec![ ToolDef::new("memory_render", "Read a memory node's content and links.", json!({"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]})), ToolDef::new("memory_write", "Create or update a memory node.", json!({"type":"object","properties":{"key":{"type":"string","description":"Node key"},"content":{"type":"string","description":"Full content (markdown)"}},"required":["key","content"]})), ToolDef::new("memory_search", "Search the memory graph by keyword.", json!({"type":"object","properties":{"query":{"type":"string","description":"Search terms"}},"required":["query"]})), ToolDef::new("memory_links", "Show a node's neighbors with link strengths.", json!({"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]})), ToolDef::new("memory_link_set", "Set link strength between two nodes.", json!({"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"},"strength":{"type":"number","description":"0.01 to 1.0"}},"required":["source","target","strength"]})), ToolDef::new("memory_link_add", "Add a new link between two nodes.", json!({"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"]})), ToolDef::new("memory_used", "Mark a node as useful (boosts weight).", json!({"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]})), ToolDef::new("memory_weight_set", "Set a node's weight directly (0.01 to 1.0).", json!({"type":"object","properties":{"key":{"type":"string"},"weight":{"type":"number","description":"0.01 to 1.0"}},"required":["key","weight"]})), ToolDef::new("memory_supersede", "Mark a node as superseded by another (sets weight to 0.01).", json!({"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"},"reason":{"type":"string"}},"required":["old_key","new_key"]})), ] } /// Dispatch a memory tool call. Direct library calls, no subprocesses. pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>) -> Result { let prov = provenance.unwrap_or("manual"); match name { "memory_render" => { let key = get_str(args, "key")?; Ok(MemoryNode::load(key) .ok_or_else(|| anyhow::anyhow!("node not found: {}", key))? .render()) } "memory_write" => { let key = get_str(args, "key")?; let content = get_str(args, "content")?; let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let result = store.upsert_provenance(key, content, prov) .map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("{} '{}'", result, key)) } "memory_search" => { let query = get_str(args, "query")?; let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let results = crate::search::search(query, &store); if results.is_empty() { Ok("no results".into()) } else { Ok(results.iter().take(20) .map(|r| format!("({:.2}) {} — {}", r.activation, r.key, r.snippet.as_deref().unwrap_or(""))) .collect::>().join("\n")) } } "memory_links" => { let key = get_str(args, "key")?; let node = MemoryNode::load(key) .ok_or_else(|| anyhow::anyhow!("node not found: {}", key))?; let mut out = format!("Neighbors of '{}':\n", key); for (target, strength) in &node.links { out.push_str(&format!(" ({:.2}) {}\n", strength, target)); } Ok(out) } "memory_link_set" | "memory_link_add" | "memory_used" | "memory_weight_set" => { with_store(name, args, prov) } "memory_supersede" => { let old_key = get_str(args, "old_key")?; let new_key = get_str(args, "new_key")?; let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or("superseded"); let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let content = store.nodes.get(old_key) .map(|n| n.content.clone()) .ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?; let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}", new_key, reason, content.trim()); store.upsert_provenance(old_key, ¬ice, prov) .map_err(|e| anyhow::anyhow!("{}", e))?; store.set_weight(old_key, 0.01).map_err(|e| anyhow::anyhow!("{}", e))?; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("superseded {} → {} ({})", old_key, new_key, reason)) } _ => anyhow::bail!("Unknown memory tool: {}", name), } } /// Store mutations that follow the same pattern: load, resolve, mutate, save. fn with_store(name: &str, args: &serde_json::Value, prov: &str) -> Result { let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; let msg = match name { "memory_link_set" => { let s = store.resolve_key(get_str(args, "source")?).map_err(|e| anyhow::anyhow!("{}", e))?; let t = store.resolve_key(get_str(args, "target")?).map_err(|e| anyhow::anyhow!("{}", e))?; let strength = get_f64(args, "strength")? as f32; let old = store.set_link_strength(&s, &t, strength).map_err(|e| anyhow::anyhow!("{}", e))?; format!("{} ↔ {} strength {:.2} → {:.2}", s, t, old, strength) } "memory_link_add" => { let s = store.resolve_key(get_str(args, "source")?).map_err(|e| anyhow::anyhow!("{}", e))?; let t = store.resolve_key(get_str(args, "target")?).map_err(|e| anyhow::anyhow!("{}", e))?; let strength = store.add_link(&s, &t, prov).map_err(|e| anyhow::anyhow!("{}", e))?; format!("linked {} → {} (strength={:.2})", s, t, strength) } "memory_used" => { let key = get_str(args, "key")?; if !store.nodes.contains_key(key) { anyhow::bail!("node not found: {}", key); } store.mark_used(key); format!("marked {} as used", key) } "memory_weight_set" => { let key = store.resolve_key(get_str(args, "key")?).map_err(|e| anyhow::anyhow!("{}", e))?; let weight = get_f64(args, "weight")? as f32; let (old, new) = store.set_weight(&key, weight).map_err(|e| anyhow::anyhow!("{}", e))?; format!("weight {} {:.2} → {:.2}", key, old, new) } _ => unreachable!(), }; store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(msg) } fn get_str<'a>(args: &'a serde_json::Value, name: &'a str) -> Result<&'a str> { args.get(name).and_then(|v| v.as_str()).context(format!("{} is required", name)) } fn get_f64(args: &serde_json::Value, name: &str) -> Result { args.get(name).and_then(|v| v.as_f64()).context(format!("{} is required", name)) }