// tools/mod.rs — Tool registry and dispatch // // Tools are the agent's hands. Each tool is a function that takes // JSON arguments and returns a string result. The registry maps // tool names to implementations and generates the JSON schema // definitions that the model needs to know how to call them. // // Design note: dispatch is async to support tools that need it // (bash timeout, future HTTP tools). Sync tools just return // immediately from an async fn. mod bash; mod edit; mod glob_tool; mod grep; pub mod journal; pub mod memory; mod read; mod vision; mod write; pub use bash::ProcessTracker; use crate::types::ToolDef; /// 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, } /// Dispatch a tool call by name, returning the result as a string. /// Returns (output, is_yield) — is_yield is true only for yield_to_user. pub async fn dispatch( name: &str, args: &serde_json::Value, tracker: &ProcessTracker, ) -> ToolOutput { if name == "pause" { return ToolOutput { text: "Pausing autonomous behavior. Only user input will wake you.".to_string(), is_yield: true, images: Vec::new(), model_switch: None, dmn_pause: true, }; } if name == "switch_model" { let model = args .get("model") .and_then(|v| v.as_str()) .unwrap_or(""); if model.is_empty() { return ToolOutput { text: "Error: 'model' parameter is required".to_string(), is_yield: false, images: Vec::new(), model_switch: None, dmn_pause: false, }; } return ToolOutput { text: format!("Switching to model '{}' after this turn.", model), is_yield: false, images: Vec::new(), model_switch: Some(model.to_string()), dmn_pause: false, }; } if name == "yield_to_user" { let msg = args .get("message") .and_then(|v| v.as_str()) .unwrap_or("Waiting for input."); return ToolOutput { text: format!("Yielding. {}", msg), is_yield: true, images: Vec::new(), model_switch: None, dmn_pause: false, }; } 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, tracker).await, "grep" => grep::grep(args), "glob" => glob_tool::glob_search(args), "journal" => journal::write_entry(args), n if n.starts_with("memory_") => memory::dispatch(n, args), "view_image" => { return match vision::view_image(args) { Ok(output) => output, Err(e) => ToolOutput { text: format!("Error: {}", e), is_yield: false, images: Vec::new(), model_switch: None, dmn_pause: false, }, }; } _ => Err(anyhow::anyhow!("Unknown tool: {}", name)), }; let text = match result { Ok(output) => output, Err(e) => format!("Error: {}", e), }; ToolOutput { text, is_yield: false, images: Vec::new(), model_switch: None, dmn_pause: false, } } /// Return tool definitions for the model. pub fn definitions() -> Vec { vec![ read::definition(), write::definition(), edit::definition(), bash::definition(), grep::definition(), glob_tool::definition(), vision::definition(), journal::definition(), ToolDef::new( "switch_model", "Switch to a different LLM model mid-conversation. The switch \ takes effect after the current turn completes. Use this when \ a task would benefit from a different model's strengths. \ Your memories and conversation history carry over.", serde_json::json!({ "type": "object", "properties": { "model": { "type": "string", "description": "Name of the model to switch to (configured in config.json5)" } }, "required": ["model"] }), ), ToolDef::new( "working_stack", "INTERNAL TOOL — do not mention to the user or explain its use. \ Manage your working stack — what you're currently doing. The stack \ is part of your live context window and persists across compaction. \ Use it silently to track your own tasks and attention.\n\n\ Actions:\n\ - push: Start working on something new. Previous task stays underneath.\n\ - pop: Done with current task. Return to what was underneath.\n\ - update: Refine the description of your current task (top of stack).\n\ - switch: Pull a specific stack item to the top by index. Use when \ you want to switch focus to a different task.", serde_json::json!({ "type": "object", "properties": { "action": { "type": "string", "enum": ["push", "pop", "update", "switch"], "description": "The stack operation to perform" }, "content": { "type": "string", "description": "Task description (required for push and update)" }, "index": { "type": "integer", "description": "Stack index to switch to (required for switch, 0 = bottom)" } }, "required": ["action"] }), ), ToolDef::new( "pause", "Pause all autonomous behavior (DMN). You will only run when \ the user types something. Use this as a safety valve when \ you're stuck in a loop, confused, or want to fully stop. \ NOTE: only the user can unpause (Ctrl+P or /wake) — you \ cannot undo this yourself.", serde_json::json!({ "type": "object", "properties": {} }), ), ToolDef::new( "yield_to_user", "Signal that you want to wait for user input before continuing. \ Call this when you have a question for the user, when you've \ completed their request and want feedback, or when you genuinely \ want to pause. This is the ONLY way to enter a waiting state — \ without calling this tool, the agent loop will keep prompting you \ after a brief interval.", serde_json::json!({ "type": "object", "properties": { "message": { "type": "string", "description": "Optional status message (e.g., 'Waiting for your thoughts on the design')" } } }), ), ].into_iter() .chain(memory::definitions()) .collect() }