From 598f0112a4884ef16830cff9c4dbd1a2ae3df1c7 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Mon, 13 Apr 2026 15:12:06 -0400 Subject: [PATCH] memory_links: return typed Vec with node weights - hippocampus::memory_links now returns Vec with key, link_strength, and node_weight for each neighbor - Unified memory_tool! macro: mut/ref as token, single main rule - All tools use serde serialize/deserialize for RPC consistency - jsonargs handlers now work in client mode (RPC to daemon) - cli/graph.rs formats LinkInfo for display Co-Authored-By: Proof of Concept --- src/agent/tools/memory.rs | 91 ++++++++++++++++++--------------------- src/cli/graph.rs | 7 ++- src/hippocampus/mod.rs | 26 ++++++++--- 3 files changed, 67 insertions(+), 57 deletions(-) diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 042f808..c95d19b 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -209,6 +209,12 @@ macro_rules! memory_tool { (@param_type Option) => { Option }; (@param_type Option) => { Option }; + // Serialize result for jsonargs + (@serialize $t:ty, $result:expr) => { serde_json::to_string(&$result)? }; + + // Deserialize RPC response + (@deserialize $t:ty, $json:expr) => { serde_json::from_str(&$json).map_err(|e| anyhow::anyhow!("{}", e)) }; + // Serialize to JSON for RPC (@insert_json $map:ident, $name:ident, str) => { $map.insert(stringify!($name).into(), serde_json::json!($name)); @@ -241,64 +247,45 @@ macro_rules! memory_tool { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; - // ── Main rules ───────────────────────────────────────────────── - - // Mutable store variant - ($name:ident, mut $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { - paste::paste! { - async fn [](agent: &Option>, args: &serde_json::Value) -> Result { - $($(let $arg = memory_tool!(@extract args, $arg, $($typ)+);)*)? - let prov = get_provenance(agent).await; - match access() { - StoreAccess::Daemon(arc) => { - let mut store = arc.lock().await; - crate::hippocampus::$name(&mut store, &prov $($(, $arg)*)?) - } - StoreAccess::Client => anyhow::bail!("jsonargs called in client mode"), - StoreAccess::None(err) => anyhow::bail!("{}", err), - } - } - - pub async fn $name(agent: Option<&crate::agent::Agent> $($(, $arg: memory_tool!(@param_type $($typ)+))*)?) -> Result { - let prov = match agent { - Some(a) => a.state.lock().await.provenance.clone(), - None => "manual".to_string(), - }; - - match access() { - StoreAccess::Daemon(arc) => { - let mut store = arc.lock().await; - crate::hippocampus::$name(&mut store, &prov $($(, $arg)*)?) - } - StoreAccess::Client => { - #[allow(unused_mut)] - let mut map = serde_json::Map::new(); - $($(memory_tool!(@insert_json map, $arg, $($typ)+);)*)? - memory_rpc(stringify!($name), serde_json::Value::Object(map)) - } - StoreAccess::None(err) => anyhow::bail!("{}", err), - } - } - } + // Call hippocampus with appropriate mutability + (@call mut, $name:ident, $store:ident, $prov:expr $(, $arg:expr)*) => { + crate::hippocampus::$name(&mut $store, $prov $(, $arg)*) + }; + (@call ref, $name:ident, $store:ident, $prov:expr $(, $arg:expr)*) => { + crate::hippocampus::$name(&$store, $prov $(, $arg)*) }; - // Immutable store variant - ($name:ident, ref $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { + // ── Main rules ───────────────────────────────────────────────── + + // Shorthand: mut/ref without return type defaults to String + ($name:ident, $m:ident $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { + memory_tool!($name, $m -> String $(, $($arg : [$($typ)+]),*)?); + }; + + // Full form with return type + ($name:ident, $m:ident -> $ret:ty $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { paste::paste! { async fn [](agent: &Option>, args: &serde_json::Value) -> Result { $($(let $arg = memory_tool!(@extract args, $arg, $($typ)+);)*)? let prov = get_provenance(agent).await; match access() { StoreAccess::Daemon(arc) => { - let store = arc.lock().await; - crate::hippocampus::$name(&store, &prov $($(, $arg)*)?) + #[allow(unused_mut)] + let mut store = arc.lock().await; + let result: $ret = memory_tool!(@call $m, $name, store, &prov $($(, $arg)*)?)?; + Ok(memory_tool!(@serialize $ret, result)) + } + StoreAccess::Client => { + #[allow(unused_mut)] + let mut map = serde_json::Map::new(); + $($(memory_tool!(@insert_json map, $arg, $($typ)+);)*)? + memory_rpc(stringify!($name), serde_json::Value::Object(map)) } - StoreAccess::Client => anyhow::bail!("jsonargs called in client mode"), StoreAccess::None(err) => anyhow::bail!("{}", err), } } - pub async fn $name(agent: Option<&crate::agent::Agent> $($(, $arg: memory_tool!(@param_type $($typ)+))*)?) -> Result { + pub async fn $name(agent: Option<&crate::agent::Agent> $($(, $arg: memory_tool!(@param_type $($typ)+))*)?) -> Result<$ret> { let prov = match agent { Some(a) => a.state.lock().await.provenance.clone(), None => "manual".to_string(), @@ -306,14 +293,16 @@ macro_rules! memory_tool { match access() { StoreAccess::Daemon(arc) => { - let store = arc.lock().await; - crate::hippocampus::$name(&store, &prov $($(, $arg)*)?) + #[allow(unused_mut)] + let mut store = arc.lock().await; + memory_tool!(@call $m, $name, store, &prov $($(, $arg)*)?) } StoreAccess::Client => { #[allow(unused_mut)] let mut map = serde_json::Map::new(); $($(memory_tool!(@insert_json map, $arg, $($typ)+);)*)? - memory_rpc(stringify!($name), serde_json::Value::Object(map)) + let json = memory_rpc(stringify!($name), serde_json::Value::Object(map))?; + memory_tool!(@deserialize $ret, json) } StoreAccess::None(err) => anyhow::bail!("{}", err), } @@ -327,7 +316,6 @@ macro_rules! memory_tool { memory_tool!(memory_render, ref, key: [str], raw: [Option]); memory_tool!(memory_write, mut, key: [str], content: [str]); memory_tool!(memory_search, ref, keys: [Vec], max_hops: [Option], edge_decay: [Option], min_activation: [Option], limit: [Option]); -memory_tool!(memory_links, ref, key: [str]); memory_tool!(memory_link_set, mut, source: [str], target: [str], strength: [f32]); memory_tool!(memory_link_add, mut, source: [str], target: [str]); memory_tool!(memory_delete, mut, key: [str]); @@ -337,6 +325,11 @@ memory_tool!(memory_rename, mut, old_key: [str], new_key: [str]); memory_tool!(memory_supersede, mut, old_key: [str], new_key: [str], reason: [Option<&str>]); memory_tool!(memory_query, ref, query: [str], format: [Option<&str>]); +// Re-export LinkInfo for callers +pub use crate::hippocampus::LinkInfo; + +memory_tool!(memory_links, ref -> Vec, key: [str]); + // ── Journal tools ────────────────────────────────────────────── memory_tool!(journal_tail, ref, count: [Option], level: [Option], format: [Option<&str>], after: [Option<&str>]); diff --git a/src/cli/graph.rs b/src/cli/graph.rs index d8d667e..fad9a45 100644 --- a/src/cli/graph.rs +++ b/src/cli/graph.rs @@ -28,9 +28,12 @@ pub async fn cmd_link(key: &[String]) -> Result<(), String> { return Err("link requires a key".into()); } let key = key.join(" "); - let result = memory::memory_links(None, &key).await + let links = memory::memory_links(None, &key).await .map_err(|e| e.to_string())?; - print!("{}", result); + println!("Neighbors of '{}':", key); + for link in links { + println!(" ({:.2}) {} [w={:.2}]", link.link_strength, link.key, link.node_weight); + } Ok(()) } diff --git a/src/hippocampus/mod.rs b/src/hippocampus/mod.rs index d659fd4..6e003ca 100644 --- a/src/hippocampus/mod.rs +++ b/src/hippocampus/mod.rs @@ -84,15 +84,29 @@ pub fn memory_search( .collect::>().join("\n")) } -pub fn memory_links(store: &Store, _provenance: &str, key: &str) -> Result { +/// Info about a linked neighbor node. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct LinkInfo { + pub key: String, + pub link_strength: f32, + pub node_weight: f32, +} + +pub fn memory_links(store: &Store, _provenance: &str, key: &str) -> Result> { let node = MemoryNode::from_store(store, key) .ok_or_else(|| anyhow::anyhow!("node not found: {}", key))?; - let mut out = format!("Neighbors of '{}':\n", key); - for (target, strength, is_new) in &node.links { - let tag = if *is_new { " (new)" } else { "" }; - out.push_str(&format!(" ({:.2}) {}{}\n", strength, target, tag)); + let mut links = Vec::new(); + for (target, strength, _is_new) in &node.links { + let node_weight = store.nodes.get(target.as_str()) + .map(|n| n.weight) + .unwrap_or(0.5); + links.push(LinkInfo { + key: target.clone(), + link_strength: *strength, + node_weight, + }); } - Ok(out) + Ok(links) } pub fn memory_link_set(store: &mut Store, _provenance: &str, source: &str, target: &str, strength: f32) -> Result {