memory: fix timestamp and provenance on agent writes
Two bugs: upsert_provenance didn't update node.timestamp, so history
showed the original creation date for every version. And native memory
tools (poc-agent dispatch) didn't set POC_PROVENANCE, so all agent
writes showed provenance "manual" instead of "agent:organize" etc.
Fix: set node.timestamp = now_epoch() in upsert_provenance. Thread
provenance through memory::dispatch as Option<&str>, set it via
.env("POC_PROVENANCE") on each subprocess Command. api.rs passes
"agent:{name}" for daemon agent calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f45f663dc0
commit
5ef9098deb
4 changed files with 124 additions and 17 deletions
|
|
@ -129,25 +129,71 @@ pub fn definitions() -> Vec<ToolDef> {
|
||||||
"required": ["key"]
|
"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.
|
/// Dispatch a memory tool call. Shells out to poc-memory CLI.
|
||||||
pub fn dispatch(name: &str, args: &serde_json::Value) -> Result<String> {
|
pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>) -> Result<String> {
|
||||||
match name {
|
match name {
|
||||||
"memory_render" => {
|
"memory_render" => {
|
||||||
let key = args["key"].as_str().context("key is required")?;
|
let key = args["key"].as_str().context("key is required")?;
|
||||||
run_poc_memory(&["render", key])
|
run_poc_memory(&["render", key], provenance)
|
||||||
}
|
}
|
||||||
"memory_write" => {
|
"memory_write" => {
|
||||||
let key = args["key"].as_str().context("key is required")?;
|
let key = args["key"].as_str().context("key is required")?;
|
||||||
let content = args["content"].as_str().context("content is required")?;
|
let content = args["content"].as_str().context("content is required")?;
|
||||||
let mut child = Command::new("poc-memory")
|
let mut cmd = Command::new("poc-memory");
|
||||||
.args(["write", key])
|
cmd.args(["write", key])
|
||||||
.stdin(std::process::Stdio::piped())
|
.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
.stderr(std::process::Stdio::piped())
|
.stderr(std::process::Stdio::piped());
|
||||||
.spawn()
|
if let Some(prov) = provenance {
|
||||||
|
cmd.env("POC_PROVENANCE", prov);
|
||||||
|
}
|
||||||
|
let mut child = cmd.spawn()
|
||||||
.context("spawn poc-memory write")?;
|
.context("spawn poc-memory write")?;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
child.stdin.take().unwrap().write_all(content.as_bytes())
|
child.stdin.take().unwrap().write_all(content.as_bytes())
|
||||||
|
|
@ -158,35 +204,78 @@ pub fn dispatch(name: &str, args: &serde_json::Value) -> Result<String> {
|
||||||
}
|
}
|
||||||
"memory_search" => {
|
"memory_search" => {
|
||||||
let query = args["query"].as_str().context("query is required")?;
|
let query = args["query"].as_str().context("query is required")?;
|
||||||
run_poc_memory(&["search", query])
|
run_poc_memory(&["search", query], provenance)
|
||||||
}
|
}
|
||||||
"memory_links" => {
|
"memory_links" => {
|
||||||
let key = args["key"].as_str().context("key is required")?;
|
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" => {
|
"memory_link_set" => {
|
||||||
let source = args["source"].as_str().context("source is required")?;
|
let source = args["source"].as_str().context("source is required")?;
|
||||||
let target = args["target"].as_str().context("target is required")?;
|
let target = args["target"].as_str().context("target is required")?;
|
||||||
let strength = args["strength"].as_f64().context("strength 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" => {
|
"memory_link_add" => {
|
||||||
let source = args["source"].as_str().context("source is required")?;
|
let source = args["source"].as_str().context("source is required")?;
|
||||||
let target = args["target"].as_str().context("target 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" => {
|
"memory_used" => {
|
||||||
let key = args["key"].as_str().context("key is required")?;
|
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)),
|
_ => Err(anyhow::anyhow!("Unknown memory tool: {}", name)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_poc_memory(args: &[&str]) -> Result<String> {
|
fn run_poc_memory(args: &[&str], provenance: Option<&str>) -> Result<String> {
|
||||||
let output = Command::new("poc-memory")
|
let mut cmd = Command::new("poc-memory");
|
||||||
.args(args)
|
cmd.args(args);
|
||||||
.output()
|
if let Some(prov) = provenance {
|
||||||
|
cmd.env("POC_PROVENANCE", prov);
|
||||||
|
}
|
||||||
|
let output = cmd.output()
|
||||||
.context("run poc-memory")?;
|
.context("run poc-memory")?;
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ pub async fn dispatch(
|
||||||
"grep" => grep::grep(args),
|
"grep" => grep::grep(args),
|
||||||
"glob" => glob_tool::glob_search(args),
|
"glob" => glob_tool::glob_search(args),
|
||||||
"journal" => journal::write_entry(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" => {
|
"view_image" => {
|
||||||
return match vision::view_image(args) {
|
return match vision::view_image(args) {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,24 @@ pub async fn call_api_with_tools(
|
||||||
let args: serde_json::Value = serde_json::from_str(&call.function.arguments)
|
let args: serde_json::Value = serde_json::from_str(&call.function.arguments)
|
||||||
.unwrap_or_default();
|
.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()));
|
log(&format!("tool result: {} chars", output.text.len()));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ impl Store {
|
||||||
let mut node = existing.clone();
|
let mut node = existing.clone();
|
||||||
node.content = content.to_string();
|
node.content = content.to_string();
|
||||||
node.provenance = provenance.to_string();
|
node.provenance = provenance.to_string();
|
||||||
|
node.timestamp = now_epoch();
|
||||||
node.version += 1;
|
node.version += 1;
|
||||||
self.append_nodes_unlocked(std::slice::from_ref(&node))?;
|
self.append_nodes_unlocked(std::slice::from_ref(&node))?;
|
||||||
self.nodes.insert(key.to_string(), node);
|
self.nodes.insert(key.to_string(), node);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue