# Tool Dispatch

How one round of tool calls is checked, run, and recorded: the three passes, deferred schemas, which tools run concurrently, and how repeated calls are deduplicated.

## Three-pass tool concurrency

`toolCall.go` splits each round's tool calls into three serial passes; only Pass 2 fans out:

| Pass | Mode | Work |
|---|---|---|
| 1 — pre-flight | Serial | Unwrap `run_tool` into the named tool; refuse a tool called directly that is not in the loaded set (with a hint to fetch its schema with `find_tools` and call `run_tool`); duplicate-call dedupe (`tool\|args` hash); return the schema, and ask for a re-invoke, when a tool is called before its schema was fetched; existence check, so a name with no registered handler never runs (since v1.2.0); confirm gate; JSON-schema validation |
| 2 — execute | Concurrent for `IsConcurrent`-tagged tools, at most `MAX_CONCURRENT_TOOLS` at a time — half the CPU cores, at least 3, since v1.3.0 (a fixed 5 from v1.0.20) — with the rest queued; serial otherwise | `tools.Execute` |
| 3 — commit | Serial | Land `sessionData.Tools` and `ToolHistories`, update the dedupe map, write the result cache, emit `EventToolResult` |

Since v1.2.0 tool schemas are deferred. For every provider except `claude-code`, the request's tools array holds only `find_tools`, `run_tool`, `reasoning_guide`, `write_todo`, `chat_history`, `ask_user` and `write_result` (plus client tools; `reasoning_guide` joined in v1.2.1, `write_todo` in v1.2.3, the last three in v1.3.5); every other tool is listed by name in the turn context, its schema is fetched with `find_tools`, and it is called as `run_tool(name, args)`. `claude-code` gets every tool except `run_tool`, but all of them except `find_tools` and client tools start unmarked, so the first call to each returns its schema instead of running. Since v1.3.5 a failed tool result ends with a `next:` hint: `store_secret` when the error points to a missing or rejected credential, otherwise `write_error(outcome=resolved)` once a different fix works (before v1.3.11 it pointed to `error_history(mode=write)`, Removed in v1.3.11). The per-tool `AlwaysLoad` flag was removed; use `find_tools` to load a schema. Removed in v1.2.0.

Concurrent-tagged built-in tools (21): `read_files`, `find_files`, `find_tools`, `file_history`, `chat_history`, `error_history`, `write_error` (since v1.3.11), `fetch_page`, `search_web`, `http_request`, `public_api_list`, `download_file`, `test_tool`, `calculate`, `subagents`, `background_subagents`, `list_chatbot`, `send_to_chatbot`, `reasoning_guide`, `html_template`, `write_todo` (since v1.0.26; `find_note` left the set when it was removed in v1.0.23). `generate_audio` and `generate_image` do not carry the flag. `script_*` / `ext_*` tools run concurrently only when their own definition declares it; user-defined `api_*` tools were dropped (Removed in v1.3.2), use the built-in `public_api_list` with `http_request` instead. `edit_file`, `remove_file`, `restore_file`, `run_command`, `background_command`, interactive tools, and MCP tools always run serially — the parallel `run_command_readonly` tool added in v1.0.23 was removed in v1.0.25. `run_command_background` was renamed `background_command` (Removed in v1.3.2), and `run_websocket` was dropped (Removed in v1.3.2) with no replacement.

Dedupe is per run, not per session: a repeated `tool|args` pair returns the earlier result instead of executing again, and a successful `edit_file`, `remove_file` or `restore_file` invalidates the `read_files` entries covering the paths it touched. A separate 30-minute result cache in ToriiDB (`db_0`) covers `fetch_page`, `search_web`, and `http_request` `GET` calls only. Pass 3 writes it for `http_request` alone; since v1.3.1 `fetch_page` and `search_web` write their own unfiltered result from the handler and apply the decision-model filters on every return, cached or fresh. `fetch_page` entries are shared across sessions.
