Skip to main content

hematite/agent/
tool_registry.rs

1use crate::agent::inference::{tool_metadata_for_name, ToolDefinition, ToolFunction};
2use serde_json::Value;
3
4fn make_tool(name: &str, description: &str, parameters: Value) -> ToolDefinition {
5    ToolDefinition {
6        tool_type: "function".into(),
7        function: ToolFunction {
8            name: name.into(),
9            description: description.into(),
10            parameters,
11        },
12        metadata: tool_metadata_for_name(name),
13    }
14}
15
16/// Returns the full set of tools exposed to the model.
17pub fn get_tools() -> Vec<ToolDefinition> {
18    let os = std::env::consts::OS;
19    let mut tools = vec![
20        make_tool(
21            "shell",
22            &format!(
23                "Execute a command in the host shell ({os}). \
24                     Use this for building, testing, or system operations. \
25                     Output is capped at 64KB. Prefer non-interactive commands."
26            ),
27            serde_json::json!({
28                "type": "object",
29                "properties": {
30                    "command": {
31                        "type": "string",
32                        "description": "The command to run"
33                    },
34                    "reason": {
35                        "type": "string",
36                        "description": "For risky shell calls, explain what this command is verifying or changing."
37                    },
38                    "timeout_secs": {
39                        "type": "integer",
40                        "description": "Optional timeout in seconds (default 60)"
41                    }
42                },
43                "required": ["command"]
44            }),
45        ),
46        make_tool(
47            "run_code",
48            "Execute a short JavaScript/TypeScript or Python snippet in a sandboxed subprocess. \
49             No network access, no filesystem escape, hard 10-second timeout. \
50             Use this to verify logic, test algorithms, compute values, or test functions \
51             when you need real output rather than a guess. \
52             ALWAYS include the `language` field — there is no default. \
53             \
54             JAVASCRIPT/TYPESCRIPT (language: \"javascript\"): \
55             Runs via Deno, NOT Node.js. `require()` does not exist — never use it. \
56             URL imports (e.g. from 'https://deno.land/...') are blocked — network is off. \
57             Use built-in Web APIs only: `crypto.subtle`, `TextEncoder`, `URL`, `atob`/`btoa`, etc. \
58             SHA-256 example: \
59               const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('hello')); \
60               console.log([...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join('')); \
61             \
62             PYTHON (language: \"python\"): \
63             Standard library is available. `hashlib`, `json`, `math`, `datetime`, `re`, `itertools` all work. \
64             `subprocess`, `socket`, `urllib`, `requests` are blocked. \
65             SHA-256 example: import hashlib; print(hashlib.sha256(b'hello').hexdigest()) \
66             \
67             Do NOT fall back to shell to run deno, python, or node — use this tool directly.",
68            serde_json::json!({
69                "type": "object",
70                "properties": {
71                    "language": {
72                        "type": "string",
73                        "enum": ["javascript", "typescript", "python"],
74                        "description": "The language to run. javascript/typescript requires Deno; python requires Python 3."
75                    },
76                    "code": {
77                        "type": "string",
78                        "description": "The code to execute. Keep it short and self-contained. Print results to stdout."
79                    },
80                    "timeout_seconds": {
81                        "type": "integer",
82                        "description": "Max execution time in seconds (default 10, max 60). Use higher values for longer computations."
83                    }
84                },
85                "required": ["language", "code"]
86            }),
87        ),
88        make_tool(
89            "map_project",
90            "Compact architecture-aware map of the project structure, key configuration files, \
91             likely entrypoints, and core owner files. Use this at the start of a task to gain \
92             spatial awareness before deeper file reads or LSP inspection.",
93            serde_json::json!({
94                "type": "object",
95                "properties": {
96                    "focus": {
97                        "type": "string",
98                        "description": "Optional relative subpath to focus the map on instead of the whole workspace."
99                    },
100                    "include_symbols": {
101                        "type": "boolean",
102                        "description": "Whether to extract a small set of top symbols from core files. Defaults to true."
103                    },
104                    "max_depth": {
105                        "type": "integer",
106                        "description": "Optional tree depth cap for the directory section. Defaults to 4 and is capped internally."
107                    }
108                }
109            }),
110        ),
111        make_tool(
112            "trace_runtime_flow",
113            "Return an authoritative read-only trace of Hematite runtime flow. \
114             Use this for architecture questions about keyboard input to final output, \
115             reasoning/specular separation, startup wiring, runtime subsystems, \
116             voice synthesis and Ctrl+T toggle, or \
117             session reset commands like /clear, /new, and /forget. Prefer this over guessing.",
118            serde_json::json!({
119                "type": "object",
120                "properties": {
121                    "topic": {
122                        "type": "string",
123                        "enum": ["user_turn", "session_reset", "reasoning_split", "runtime_subsystems", "startup", "voice"],
124                        "description": "Which verified runtime report to return. Use 'voice' for any question about Ctrl+T, voice toggle, or TTS pipeline. Use 'user_turn' for keyboard-to-output flow. Use 'session_reset' for /clear, /forget, /new. Use 'startup' for startup wiring. Use 'reasoning_split' for specular/thought routing. Use 'runtime_subsystems' for background subsystem overview."
125                    },
126                    "input": {
127                        "type": "string",
128                        "description": "Optional user input to label a normal user-turn trace"
129                    },
130                    "command": {
131                        "type": "string",
132                        "enum": ["/clear", "/new", "/forget", "all"],
133                        "description": "Optional reset command when topic=session_reset"
134                    }
135                },
136                "required": ["topic"]
137            }),
138        ),
139        make_tool(
140            "describe_toolchain",
141            "Return an authoritative read-only description of Hematite's actual tool surface and investigation strategy. \
142             Use this for tooling-discipline questions, best-tool selection, or read-only plans for tracing runtime behavior. \
143             Prefer this over improvising tool names or investigation steps from memory.",
144            serde_json::json!({
145                "type": "object",
146                "properties": {
147                    "topic": {
148                        "type": "string",
149                        "enum": ["read_only_codebase", "user_turn_plan", "voice_latency_plan", "host_inspection_plan", "all"],
150                        "description": "Which authoritative toolchain report to return"
151                    },
152                    "question": {
153                        "type": "string",
154                        "description": "Optional user question to label or tailor the read-only investigation plan"
155                    }
156                }
157            }),
158        ),
159        make_tool(
160            "inspect_host",
161            "Return a structured read-only inspection of the current machine and environment. \
162             Prefer this over raw shell for questions about installed developer tools, PATH issues, package-manager and environment health, network state, service state, running processes, desktop items, Downloads size, open listening ports, repo health, or directory/disk summaries. \
163             For remediation questions phrased like 'how do I fix cargo not found', 'how do I fix port 3000 already in use', or 'how do I fix LM Studio not reachable', use topic=fix_plan instead of diagnosis-only topics like env_doctor, path, or ports. \
164             Use topic=summary for a compact host snapshot, topic=toolchains for common dev tool versions, topic=path for PATH analysis, topic=env_doctor for package-manager and PATH health, topic=fix_plan for structured remediation plans, topic=network for adapters/IPs/gateways/DNS, topic=services for service status and startup mode, \
165             topic=processes for running-process snapshots, topic=desktop or topic=downloads for known folders, topic=ports for listening endpoints, topic=repo_doctor for a structured workspace health report, \
166             and topic=directory or topic=disk for arbitrary paths.",
167            serde_json::json!({
168                "type": "object",
169                "properties": {
170                    "topic": {
171                        "type": "string",
172                        "enum": ["summary", "toolchains", "path", "env_doctor", "fix_plan", "network", "services", "processes", "desktop", "downloads", "directory", "disk", "ports", "repo_doctor"],
173                        "description": "Which structured host inspection to run."
174                    },
175                    "name": {
176                        "type": "string",
177                        "description": "Optional when topic=processes or topic=services. Case-insensitive substring filter for process or service names."
178                    },
179                    "issue": {
180                        "type": "string",
181                        "description": "Optional when topic=fix_plan. Plain-English issue description such as 'cargo not found', 'port 3000 already in use', or 'LM Studio not reachable on localhost:1234'."
182                    },
183                    "path": {
184                        "type": "string",
185                        "description": "Required when topic=directory. Optional for topic=disk or topic=repo_doctor. Absolute or relative path to inspect."
186                    },
187                    "port": {
188                        "type": "integer",
189                        "description": "Optional when topic=ports or topic=fix_plan. Filter the result to one listening TCP port or anchor a port-conflict fix plan."
190                    },
191                    "max_entries": {
192                        "type": "integer",
193                        "description": "Optional cap for listed entries. Defaults to 10 and is capped internally."
194                    }
195                }
196            }),
197        ),
198        make_tool(
199            "read_file",
200            "Read the contents of a file. For large files, use 'offset' and 'limit' to navigate.",
201            serde_json::json!({
202                "type": "object",
203                "properties": {
204                    "path": {
205                        "type": "string",
206                        "description": "Path to the file, relative to the project root"
207                    },
208                    "offset": {
209                        "type": "integer",
210                        "description": "Starting line number (0-indexed)"
211                    },
212                    "limit": {
213                        "type": "integer",
214                        "description": "Number of lines to read"
215                    }
216                },
217                "required": ["path"]
218            }),
219        ),
220        make_tool(
221            "lsp_definitions",
222            "Get the precise definition location (file:line:char) for a symbol at a specific position. \
223             Use this to jump to function/struct source code accurately.",
224            serde_json::json!({
225                "type": "object",
226                "properties": {
227                    "path": { "type": "string", "description": "File path" },
228                    "line": { "type": "integer", "description": "0-indexed line" },
229                    "character": { "type": "integer", "description": "0-indexed character" }
230                },
231                "required": ["path", "line", "character"]
232            }),
233        ),
234        make_tool(
235            "lsp_references",
236            "Find all locations where a symbol is used across the entire workspace. \
237             Use this to understand the impact of a refactor or discover internal API users.",
238            serde_json::json!({
239                "type": "object",
240                "properties": {
241                    "path": { "type": "string", "description": "File path" },
242                    "line": { "type": "integer", "description": "0-indexed line" },
243                    "character": { "type": "integer", "description": "0-indexed character" }
244                },
245                "required": ["path", "line", "character"]
246            }),
247        ),
248        make_tool(
249            "lsp_hover",
250            "Get hover information (documentation, function signature, type details) for a symbol. \
251             Use this for rapid spatial awareness without opening every file.",
252            serde_json::json!({
253                "type": "object",
254                "properties": {
255                    "path": { "type": "string", "description": "File path" },
256                    "line": { "type": "integer", "description": "0-indexed line" },
257                    "character": { "type": "integer", "description": "0-indexed character" }
258                },
259                "required": ["path", "line", "character"]
260            }),
261        ),
262        make_tool(
263            "lsp_rename_symbol",
264            "Rename a symbol project-wide using the Language Server. Ensures all references are updated safely.",
265            serde_json::json!({
266                "type": "object",
267                "properties": {
268                    "path": { "type": "string", "description": "File path" },
269                    "line": { "type": "integer", "description": "0-indexed line" },
270                    "character": { "type": "integer", "description": "0-indexed character" },
271                    "new_name": { "type": "string", "description": "The new name for the symbol" }
272                },
273                "required": ["path", "line", "character", "new_name"]
274            }),
275        ),
276        make_tool(
277            "lsp_get_diagnostics",
278            "Get a list of current compiler errors and warnings for a specific file. \
279             Use this to verify your code compiles and and to find exactly where errors are located.",
280            serde_json::json!({
281                "type": "object",
282                "properties": {
283                    "path": { "type": "string", "description": "File path" }
284                },
285                "required": ["path"]
286            }),
287        ),
288        make_tool(
289            "vision_analyze",
290            "Send an image file (screenshot, diagram, or UI mockup) to the multimodal vision model for technical analysis. \
291             Use this to identify UI bugs, confirm visual states, or understand architectural diagrams.",
292            serde_json::json!({
293                "type": "object",
294                "properties": {
295                    "path": { "type": "string", "description": "Absolute or relative path to the image file." },
296                    "prompt": { "type": "string", "description": "The specific question or analysis request for the vision model." }
297                },
298                "required": ["path", "prompt"]
299            }),
300        ),
301        make_tool(
302            "patch_hunk",
303            "Replace a specific line range [start_line, end_line] with new content. \
304             This is the most precise way to edit code and avoids search string failures.",
305            serde_json::json!({
306                "type": "object",
307                "properties": {
308                    "path": { "type": "string", "description": "File path" },
309                    "start_line": { "type": "integer", "description": "Starting line (1-indexed)" },
310                    "end_line": { "type": "integer", "description": "Ending line (inclusive)" },
311                    "replacement": { "type": "string", "description": "The new content for this range" }
312                },
313                "required": ["path", "start_line", "end_line", "replacement"]
314            }),
315        ),
316        make_tool(
317            "multi_search_replace",
318            "Replace multiple existing code blocks in a single file with new content. \
319             Each hunk specifies an EXACT 'search' string and a 'replace' string. \
320             The 'search' string MUST exactly match the existing file contents (including whitespace). \
321             This is the safest and most reliable way to make multiple structural edits.",
322            serde_json::json!({
323                "type": "object",
324                "properties": {
325                    "path": { "type": "string", "description": "File path" },
326                    "hunks": {
327                        "type": "array",
328                        "items": {
329                            "type": "object",
330                            "properties": {
331                                "search": { "type": "string", "description": "Exact existing text to find and replace" },
332                                "replace": { "type": "string", "description": "The new replacement text" }
333                            },
334                            "required": ["search", "replace"]
335                        }
336                    }
337                },
338                "required": ["path", "hunks"]
339            }),
340        ),
341        make_tool(
342            "write_file",
343            "Write content to a file, creating it (and any parent dirs) if needed. \
344             Overwrites existing files.",
345            serde_json::json!({
346                "type": "object",
347                "properties": {
348                    "path": { "type": "string", "description": "File path" },
349                    "content": { "type": "string", "description": "Full file content to write" }
350                },
351                "required": ["path", "content"]
352            }),
353        ),
354        make_tool(
355            "research_web",
356            "Perform a zero-cost technical search using DuckDuckGo. \
357             Use this to find documentation, latest API changes, or solutions to complex errors \
358             when your internal knowledge is insufficient. Returns snippets and URLs.",
359            serde_json::json!({
360                "type": "object",
361                "properties": {
362                    "query": { "type": "string", "description": "The technical search query" }
363                },
364                "required": ["query"]
365            }),
366        ),
367        make_tool(
368            "fetch_docs",
369            "Fetch a URL and convert it to clean Markdown. Use this to 'read' the documentation \
370             links found via research_web. This tool uses a proxy to bypass IP blocks.",
371            serde_json::json!({
372                "type": "object",
373                "properties": {
374                    "url": { "type": "string", "description": "The URL of the documentation to fetch" }
375                },
376                "required": ["url"]
377            }),
378        ),
379        make_tool(
380            "edit_file",
381            "Edit a file by replacing an exact string with another. \
382             The 'search' string does NOT need perfectly matching indentation (it is fuzzy), \
383             but the non-whitespace text must match exactly. Use this for targeted edits.",
384            serde_json::json!({
385                "type": "object",
386                "properties": {
387                    "path": { "type": "string", "description": "File path" },
388                    "search": {
389                        "type": "string",
390                        "description": "The exact text to find (must match whitespace/indentation precisely)"
391                    },
392                    "replace": {
393                        "type": "string",
394                        "description": "The replacement text"
395                    }
396                },
397                "required": ["path", "search", "replace"]
398            }),
399        ),
400        make_tool(
401            "auto_pin_context",
402            "Select 1-3 core files to 'Lock' into high-fidelity memory. \
403             Use this after map_project to ensure the most important architecture files \
404             are always visible during complex refactorings.",
405            serde_json::json!({
406                "type": "object",
407                "properties": {
408                    "paths": {
409                        "type": "array",
410                        "items": { "type": "string" }
411                    },
412                    "reason": { "type": "string" }
413                },
414                "required": ["paths", "reason"]
415            }),
416        ),
417        make_tool(
418            "list_pinned",
419            "List all files currently pinned in the model's active context.",
420            serde_json::json!({
421                "type": "object",
422                "properties": {}
423            }),
424        ),
425        make_tool(
426            "list_files",
427            "List files in a directory, optionally filtered by extension.",
428            serde_json::json!({
429                "type": "object",
430                "properties": {
431                    "path": {
432                        "type": "string",
433                        "description": "Directory to list (default: current dir)"
434                    },
435                    "extension": {
436                        "type": "string",
437                        "description": "Only return files with this extension, e.g. 'rs', 'toml' (no dot)"
438                    }
439                },
440                "required": []
441            }),
442        ),
443        make_tool(
444            "grep_files",
445            "Search file contents for a regex pattern. Supports context lines, files-only mode, \
446             and pagination. Returns file:line:content format by default.",
447            serde_json::json!({
448                "type": "object",
449                "properties": {
450                    "pattern": {
451                        "type": "string",
452                        "description": "Regex pattern to search for (case-insensitive by default)"
453                    },
454                    "path": {
455                        "type": "string",
456                        "description": "Directory to search (default: current dir)"
457                    },
458                    "extension": {
459                        "type": "string",
460                        "description": "Only search files with this extension, e.g. 'rs'"
461                    },
462                    "mode": {
463                        "type": "string",
464                        "enum": ["content", "files_only"],
465                        "description": "'content' (default) returns matching lines; 'files_only' returns only filenames"
466                    },
467                    "context": {
468                        "type": "integer",
469                        "description": "Lines of context before AND after each match (like rg -C)"
470                    },
471                    "before": {
472                        "type": "integer",
473                        "description": "Lines of context before each match (overrides context)"
474                    },
475                    "after": {
476                        "type": "integer",
477                        "description": "Lines of context after each match (overrides context)"
478                    },
479                    "head_limit": {
480                        "type": "integer",
481                        "description": "Max hunks (or files in files_only) to return (default: 50)"
482                    },
483                    "offset": {
484                        "type": "integer",
485                        "description": "Skip first N hunks/files - for pagination (default: 0)"
486                    }
487                },
488                "required": ["pattern"]
489            }),
490        ),
491        make_tool(
492            "git_commit",
493            "Stage all changes (git add -A) and create a commit. You MUST use 'Conventional Commits' (e.g. 'feat: description').",
494            serde_json::json!({
495                "type": "object",
496                "properties": {
497                    "message": { "type": "string", "description": "Commit message (Conventional Commit style)" }
498                },
499                "required": ["message"]
500            }),
501        ),
502        make_tool(
503            "git_push",
504            "Push current branched changes to the remote origin. Requires an existing remote connection.",
505            serde_json::json!({
506                "type": "object",
507                "properties": {},
508                "required": []
509            }),
510        ),
511        make_tool(
512            "git_remote",
513            "View or manage git remotes. Use this for onboarding to GitHub/GitLab services.",
514            serde_json::json!({
515                "type": "object",
516                "properties": {
517                    "action": {
518                        "type": "string",
519                        "enum": ["list", "add", "remove"],
520                        "description": "Operation to perform"
521                    },
522                    "name": { "type": "string", "description": "Remote name (e.g. origin)" },
523                    "url": { "type": "string", "description": "Remote URL (for 'add' action)" }
524                },
525                "required": ["action"]
526            }),
527        ),
528        make_tool(
529            "git_onboarding",
530            "High-level wizard to connect this repository to a remote host (GitHub/GitLab). \
531             Handles adding the remote and performing the initial tracking push in one step.",
532            serde_json::json!({
533                "type": "object",
534                "properties": {
535                    "url": { "type": "string", "description": "The remote repository URL (HTTPS or SSH)" },
536                    "name": { "type": "string", "description": "The remote name (default: origin)" },
537                    "push": { "type": "boolean", "description": "Whether to perform an initial push to establish tracking (default: false)" }
538                },
539                "required": ["url"]
540            }),
541        ),
542        make_tool(
543            "verify_build",
544            "Run project verification for build, test, lint, or fix workflows. \
545             Prefer per-project verify profiles from `.hematite/settings.json`, and fall back to \
546             auto-detected defaults when no profile is configured. Returns BUILD OK or BUILD FAILED \
547             with command output. ALWAYS call this after scaffolding a new project or making structural changes.",
548            serde_json::json!({
549                "type": "object",
550                "properties": {
551                    "action": {
552                        "type": "string",
553                        "enum": ["build", "test", "lint", "fix"],
554                        "description": "Which verification action to run. Defaults to build."
555                    },
556                    "profile": {
557                        "type": "string",
558                        "description": "Optional named verify profile from `.hematite/settings.json`."
559                    },
560                    "timeout_secs": {
561                        "type": "integer",
562                        "description": "Optional timeout override for this verification run."
563                    }
564                }
565            }),
566        ),
567        make_tool(
568            "git_worktree",
569            "Manage Git worktrees - isolated working directories on separate branches. \
570             Use 'add' to create a safe sandbox for risky/experimental work, \
571             'list' to see all worktrees, 'remove' to clean up, 'prune' to remove stale entries.",
572            serde_json::json!({
573                "type": "object",
574                "properties": {
575                    "action": {
576                        "type": "string",
577                        "enum": ["list", "add", "remove", "prune"],
578                        "description": "Worktree operation to perform"
579                    },
580                    "path": {
581                        "type": "string",
582                        "description": "Directory path for the new worktree (required for add/remove)"
583                    },
584                    "branch": {
585                        "type": "string",
586                        "description": "Branch name for the worktree (add only; defaults to path basename)"
587                    }
588                },
589                "required": ["action"]
590            }),
591        ),
592        make_tool(
593            "clarify",
594            "Ask the user a clarifying question when you genuinely cannot proceed without \
595             more information. Use this ONLY when you are blocked and cannot make a \
596             reasonable assumption. Do NOT use it to ask permission - just act.",
597            serde_json::json!({
598                "type": "object",
599                "properties": {
600                    "question": {
601                        "type": "string",
602                        "description": "The specific question to ask the user"
603                    }
604                },
605                "required": ["question"]
606            }),
607        ),
608        make_tool(
609            "manage_tasks",
610            "Manage the persistent task ledger in .hematite/TASK.md. Use this to track long-term goals across restarts.",
611            crate::tools::tasks::get_tasks_params(),
612        ),
613        make_tool(
614            "maintain_plan",
615            "Document the architectural strategy and session blueprint in .hematite/PLAN.md. Use this to maintain context across restarts.",
616            crate::tools::plan::get_plan_params(),
617        ),
618        make_tool(
619            "generate_walkthrough",
620            "Generate a final session report in .hematite/WALKTHROUGH.md including achievements and verification results.",
621            crate::tools::plan::get_walkthrough_params(),
622        ),
623        make_tool(
624            "swarm",
625            "Delegate high-volume parallel tasks to a swarm of background workers. \
626             Use this for large-scale refactors, multi-file research, or parallel documentation updates. \
627             You must provide a 'tasks' array where each task has an 'id', 'target' (file), and 'instruction'.",
628            serde_json::json!({
629                "type": "object",
630                "properties": {
631                    "tasks": {
632                        "type": "array",
633                        "items": {
634                            "type": "object",
635                            "properties": {
636                                "id": { "type": "string" },
637                                "target": { "type": "string", "description": "Target file or directory" },
638                                "instruction": { "type": "string", "description": "Specific task for this worker" }
639                            },
640                            "required": ["id", "target", "instruction"]
641                        }
642                    },
643                    "max_workers": {
644                        "type": "integer",
645                        "description": "Max parallel workers (default 3, auto-throttled by hardware)",
646                        "default": 3
647                    }
648                },
649                "required": ["tasks"]
650            }),
651        ),
652    ];
653
654    let lsp_defs = crate::tools::lsp_tools::get_lsp_definitions();
655    tools.push(make_tool(
656        "lsp_search_symbol",
657        "Find the location (file/line) of any function, struct, or variable in the entire project workspace. \
658         This is the fastest 'Golden Path' for navigating to a symbol by name.",
659        serde_json::json!({
660            "type": "object",
661            "properties": {
662                "query": { "type": "string", "description": "The name of the symbol to find (e.g. 'initialize_mcp')" }
663            },
664            "required": ["query"]
665        }),
666    ));
667    for def in lsp_defs {
668        let name = def["name"].as_str().unwrap();
669        tools.push(ToolDefinition {
670            tool_type: "function".into(),
671            function: ToolFunction {
672                name: name.into(),
673                description: def["description"].as_str().unwrap().into(),
674                parameters: def["parameters"].clone(),
675            },
676            metadata: tool_metadata_for_name(name),
677        });
678    }
679
680    tools
681}
682
683pub async fn dispatch_builtin_tool(name: &str, args: &Value) -> Result<String, String> {
684    match name {
685        "shell" => crate::tools::shell::execute(args).await,
686        "run_code" => crate::tools::code_sandbox::execute(args).await,
687        "map_project" => crate::tools::project_map::map_project(args).await,
688        "trace_runtime_flow" => crate::tools::runtime_trace::trace_runtime_flow(args).await,
689        "describe_toolchain" => crate::tools::toolchain::describe_toolchain(args).await,
690        "inspect_host" => crate::tools::host_inspect::inspect_host(args).await,
691        "read_file" => crate::tools::file_ops::read_file(args).await,
692        "inspect_lines" => crate::tools::file_ops::inspect_lines(args).await,
693        "write_file" => crate::tools::file_ops::write_file(args).await,
694        "edit_file" => crate::tools::file_ops::edit_file(args).await,
695        "patch_hunk" => crate::tools::file_ops::patch_hunk(args).await,
696        "multi_search_replace" => crate::tools::file_ops::multi_search_replace(args).await,
697        "list_files" => crate::tools::file_ops::list_files(args).await,
698        "grep_files" => crate::tools::file_ops::grep_files(args).await,
699        "git_commit" => crate::tools::git::execute(args).await,
700        "git_push" => crate::tools::git::execute_push(args).await,
701        "git_remote" => crate::tools::git::execute_remote(args).await,
702        "git_onboarding" => crate::tools::git_onboarding::execute(args).await,
703        "verify_build" => crate::tools::verify_build::execute(args).await,
704        "git_worktree" => crate::tools::git::execute_worktree(args).await,
705        "health" => crate::tools::health::execute(args).await,
706        "research_web" => crate::tools::research::execute_search(args).await,
707        "fetch_docs" => crate::tools::research::execute_fetch(args).await,
708        "manage_tasks" => crate::tools::tasks::manage_tasks(args).await,
709        "maintain_plan" => crate::tools::plan::maintain_plan(args).await,
710        "generate_walkthrough" => crate::tools::plan::generate_walkthrough(args).await,
711        "clarify" => {
712            let q = args.get("question").and_then(|v| v.as_str()).unwrap_or("?");
713            Ok(format!("[clarify] {q}"))
714        }
715        "vision_analyze" => Err(
716            "Tool 'vision_analyze' must be dispatched by ConversationManager (it requires hardware engine access)."
717                .into(),
718        ),
719        other => {
720            if other.contains('.') || other.contains('/') || other.contains('\\') {
721                Err(format!(
722                    "'{}' is a PATH, not a tool. You correctly identified the location, but you MUST use `read_file` or `list_files` (internal) or `powershell` (external) to access it.",
723                    other
724                ))
725            } else if matches!(other.to_lowercase().as_str(), "hematite" | "assistant" | "ai") {
726                Err(format!(
727                    "'{}' is YOUR IDENTITY, not a tool. Use list_files or read_file to explore the codebase.",
728                    other
729                ))
730            } else if matches!(
731                other.to_lowercase().as_str(),
732                "thought" | "think" | "reasoning" | "thinking" | "internal"
733            ) {
734                Err(format!(
735                    "'{}' is NOT a tool - it is a reasoning tag. Output your answer as plain text after your <think> block.",
736                    other
737                ))
738            } else {
739                Err(format!("Unknown tool: '{}'", other))
740            }
741        }
742    }
743}