返回 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,file_tool,shell}.rs` own the small foreground
15 primitive behavior and schemas; the other native tools remain searchable.
16 - `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.
17
18 ## Default-active contract
19
20 New turns start with ten eager native names plus synthetic `tool_search`:
21
22 1. `read`
23 2. `write`
24 3. `edit`
25 4. `bash`
26 5. `agent`
27 6. `workflow`
28 7. `todo_write`
29 8. `create_goal`
30 9. `get_goal`
31 10. `update_goal`
32 11. `tool_search` (synthetic, always active)
33
34 The ten native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in
35 `crates/tui/src/core/engine/tool_catalog.rs`, pinned by
36 `default_active_contract_keeps_discovery_and_core_tools_eager`. An authority
37 boundary may remove `agent` at the maximum child depth, but route size alone
38 must not change this core vocabulary.
39
40 The direct schemas deliberately stay small:
41
42 | Tool | Input | Purpose |
43 |---|---|---|
44 | `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |
45 | `write` | `path`, `content` | Create or replace a file. |
46 | `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |
47 | `bash` | `command`, optional `timeout` | Run one cancellable foreground shell command and return a bounded tail. |
48 | `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |
49 | `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |
50 | `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |
51 | `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |
52 | `get_goal` | none | Read the active goal and its progress. |
53 | `update_goal` | terminal status | Mark the goal complete or blocked. |
54 | `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |
55
56 Mode is an authority decision, not a synonym system. Plan, Work, and Operate
57 use the same primitive identities. Plan centrally refuses `write`, `edit`, and
58 `bash`; Work and Operate still pass those calls through approval, sandbox,
59 trusted-path, repository-law, and managed-policy gates. Full Access changes
60 ordinary approval behavior but does not bypass hard safety or repository law.
61
62 `update_plan` remains registered only for saved-artifact compatibility and is
63 not model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other
64 specialized capabilities are searchable rather than first-turn ceremony.
65
66 ## Deferred and dynamic tools
67
68 `Web` is conditional and deferred. It is discoverable through `tool_search`
69 only when the active policy and runtime backend permit it. Read-only
70 children retain its read-only search/fetch evidence path; read-only authority
71 does not mean "unable to research."
72
73 The durable `github`, `automation`, and `rlm` action families are also deferred
74 by default. `rlm` owns `open`, `eval`, `configure`, and `close` actions for a
75 persistent sandboxed Python session. Feature-gated native tools may be added to
76 the active or deferred catalog only when their implementation and host
77 dependencies are available.
78
79 MCP tools are dynamic. Successfully connected servers register names such as
80 `mcp_<server>_<tool>` from `~/.codewhale/mcp.json`; a failed or disabled server
81 must not be presented as available. MCP and plugin tools are deferred unless a
82 user explicitly names them in `[tools].always_load`.
83
84 `execute_tools` is deferred and engine-injected, alongside the synthetic
85 interpreter tools. It runs a JavaScript program whose only host surface is
86 `tools.call(name, args)`; nested calls must be read-only and auto-approved,
87 and anything else aborts the program with a host-owned receipt. It is hidden
88 from Plan mode and refused under a worker authority envelope.
89
90 ### Conversation toolbox cache
91
92 A successful search activation is remembered by name for the current
93 conversation. The cache holds at most eight deferred names and 16 KiB of
94 serialized schemas, evicts least-recently-used entries, and revalidates every
95 entry against the current catalog and policy before advertising it again. A
96 session sync clears it. The cache cannot resurrect a removed, denied, or
97 newly-eager tool.
98
99 Each subagent gets its own policy-filtered deferred catalog, always-present
100 `tool_search`, and bounded activation cache. Forked messages and instructions
101 remain in context, but the child cache starts empty and discovers tools locally;
102 neither forked context nor a cache can become a discovery allowlist. A child can
103 still search every tool its own authority permits, including Web search/fetch
104 for read-only research roles.
105
106 ## Inspect the model-client request tool payload
107
108 Run `/tools` after a model turn to inspect a bounded projection of the exact
109 tool field in the latest prepared model-client request. `/tools json` emits the
110 same evidence as bounded machine-readable JSON. Both formats open in a pager;
111 they are not copied into transcript history. `/tool-studio` remains a human-
112 command compatibility alias; it is not a model tool.
113
114 The snapshot distinguishes an absent tool field from a present empty array. It
115 reports the exact model-client tool JSON byte count and SHA-256 digest only when
116 measurement fits the one-MiB inspection bound; larger payloads stay unavailable.
117 Provider adapters may transform, sanitize, or omit those fields while building
118 a provider-specific wire body, so `/tools` marks provider delivery and the wire
119 payload unavailable. Capture and rendering are bounded: retained schemas,
120 descriptions, caller lists, catalog rows, turn IDs, and payload measurement all
121 carry explicit truncation, omission, or unavailable receipts. The snapshot stays
122 in memory only for the current session and is replaced on each prepared request.
123
124 Provider, model, approval, registry provenance, and runtime capability metadata
125 are not fields in the request tool schema. `/tools` therefore reports them as
126 unavailable instead of joining against mutable state or inferring values. Use
127 the separate route and permission receipts for those facts.
128
129 ## Modes and permission postures
130
131 Modes and permission postures are separate controls:
132
133 - **Plan** keeps the stable primitive vocabulary but centrally refuses shell
134 execution and file mutation.
135 - **Work** is ordinary interactive execution.
136 - **Operate** uses the same direct-tool authority as Work. Small work stays
137 direct; multi-step delegation uses a compact Workflow plan with dependencies,
138 bounded scopes, and completion evidence. Fleet manages the same sub-agents
139 and roles. One bounded, independent task can use a direct agent; `followup`
140 reuses that agent for continued work.
141 - **Ask**, **Auto-Review**, and **Full Access** control approval behavior within
142 an action-capable mode. They never widen Plan into write or shell access.
143
144 See `docs/MODES.md` for the full mode and posture contract.
145
146 ## Compatibility names
147
148 The model-facing contract is the lowercase core above. Saved v0.9.x
149 transcripts and protocol clients may still call exact hidden compatibility
150 names such as `File`, `Bash`, and the older single-operation file names. Those
151 names never enter a new model catalog or `tool_search` result.
152
153 Compatibility is execution compatibility, not fuzzy aliasing: an exact legacy
154 call must reach the handler for its legacy schema. It must not be rewritten
155 into a small lowercase primitive whose input shape is different. Unknown or
156 retired names still fail closed instead of guessing a destination.
157
158 Specialized native families such as `Git`, `Run`, and `Web` are not aliases for
159 the lowercase core. They remain real, policy-filtered deferred tools and are
160 loaded through `tool_search` when needed.
161
162 ## Long-running work
163
164 `bash` runs one cancellable foreground command. It does not carry background,
165 TTY, wait, interact, or cancel action fields. Stateful process and terminal
166 control is specialized functionality that must be discovered explicitly; it
167 does not enlarge the first-turn shell schema.
168
169 Use `tasks` when the work itself needs a durable lifecycle, structured gates,
170 artifacts, replayable timelines, or a stable task id. Large tool results should
171 remain behind bounded handles or artifacts instead of being copied wholesale
172 into the parent transcript.
173
174 ## Parallel fan-out
175
176 The sub-agent capacity source of truth is
177 `crates/tui/src/config/subagent_limits.rs`:
178
179 - default configured concurrency: **64**;
180 - maximum configured concurrency: **128**;
181 - maximum admitted running-plus-queued work: **1024**.
182
183 These are capacity ceilings, not advice to dispatch every available slot. A
184 manager should use the smallest useful fan-out, preserve a single owner for
185 fan-in, and verify worker receipts before reporting combined completion.
186
187 RLM child-query batching is a different, cheaper cost class. Its
188 `sub_query_batch` helper accepts 1–16 one-shot children inside a live `rlm`
189 session; it is not a substitute for tool-carrying `agent` workers.
190
191 ## Human inspection: `/tools` (`/tool-studio`)
192
193 `/tools` renders a **read-only, bounded human projection** of the tool field of
194 the request that was prepared for one `(turn, step)`. It is not a second
195 registry and not an execution surface.
196
197 **The seam.** The snapshot is built in `crates/tui/src/core/engine/turn_loop.rs`
198 immediately after `MessageRequest` is constructed, from `request.tools` — the
199 same value the model client is handed. The engine resolves the surrounding
200 per-turn data once in `engine.rs` (`ToolSurfaceContext`: flattened registry
201 facts, the MCP pool's own server attribution, the engine-injected catalog names,
202 and the resolved model client's receipt) and passes it as plain data, so the
203 per-step seam never re-locks the MCP pool or holds a tool object.
204
205 **Turn and step identity.** The tool set can differ between steps of a turn, so
206 each snapshot is stamped with turn id and step and each seam emits its own. The
207 TUI keeps only the latest (`SessionState.last_tool_request_snapshot`). Before
208 the first seam there is no snapshot and `/tools` says so rather than rebuilding
209 a registry in the UI.
210
211 Two kinds of fact are kept apart:
212
213 - **Wire facts** come from the prepared request: name, description, schema,
214 `defer_loading` / `strict` / `allowed_callers` / `cache_control`, byte
215 accounting, and the catalog digest.
216 - **Surface facts** come from the `ToolSurfaceContext`: provenance
217 (`builtin` / `plugin` / `mcp` / `synthetic` / `unknown`), MCP server identity,
218 declared capabilities, declared approval requirement, and model visibility.
219
220 Contract:
221
222 - **One digest.** `active_tool_catalog_sha256`
223 (`crates/tui/src/core/engine/preview.rs`) is the single definition of the
224 active-tool-catalog hash. The request manifest publishes it as
225 `ToolSurfaceFacts::active_tool_catalog_sha256` and `/tools` reports the same
226 value for the same prepared request; neither surface keeps a hash of its own.
227 - **Nothing is guessed.** MCP server identity is shown only when the real pool
228 attributed that exact model tool name. `McpPool::mcp_model_tool_name` is the
229 single definition shared by the model catalog and the human attribution, and
230 an ambiguous name (two servers colliding on one model name) resolves to no
231 server. Synthetic provenance comes from
232 `default_synthetic_catalog_tool_names`, which is asserted against the engine's
233 own `is_synthetic_catalog_tool` predicate. A transmitted tool with no registry
234 entry reports `capabilities: unknown`, never "none".
235 - **Provider availability follows the resolved client.** It comes from
236 `Engine::tool_surface_provider_receipt`, never from "a tool registry exists".
237 With no client the receipt is `unavailable` even when the registry is full.
238 - **Unknown shrinks, it does not vanish.** `unavailable_for_this_request` always
239 contains `provider_wire_payload`: nothing on this path observes what the
240 provider adapter finally transmits. It additionally contains `provider` and
241 `model` without a resolved client, and `provenance` / `capabilities` /
242 `approval` when no surface context was captured.
243 - **Absent stays distinct from empty.** A request with no tools field is not a
244 request with an empty tools array; an unresolved field is `unknown` with a
245 reason, not a default.
246 - **Bounded.** Rendering is capped by tool count (32), name, description, schema
247 bytes, allowed-caller count, and a payload measurement bound, each with an
248 explicit truncation or omission receipt. Registered tools that this request
249 does *not* carry are reported as a bounded name list plus an exact count
250 rather than expanding the projection.
251 - **Inert.** The snapshot lives beside the transcript, never in
252 `session.messages`, so it cannot enter a model request or perturb the
253 provider's prefix cache. It never executes a tool, never reads credentials,
254 never reorders the catalog, and is never registered as a model-callable tool.
255 - **Delivery is never claimed.** The capture happens before connection setup, so
256 `delivery_status` stays `unknown`.
257
258 ## Release verification
259
260 Do not infer the public surface from handler function names. Verify the model
261 catalog and alias visibility at the exact candidate SHA:
262
263 ```bash
264 python3 scripts/measure-runtime-contract.py
265 cargo test -p codewhale-tui --lib --locked core::engine::tests::default_active_contract_keeps_discovery_and_core_tools_eager -- --exact
266 cargo test -p codewhale-tui --lib --locked tools::file_tool::tests::primitive_schemas_are_separate_and_small_contract_shaped -- --exact
267 cargo test -p codewhale-tui --lib --locked tools::shell::tests::lowercase_bash_schema_is_small_contract -- --exact
268 cargo test --locked -p codewhale-tui --lib core::engine::tests::print_mode_tool_catalog_metrics -- --ignored --exact --nocapture
269 ```
270
271 Check the test names against the source before trusting a green run: `cargo test`
272 exits 0 with "0 passed; N filtered out" when a filter matches nothing, so a
273 misspelled filter is indistinguishable from a pass. (Three filters printed here
274 before v0.9.4 named tests that did not exist.)
275
276 The provider-free receipt must report the eleven default-active names listed
277 above. A separate repository-wide tool count may include deferred, dynamic,
278 feature-gated, and compatibility-only registrations; it is not the number of
279 tools placed in the first-turn model catalog.
280
280 lines MARKDOWN