// tools/mod.rs — Agent-specific tool dispatch // // Shared tools (memory, files, bash, journal) live in thought/. // This module handles agent-specific tools (control, vision, // working_stack) and delegates everything else to thought::dispatch. // Core tools mod bash; pub mod channels; mod edit; mod glob; mod grep; pub mod memory; mod read; mod web; mod write; // Agent-specific tools mod control; mod vision; pub mod working_stack; use serde::{Serialize, Deserialize}; use std::future::Future; use std::pin::Pin; use std::time::Instant; fn default_timeout() -> u64 { 120 } /// Async tool handler function. /// Agent is None when called from contexts without an agent (MCP server, subconscious). pub type ToolHandler = fn( Option>>, serde_json::Value, ) -> Pin> + Send>>; /// A tool with its definition and handler — single source of truth. pub struct Tool { pub def: ToolDef, pub handler: ToolHandler, } /// Function call within a tool call — name + JSON arguments. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionCall { pub name: String, pub arguments: String, } /// Function definition for tool schema. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionDef { pub name: String, pub description: String, pub parameters: serde_json::Value, } /// Partial function call within a streaming delta. #[derive(Debug, Deserialize)] pub struct FunctionCallDelta { pub name: Option, pub arguments: Option, } /// Tool definition sent to the model. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDef { #[serde(rename = "type")] pub tool_type: String, pub function: FunctionDef, } impl ToolDef { pub fn new(name: &str, description: &str, parameters: serde_json::Value) -> Self { Self { tool_type: "function".to_string(), function: FunctionDef { name: name.to_string(), description: description.to_string(), parameters, }, } } } /// A tool call requested by the model. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolCall { pub id: String, #[serde(rename = "type")] pub call_type: String, pub function: FunctionCall, } /// A partial tool call within a streaming delta. The first chunk for a /// given tool call carries the id and function name; subsequent chunks /// carry argument fragments. #[derive(Debug, Deserialize)] pub struct ToolCallDelta { pub index: usize, pub id: Option, #[serde(rename = "type")] pub call_type: Option, pub function: Option, } /// Result of dispatching a tool call. pub struct ToolOutput { pub text: String, pub is_yield: bool, /// Base64 data URIs for images to attach to the next message. pub images: Vec, /// Model name to switch to (deferred to session level). pub model_switch: Option, /// Agent requested DMN pause (deferred to session level). pub dmn_pause: bool, } impl ToolOutput { pub fn error(e: impl std::fmt::Display) -> Self { Self { text: format!("Error: {}", e), is_yield: false, images: Vec::new(), model_switch: None, dmn_pause: false, } } pub fn text(s: String) -> Self { Self { text: s, is_yield: false, images: Vec::new(), model_switch: None, dmn_pause: false, } } } /// A tool call in flight — metadata for TUI + JoinHandle for /// result collection and cancellation. pub struct ActiveToolCall { pub id: String, pub name: String, pub detail: String, pub started: Instant, pub background: bool, pub handle: tokio::task::JoinHandle<(ToolCall, ToolOutput)>, } /// Truncate output if it exceeds max length, appending a truncation notice. pub fn truncate_output(mut s: String, max: usize) -> String { if s.len() > max { s.truncate(max); s.push_str("\n... (output truncated)"); } s } /// Dispatch a tool call by name through the registry. pub async fn dispatch( name: &str, args: &serde_json::Value, ) -> ToolOutput { dispatch_with_agent(name, args, None).await } /// Dispatch a tool call with optional agent context. pub async fn dispatch_with_agent( name: &str, args: &serde_json::Value, agent: Option>>, ) -> 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 — used by subconscious agents. pub async fn dispatch_shared( name: &str, args: &serde_json::Value, _provenance: Option<&str>, ) -> Option { 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), }); } } None } /// Return all registered tools with definitions + handlers. pub fn tools() -> Vec { 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) }) }, // Execution tools 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 }) }, // Vision Tool { def: vision::definition(), handler: |_a, v| Box::pin(async move { vision::view_image_text(&v) }) }, // Memory tools Tool { def: memory::render_def(), handler: |_a, v| Box::pin(async move { memory::render(&v) }) }, Tool { def: memory::write_def(), handler: |_a, v| Box::pin(async move { memory::write(&v) }) }, Tool { def: memory::search_def(), handler: |_a, v| Box::pin(async move { memory::search(&v) }) }, Tool { def: memory::links_def(), handler: |_a, v| Box::pin(async move { memory::links(&v) }) }, Tool { def: memory::link_set_def(), handler: |_a, v| Box::pin(async move { memory::link_set(&v) }) }, Tool { def: memory::link_add_def(), handler: |_a, v| Box::pin(async move { memory::link_add(&v) }) }, Tool { def: memory::used_def(), handler: |_a, v| Box::pin(async move { memory::used(&v) }) }, Tool { def: memory::weight_set_def(), handler: |_a, v| Box::pin(async move { memory::weight_set(&v) }) }, Tool { def: memory::rename_def(), handler: |_a, v| Box::pin(async move { memory::rename(&v) }) }, Tool { def: memory::supersede_def(), handler: |_a, v| Box::pin(async move { memory::supersede(&v) }) }, Tool { def: memory::query_def(), handler: |_a, v| Box::pin(async move { memory::query(&v) }) }, Tool { def: memory::output_def(), handler: |_a, v| Box::pin(async move { memory::output(&v) }) }, // Channel tools Tool { def: channels::definitions()[0].clone(), handler: |_a, v| Box::pin(async move { channels::channel_list().await }) }, Tool { def: channels::definitions()[1].clone(), handler: |_a, v| Box::pin(async move { channels::channel_recv(&v).await }) }, Tool { def: channels::definitions()[2].clone(), handler: |_a, v| Box::pin(async move { channels::channel_send(&v).await }) }, Tool { def: channels::definitions()[3].clone(), handler: |_a, v| Box::pin(async move { channels::channel_notifications().await }) }, // Control tools Tool { def: control::definitions()[0].clone(), handler: |_a, _v| Box::pin(async { Ok("Pausing autonomous behavior. Only user input will wake you.".into()) }) }, Tool { def: control::definitions()[1].clone(), handler: |_a, v| Box::pin(async move { let model = v.get("model").and_then(|v| v.as_str()).unwrap_or(""); Ok(format!("Switching to model: {}", model)) }) }, Tool { def: control::definitions()[2].clone(), handler: |_a, v| Box::pin(async move { let msg = v.get("message").and_then(|v| v.as_str()).unwrap_or("(yielding to user)"); Ok(msg.to_string()) }) }, ] } /// Return all tool definitions (extracted from tools()). pub fn definitions() -> Vec { tools().into_iter().map(|t| t.def).collect() } /// Return memory + journal tool definitions only. pub fn memory_and_journal_definitions() -> Vec { let mut defs = memory::definitions(); defs.extend(memory::journal_definitions()); defs } /// Create a short summary of tool args for the tools pane header. pub fn summarize_args(tool_name: &str, args: &serde_json::Value) -> String { match tool_name { "read_file" | "write_file" | "edit_file" => args["file_path"] .as_str() .unwrap_or("") .to_string(), "bash" => { let cmd = args["command"].as_str().unwrap_or(""); if cmd.len() > 60 { let end = cmd.char_indices() .map(|(i, _)| i) .take_while(|&i| i <= 60) .last() .unwrap_or(0); format!("{}...", &cmd[..end]) } else { cmd.to_string() } } "grep" => { let pattern = args["pattern"].as_str().unwrap_or(""); let path = args["path"].as_str().unwrap_or("."); format!("{} in {}", pattern, path) } "glob" => args["pattern"] .as_str() .unwrap_or("") .to_string(), "view_image" => { if let Some(pane) = args["pane_id"].as_str() { format!("pane {}", pane) } else { args["file_path"].as_str().unwrap_or("").to_string() } } "journal" => { let entry = args["entry"].as_str().unwrap_or(""); if entry.len() > 60 { format!("{}...", &entry[..60]) } else { entry.to_string() } } "yield_to_user" => args["message"] .as_str() .unwrap_or("") .to_string(), "switch_model" => args["model"] .as_str() .unwrap_or("") .to_string(), "pause" => String::new(), _ => String::new(), } }