consciousness/poc-agent/src/tools/memory.rs

288 lines
11 KiB
Rust
Raw Normal View History

// tools/memory.rs — Native memory graph operations
//
// Structured tool calls for the memory graph, replacing bash
// poc-memory commands. Cleaner for LLMs — no shell quoting,
// multi-line content as JSON strings, typed parameters.
use anyhow::{Context, Result};
use serde_json::json;
use std::process::Command;
use crate::types::ToolDef;
pub fn definitions() -> Vec<ToolDef> {
vec![
ToolDef::new(
"memory_render",
"Read a memory node's content and links. Returns the full content \
with neighbor links sorted by strength.",
json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Node key to render"
}
},
"required": ["key"]
}),
),
ToolDef::new(
"memory_write",
"Create or update a memory node with new content. Use for writing \
prose, analysis, or any node content. Multi-line content is fine.",
json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Node key to create or update"
},
"content": {
"type": "string",
"description": "Full content for the node (markdown)"
}
},
"required": ["key", "content"]
}),
),
ToolDef::new(
"memory_search",
"Search the memory graph for nodes by keyword.",
json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search terms"
}
},
"required": ["query"]
}),
),
ToolDef::new(
"memory_links",
"Show a node's neighbors with link strengths and clustering coefficients.",
json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Node key to show links for"
}
},
"required": ["key"]
}),
),
ToolDef::new(
"memory_link_set",
"Set the strength of a link between two nodes. Also deduplicates \
if multiple links exist between the same pair.",
json!({
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "Source node key"
},
"target": {
"type": "string",
"description": "Target node key"
},
"strength": {
"type": "number",
"description": "Link strength (0.01 to 1.0)"
}
},
"required": ["source", "target", "strength"]
}),
),
ToolDef::new(
"memory_link_add",
"Add a new link between two nodes.",
json!({
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "Source node key"
},
"target": {
"type": "string",
"description": "Target node key"
}
},
"required": ["source", "target"]
}),
),
ToolDef::new(
"memory_used",
"Mark a node as useful (boosts its weight in the graph).",
json!({
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Node key to mark as used"
}
},
"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, provenance: Option<&str>) -> Result<String> {
match name {
"memory_render" => {
let key = args["key"].as_str().context("key is required")?;
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 cmd = Command::new("poc-memory");
cmd.args(["write", 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(content.as_bytes())
.context("write content to stdin")?;
let output = child.wait_with_output().context("wait poc-memory write")?;
Ok(String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr))
}
"memory_search" => {
let query = args["query"].as_str().context("query is required")?;
run_poc_memory(&["search", query], provenance)
}
"memory_links" => {
let key = args["key"].as_str().context("key is required")?;
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)], 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], provenance)
}
"memory_used" => {
let key = args["key"].as_str().context("key is required")?;
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(&["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(&["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], provenance: Option<&str>) -> Result<String> {
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);
if output.status.success() {
Ok(stdout.to_string())
} else {
Ok(format!("{}{}", stdout, stderr))
}
}