tools: route all dispatch through Tool registry

dispatch() and dispatch_shared() now look up tools by name in the
registry and call the handler directly. No more match-on-name-strings.

MCP server also uses the registry for both definitions and dispatch,
eliminating the last duplicated tool logic.

dispatch_with_agent() passes the optional Arc<Mutex<Agent>> through
for tools that need agent context (control tools, working stack).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 15:10:13 -04:00 committed by Kent Overstreet
parent 3e6c77e31e
commit 03cf13e9eb
2 changed files with 46 additions and 83 deletions

View file

@ -158,81 +158,46 @@ pub fn truncate_output(mut s: String, max: usize) -> String {
s s
} }
/// Dispatch a tool call by name. /// Dispatch a tool call by name through the registry.
///
/// Tries agent-specific tools first (control, vision), then
/// delegates to thought::dispatch for shared tools.
///
/// Note: working_stack is handled in runner.rs before reaching this
/// function (it needs mutable context access).
/// Dispatch a tool call by name. Handles all tools:
/// agent-specific (control, vision), memory/journal, file/bash.
pub async fn dispatch( pub async fn dispatch(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
) -> ToolOutput { ) -> ToolOutput {
// Agent-specific tools dispatch_with_agent(name, args, None).await
let rich_result = match name { }
"pause" => Some(control::pause(args)),
"switch_model" => Some(control::switch_model(args)), /// Dispatch a tool call with optional agent context.
"yield_to_user" => Some(control::yield_to_user(args)), pub async fn dispatch_with_agent(
"view_image" => Some(vision::view_image(args)), name: &str,
_ => None, args: &serde_json::Value,
agent: Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>,
) -> ToolOutput {
for tool in tools() {
if tool.def.function.name == name {
return match (tool.handler)(agent, args.clone()).await {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
}; };
if let Some(result) = rich_result {
return result.unwrap_or_else(ToolOutput::error);
} }
if let Some(output) = dispatch_shared(name, args, None).await {
return output;
} }
ToolOutput::error(format!("Unknown tool: {}", name)) ToolOutput::error(format!("Unknown tool: {}", name))
} }
/// Dispatch shared tools (memory, file, bash). Used by both the /// Dispatch shared tools — used by subconscious agents.
/// interactive agent and subconscious agents. Provenance tracks
/// which agent made the call for memory attribution.
pub async fn dispatch_shared( pub async fn dispatch_shared(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
provenance: Option<&str>, _provenance: Option<&str>,
) -> Option<ToolOutput> { ) -> Option<ToolOutput> {
// Memory and journal tools for tool in tools() {
if name.starts_with("memory_") || name.starts_with("journal_") || name == "output" { if tool.def.function.name == name {
let result = memory::dispatch(name, args, provenance); return Some(match (tool.handler)(None, args.clone()).await {
return Some(match result {
Ok(s) => ToolOutput::text(s), Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e), Err(e) => ToolOutput::error(e),
}); });
} }
// Channel tools
if name.starts_with("channel_") {
let result = channels::dispatch(name, args).await;
return Some(match result {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
});
} }
None
// File and execution tools
let result = match name {
"read_file" => read::read_file(args),
"write_file" => write::write_file(args),
"edit_file" => edit::edit_file(args),
"bash" => bash::run_bash(args).await,
"web_fetch" => web::web_fetch(args).await,
"web_search" => web::web_search(args).await,
"grep" => grep::grep(args),
"glob" => glob::glob_search(args),
_ => return None,
};
Some(match result {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
})
} }
/// Return all registered tools with definitions + handlers. /// Return all registered tools with definitions + handlers.

View file

@ -68,34 +68,32 @@ fn notify(method: &str, params: Value) {
// ── Tool definitions ──────────────────────────────────────────── // ── Tool definitions ────────────────────────────────────────────
fn tool_definitions() -> Vec<Value> { fn tool_definitions() -> Vec<Value> {
use poc_memory::agent::tools; poc_memory::agent::tools::tools().into_iter()
.map(|t| json!({
let all_defs = tools::memory::definitions().into_iter() "name": t.def.function.name,
.chain(tools::channels::definitions()); "description": t.def.function.description,
"inputSchema": t.def.function.parameters,
all_defs.map(|td| json!({ }))
"name": td.function.name, .collect()
"description": td.function.description,
"inputSchema": td.function.parameters,
})).collect()
} }
// ── Tool dispatch ─────────────────────────────────────────────── // ── Tool dispatch ───────────────────────────────────────────────
fn dispatch_tool(name: &str, args: &Value) -> Result<String, String> { fn dispatch_tool(name: &str, args: &Value) -> Result<String, String> {
use poc_memory::agent::tools; let tools = poc_memory::agent::tools::tools();
let tool = tools.iter().find(|t| t.def.function.name == name);
let Some(tool) = tool else {
return Err(format!("unknown tool: {name}"));
};
if name.starts_with("memory_") || name.starts_with("journal_") || name == "output" { // Run async handler on a blocking runtime
return tools::memory::dispatch(name, args, None) let rt = tokio::runtime::Builder::new_current_thread()
.map_err(|e| e.to_string()); .enable_all()
} .build()
.map_err(|e| e.to_string())?;
if name.starts_with("channel_") { let local = tokio::task::LocalSet::new();
return tools::channels::dispatch_blocking(name, args) local.block_on(&rt, (tool.handler)(None, args.clone()))
.map_err(|e| e.to_string()); .map_err(|e| e.to_string())
}
Err(format!("unknown tool: {name}"))
} }
// ── Main loop ─────────────────────────────────────────────────── // ── Main loop ───────────────────────────────────────────────────