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
}
/// Dispatch a tool call by name.
///
/// 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.
/// Dispatch a tool call by name through the registry.
pub async fn dispatch(
name: &str,
args: &serde_json::Value,
) -> ToolOutput {
// Agent-specific tools
let rich_result = match name {
"pause" => Some(control::pause(args)),
"switch_model" => Some(control::switch_model(args)),
"yield_to_user" => Some(control::yield_to_user(args)),
"view_image" => Some(vision::view_image(args)),
_ => None,
};
if let Some(result) = rich_result {
return result.unwrap_or_else(ToolOutput::error);
}
dispatch_with_agent(name, args, None).await
}
if let Some(output) = dispatch_shared(name, args, None).await {
return output;
/// Dispatch a tool call with optional agent context.
pub async fn dispatch_with_agent(
name: &str,
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),
};
}
}
ToolOutput::error(format!("Unknown tool: {}", name))
}
/// Dispatch shared tools (memory, file, bash). Used by both the
/// interactive agent and subconscious agents. Provenance tracks
/// which agent made the call for memory attribution.
/// Dispatch shared tools — used by subconscious agents.
pub async fn dispatch_shared(
name: &str,
args: &serde_json::Value,
provenance: Option<&str>,
_provenance: Option<&str>,
) -> Option<ToolOutput> {
// Memory and journal tools
if name.starts_with("memory_") || name.starts_with("journal_") || name == "output" {
let result = memory::dispatch(name, args, provenance);
return Some(match result {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
});
for tool in tools() {
if tool.def.function.name == name {
return Some(match (tool.handler)(None, args.clone()).await {
Ok(s) => ToolOutput::text(s),
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),
});
}
// 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),
})
None
}
/// Return all registered tools with definitions + handlers.

View file

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