// 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; mod edit; mod glob; mod grep; 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::time::Instant; fn default_timeout() -> u64 { 120 } /// 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. /// /// 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( 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); } if let Some(output) = dispatch_shared(name, args, None).await { return output; } 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. pub async fn dispatch_shared( name: &str, args: &serde_json::Value, provenance: Option<&str>, ) -> Option { // 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), }); } // 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 tool definitions (agent-specific + shared + memory). pub fn definitions() -> Vec { let mut defs = vec![ vision::definition(), working_stack::definition(), read::definition(), write::definition(), edit::definition(), bash::definition(), web::fetch_definition(), web::search_definition(), grep::definition(), glob::definition(), ]; defs.extend(control::definitions()); defs.extend(memory::definitions()); defs } /// 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(), } }