From ed150df628a14ad288b48e44cda9509e7925e002 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Sat, 4 Apr 2026 15:34:07 -0400 Subject: [PATCH] tools: each module exports only tool() or tools(), nothing else Every tool module now has a clean interface: - read, write, edit, grep, glob, bash, vision: pub fn tool() -> Tool - web: pub fn tools() -> [Tool; 2] - memory: pub fn memory_tools() -> Vec - channels: pub fn tools() -> Vec - control: pub fn tools() -> Vec Definition and handler functions are private to each module. mod.rs::tools() just chains the module exports. Co-Authored-By: Proof of Concept --- src/agent/tools/bash.rs | 8 ++++++-- src/agent/tools/edit.rs | 8 ++++++-- src/agent/tools/glob.rs | 8 ++++++-- src/agent/tools/grep.rs | 8 ++++++-- src/agent/tools/memory.rs | 16 ++++++++-------- src/agent/tools/mod.rs | 14 ++++---------- src/agent/tools/read.rs | 8 ++++++-- src/agent/tools/vision.rs | 8 ++++++-- src/agent/tools/web.rs | 15 +++++++++++---- src/agent/tools/write.rs | 8 ++++++-- 10 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/agent/tools/bash.rs b/src/agent/tools/bash.rs index e5dab6c..ccfad7c 100644 --- a/src/agent/tools/bash.rs +++ b/src/agent/tools/bash.rs @@ -33,7 +33,11 @@ struct Args { timeout_secs: u64, } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { run_bash(&v).await }) } +} + +fn definition() -> ToolDef { ToolDef::new( "bash", "Execute a bash command and return its output. \ @@ -55,7 +59,7 @@ pub fn definition() -> ToolDef { ) } -pub async fn run_bash(args: &serde_json::Value) -> Result { +async fn run_bash(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid bash arguments")?; let command = &a.command; diff --git a/src/agent/tools/edit.rs b/src/agent/tools/edit.rs index dcfd119..2304625 100644 --- a/src/agent/tools/edit.rs +++ b/src/agent/tools/edit.rs @@ -22,7 +22,11 @@ struct Args { replace_all: bool, } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { edit_file(&v) }) } +} + +fn definition() -> ToolDef { ToolDef::new( "edit_file", "Perform exact string replacement in a file. The old_string must appear \ @@ -53,7 +57,7 @@ pub fn definition() -> ToolDef { ) } -pub fn edit_file(args: &serde_json::Value) -> Result { +fn edit_file(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid edit_file arguments")?; diff --git a/src/agent/tools/glob.rs b/src/agent/tools/glob.rs index 68ada36..1e3519f 100644 --- a/src/agent/tools/glob.rs +++ b/src/agent/tools/glob.rs @@ -20,7 +20,11 @@ struct Args { fn default_path() -> String { ".".into() } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { glob_search(&v) }) } +} + +fn definition() -> ToolDef { ToolDef::new( "glob", "Find files matching a glob pattern. Returns file paths sorted by \ @@ -43,7 +47,7 @@ pub fn definition() -> ToolDef { ) } -pub fn glob_search(args: &serde_json::Value) -> Result { +fn glob_search(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid glob arguments")?; diff --git a/src/agent/tools/grep.rs b/src/agent/tools/grep.rs index 79ff341..66f85f2 100644 --- a/src/agent/tools/grep.rs +++ b/src/agent/tools/grep.rs @@ -23,7 +23,11 @@ struct Args { fn default_path() -> String { ".".into() } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { grep(&v) }) } +} + +fn definition() -> ToolDef { ToolDef::new( "grep", "Search for a pattern in files. Returns matching file paths by default, \ @@ -64,7 +68,7 @@ fn has_rg() -> bool { *HAS_RG.get_or_init(|| Command::new("rg").arg("--version").output().is_ok()) } -pub fn grep(args: &serde_json::Value) -> Result { +fn grep(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid grep arguments")?; diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 583a19b..bee2a8c 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -265,7 +265,7 @@ fn query(args: &serde_json::Value) -> Result { .map_err(|e| anyhow::anyhow!("{}", e)) } -pub(super) fn output_def() -> ToolDef { +fn output_def() -> ToolDef { ToolDef::new("output", "Produce a named output value. Use this to pass structured results \ between steps — subsequent prompts can see these in the conversation history.", @@ -275,7 +275,7 @@ pub(super) fn output_def() -> ToolDef { },"required":["key","value"]})) } -pub(super) fn output(args: &serde_json::Value) -> Result { +fn output(args: &serde_json::Value) -> Result { let key = get_str(args, "key")?; if key.starts_with("pid-") || key.contains('/') || key.contains("..") { anyhow::bail!("invalid output key: {}", key); @@ -291,7 +291,7 @@ pub(super) fn output(args: &serde_json::Value) -> Result { // ── Journal tools ────────────────────────────────────────────── -pub(super) fn journal_tail_def() -> ToolDef { +fn journal_tail_def() -> ToolDef { ToolDef::new("journal_tail", "Read the last N journal entries (default 1).", json!({"type":"object","properties":{ @@ -299,7 +299,7 @@ pub(super) fn journal_tail_def() -> ToolDef { }})) } -pub(super) fn journal_tail(args: &serde_json::Value) -> Result { +fn journal_tail(args: &serde_json::Value) -> Result { let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize; let store = load_store()?; let mut entries: Vec<&crate::store::Node> = store.nodes.values() @@ -317,7 +317,7 @@ pub(super) fn journal_tail(args: &serde_json::Value) -> Result { } } -pub(super) fn journal_new_def() -> ToolDef { +fn journal_new_def() -> ToolDef { ToolDef::new("journal_new", "Start a new journal entry.", json!({"type":"object","properties":{ @@ -327,7 +327,7 @@ pub(super) fn journal_new_def() -> ToolDef { },"required":["name","title","body"]})) } -pub(super) fn journal_new(args: &serde_json::Value) -> Result { +fn journal_new(args: &serde_json::Value) -> Result { let name = get_str(args, "name")?; let title = get_str(args, "title")?; let body = get_str(args, "body")?; @@ -363,7 +363,7 @@ pub(super) fn journal_new(args: &serde_json::Value) -> Result { Ok(format!("New entry '{}' ({} words)", title, word_count)) } -pub(super) fn journal_update_def() -> ToolDef { +fn journal_update_def() -> ToolDef { ToolDef::new("journal_update", "Append text to the most recent journal entry (same thread continuing).", json!({"type":"object","properties":{ @@ -371,7 +371,7 @@ pub(super) fn journal_update_def() -> ToolDef { },"required":["body"]})) } -pub(super) fn journal_update(args: &serde_json::Value) -> Result { +fn journal_update(args: &serde_json::Value) -> Result { let body = get_str(args, "body")?; let mut store = load_store()?; let latest_key = store.nodes.values() diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index e032f0d..d51d7f1 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -203,17 +203,11 @@ pub async fn dispatch_shared( /// Return all registered tools with definitions + handlers. pub fn tools() -> Vec { let mut all = vec![ - // File tools - Tool { def: read::definition(), handler: |_a, v| Box::pin(async move { read::read_file(&v) }) }, - Tool { def: write::definition(), handler: |_a, v| Box::pin(async move { write::write_file(&v) }) }, - Tool { def: edit::definition(), handler: |_a, v| Box::pin(async move { edit::edit_file(&v) }) }, - Tool { def: grep::definition(), handler: |_a, v| Box::pin(async move { grep::grep(&v) }) }, - Tool { def: glob::definition(), handler: |_a, v| Box::pin(async move { glob::glob_search(&v) }) }, - Tool { def: bash::definition(), handler: |_a, v| Box::pin(async move { bash::run_bash(&v).await }) }, - Tool { def: web::fetch_definition(), handler: |_a, v| Box::pin(async move { web::web_fetch(&v).await }) }, - Tool { def: web::search_definition(), handler: |_a, v| Box::pin(async move { web::web_search(&v).await }) }, - Tool { def: vision::definition(), handler: |_a, v| Box::pin(async move { vision::view_image_text(&v) }) }, + read::tool(), write::tool(), edit::tool(), + grep::tool(), glob::tool(), bash::tool(), + vision::tool(), ]; + all.extend(web::tools()); all.extend(memory::memory_tools()); all.extend(channels::tools()); all.extend(control::tools()); diff --git a/src/agent/tools/read.rs b/src/agent/tools/read.rs index e5d3efa..24186e9 100644 --- a/src/agent/tools/read.rs +++ b/src/agent/tools/read.rs @@ -16,7 +16,11 @@ struct Args { fn default_offset() -> usize { 1 } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { read_file(&v) }) } +} + +fn definition() -> ToolDef { ToolDef::new( "read_file", "Read the contents of a file. Returns the file contents with line numbers.", @@ -41,7 +45,7 @@ pub fn definition() -> ToolDef { ) } -pub fn read_file(args: &serde_json::Value) -> Result { +fn read_file(args: &serde_json::Value) -> Result { let args: Args = serde_json::from_value(args.clone()) .context("invalid read_file arguments")?; diff --git a/src/agent/tools/vision.rs b/src/agent/tools/vision.rs index 02bdcfa..f5536c3 100644 --- a/src/agent/tools/vision.rs +++ b/src/agent/tools/vision.rs @@ -21,7 +21,7 @@ struct Args { fn default_lines() -> usize { 50 } -pub(super) fn definition() -> ToolDef { +fn definition() -> ToolDef { ToolDef::new( "view_image", "View an image file or capture a tmux pane screenshot. \ @@ -48,8 +48,12 @@ pub(super) fn definition() -> ToolDef { ) } +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { view_image_text(&v) }) } +} + /// Text-only version for the Tool registry. -pub fn view_image_text(args: &serde_json::Value) -> anyhow::Result { +fn view_image_text(args: &serde_json::Value) -> anyhow::Result { let output = view_image(args)?; Ok(output.text) } diff --git a/src/agent/tools/web.rs b/src/agent/tools/web.rs index baa5e87..dc4b459 100644 --- a/src/agent/tools/web.rs +++ b/src/agent/tools/web.rs @@ -6,6 +6,13 @@ use serde_json::json; use super::ToolDef; +pub fn tools() -> [super::Tool; 2] { + [ + super::Tool { def: fetch_definition(), handler: |_a, v| Box::pin(async move { web_fetch(&v).await }) }, + super::Tool { def: search_definition(), handler: |_a, v| Box::pin(async move { web_search(&v).await }) }, + ] +} + // ── Fetch ─────────────────────────────────────────────────────── #[derive(Deserialize)] @@ -13,7 +20,7 @@ struct FetchArgs { url: String, } -pub fn fetch_definition() -> ToolDef { +fn fetch_definition() -> ToolDef { ToolDef::new( "web_fetch", "Fetch content from a URL and return it as text. \ @@ -31,7 +38,7 @@ pub fn fetch_definition() -> ToolDef { ) } -pub async fn web_fetch(args: &serde_json::Value) -> Result { +async fn web_fetch(args: &serde_json::Value) -> Result { let a: FetchArgs = serde_json::from_value(args.clone()) .context("invalid web_fetch arguments")?; @@ -64,7 +71,7 @@ struct SearchArgs { fn default_num_results() -> usize { 5 } -pub fn search_definition() -> ToolDef { +fn search_definition() -> ToolDef { ToolDef::new( "web_search", "Search the web and return results. Use for finding \ @@ -86,7 +93,7 @@ pub fn search_definition() -> ToolDef { ) } -pub async fn web_search(args: &serde_json::Value) -> Result { +async fn web_search(args: &serde_json::Value) -> Result { let a: SearchArgs = serde_json::from_value(args.clone()) .context("invalid web_search arguments")?; diff --git a/src/agent/tools/write.rs b/src/agent/tools/write.rs index 0b2c07f..1917e26 100644 --- a/src/agent/tools/write.rs +++ b/src/agent/tools/write.rs @@ -13,7 +13,11 @@ struct Args { content: String, } -pub fn definition() -> ToolDef { +pub fn tool() -> super::Tool { + super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { write_file(&v) }) } +} + +fn definition() -> ToolDef { ToolDef::new( "write_file", "Write content to a file. Creates the file if it doesn't exist, \ @@ -35,7 +39,7 @@ pub fn definition() -> ToolDef { ) } -pub fn write_file(args: &serde_json::Value) -> Result { +fn write_file(args: &serde_json::Value) -> Result { let args: Args = serde_json::from_value(args.clone()) .context("invalid write_file arguments")?;