返回 CodeWhale
TOOL_SURFACE.md
根目录 / docs / TOOL_SURFACE.md
1 # Tool surface
2
3 This document describes the current model-facing tool contract. The v0.9.1
4 cutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;
5 read the workspace version from `Cargo.toml`, not from this line. The registry
6 remains larger than the first-turn catalog so
7 saved transcripts can replay and uncommon capabilities can be loaded on demand.
8 The model should learn one canonical name for each common operation.
9
10 Implementation sources:
11
12 - `crates/tui/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.
13 - `crates/tui/src/tools/registry.rs` registers canonical tools and hidden aliases.
14 - `crates/tui/src/tools/{file_tool,git_tool,run_tool,web_tool,shell}.rs` own the
15 canonical action schemas.
16 - `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.
17
18 ## Default-active contract
19
20 The default-active policy contains exactly these nine names:
21
22 1. `Bash`
23 2. `File`
24 3. `Git`
25 4. `Run`
26 5. `agent`
27 6. `remember`
28 7. `tasks`
29 8. `work_update`
30 9. `tool_search`
31
32 The first eight are `DEFAULT_ACTIVE_NATIVE_TOOLS` in
33 `crates/tui/src/core/engine/tool_catalog.rs`. `tool_search` is synthetic rather
34 than registry-backed and is always active.
35
36 `remember` is registered only when the user enables the built-in memory path;
37 once present, it stays eager so a model can capture a durable preference without
38 first discovering the tool. A memory-disabled runtime omits that registration and
39 therefore exposes eight of the nine policy names.
40
41 `update_plan` is **hidden from the model**. It is registered
42 (`crates/tui/src/tools/plan.rs:401`) but `model_visible()` returns `false`
43 (`plan.rs:408-413`), and `build_api_tools` filters on that (`registry.rs:235`),
44 so it never enters the API tool list — which is what `tool_search` indexes.
45 `tool_search` cannot surface it either. Its own description calls it a "Legacy
46 compatibility tool for loading older Plan artifacts", and
47 `update_plan_is_hidden_replay_compatibility` (`plan.rs:598-605`) pins that.
48
49 Plan mode narrows the active set: `Bash` and `Run` drop out, leaving `File`,
50 `Git`, `agent`, `tasks`, `work_update`, `tool_search`, and — when memory is
51 enabled — `remember` (`should_register_remember_tool`,
52 `crates/tui/src/core/engine/tool_setup.rs:113-118`).
53
54 The surface is action-based. A model calls one stable tool name and selects the
55 operation through its `action` field instead of choosing among many synonymous
56 single-purpose tools.
57
58 ### Core action tools
59
60 | Tool | Actions | Purpose |
61 |---|---|---|
62 | `Bash` | `run`, `wait`, `interact`, `cancel` | Run bounded commands, continue background work, send input, and cancel processes. |
63 | `File` | `read`, `list`, `search_name`, `search_content`, `write`, `edit`, `patch` | Read, find, and modify workspace files with structured, workspace-aware results. |
64 | `Git` | `status`, `diff`, `log`, `show`, `blame` | Inspect repository state and history without parsing shell output. |
65 | `Run` | `tests`, `verifiers` | Run project tests or independent verifier gates with structured results. |
66
67 `Bash` appears only when the active session/profile permits shell use. Plan
68 keeps it unavailable. In Act and Operate, the active permission posture,
69 sandbox, command policy, trusted paths, repository law, and managed policy still
70 apply. Full Access removes ordinary approval prompts; it does not bypass hard
71 safety or repository-policy holds.
72
73 `File` is capability-filtered by mode. Plan advertises its read-only actions;
74 write/edit actions require Act or Operate, and `patch` also requires the
75 apply-patch feature. The same read-before-edit, workspace, and policy checks used
76 by the former spellings remain in force.
77
78 ### Coordination tools
79
80 | Tool | Purpose |
81 |---|---|
82 | `agent` | Dispatch one focused sub-agent run and return an id, compact receipt, and transcript handle. |
83 | `remember` | Append one terse durable preference or convention when the user has enabled built-in memory. |
84 | `tasks` | Create, list, read, cancel, gate, and inspect durable task work through one action family. |
85 | `update_plan` | Registered but not model-visible; replays older Plan artifacts only. New work uses `work_update` plus a normal Plan-mode response. |
86 | `work_update` | Replace the concrete To-do / Work progress projection for the active thread or durable task. |
87 | `tool_search` | Discover and load a deferred tool only when the current turn needs it. |
88
89 `work_update` writes the **sole canonical Work ledger**. `update_plan` is
90 conversational reasoning — strategy, constraints, and route notes that help a
91 reader understand the approach. It is not a second Work surface, and plan-only
92 state never becomes model-facing Work grounding.
93
94 That distinction is enforced at the request boundary (#3983): the current To-do
95 snapshot is rendered by one bounded renderer
96 (`crates/tui/src/work_grounding.rs`) and appended to each parent turn-loop and
97 sub-agent step request as a transient `<codewhale:work_state>` block. Forked
98 sub-agents and `/relay` handoffs embed the byte-identical body. An empty To-do
99 emits no block at all.
100
101 ## Deferred and dynamic tools
102
103 `Web` is a conditional, deferred action tool with `search`, `fetch`, and `wait`
104 actions. It is discoverable through `tool_search` only when the active network
105 policy and runtime backend permit it; it is not one of the nine default-active
106 names.
107
108 The durable `github`, `automation`, and `rlm` action families are also deferred
109 by default. `rlm` owns `open`, `eval`, `configure`, and `close` actions for a
110 persistent sandboxed Python session. Feature-gated native tools may be added to
111 the active or deferred catalog only when their implementation and host
112 dependencies are available.
113
114 MCP tools are dynamic. Successfully connected servers register names such as
115 `mcp_<server>_<tool>` from `~/.codewhale/mcp.json`; a failed or disabled server
116 must not be presented as an available model tool.
117
118 ## Inspect the model-client request tool payload
119
120 Run `/tools` after a model turn to inspect a bounded projection of the exact
121 tool field in the latest prepared model-client request. `/tools json` emits the
122 same evidence as bounded machine-readable JSON. Both formats open in a pager;
123 they are not copied into transcript history. `/tool-studio` remains a human-
124 command compatibility alias; it is not a model tool.
125
126 The snapshot distinguishes an absent tool field from a present empty array. It
127 reports the exact model-client tool JSON byte count and SHA-256 digest only when
128 measurement fits the one-MiB inspection bound; larger payloads stay unavailable.
129 Provider adapters may transform, sanitize, or omit those fields while building
130 a provider-specific wire body, so `/tools` marks provider delivery and the wire
131 payload unavailable. Capture and rendering are bounded: retained schemas,
132 descriptions, caller lists, catalog rows, turn IDs, and payload measurement all
133 carry explicit truncation, omission, or unavailable receipts. The snapshot stays
134 in memory only for the current session and is replaced on each prepared request.
135
136 Provider, model, approval, registry provenance, and runtime capability metadata
137 are not fields in the request tool schema. `/tools` therefore reports them as
138 unavailable instead of joining against mutable state or inferring values. Use
139 the separate route and permission receipts for those facts.
140
141 ## Modes and permission postures
142
143 Modes and permission postures are separate controls:
144
145 - **Plan** is read-only. It exposes the read-only `File` projection and other
146 safe inspection capabilities, but no shell or file mutation.
147 - **Act** is ordinary interactive execution.
148 - **Operate** uses the same direct-tool authority as Act while preferring Fleet
149 workers for independent, parallel, isolated, background, or long-running work.
150 - **Ask**, **Auto-Review**, and **Full Access** control approval behavior within
151 an action-capable mode. They never widen a Plan turn into write access.
152
153 See `docs/MODES.md` for the full mode and posture contract.
154
155 ## Removed spellings
156
157 The per-action single-purpose names below are **not registered**. They were
158 deleted, not hidden: a call to any of them fails with `tool '<name>' is not
159 registered`, because `resolve` has deliberately no fuzzy step
160 (`crates/tui/src/tools/registry.rs:313-316` — "a hallucinated name must fail,
161 never dispatch"). There is no replay path for them; a transcript that calls one
162 will not re-execute.
163
164 | Removed spelling | Use instead |
165 |---|---|
166 | `exec_shell`, `exec_shell_wait`, `exec_wait`, `exec_shell_interact`, `exec_interact`, `exec_shell_cancel` | `Bash`: `run`, `wait`, `interact`, `cancel` |
167 | `read_file`, `list_dir`, `grep_files`, `file_search`, `write_file`, `edit_file` | `File`: `read`, `list`, `search_content`, `search_name`, `write`, `edit` |
168 | `git_status`, `git_diff`, `git_log`, `git_show`, `git_blame` | `Git`: matching action |
169 | `run_tests`, `run_verifiers` | `Run`: `tests`, `verifiers` |
170 | `web_search`, `fetch_url`, `wait_for_dev_server` | `Web`: `search`, `fetch`, `wait` |
171
172 Enforced by `shell_surface_contains_only_the_canonical_bash_tool`
173 (registry.rs:2290, `"{alias} must be removed"`) and the retired-name loop at
174 registry.rs:2066-2088 (`"{retired} must stay removed"` /
175 `"{retired} must not be advertised"`).
176
177 ## Replay-only aliases
178
179 There is exactly one: `apply_patch`.
180
181 | Replay-only spelling | Canonical action |
182 |---|---|
183 | `apply_patch` | `File`: `patch` (also DeepSeek Responses' one custom tool) |
184
185 It is registered as a `FileTool::alias` (registry.rs:831) and hidden from the
186 advertised catalog — `registry.rs:2092-2093` asserts both halves: `contains`
187 is true, and no API tool carries the name.
188
189 Every other legacy spelling that used to be listed here is **removed, not
190 hidden**. Calling one hard-errors as an unknown tool; there is no replay
191 compatibility for them. Tests pin the removals:
192
193 | Removed spellings | Use instead | Pinned by |
194 |---|---|---|
195 | `task_create`, `task_list`, `task_read`, `task_cancel`, `task_gate_run` | `tasks` | `runtime_task_families_expose_only_canonical_tools`, registry.rs:2337-2371 |
196 | `pr_attempt_*` | `tasks` | same test |
197 | `github_issue_context`, `github_pr_context`, `github_comment`, `github_close_issue`, `github_close_pr` | `github` | same test |
198 | `automation_create/list/read/update/pause/resume/delete/run` | `automation` | same test |
199 | `rlm_session_objects`, `rlm_open`, `rlm_eval`, `rlm_configure`, `rlm_close` | `rlm` | `rlm_is_the_only_registered_session_surface`, registry.rs:1519-1538 |
200 | `checklist_write/add/update/list`, `todo_write/add/update/list` | `work_update` | registry.rs:1476-1490 |
201
202 This matches the "Removed spellings" section above rather than contradicting
203 it. Replay compatibility does not make an alias a supported spelling for new
204 model calls; `apply_patch` execution must stay behaviorally equivalent to
205 `File`: `patch` and must not be added back to the advertised catalog.
206
207 ## Long-running work
208
209 Use `Bash` with `action: "run"` for bounded commands. Set its background option
210 for work that may outlive a normal foreground wait, then use `wait`, `interact`,
211 or `cancel` against the returned process id. Live shell jobs are also visible in
212 `/jobs`; process-local jobs must be marked stale after restart rather than shown
213 as reattached processes.
214
215 Use `tasks` when the work itself needs a durable lifecycle, structured gates,
216 artifacts, replayable timelines, or a stable task id. Large tool results should
217 remain behind bounded handles or artifacts instead of being copied wholesale
218 into the parent transcript.
219
220 ## Parallel fan-out
221
222 The sub-agent capacity source of truth is
223 `crates/tui/src/config/subagent_limits.rs`:
224
225 - default configured concurrency: **64**;
226 - maximum configured concurrency: **128**;
227 - maximum admitted running-plus-queued work: **1024**.
228
229 These are capacity ceilings, not advice to dispatch every available slot. A
230 manager should use the smallest useful fan-out, preserve a single owner for
231 fan-in, and verify worker receipts before reporting combined completion.
232
233 RLM child-query batching is a different, cheaper cost class. Its
234 `sub_query_batch` helper accepts 1–16 one-shot children inside a live `rlm`
235 session; it is not a substitute for tool-carrying `agent` workers.
236
237 ## Human inspection: `/tools` (`/tool-studio`)
238
239 `/tools` renders a **read-only, bounded human projection** of the tool field of
240 the request that was prepared for one `(turn, step)`. It is not a second
241 registry and not an execution surface.
242
243 **The seam.** The snapshot is built in `crates/tui/src/core/engine/turn_loop.rs`
244 immediately after `MessageRequest` is constructed, from `request.tools` — the
245 same value the model client is handed. The engine resolves the surrounding
246 per-turn data once in `engine.rs` (`ToolSurfaceContext`: flattened registry
247 facts, the MCP pool's own server attribution, the engine-injected catalog names,
248 and the resolved model client's receipt) and passes it as plain data, so the
249 per-step seam never re-locks the MCP pool or holds a tool object.
250
251 **Turn and step identity.** The tool set can differ between steps of a turn, so
252 each snapshot is stamped with turn id and step and each seam emits its own. The
253 TUI keeps only the latest (`SessionState.last_tool_request_snapshot`). Before
254 the first seam there is no snapshot and `/tools` says so rather than rebuilding
255 a registry in the UI.
256
257 Two kinds of fact are kept apart:
258
259 - **Wire facts** come from the prepared request: name, description, schema,
260 `defer_loading` / `strict` / `allowed_callers` / `cache_control`, byte
261 accounting, and the catalog digest.
262 - **Surface facts** come from the `ToolSurfaceContext`: provenance
263 (`builtin` / `plugin` / `mcp` / `synthetic` / `unknown`), MCP server identity,
264 declared capabilities, declared approval requirement, and model visibility.
265
266 Contract:
267
268 - **One digest.** `active_tool_catalog_sha256`
269 (`crates/tui/src/core/engine/preview.rs`) is the single definition of the
270 active-tool-catalog hash. The request manifest publishes it as
271 `ToolSurfaceFacts::active_tool_catalog_sha256` and `/tools` reports the same
272 value for the same prepared request; neither surface keeps a hash of its own.
273 - **Nothing is guessed.** MCP server identity is shown only when the real pool
274 attributed that exact model tool name. `McpPool::mcp_model_tool_name` is the
275 single definition shared by the model catalog and the human attribution, and
276 an ambiguous name (two servers colliding on one model name) resolves to no
277 server. Synthetic provenance comes from
278 `default_synthetic_catalog_tool_names`, which is asserted against the engine's
279 own `is_synthetic_catalog_tool` predicate. A transmitted tool with no registry
280 entry reports `capabilities: unknown`, never "none".
281 - **Provider availability follows the resolved client.** It comes from
282 `Engine::tool_surface_provider_receipt`, never from "a tool registry exists".
283 With no client the receipt is `unavailable` even when the registry is full.
284 - **Unknown shrinks, it does not vanish.** `unavailable_for_this_request` always
285 contains `provider_wire_payload`: nothing on this path observes what the
286 provider adapter finally transmits. It additionally contains `provider` and
287 `model` without a resolved client, and `provenance` / `capabilities` /
288 `approval` when no surface context was captured.
289 - **Absent stays distinct from empty.** A request with no tools field is not a
290 request with an empty tools array; an unresolved field is `unknown` with a
291 reason, not a default.
292 - **Bounded.** Rendering is capped by tool count (32), name, description, schema
293 bytes, allowed-caller count, and a payload measurement bound, each with an
294 explicit truncation or omission receipt. Registered tools that this request
295 does *not* carry are reported as a bounded name list plus an exact count
296 rather than expanding the projection.
297 - **Inert.** The snapshot lives beside the transcript, never in
298 `session.messages`, so it cannot enter a model request or perturb the
299 provider's prefix cache. It never executes a tool, never reads credentials,
300 never reorders the catalog, and is never registered as a model-callable tool.
301 - **Delivery is never claimed.** The capture happens before connection setup, so
302 `delivery_status` stays `unknown`.
303
304 ## Release verification
305
306 Do not infer the public surface from handler function names. Verify the model
307 catalog and alias visibility at the exact candidate SHA:
308
309 ```bash
310 python3 scripts/measure-runtime-contract.py
311 cargo test -p codewhale-tui --bin codewhale-tui --locked shell_surface_contains_only_the_canonical_bash_tool
312 cargo test -p codewhale-tui --bin codewhale-tui --locked runtime_task_families_expose_only_canonical_tools
313 cargo test --locked -p codewhale-tui --bin codewhale-tui print_mode_tool_catalog_metrics -- --ignored --nocapture
314 ```
315
316 Check the test names against the source before trusting a green run: `cargo test`
317 exits 0 with "0 passed; N filtered out" when a filter matches nothing, so a
318 misspelled filter is indistinguishable from a pass. (Three filters printed here
319 before v0.9.4 named tests that did not exist.)
320
321 The provider-free full-policy receipt enables built-in memory and must report the
322 nine default-active names listed above. A memory-disabled receipt truthfully omits
323 `remember` and reports eight. A separate repository-wide tool count may include deferred, dynamic,
324 feature-gated, and replay-only registrations; it is not the number of tools
325 placed in the first-turn model catalog.
326
326 lines MARKDOWN