diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 98404b6..9a36799 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -38,6 +38,11 @@ pub fn definitions() -> Vec { 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"]})), + ToolDef::new("memory_query", + "Run a structured query against the memory graph. Supports filtering, \ + sorting, field selection. Examples: \"degree > 10 | sort weight | limit 5\", \ + \"neighbors('identity') | select strength\", \"key ~ 'journal.*' | count\"", + json!({"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]})), ] } @@ -102,6 +107,13 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>) store.save().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(format!("superseded {} → {} ({})", old_key, new_key, reason)) } + "memory_query" => { + let query = get_str(args, "query")?; + let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; + let graph = store.build_graph(); + crate::query_parser::query_to_string(&store, &graph, query) + .map_err(|e| anyhow::anyhow!("{}", e)) + } _ => anyhow::bail!("Unknown memory tool: {}", name), } } diff --git a/src/hippocampus/query/parser.rs b/src/hippocampus/query/parser.rs index 50ffdd3..64ffc00 100644 --- a/src/hippocampus/query/parser.rs +++ b/src/hippocampus/query/parser.rs @@ -520,6 +520,51 @@ pub fn run_query(store: &Store, graph: &Graph, query_str: &str) -> Result<(), St Ok(()) } +/// Run a query and return the output as a string (for tool calls). +pub fn query_to_string(store: &Store, graph: &Graph, query_str: &str) -> Result { + let q = query_parser::query(query_str) + .map_err(|e| format!("Parse error: {}", e))?; + + let results = execute_parsed(store, graph, &q)?; + + if q.stages.iter().any(|s| matches!(s, Stage::Count)) { + return Ok(results.len().to_string()); + } + if results.is_empty() { + return Ok("no results".to_string()); + } + + let fields: Option<&Vec> = q.stages.iter().find_map(|s| match s { + Stage::Select(f) => Some(f), + _ => None, + }); + + let mut out = String::new(); + if let Some(fields) = fields { + let mut header = vec!["key".to_string()]; + header.extend(fields.iter().cloned()); + out.push_str(&header.join("\t")); + out.push('\n'); + for r in &results { + let mut row = vec![r.key.clone()]; + for f in fields { + row.push(match r.fields.get(f) { + Some(v) => format_value(v), + None => "-".to_string(), + }); + } + out.push_str(&row.join("\t")); + out.push('\n'); + } + } else { + for r in &results { + out.push_str(&r.key); + out.push('\n'); + } + } + Ok(out) +} + // -- Connectivity analysis -- /// BFS shortest path between two nodes, max_hops limit.