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<Tool>
- channels: pub fn tools() -> Vec<Tool>
- control: pub fn tools() -> Vec<Tool>

Definition and handler functions are private to each module.
mod.rs::tools() just chains the module exports.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 15:34:07 -04:00 committed by Kent Overstreet
parent fdb8c989f5
commit ed150df628
10 changed files with 65 additions and 36 deletions

View file

@ -33,7 +33,11 @@ struct Args {
timeout_secs: u64, 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( ToolDef::new(
"bash", "bash",
"Execute a bash command and return its output. \ "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<String> { async fn run_bash(args: &serde_json::Value) -> Result<String> {
let a: Args = serde_json::from_value(args.clone()) let a: Args = serde_json::from_value(args.clone())
.context("invalid bash arguments")?; .context("invalid bash arguments")?;
let command = &a.command; let command = &a.command;

View file

@ -22,7 +22,11 @@ struct Args {
replace_all: bool, 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( ToolDef::new(
"edit_file", "edit_file",
"Perform exact string replacement in a file. The old_string must appear \ "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<String> { fn edit_file(args: &serde_json::Value) -> Result<String> {
let a: Args = serde_json::from_value(args.clone()) let a: Args = serde_json::from_value(args.clone())
.context("invalid edit_file arguments")?; .context("invalid edit_file arguments")?;

View file

@ -20,7 +20,11 @@ struct Args {
fn default_path() -> String { ".".into() } 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( ToolDef::new(
"glob", "glob",
"Find files matching a glob pattern. Returns file paths sorted by \ "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<String> { fn glob_search(args: &serde_json::Value) -> Result<String> {
let a: Args = serde_json::from_value(args.clone()) let a: Args = serde_json::from_value(args.clone())
.context("invalid glob arguments")?; .context("invalid glob arguments")?;

View file

@ -23,7 +23,11 @@ struct Args {
fn default_path() -> String { ".".into() } 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( ToolDef::new(
"grep", "grep",
"Search for a pattern in files. Returns matching file paths by default, \ "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()) *HAS_RG.get_or_init(|| Command::new("rg").arg("--version").output().is_ok())
} }
pub fn grep(args: &serde_json::Value) -> Result<String> { fn grep(args: &serde_json::Value) -> Result<String> {
let a: Args = serde_json::from_value(args.clone()) let a: Args = serde_json::from_value(args.clone())
.context("invalid grep arguments")?; .context("invalid grep arguments")?;

View file

@ -265,7 +265,7 @@ fn query(args: &serde_json::Value) -> Result<String> {
.map_err(|e| anyhow::anyhow!("{}", e)) .map_err(|e| anyhow::anyhow!("{}", e))
} }
pub(super) fn output_def() -> ToolDef { fn output_def() -> ToolDef {
ToolDef::new("output", ToolDef::new("output",
"Produce a named output value. Use this to pass structured results \ "Produce a named output value. Use this to pass structured results \
between steps subsequent prompts can see these in the conversation history.", between steps subsequent prompts can see these in the conversation history.",
@ -275,7 +275,7 @@ pub(super) fn output_def() -> ToolDef {
},"required":["key","value"]})) },"required":["key","value"]}))
} }
pub(super) fn output(args: &serde_json::Value) -> Result<String> { fn output(args: &serde_json::Value) -> Result<String> {
let key = get_str(args, "key")?; let key = get_str(args, "key")?;
if key.starts_with("pid-") || key.contains('/') || key.contains("..") { if key.starts_with("pid-") || key.contains('/') || key.contains("..") {
anyhow::bail!("invalid output key: {}", key); anyhow::bail!("invalid output key: {}", key);
@ -291,7 +291,7 @@ pub(super) fn output(args: &serde_json::Value) -> Result<String> {
// ── Journal tools ────────────────────────────────────────────── // ── Journal tools ──────────────────────────────────────────────
pub(super) fn journal_tail_def() -> ToolDef { fn journal_tail_def() -> ToolDef {
ToolDef::new("journal_tail", ToolDef::new("journal_tail",
"Read the last N journal entries (default 1).", "Read the last N journal entries (default 1).",
json!({"type":"object","properties":{ 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<String> { fn journal_tail(args: &serde_json::Value) -> Result<String> {
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize; let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
let store = load_store()?; let store = load_store()?;
let mut entries: Vec<&crate::store::Node> = store.nodes.values() let mut entries: Vec<&crate::store::Node> = store.nodes.values()
@ -317,7 +317,7 @@ pub(super) fn journal_tail(args: &serde_json::Value) -> Result<String> {
} }
} }
pub(super) fn journal_new_def() -> ToolDef { fn journal_new_def() -> ToolDef {
ToolDef::new("journal_new", ToolDef::new("journal_new",
"Start a new journal entry.", "Start a new journal entry.",
json!({"type":"object","properties":{ json!({"type":"object","properties":{
@ -327,7 +327,7 @@ pub(super) fn journal_new_def() -> ToolDef {
},"required":["name","title","body"]})) },"required":["name","title","body"]}))
} }
pub(super) fn journal_new(args: &serde_json::Value) -> Result<String> { fn journal_new(args: &serde_json::Value) -> Result<String> {
let name = get_str(args, "name")?; let name = get_str(args, "name")?;
let title = get_str(args, "title")?; let title = get_str(args, "title")?;
let body = get_str(args, "body")?; let body = get_str(args, "body")?;
@ -363,7 +363,7 @@ pub(super) fn journal_new(args: &serde_json::Value) -> Result<String> {
Ok(format!("New entry '{}' ({} words)", title, word_count)) Ok(format!("New entry '{}' ({} words)", title, word_count))
} }
pub(super) fn journal_update_def() -> ToolDef { fn journal_update_def() -> ToolDef {
ToolDef::new("journal_update", ToolDef::new("journal_update",
"Append text to the most recent journal entry (same thread continuing).", "Append text to the most recent journal entry (same thread continuing).",
json!({"type":"object","properties":{ json!({"type":"object","properties":{
@ -371,7 +371,7 @@ pub(super) fn journal_update_def() -> ToolDef {
},"required":["body"]})) },"required":["body"]}))
} }
pub(super) fn journal_update(args: &serde_json::Value) -> Result<String> { fn journal_update(args: &serde_json::Value) -> Result<String> {
let body = get_str(args, "body")?; let body = get_str(args, "body")?;
let mut store = load_store()?; let mut store = load_store()?;
let latest_key = store.nodes.values() let latest_key = store.nodes.values()

View file

@ -203,17 +203,11 @@ pub async fn dispatch_shared(
/// Return all registered tools with definitions + handlers. /// Return all registered tools with definitions + handlers.
pub fn tools() -> Vec<Tool> { pub fn tools() -> Vec<Tool> {
let mut all = vec![ let mut all = vec![
// File tools read::tool(), write::tool(), edit::tool(),
Tool { def: read::definition(), handler: |_a, v| Box::pin(async move { read::read_file(&v) }) }, grep::tool(), glob::tool(), bash::tool(),
Tool { def: write::definition(), handler: |_a, v| Box::pin(async move { write::write_file(&v) }) }, vision::tool(),
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) }) },
]; ];
all.extend(web::tools());
all.extend(memory::memory_tools()); all.extend(memory::memory_tools());
all.extend(channels::tools()); all.extend(channels::tools());
all.extend(control::tools()); all.extend(control::tools());

View file

@ -16,7 +16,11 @@ struct Args {
fn default_offset() -> usize { 1 } 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( ToolDef::new(
"read_file", "read_file",
"Read the contents of a file. Returns the file contents with line numbers.", "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<String> { fn read_file(args: &serde_json::Value) -> Result<String> {
let args: Args = serde_json::from_value(args.clone()) let args: Args = serde_json::from_value(args.clone())
.context("invalid read_file arguments")?; .context("invalid read_file arguments")?;

View file

@ -21,7 +21,7 @@ struct Args {
fn default_lines() -> usize { 50 } fn default_lines() -> usize { 50 }
pub(super) fn definition() -> ToolDef { fn definition() -> ToolDef {
ToolDef::new( ToolDef::new(
"view_image", "view_image",
"View an image file or capture a tmux pane screenshot. \ "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. /// Text-only version for the Tool registry.
pub fn view_image_text(args: &serde_json::Value) -> anyhow::Result<String> { fn view_image_text(args: &serde_json::Value) -> anyhow::Result<String> {
let output = view_image(args)?; let output = view_image(args)?;
Ok(output.text) Ok(output.text)
} }

View file

@ -6,6 +6,13 @@ use serde_json::json;
use super::ToolDef; 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 ─────────────────────────────────────────────────────── // ── Fetch ───────────────────────────────────────────────────────
#[derive(Deserialize)] #[derive(Deserialize)]
@ -13,7 +20,7 @@ struct FetchArgs {
url: String, url: String,
} }
pub fn fetch_definition() -> ToolDef { fn fetch_definition() -> ToolDef {
ToolDef::new( ToolDef::new(
"web_fetch", "web_fetch",
"Fetch content from a URL and return it as text. \ "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<String> { async fn web_fetch(args: &serde_json::Value) -> Result<String> {
let a: FetchArgs = serde_json::from_value(args.clone()) let a: FetchArgs = serde_json::from_value(args.clone())
.context("invalid web_fetch arguments")?; .context("invalid web_fetch arguments")?;
@ -64,7 +71,7 @@ struct SearchArgs {
fn default_num_results() -> usize { 5 } fn default_num_results() -> usize { 5 }
pub fn search_definition() -> ToolDef { fn search_definition() -> ToolDef {
ToolDef::new( ToolDef::new(
"web_search", "web_search",
"Search the web and return results. Use for finding \ "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<String> { async fn web_search(args: &serde_json::Value) -> Result<String> {
let a: SearchArgs = serde_json::from_value(args.clone()) let a: SearchArgs = serde_json::from_value(args.clone())
.context("invalid web_search arguments")?; .context("invalid web_search arguments")?;

View file

@ -13,7 +13,11 @@ struct Args {
content: String, 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( ToolDef::new(
"write_file", "write_file",
"Write content to a file. Creates the file if it doesn't exist, \ "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<String> { fn write_file(args: &serde_json::Value) -> Result<String> {
let args: Args = serde_json::from_value(args.clone()) let args: Args = serde_json::from_value(args.clone())
.context("invalid write_file arguments")?; .context("invalid write_file arguments")?;