Rename the lowercase bash tool struct and its bounded output accumulator, plus the file read/write helper symbols and their tests, to contract-based names. Tool names, schemas, and behavior are unchanged; doc and test references updated to match; the model-visible catalog text and budget files are untouched. Verified: cargo test -p codewhale-tui --lib (shell/registry/file/ file_tool/tool_catalog filters) 308 passed; web public-surface-contract 13/13 passed.
14 KiB
Tool surface
This document describes the current model-facing tool contract. The v0.9.1
cutover that produced it is recorded in docs/RUNTIME_SIMPLIFICATION_DESIGN.md;
read the workspace version from Cargo.toml, not from this line. The registry
remains larger than the first-turn catalog so
saved transcripts can replay and uncommon capabilities can be loaded on demand.
The model should learn one canonical name for each common operation.
Implementation sources:
crates/tui/src/core/engine/tool_catalog.rsowns the eager/deferred catalog.crates/tui/src/tools/registry.rsregisters canonical tools and hidden aliases.crates/tui/src/tools/{file,file_tool,shell}.rsown the small foreground primitive behavior and schemas; the other native tools remain searchable.docs/RUNTIME_SIMPLIFICATION_DESIGN.mdrecords the v0.9.1 cutover and receipt.
Default-active contract
New turns start with exactly seven model-facing names:
readwriteeditbashagenttodo_writetool_search
The first six are DEFAULT_ACTIVE_NATIVE_TOOLS in
crates/tui/src/core/engine/tool_catalog.rs. tool_search is synthetic and is
always active. An authority boundary may remove agent at the maximum child
depth, but route size alone must not change this core vocabulary.
The direct schemas deliberately stay small:
| Tool | Input | Purpose |
|---|---|---|
read |
path, optional offset, optional limit |
Read a bounded file window with explicit continuation or truncation notices. |
write |
path, content |
Create or replace a file. |
edit |
path, edits |
Apply one or more unambiguous text replacements against one original snapshot. |
bash |
command, optional timeout |
Run one cancellable foreground shell command and return a bounded tail. |
agent |
delegated task and optional scope/context controls | Start or inspect focused child work. |
todo_write |
complete replacement list of {content, status} items |
Keep optional, agent-owned progress notes for genuinely multi-step work. |
tool_search |
query, optional matching controls |
Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |
Mode is an authority decision, not a synonym system. Plan, Work, and Operate
use the same primitive identities. Plan centrally refuses write, edit, and
bash; Work and Operate still pass those calls through approval, sandbox,
trusted-path, repository-law, and managed-policy gates. Full Access changes
ordinary approval behavior but does not bypass hard safety or repository law.
update_plan remains registered only for saved-artifact compatibility and is
not model-visible. tasks, Git, Run, Web, remember, and other
specialized capabilities are searchable rather than first-turn ceremony.
Deferred and dynamic tools
Web is conditional and deferred. It is discoverable through tool_search
only when the active policy and runtime backend permit it. Read-only
children retain its read-only search/fetch evidence path; read-only authority
does not mean "unable to research."
The durable github, automation, and rlm action families are also deferred
by default. rlm owns open, eval, configure, and close actions for a
persistent sandboxed Python session. Feature-gated native tools may be added to
the active or deferred catalog only when their implementation and host
dependencies are available.
MCP tools are dynamic. Successfully connected servers register names such as
mcp_<server>_<tool> from ~/.codewhale/mcp.json; a failed or disabled server
must not be presented as available. MCP and plugin tools are deferred unless a
user explicitly names them in [tools].always_load.
Conversation toolbox cache
A successful search activation is remembered by name for the current conversation. The cache holds at most eight deferred names and 16 KiB of serialized schemas, evicts least-recently-used entries, and revalidates every entry against the current catalog and policy before advertising it again. A session sync clears it. The cache cannot resurrect a removed, denied, or newly-eager tool.
Each subagent gets its own policy-filtered deferred catalog, always-present
tool_search, and bounded activation cache. Forked messages and instructions
remain in context, but the child cache starts empty and discovers tools locally;
neither forked context nor a cache can become a discovery allowlist. A child can
still search every tool its own authority permits, including Web search/fetch
for read-only research roles.
Inspect the model-client request tool payload
Run /tools after a model turn to inspect a bounded projection of the exact
tool field in the latest prepared model-client request. /tools json emits the
same evidence as bounded machine-readable JSON. Both formats open in a pager;
they are not copied into transcript history. /tool-studio remains a human-
command compatibility alias; it is not a model tool.
The snapshot distinguishes an absent tool field from a present empty array. It
reports the exact model-client tool JSON byte count and SHA-256 digest only when
measurement fits the one-MiB inspection bound; larger payloads stay unavailable.
Provider adapters may transform, sanitize, or omit those fields while building
a provider-specific wire body, so /tools marks provider delivery and the wire
payload unavailable. Capture and rendering are bounded: retained schemas,
descriptions, caller lists, catalog rows, turn IDs, and payload measurement all
carry explicit truncation, omission, or unavailable receipts. The snapshot stays
in memory only for the current session and is replaced on each prepared request.
Provider, model, approval, registry provenance, and runtime capability metadata
are not fields in the request tool schema. /tools therefore reports them as
unavailable instead of joining against mutable state or inferring values. Use
the separate route and permission receipts for those facts.
Modes and permission postures
Modes and permission postures are separate controls:
- Plan keeps the stable primitive vocabulary but centrally refuses shell execution and file mutation.
- Work is ordinary interactive execution.
- Operate uses the same direct-tool authority as Work while preferring Fleet workers for independent, parallel, isolated, background, or long-running work.
- Ask, Auto-Review, and Full Access control approval behavior within an action-capable mode. They never widen Plan into write or shell access.
See docs/MODES.md for the full mode and posture contract.
Compatibility names
The model-facing contract is the lowercase core above. Saved v0.9.x
transcripts and protocol clients may still call exact hidden compatibility
names such as File, Bash, and the older single-operation file names. Those
names never enter a new model catalog or tool_search result.
Compatibility is execution compatibility, not fuzzy aliasing: an exact legacy call must reach the handler for its legacy schema. It must not be rewritten into a small lowercase primitive whose input shape is different. Unknown or retired names still fail closed instead of guessing a destination.
Specialized native families such as Git, Run, and Web are not aliases for
the lowercase core. They remain real, policy-filtered deferred tools and are
loaded through tool_search when needed.
Long-running work
bash runs one cancellable foreground command. It does not carry background,
TTY, wait, interact, or cancel action fields. Stateful process and terminal
control is specialized functionality that must be discovered explicitly; it
does not enlarge the first-turn shell schema.
Use tasks when the work itself needs a durable lifecycle, structured gates,
artifacts, replayable timelines, or a stable task id. Large tool results should
remain behind bounded handles or artifacts instead of being copied wholesale
into the parent transcript.
Parallel fan-out
The sub-agent capacity source of truth is
crates/tui/src/config/subagent_limits.rs:
- default configured concurrency: 64;
- maximum configured concurrency: 128;
- maximum admitted running-plus-queued work: 1024.
These are capacity ceilings, not advice to dispatch every available slot. A manager should use the smallest useful fan-out, preserve a single owner for fan-in, and verify worker receipts before reporting combined completion.
RLM child-query batching is a different, cheaper cost class. Its
sub_query_batch helper accepts 1–16 one-shot children inside a live rlm
session; it is not a substitute for tool-carrying agent workers.
Human inspection: /tools (/tool-studio)
/tools renders a read-only, bounded human projection of the tool field of
the request that was prepared for one (turn, step). It is not a second
registry and not an execution surface.
The seam. The snapshot is built in crates/tui/src/core/engine/turn_loop.rs
immediately after MessageRequest is constructed, from request.tools — the
same value the model client is handed. The engine resolves the surrounding
per-turn data once in engine.rs (ToolSurfaceContext: flattened registry
facts, the MCP pool's own server attribution, the engine-injected catalog names,
and the resolved model client's receipt) and passes it as plain data, so the
per-step seam never re-locks the MCP pool or holds a tool object.
Turn and step identity. The tool set can differ between steps of a turn, so
each snapshot is stamped with turn id and step and each seam emits its own. The
TUI keeps only the latest (SessionState.last_tool_request_snapshot). Before
the first seam there is no snapshot and /tools says so rather than rebuilding
a registry in the UI.
Two kinds of fact are kept apart:
- Wire facts come from the prepared request: name, description, schema,
defer_loading/strict/allowed_callers/cache_control, byte accounting, and the catalog digest. - Surface facts come from the
ToolSurfaceContext: provenance (builtin/plugin/mcp/synthetic/unknown), MCP server identity, declared capabilities, declared approval requirement, and model visibility.
Contract:
- One digest.
active_tool_catalog_sha256(crates/tui/src/core/engine/preview.rs) is the single definition of the active-tool-catalog hash. The request manifest publishes it asToolSurfaceFacts::active_tool_catalog_sha256and/toolsreports the same value for the same prepared request; neither surface keeps a hash of its own. - Nothing is guessed. MCP server identity is shown only when the real pool
attributed that exact model tool name.
McpPool::mcp_model_tool_nameis the single definition shared by the model catalog and the human attribution, and an ambiguous name (two servers colliding on one model name) resolves to no server. Synthetic provenance comes fromdefault_synthetic_catalog_tool_names, which is asserted against the engine's ownis_synthetic_catalog_toolpredicate. A transmitted tool with no registry entry reportscapabilities: unknown, never "none". - Provider availability follows the resolved client. It comes from
Engine::tool_surface_provider_receipt, never from "a tool registry exists". With no client the receipt isunavailableeven when the registry is full. - Unknown shrinks, it does not vanish.
unavailable_for_this_requestalways containsprovider_wire_payload: nothing on this path observes what the provider adapter finally transmits. It additionally containsproviderandmodelwithout a resolved client, andprovenance/capabilities/approvalwhen no surface context was captured. - Absent stays distinct from empty. A request with no tools field is not a
request with an empty tools array; an unresolved field is
unknownwith a reason, not a default. - Bounded. Rendering is capped by tool count (32), name, description, schema bytes, allowed-caller count, and a payload measurement bound, each with an explicit truncation or omission receipt. Registered tools that this request does not carry are reported as a bounded name list plus an exact count rather than expanding the projection.
- Inert. The snapshot lives beside the transcript, never in
session.messages, so it cannot enter a model request or perturb the provider's prefix cache. It never executes a tool, never reads credentials, never reorders the catalog, and is never registered as a model-callable tool. - Delivery is never claimed. The capture happens before connection setup, so
delivery_statusstaysunknown.
Release verification
Do not infer the public surface from handler function names. Verify the model catalog and alias visibility at the exact candidate SHA:
python3 scripts/measure-runtime-contract.py
cargo test -p codewhale-tui --lib --locked core::engine::tests::default_active_contract_keeps_discovery_and_core_tools_eager -- --exact
cargo test -p codewhale-tui --lib --locked tools::file_tool::tests::primitive_schemas_are_separate_and_small_contract_shaped -- --exact
cargo test -p codewhale-tui --lib --locked tools::shell::tests::lowercase_bash_schema_is_small_contract -- --exact
cargo test --locked -p codewhale-tui --lib core::engine::tests::print_mode_tool_catalog_metrics -- --ignored --exact --nocapture
Check the test names against the source before trusting a green run: cargo test
exits 0 with "0 passed; N filtered out" when a filter matches nothing, so a
misspelled filter is indistinguishable from a pass. (Three filters printed here
before v0.9.4 named tests that did not exist.)
The provider-free receipt must report the seven default-active names listed above. A separate repository-wide tool count may include deferred, dynamic, feature-gated, and compatibility-only registrations; it is not the number of tools placed in the first-turn model catalog.