From 6f2e0938f0fc01ed73f2ac85fbb6ff625d833041 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 31 Mar 2026 18:21:01 -0400 Subject: [PATCH] memory: add spreading activation tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `poc-memory graph spread` command that takes multiple seed node keys, runs spreading activation through the graph, and returns nodes ranked by total activation — nodes that bridge multiple seed concepts score highest. Expose spreading_activation() as pub from the query engine. Add memory_spread and memory_search_content tool definitions for MCP. Co-Authored-By: Proof of Concept --- src/cli/graph.rs | 40 +++++++++++++++++++++++++++++++++ src/hippocampus/query/engine.rs | 2 +- src/thought/memory.rs | 8 +++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/cli/graph.rs b/src/cli/graph.rs index 05c7f9e..2b9790f 100644 --- a/src/cli/graph.rs +++ b/src/cli/graph.rs @@ -6,6 +6,7 @@ // trace, spectral-*, organize, interference. use crate::{store, graph, neuro, spectral}; +use crate::store::StoreView; pub fn cmd_graph() -> Result<(), String> { let store = store::Store::load()?; @@ -109,6 +110,45 @@ pub fn cmd_normalize_strengths(apply: bool) -> Result<(), String> { Ok(()) } +pub fn cmd_spread(keys: &[String], max_results: usize) -> Result<(), String> { + if keys.is_empty() { + return Err("spread requires at least one seed key".into()); + } + + let store = store::Store::load()?; + let graph = graph::build_graph_fast(&store); + let params = store.params(); + + let seeds: Vec<(String, f64)> = keys.iter() + .filter_map(|k| { + let resolved = store.resolve_key(k).ok()?; + Some((resolved, 1.0)) + }) + .collect(); + + if seeds.is_empty() { + return Err("no valid seed keys found".into()); + } + + let results = crate::search::spreading_activation( + &seeds, &graph, &store, + params.max_hops, params.edge_decay, params.min_activation, + ); + + let seed_keys: std::collections::HashSet<&str> = seeds.iter() + .map(|(k, _)| k.as_str()) + .collect(); + + for (key, score) in results.iter() + .filter(|(k, _)| !seed_keys.contains(k.as_str())) + .take(max_results) + { + println!(" {:.2} {}", score, key); + } + + Ok(()) +} + pub fn cmd_link(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("link requires a key".into()); diff --git a/src/hippocampus/query/engine.rs b/src/hippocampus/query/engine.rs index 1237008..890b879 100644 --- a/src/hippocampus/query/engine.rs +++ b/src/hippocampus/query/engine.rs @@ -1377,7 +1377,7 @@ fn run_manifold( /// sum at each node, and the combined activation map propagates on /// the next hop. This creates interference patterns — nodes where /// multiple wavefronts overlap get reinforced and radiate stronger. -fn spreading_activation( +pub fn spreading_activation( seeds: &[(String, f64)], graph: &Graph, store: &impl StoreView, diff --git a/src/thought/memory.rs b/src/thought/memory.rs index f0206a2..fdac7de 100644 --- a/src/thought/memory.rs +++ b/src/thought/memory.rs @@ -20,6 +20,14 @@ pub fn definitions() -> Vec { 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_search_content", + "Search the memory graph by keyword (searches node content, not just keys).", + json!({"type":"object","properties":{"query":{"type":"string","description":"Search terms"}},"required":["query"]})), + ToolDef::new("memory_spread", + "Find related nodes via spreading activation from multiple seed nodes. \ + Propagates activation through the graph and returns nodes ranked by \ + total activation. Use to find nodes that connect multiple concepts.", + json!({"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"},"description":"Seed node keys to activate from"}},"required":["keys"]})), ToolDef::new("memory_links", "Show a node's neighbors with link strengths.", json!({"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]})),