# Tools ## Tool Trait ```rust pub trait Tool: Send + Sync { fn name(&self) -> &str; fn description(&self) -> &str; fn parameters_schema(&self) -> Value; // JSON Schema object fn describe(&self, args, length) -> String { name } // default impl — UI/notification label fn target_path(&self, _args: &Value) -> Option { None } // default impl — file this call opens, if any fn execute(&self, _args: Value) -> Result { /* default: Err */ } fn execute_async<'a>(&'a self, args: Value) -> Pin> + Send + 'a>>; fn run<'a>(&'a self, args: Value) -> Box { /* default: SimpleExecution(execute_async) */ } fn category(&self) -> ToolCategory; // access-control grouping fn sub_agents_only(&self) -> bool { false } // default impl — visible only to sub-agents (depth > 0) fn root_agent_only(&self) -> bool { false } // default impl — visible only to root agent (depth == 0) fn openai_definition(&self) -> Value { ... } // default impl, rarely overridden } ``` `Tool` is the **definition** (the catalogue entry in `ToolRegistry`); a single live invocation is a `ToolExecution` produced by `run()`. See [Tool execution lifecycle](#tool-execution-lifecycle) below. **Two execution paths:** - **Sync tools** implement `execute(&self, args)` only. The default `execute_async` wraps it in a ready future — no changes needed. - **Async tools** (e.g. `image_generate`, `image_generate_providers_list`) implement `execute_async` directly and omit `execute`. Do NOT use `block_in_place` — override `execute_async` instead. The dispatcher in `llm_loop.rs` drives every tool through `Tool::run(args) → ToolExecution`, so sync and async tools are dispatched uniformly (and cancellably). --- ## Tool execution lifecycle `Tool` (definition) and `ToolExecution` (a single in-flight invocation) are split on the `Command → spawn() → Child` pattern. `Tool::run(args)` starts one execution and returns a handle that owns its state and implements its own stop. Defined in [`crates/core-api/src/tool.rs`](../crates/core-api/src/tool.rs). ```rust pub trait ToolExecution: Send + Sync { fn state(&self) -> ToolExecutionState; fn wait<'a>(&'a self) -> Pin + Send + 'a>>; fn stop<'a>(&'a self) -> Pin + Send + 'a>> { /* default: no-op */ } } ``` **States** (`ToolExecutionState`, richer than the persisted status string): | State | Meaning | DB `status` | | --- | --- | --- | | `Pending` | created, not yet started (transient, not persisted alone) | `running` | | `AwaitingApproval` | blocked on a human approve/clarification | `pending` | | `Running` | actively executing | `running` | | `Completed` | finished OK | `done` | | `Failed` | tool/runtime error | `failed` | | `Cancelled` | stopped by `/stop` — **not** an error | `cancelled` | | `Rejected` | denied by policy/human — **not** an error | `rejected` | `Cancelled` and `Rejected` are deliberately distinct from `Failed` so a stop or a policy denial never pollutes error metrics. `wait()` only ever returns the three terminal `ExecutionOutcome`s (`Completed` / `Failed` / `Cancelled`); the approval-phase states are owned by the session driver. **Purity.** A `ToolExecution` never touches the DB or the WebSocket. The session driver (`ChatSessionHandler`) mirrors its state to `chat_llm_tools` and emits the `ToolStart`/`ToolDone`/`ToolError`/`ToolCancelled`/`ToolRejected` events. **`SimpleExecution`** is the default handle: a work future + a stop-token. `wait` races the two, so `stop()` (or the driver dropping `wait`) drops the work future, aborting the in-flight I/O — enough to make `/stop` responsive for **every** I/O-bound tool with zero per-tool code (this includes `execute_cmd`, whose `kill_on_drop(true)` child dies when the future is dropped). **`drive_execution(exec, cancel_token)`** is the generic driver: it runs `wait` and, when the turn's `/stop` token fires, calls `exec.stop()` once. Used by both the live loop (`llm_loop.rs`) and `resume_pending_tools` (`resume.rs`). **Bespoke `stop()` (extension point).** Tools that must tear down *remote* work override `run()` to return their own `ToolExecution` whose `stop()` does more than drop the future — e.g. a ComfyUI image tool POSTing `/interrupt` so the server stops generating too. Dropping the future already frees the client; a bespoke `stop()` propagates the cancellation to the far side. (Not yet wired for any built-in tool — the default covers responsive `/stop` today.) **Rehydration = re-run from intent.** A persisted tool call is `(name, args, status)`; the live future is never serialized. `resume_pending_tools` reconstructs an execution via `build_execution(name, args)` and re-runs it from the start — it does not resume a checkpoint. **`sub_agents_only`**: if a tool returns `true`, it is excluded from the root agent's tool list and only added to sub-agent configs (depth ≥ 1) in `dispatch_sub_agent`. Default is `false`. **`root_agent_only`**: if a tool returns `true`, it is included in the root agent's tool list but filtered out from sub-agent configs in `AgentRunConfig::for_sub_agent()`. Default is `false`. Both flags are mutually exclusive — a tool should never return `true` for both. If it does, it will be invisible to all agents. --- ## ToolCategory Every tool declares a `ToolCategory`, used for access-control filtering and audit: | Variant | Used by | | --- | --- | | `Filesystem` | File read/write tools (`read_file`, `write_file`, `edit_file`, …) | | `Shell` | `execute_cmd`, `restart` | | `Subagent` | `execute_task` / `execute_subtask` (synthetic — not in registry) | | `Introspection` | `list_items`, `image_generate_providers_list` | | `Config` | `register_mcp`, `delete_mcp`, `toggle_item`, `execute_task` (InterfaceTool, interactive only), `delete_cron_job`, `configure_plugin`, `image_generate`, `set_secret`, `list_secrets` | --- ## ToolRegistry `HashMap>` with four public methods: | Method | Purpose | | --- | --- | | `register(tool)` | Insert tool keyed by `tool.name()` | | `openai_definitions()` | Returns definitions for root-agent tools (excludes `sub_agents_only`) | | `openai_definitions_sub_agents_only()` | Returns definitions for tools where `sub_agents_only() == true` | | `root_agent_only_names()` | Returns names of all tools where `root_agent_only() == true` — used by `for_sub_agent()` to filter | | `list_all()` | Returns `(name, description)` for all registered tools (sorted) | | `category_of(name)` | Returns `Option` for a registered tool; `None` for MCP/interface/unknown tools | | `dispatch(name, args)` | Executes tool by name to a `Result`; errors on unknown name (used by the REST resolve endpoint) | | `run(name, args)` | Starts a `ToolExecution` for a registered tool; `None` for unknown names (MCP/interface handled by the caller). The cancellable dispatch path. | | `describe_call(name, args, length)` | Returns a human-readable label for any tool call (including non-registry tools). Falls back to `name` for unknown tools. | --- ## ToolCatalog `ToolCatalog` (`src/core/tool_catalog.rs`) is a unified façade wrapping `ToolRegistry` + `McpManager`: | Method | Purpose | | --- | --- | | `list_all() -> AllTools` | Returns all built-in tools (registry), a small **static list** of core tools injected outside the registry (`synthetic_tools()`: `execute_task`, `execute_subtask`, `update_scratchpad`, `ask_user_clarification`, `write_todos`, `activate_tools`, `notify`, `show_file_to_user`, `image_generate`), and MCP tools as a single `AllTools { built_in, mcp }` struct. Used by `GET /api/approval/tools`. | | `describe_call(name, args, length) -> String` | Pass-through to `ToolRegistry::describe_call()`. | `AllTools` and `ToolInfo` are `#[derive(Serialize)]` — the frontend handler can return `Json` directly. ### Dynamically-injected tools & discovery Many tools reach the LLM **outside** the `ToolRegistry` — `InterfaceTool` closures (`write_todos`, `notify`, `show_file_to_user`, `activate_tools`), plugin tools (Telegram `send_voice_message`/`send_attachment`, honcho `memory_*`) and provider tools (`image_generate`). The `synthetic_tools()` static list above eagerly surfaces the *core-owned* ones so they can be pre-configured in the Security-groups UI before first use; plugin/provider names are intentionally left out so core stays decoupled from them. To cover the rest without a hand-maintained mirror, [`ToolDiscovery`](../src/core/tool_discovery.rs) taps the single point where the tool array is assembled — `AgentRunConfig::all_tool_defs()`, observed each round from `llm_loop.rs` — and upserts every offered tool into the `known_tools` table (in-memory seen-set guard → DB write only for new names, off the turn's critical path). `GET /api/approval/tools` merges `known_tools` into the response so any tool that has been offered at least once becomes gate-able. This **cannot drift** from what is really offered and needs no per-tool/per-plugin wiring. See [approval docs](approval/index.md#alltools-response-get-apiapprovaltools). --- ## Tool Name Constants All system tool names are centralised in `src/core/tools/tool_names.rs` as `pub const` strings. Import with `use crate::tools::tool_names as tn;`. | Constant | Value | | --- | --- | | `tn::EXECUTE_TASK` | `"execute_task"` | | `tn::EXECUTE_SUBTASK` | `"execute_subtask"` | | `tn::RESTART` | `"restart"` | | `tn::UPDATE_SCRATCHPAD` | `"update_scratchpad"` | | `tn::WRITE_TODOS` | `"write_todos"` | | `tn::ASK_USER_CLARIFICATION` | `"ask_user_clarification"` | | `tn::ACTIVATE_TOOLS` | `"activate_tools"` | | `tn::NOTIFY` | `"notify"` | | `tn::READ_NOTIFICATION` | `"read_notification"` | | `tn::EXECUTE_CMD` | `"execute_cmd"` | | `tn::SHOW_FILE_TO_USER` | `"show_file_to_user"` | **Rule:** never hardcode these strings in new code — always use the constants. This ensures that a rename is a single-file change and that typos produce a compile error rather than a silent dispatch miss. --- ## Registration Pattern All tools are registered in `src/main.rs` before `ChatSessionManager` is built. **Not in ToolRegistry — synthetic tools intercepted in `run_agent_turn`:** - `execute_task` — interactive-session sub-agent/task launcher (modes: `cron`, `sync`=inline sub-agent, `async`=background). Intercepted in `run_agent_turn` only for `mode=sync`; `cron`/`async` go through the normal cancellable path as InterfaceTools - `execute_subtask` — background-session sub-agent launcher (sync-only). Injected as an InterfaceTool in cron/async sessions in place of `execute_task` - `update_scratchpad` — writes to `session_scratchpad` table; **shared** blackboard injected into every agent in the session; available to all agents - `write_todos` — **stateless** private task list (TodoWrite-style: the agent re-sends the whole list with statuses on every call); available to all agents. Unlike the scratchpad it is **not** persisted and **not** shared: the formatted checklist lives only in the calling agent's own tool-result history, which is per-stack, so sub-agents and the caller never see it. Handled by `dispatch_write_todos` (`agent_dispatch.rs`); no DB table involved - `ask_user_clarification` — pauses and asks the user a question; available to every agent except hidden `system` agents (e.g. TIC). Routing depends on session type: - **Interactive sessions** (web, Telegram): available at any depth (root `chat` agent included); emits `ServerEvent::AgentQuestion` inline (and registers with `ClarificationManager`), so the user can answer either from the chat or the Agent Inbox - **Background sessions** (cron): available at any depth; registers with `ClarificationManager` only, visible in Agent Inbox; agent suspends until answered - `activate_tools` — activates **tool groups** on demand (lazy loading): MCP server names and/or the reserved `config` group (the built-in `Config`-category tools). Injected as an `InterfaceTool` in `build_agent_config` (root, session-scoped) and in `dispatch_sub_agent` (sub-agents, stack-scoped). See [Lazy `config` tool group](#lazy-config-tool-group) - `notify` — queues a structured notification (`Notification`: `{source, event_type, summary, event_time, refs}`) to the home conversation via `ChatHub`; **injected as an `InterfaceTool` by the caller** (`TicManager` for TIC, `TaskManager` for background task agents); not in ToolRegistry so ordinary agents cannot call it - `show_file_to_user` — opens a file in the user's UI. Emits `ServerEvent::OpenFile`, which the SPA routes to the [file viewer](frontend.md#file-viewer) (every kind, HTML included). Supported formats in the file viewer: Markdown, source code, plain text, raster images (PNG/JPG/GIF/WebP/…), SVG, PDF, LaTeX (`.tex` / `.latex` — compiled to PDF automatically on the server), and HTML (`.html` / `.htm` — rendered live in an origin-isolated `