refactor: clean up tool dispatch and extract helpers

- Move working_stack tool to tools/working_stack.rs (was orphaned in agent.rs)
- Create control.rs for pause/switch_model/yield_to_user with Result<ToolOutput>
- Add ToolOutput::error() and ToolOutput::text() helper constructors
- Clean up dispatch() with Option<Result<ToolOutput>> pattern for rich tools
- Refactor memory.rs: extract cmd(), write_node(), supersede(), get_str(), get_f64()
- Merge run_rg() and run_grep() into unified run_search() in grep.rs
- Extract truncate_output() helper shared by bash, grep, glob tools

Net: -77 lines, better structure, less duplication
This commit is contained in:
Kent Overstreet 2026-03-21 15:18:53 -04:00
parent 3fd485a2e9
commit 45b7bba22a
6 changed files with 290 additions and 264 deletions

View file

@ -6,7 +6,8 @@
use anyhow::{Context, Result};
use serde_json::json;
use std::process::Command;
use std::io::Write;
use std::process::{Command, Stdio};
use crate::types::ToolDef;
@ -177,106 +178,58 @@ pub fn definitions() -> Vec<ToolDef> {
/// 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 {
let result = match name {
"memory_render" => {
let key = args["key"].as_str().context("key is required")?;
run_poc_memory(&["render", key], provenance)
let key = get_str(args, "key")?;
cmd(&["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))
let key = get_str(args, "key")?;
let content = get_str(args, "content")?;
write_node(key, content, provenance)?
}
"memory_search" => {
let query = args["query"].as_str().context("query is required")?;
run_poc_memory(&["search", query], provenance)
let query = get_str(args, "query")?;
cmd(&["search", query], provenance)?
}
"memory_links" => {
let key = args["key"].as_str().context("key is required")?;
run_poc_memory(&["graph", "link", key], provenance)
let key = get_str(args, "key")?;
cmd(&["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)
let source = get_str(args, "source")?;
let target = get_str(args, "target")?;
let strength = get_f64(args, "strength")?;
cmd(&["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)
let source = get_str(args, "source")?;
let target = get_str(args, "target")?;
cmd(&["graph", "link-add", source, target], provenance)?
}
"memory_used" => {
let key = args["key"].as_str().context("key is required")?;
run_poc_memory(&["used", key], provenance)
let key = get_str(args, "key")?;
cmd(&["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)
let key = get_str(args, "key")?;
let weight = get_f64(args, "weight")?;
cmd(&["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)),
}
"memory_supersede" => supersede(args, provenance)?,
_ => anyhow::bail!("Unknown memory tool: {}", name),
};
Ok(result)
}
fn run_poc_memory(args: &[&str], provenance: Option<&str>) -> Result<String> {
/// Run poc-memory command and return stdout.
fn cmd(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 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() {
@ -285,3 +238,60 @@ fn run_poc_memory(args: &[&str], provenance: Option<&str>) -> Result<String> {
Ok(format!("{}{}", stdout, stderr))
}
}
/// Write content to a node via stdin.
fn write_node(key: &str, content: &str, provenance: Option<&str>) -> Result<String> {
let mut cmd = Command::new("poc-memory");
cmd.args(["write", key])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(prov) = provenance {
cmd.env("POC_PROVENANCE", prov);
}
let mut child = cmd.spawn().context("spawn poc-memory 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))
}
/// Handle memory_supersede - reads old node, prepends notice, writes back, sets weight.
fn supersede(args: &serde_json::Value, provenance: Option<&str>) -> Result<String> {
let old_key = get_str(args, "old_key")?;
let new_key = get_str(args, "new_key")?;
let reason = args.get("reason").and_then(|v| v.as_str()).unwrap_or("superseded");
// Read old node
let old_content = cmd(&["render", old_key], provenance)?;
let content_only = old_content.split("\n\n---\nLinks:").next().unwrap_or(&old_content);
// Prepend superseded notice
let notice = format!(
"**SUPERSEDED** by `{}` — {}\n\nOriginal content preserved below for reference.\n\n---\n\n{}",
new_key, reason, content_only.trim()
);
// Write back
let write_result = write_node(old_key, &notice, provenance)?;
// Set weight to 0.01
let weight_result = cmd(&["weight-set", old_key, "0.01"], provenance)?;
Ok(format!("{}\n{}", write_result.trim(), weight_result.trim()))
}
/// Helper: get required string argument.
fn get_str<'a>(args: &'a serde_json::Value, name: &'a str) -> Result<&'a str> {
args.get(name)
.and_then(|v| v.as_str())
.context(format!("{} is required", name))
}
/// Helper: get required f64 argument.
fn get_f64(args: &serde_json::Value, name: &str) -> Result<f64> {
args.get(name)
.and_then(|v| v.as_f64())
.context(format!("{} is required", name))
}