diff --git a/poc-agent/src/tools/memory.rs b/poc-agent/src/tools/memory.rs index 5fb11b7..eb374fa 100644 --- a/poc-agent/src/tools/memory.rs +++ b/poc-agent/src/tools/memory.rs @@ -129,25 +129,71 @@ pub fn definitions() -> Vec { "required": ["key"] }), ), + ToolDef::new( + "memory_weight_set", + "Set a node's weight directly. Use to downweight junk nodes (0.01) \ + or boost important ones. Normal range is 0.1 to 1.0.", + json!({ + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Node key" + }, + "weight": { + "type": "number", + "description": "New weight (0.01 to 1.0)" + } + }, + "required": ["key", "weight"] + }), + ), + ToolDef::new( + "memory_supersede", + "Mark a node as superseded by another. Sets the old node's weight \ + to 0.01 and prepends a notice pointing to the replacement. Use \ + when merging duplicates or replacing junk with proper content.", + json!({ + "type": "object", + "properties": { + "old_key": { + "type": "string", + "description": "Node being superseded" + }, + "new_key": { + "type": "string", + "description": "Replacement node" + }, + "reason": { + "type": "string", + "description": "Why this node was superseded (e.g. 'merged into X', 'duplicate of Y')" + } + }, + "required": ["old_key", "new_key"] + }), + ), ] } /// Dispatch a memory tool call. Shells out to poc-memory CLI. -pub fn dispatch(name: &str, args: &serde_json::Value) -> Result { +pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>) -> Result { match name { "memory_render" => { let key = args["key"].as_str().context("key is required")?; - run_poc_memory(&["render", key]) + run_poc_memory(&["render", key], provenance) } "memory_write" => { let key = args["key"].as_str().context("key is required")?; let content = args["content"].as_str().context("content is required")?; - let mut child = Command::new("poc-memory") - .args(["write", key]) + let mut cmd = Command::new("poc-memory"); + cmd.args(["write", key]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() + .stderr(std::process::Stdio::piped()); + if let Some(prov) = provenance { + cmd.env("POC_PROVENANCE", prov); + } + let mut child = cmd.spawn() .context("spawn poc-memory write")?; use std::io::Write; child.stdin.take().unwrap().write_all(content.as_bytes()) @@ -158,35 +204,78 @@ pub fn dispatch(name: &str, args: &serde_json::Value) -> Result { } "memory_search" => { let query = args["query"].as_str().context("query is required")?; - run_poc_memory(&["search", query]) + run_poc_memory(&["search", query], provenance) } "memory_links" => { let key = args["key"].as_str().context("key is required")?; - run_poc_memory(&["graph", "link", key]) + run_poc_memory(&["graph", "link", key], provenance) } "memory_link_set" => { let source = args["source"].as_str().context("source is required")?; let target = args["target"].as_str().context("target is required")?; let strength = args["strength"].as_f64().context("strength is required")?; - run_poc_memory(&["graph", "link-set", source, target, &format!("{:.2}", strength)]) + run_poc_memory(&["graph", "link-set", source, target, &format!("{:.2}", strength)], provenance) } "memory_link_add" => { let source = args["source"].as_str().context("source is required")?; let target = args["target"].as_str().context("target is required")?; - run_poc_memory(&["graph", "link-add", source, target]) + run_poc_memory(&["graph", "link-add", source, target], provenance) } "memory_used" => { let key = args["key"].as_str().context("key is required")?; - run_poc_memory(&["used", key]) + run_poc_memory(&["used", key], provenance) + } + "memory_weight_set" => { + let key = args["key"].as_str().context("key is required")?; + let weight = args["weight"].as_f64().context("weight is required")?; + run_poc_memory(&["admin", "weight-set", key, &format!("{:.2}", weight)], provenance) + } + "memory_supersede" => { + let old_key = args["old_key"].as_str().context("old_key is required")?; + let new_key = args["new_key"].as_str().context("new_key is required")?; + let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or("superseded"); + + // Read old node, prepend superseded notice, write back, set weight to 0.01 + let old_content = run_poc_memory(&["render", old_key], provenance).unwrap_or_default(); + // Strip the links section from render output + let content_only = old_content.split("\n\n---\nLinks:").next().unwrap_or(&old_content); + let notice = format!( + "**SUPERSEDED** by `{}` — {}\n\nOriginal content preserved below for reference.\n\n---\n\n{}", + new_key, reason, content_only.trim() + ); + let mut cmd = Command::new("poc-memory"); + cmd.args(["write", old_key]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + if let Some(prov) = provenance { + cmd.env("POC_PROVENANCE", prov); + } + let mut child = cmd.spawn() + .context("spawn poc-memory write")?; + use std::io::Write; + child.stdin.take().unwrap().write_all(notice.as_bytes()) + .context("write supersede notice")?; + let output = child.wait_with_output().context("wait poc-memory write")?; + let write_result = String::from_utf8_lossy(&output.stdout).to_string(); + + // Set weight to 0.01 + let weight_result = run_poc_memory(&["admin", "weight-set", old_key, "0.01"], provenance) + .unwrap_or_else(|e| format!("weight-set failed: {}", e)); + + Ok(format!("{}\n{}", write_result.trim(), weight_result.trim())) } _ => Err(anyhow::anyhow!("Unknown memory tool: {}", name)), } } -fn run_poc_memory(args: &[&str]) -> Result { - let output = Command::new("poc-memory") - .args(args) - .output() +fn run_poc_memory(args: &[&str], provenance: Option<&str>) -> Result { + let mut cmd = Command::new("poc-memory"); + cmd.args(args); + if let Some(prov) = provenance { + cmd.env("POC_PROVENANCE", prov); + } + let output = cmd.output() .context("run poc-memory")?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/poc-agent/src/tools/mod.rs b/poc-agent/src/tools/mod.rs index 156347d..f2bf73b 100644 --- a/poc-agent/src/tools/mod.rs +++ b/poc-agent/src/tools/mod.rs @@ -96,7 +96,7 @@ pub async fn dispatch( "grep" => grep::grep(args), "glob" => glob_tool::glob_search(args), "journal" => journal::write_entry(args), - n if n.starts_with("memory_") => memory::dispatch(n, args), + n if n.starts_with("memory_") => memory::dispatch(n, args, None), "view_image" => { return match vision::view_image(args) { Ok(output) => output, diff --git a/poc-memory/src/agents/api.rs b/poc-memory/src/agents/api.rs index 95e8b2a..7dee5b3 100644 --- a/poc-memory/src/agents/api.rs +++ b/poc-memory/src/agents/api.rs @@ -82,7 +82,24 @@ pub async fn call_api_with_tools( let args: serde_json::Value = serde_json::from_str(&call.function.arguments) .unwrap_or_default(); - let output = tools::dispatch(&call.function.name, &args, &tracker).await; + let output = if call.function.name.starts_with("memory_") { + let prov = format!("agent:{}", agent); + match poc_agent::tools::memory::dispatch( + &call.function.name, &args, Some(&prov), + ) { + Ok(text) => poc_agent::tools::ToolOutput { + text, is_yield: false, images: Vec::new(), + model_switch: None, dmn_pause: false, + }, + Err(e) => poc_agent::tools::ToolOutput { + text: format!("Error: {}", e), + is_yield: false, images: Vec::new(), + model_switch: None, dmn_pause: false, + }, + } + } else { + tools::dispatch(&call.function.name, &args, &tracker).await + }; log(&format!("tool result: {} chars", output.text.len())); diff --git a/poc-memory/src/store/ops.rs b/poc-memory/src/store/ops.rs index ae9d751..030ecdd 100644 --- a/poc-memory/src/store/ops.rs +++ b/poc-memory/src/store/ops.rs @@ -61,6 +61,7 @@ impl Store { let mut node = existing.clone(); node.content = content.to_string(); node.provenance = provenance.to_string(); + node.timestamp = now_epoch(); node.version += 1; self.append_nodes_unlocked(std::slice::from_ref(&node))?; self.nodes.insert(key.to_string(), node);