/// Tools that write or modify files on disk. /// Used by the approval gate (diff preview logic) and the LLM loop (FileChanged events). /// Update this list whenever a new file-write tool is added. pub const FILE_WRITE_TOOLS: &[&str] = &[ "write_file", "edit_file", "insert_at_line", "replace_lines", ]; /// Returns `true` if `name` is a file-write tool (i.e. it modifies files on disk). pub fn is_file_write_tool(name: &str) -> bool { FILE_WRITE_TOOLS.contains(&name) } /// Tools that read file contents or directory listings from disk. /// Used by the approval gate to apply the `RunContext` read fast-path (auto-allow /// working dir / `docs/` / `skills/` / `allow_fs_reads`). All take a `path` argument. /// Update this list whenever a new file-read tool is added. pub const FILE_READ_TOOLS: &[&str] = &[ "read_file", "grep_files", "list_files", "search_file", "get_ast_outline", ]; /// Returns `true` if `name` is a file-read tool (i.e. it reads files/dirs from disk). pub fn is_file_read_tool(name: &str) -> bool { FILE_READ_TOOLS.contains(&name) } pub mod tool_names; pub mod activate_tools; pub mod ast_outline; pub mod configure_plugin; pub mod cron_jobs; pub mod exec; pub mod fs; pub mod image_generate; pub mod list_items; pub mod list_secrets; pub mod notify; pub mod set_secret; pub mod read_notification; pub mod register_mcp; pub mod restart; pub mod show_file; pub mod toggle_item; use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; use serde_json::Value; pub use core_api::tool::{ drive_execution, ExecutionOutcome, SimpleExecution, Tool, ToolCategory, ToolDescriptionLength, ToolExecution, ToolResult, truncate_label, }; pub const MAX_LABEL_SHORT: usize = 60; pub const MAX_LABEL_FULL: usize = 120; /// Registry of all available tools. pub struct ToolRegistry { tools: HashMap>, } impl ToolRegistry { pub fn new() -> Self { Self { tools: HashMap::new() } } pub fn register(&mut self, tool: impl Tool + 'static) { self.tools.insert(tool.name().to_string(), Arc::new(tool)); } /// Register an already-boxed tool (e.g. plugin-provided tools whose /// constructors return `Arc`). pub fn register_arc(&mut self, tool: Arc) { self.tools.insert(tool.name().to_string(), tool); } /// Tool definitions for the root agent (depth = 0): excludes sub_agents_only tools. pub fn openai_definitions(&self) -> Vec { self.tools.values() .filter(|t| !t.sub_agents_only()) .map(|t| t.openai_definition()) .collect() } /// Like [`openai_definitions`], but **excludes** `Config`-category tools. /// These are lazy-loaded on demand via `activate_tools(["config"])`, so they /// are not part of the always-on base tool set. pub fn openai_definitions_excluding_config(&self) -> Vec { self.tools.values() .filter(|t| !t.sub_agents_only() && t.category() != ToolCategory::Config) .map(|t| t.openai_definition()) .collect() } /// Definitions of the `Config`-category tools only (the lazy `config` group). /// Injected dynamically by `all_tool_defs()` when the `config` group is granted. pub fn openai_definitions_config_only(&self) -> Vec { self.tools.values() .filter(|t| !t.sub_agents_only() && t.category() == ToolCategory::Config) .map(|t| t.openai_definition()) .collect() } /// Tool definitions that are marked sub_agents_only. Used in dispatch_call_agent /// to augment the child config's base_tool_defs. pub fn openai_definitions_sub_agents_only(&self) -> Vec { self.tools.values() .filter(|t| t.sub_agents_only()) .map(|t| t.openai_definition()) .collect() } /// Returns the names of all tools marked `root_agent_only`. pub fn root_agent_only_names(&self) -> Vec { self.tools.values() .filter(|t| t.root_agent_only()) .map(|t| t.name().to_string()) .collect() } /// Returns the names of all tools marked `interactive_only`. pub fn interactive_only_names(&self) -> Vec { self.tools.values() .filter(|t| t.interactive_only()) .map(|t| t.name().to_string()) .collect() } /// Returns `(name, description)` for every registered tool. pub fn list_all(&self) -> Vec<(String, String)> { let mut v: Vec<(String, String)> = self.tools.values() .map(|t| (t.name().to_string(), t.description().to_string())) .collect(); v.sort_by(|a, b| a.0.cmp(&b.0)); v } /// Human-readable label for any tool call, including non-registry tools (call_agent, MCP, …). pub fn describe_call(&self, name: &str, args: &Value, length: ToolDescriptionLength) -> String { if let Some(tool) = self.tools.get(name) { return tool.describe(args, length); } // Non-registry tools handled inline. `show_file_to_user` is an InterfaceTool // (injected in ws.rs), so it has no registry `describe`; surface its target // path in the label so the frontend renders it as a clickable file link. if name == tool_names::SHOW_FILE_TO_USER { if let Some(path) = args["path"].as_str() { let max = match length { ToolDescriptionLength::Short => MAX_LABEL_SHORT, ToolDescriptionLength::Full => MAX_LABEL_FULL, }; return truncate_label(&format!("{name} `{path}`"), max); } } // Sub-agent delegation tools (`execute_task`, `execute_subtask`, and the // legacy `run_subtask` alias) are InterfaceTools, not in the registry. // Surface agent_id + description so the UI/Telegram shows what is being // delegated instead of the bare tool name. if name == tool_names::EXECUTE_TASK || name == tool_names::EXECUTE_SUBTASK || name == "run_subtask" { return describe_sub_agent_call(name, args, length); } name.to_string() } /// Returns the category of a registered tool, or `None` for unknown tools /// (MCP tools, interface tools, call_agent, etc.). pub fn category_of(&self, name: &str) -> Option { self.tools.get(name).map(|t| t.category()) } /// Path to a single viewable file targeted by this tool call, if any. /// `None` for non-file tools, directory tools, and unknown/non-registry tools. /// /// `show_file_to_user` is an InterfaceTool (not in the registry) whose whole /// purpose is to open a file, so it is handled inline here as well — mirroring /// `describe_call`, so its label and clickable path use the same raw `path` arg. pub fn target_path(&self, name: &str, args: &Value) -> Option { if let Some(tool) = self.tools.get(name) { return tool.target_path(args); } if name == tool_names::SHOW_FILE_TO_USER { return args["path"].as_str().map(str::to_string); } None } /// Dispatch a tool call by name. pub async fn dispatch(&self, name: &str, args: Value) -> Result { match self.tools.get(name) { Some(tool) => tool.execute_async(args).await, None => anyhow::bail!("Unknown tool: {name}"), } } /// Start a [`ToolExecution`] for a registered tool, or `None` if `name` is not /// in the registry (MCP / interface tools are handled by the caller). The /// returned handle borrows the registry, which outlives the turn. pub fn run(&self, name: &str, args: Value) -> Option> { self.tools.get(name).map(|tool| tool.run(args)) } } /// Builds a human-readable label for a sub-agent delegation call /// (`execute_task` / `execute_subtask` / legacy `run_subtask`), all of which are /// InterfaceTools outside the registry. Shows `agent_id` + `description` (falling /// back to `title`, then to the bare name) so the UI/Telegram displays what is /// being delegated. When `mode` is present (only `execute_task` carries it) a /// single emoji is appended to the tool name as a compact mode marker: /// sync → ⚡, async → 🚀, cron → 📅. fn describe_sub_agent_call(name: &str, args: &Value, length: ToolDescriptionLength) -> String { let agent_id = args["agent_id"].as_str().map(|s| s.trim()).unwrap_or(""); let subject = args["description"].as_str() .map(|s| s.trim()) .filter(|s| !s.is_empty()) .or_else(|| args["title"].as_str().map(|s| s.trim()).filter(|s| !s.is_empty())) .unwrap_or(""); let mode_emoji = match args["mode"].as_str().map(|s| s.trim()) { Some("sync") => Some("⚡"), Some("async") => Some("🚀"), Some("cron") => Some("📅"), _ => None, }; let max = match length { ToolDescriptionLength::Short => MAX_LABEL_SHORT, ToolDescriptionLength::Full => MAX_LABEL_FULL, }; let prefix = match mode_emoji { Some(e) => format!("{name} {e}"), None => name.to_string(), }; let label = match (agent_id.is_empty(), subject.is_empty()) { (false, false) => format!("{prefix} → {agent_id}: {subject}"), (false, true) => format!("{prefix} → {agent_id}"), _ => prefix, }; truncate_label(&label, max) }