Files
DeepSeek-TUI/docs/TOOL_SURFACE.md
Hmbown f2ba5d15e4 docs: make the tool-surface, subagent, and config references match the code
docs/TOOL_SURFACE.md carried four claims the runtime's own tests contradict:

1. "The default-active policy contains exactly these ten names" listing
   `update_plan`. `DEFAULT_ACTIVE_NATIVE_TOOLS`
   (crates/tui/src/core/engine/tool_catalog.rs:44-58) has eight entries and
   `update_plan` is not among them — it appears nowhere in tool_catalog.rs. The
   policy is nine (those eight plus synthetic `tool_search`), eight with memory
   disabled. `update_plan` is registered (crates/tui/src/tools/plan.rs:401) but
   reachable only through `tool_search`; the tool table now says so.
2. "A memory-disabled or Moraine-fallback runtime". There is no Moraine
   fallback — docs/MEMORY.md:11-13 records the removal, and
   crates/tui/src/prompts.rs:2445-2449 is a test asserting MEMORY_GUIDANCE must
   not contain the word.
3. A "Replay-only aliases" table promising "saved transcripts, sessions, and
   recorded automation replay without migration" for 23 names, 16 of which are
   asserted REMOVED at crates/tui/src/tools/registry.rs:2066-2088 ("{retired}
   must stay removed") and 6 more at :2290-2304 ("{alias} must be removed").
   Split into a "Removed spellings" section (with the registry.rs:313-316 note
   that resolve has no fuzzy step, so those calls fail rather than dispatch) and
   a "Replay-only aliases" section holding only what is still registered:
   apply_patch, task_*, github_*, automation_*, rlm_*, checklist_*/todo_*.
4. A "Release verification" block whose three cargo filters name tests that do
   not exist (`rg` finds those three strings only in that doc). `cargo test`
   exits 0 with "0 passed; N filtered out" on a filter that matches nothing, so
   a release engineer following it got three green checkmarks having verified
   nothing. Replaced with the real names —
   `shell_surface_contains_only_the_canonical_bash_tool` (registry.rs:2290) and
   `runtime_task_families_expose_only_canonical_tools` (registry.rs:2333) — plus
   the receipt test, and a warning about the silent-pass failure mode.

docs/RUNTIME_SIMPLIFICATION_DESIGN.md repeats errors 1 and 3 and is designated
authoritative by docs/TOOL_LIFECYCLE.md:3-7, but carries no status marker. Given
a status banner naming both divergences and pointing at TOOL_SURFACE.md; the
"Rejected alternatives" provenance is worth keeping, so not deleted.

docs/SUBAGENTS.md:
- "a bounded queue of up to 200 running plus queued sub-agents by default" —
  `MAX_SUBAGENT_ADMISSION` is 1024 (crates/tui/src/config/subagent_limits.rs:21),
  which is what docs/TOOL_SURFACE.md:182 already said. The 64/128 concurrency
  figures on the same page were correct and are untouched.
- The memory section described a `memory.md` that does not exist and omitted the
  `scope` parameter. crates/tui/src/tools/remember.rs:165 states the legacy
  single-file path was removed in v0.9.4; writes go through
  `NativeMemoryStore::remember(scope, workspace_id, note)` (remember.rs:77-108).

config.example.toml documented two key sets that do not exist. Neither struct has
`deny_unknown_fields`, so both were silently discarded rather than rejected:
- `[advisor] max_tool_pairs` / `system_prompt`. `AdvisorConfigToml`
  (crates/config/src/lib.rs:2369-2394) has enabled, max_tool_calls (default 10,
  clamped 1-50 — the doc said 8, max 32), rate_limit_secs, dedup_window_secs,
  and model. `model` was undocumented; now it is.
- `[fleet.profiles.*.permissions] allow_tools` / `deny_tools`.
  `FleetProfilePermissions` (lib.rs:1966-1977) has allow_shell, trust,
  approval_required. `rg 'allow_tools|deny_tools' crates/` finds nothing. The
  example value was `"exec_shell"`, itself a removed tool name.

docs/CONFIGURATION.md: deleted the "Parsed but currently unused" section. Its one
entry, `tools_file`, is not parsed by anything — the field was removed in
346bfe3b6 and the doc bullet was orphaned. Repo-wide `rg` finds the string only
in that section, and nothing links a #parsed-but-currently-unused anchor.

docs/TTC_DESIGN.md said implementation "is deferred beyond v0.9.0". The `verify`
tool shipped and is default-on (crates/tui/src/tools/verify.rs,
features.rs:262, registry.rs:1040-1041 with verify_tool_enabled defaulted true).
Retitled as landed-in-part; capability (B) is still genuinely deferred, so the
doc stays. Its interface line said `with_verify(critic)`; the real signature is
`with_verify_tool(client, model)` (registry.rs:886).

docs/skills/README.md advertised `gh-plan-issues`, deleted in 18de2ebc0, and
credited these skills to "the v0.8.61 release" at a 0.9.4 release.

docs/architecture/provider-model-settings-v091.md pinned
`provider_is_configured` to config.rs:8625-8669; it is at :10160 and that region
is now unrelated code. Replaced with the symbol name, since config.rs is under
active edit.

docs/architecture/command-dispatch.md:133 claimed EPIC-002 was "ready for PR".
The PR (#3706) merged and #2870 closed 2026-08-01. Line 145 was an empty
"Current Evidence (Draft)" heading with no content; removed.

.gitignore: `git check-ignore -v` attributes .claude/settings.json,
scheduled_tasks.lock, worktrees/, and *.local.* to the blanket `.claude/` at
line 126, not to the specific rules above them. Dropped the redundant ones and
annotated why the HANDOFF_/CODEMAP_ patterns are deliberately kept.
2026-08-03 22:51:59 -07:00

16 KiB
Raw Blame History

Tool surface

This document describes the current model-facing tool contract in the v0.9.1 source candidate. 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.rs owns the eager/deferred catalog.
  • crates/tui/src/tools/registry.rs registers canonical tools and hidden aliases.
  • crates/tui/src/tools/{file_tool,git_tool,run_tool,web_tool,shell}.rs own the canonical action schemas.
  • docs/RUNTIME_SIMPLIFICATION_DESIGN.md records the v0.9.1 cutover and receipt.

Default-active contract

The default-active policy contains exactly these nine names:

  1. Bash
  2. File
  3. Git
  4. Run
  5. agent
  6. remember
  7. tasks
  8. work_update
  9. tool_search

The first eight are DEFAULT_ACTIVE_NATIVE_TOOLS in crates/tui/src/core/engine/tool_catalog.rs. tool_search is synthetic rather than registry-backed and is always active.

remember is registered only when the user enables the built-in memory path; once present, it stays eager so a model can capture a durable preference without first discovering the tool. A memory-disabled runtime omits that registration and therefore exposes eight of the nine policy names.

update_plan is not default-active. It is registered (crates/tui/src/tools/plan.rs:401) but reachable only through tool_search, so it does not appear in the first-turn catalog. Plan mode narrows the active set further: Bash and Run drop out, leaving File, Git, agent, tasks, work_update, and tool_search.

The surface is action-based. A model calls one stable tool name and selects the operation through its action field instead of choosing among many synonymous single-purpose tools.

Core action tools

Tool Actions Purpose
Bash run, wait, interact, cancel Run bounded commands, continue background work, send input, and cancel processes.
File read, list, search_name, search_content, write, edit, patch Read, find, and modify workspace files with structured, workspace-aware results.
Git status, diff, log, show, blame Inspect repository state and history without parsing shell output.
Run tests, verifiers Run project tests or independent verifier gates with structured results.

Bash appears only when the active session/profile permits shell use. Plan keeps it unavailable. In Act and Operate, the active permission posture, sandbox, command policy, trusted paths, repository law, and managed policy still apply. Full Access removes ordinary approval prompts; it does not bypass hard safety or repository-policy holds.

File is capability-filtered by mode. Plan advertises its read-only actions; write/edit actions require Act or Operate, and patch also requires the apply-patch feature. The same read-before-edit, workspace, and policy checks used by the former spellings remain in force.

Coordination tools

Tool Purpose
agent Dispatch one focused sub-agent run and return an id, compact receipt, and transcript handle.
remember Append one terse durable preference or convention when the user has enabled built-in memory.
tasks Create, list, read, cancel, gate, and inspect durable task work through one action family.
update_plan Publish optional high-level strategy, phases, constraints, verification, and handoff context. Reachable through tool_search only — see the default-active contract above.
work_update Replace the concrete To-do / Work progress projection for the active thread or durable task.
tool_search Discover and load a deferred tool only when the current turn needs it.

work_update writes the sole canonical Work ledger. update_plan is conversational reasoning — strategy, constraints, and route notes that help a reader understand the approach. It is not a second Work surface, and plan-only state never becomes model-facing Work grounding.

That distinction is enforced at the request boundary (#3983): the current To-do snapshot is rendered by one bounded renderer (crates/tui/src/work_grounding.rs) and appended to each parent turn-loop and sub-agent step request as a transient <codewhale:work_state> block. Forked sub-agents and /relay handoffs embed the byte-identical body. An empty To-do emits no block at all.

Deferred and dynamic tools

Web is a conditional, deferred action tool with search, fetch, and wait actions. It is discoverable through tool_search only when the active network policy and runtime backend permit it; it is not one of the ten default-active names.

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 an available model tool.

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 is read-only. It exposes the read-only File projection and other safe inspection capabilities, but no shell or file mutation.
  • Act is ordinary interactive execution.
  • Operate uses the same direct-tool authority as Act 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 a Plan turn into write access.

See docs/MODES.md for the full mode and posture contract.

Removed spellings

The per-action single-purpose names below are not registered. They were deleted, not hidden: a call to any of them fails with tool '<name>' is not registered, because resolve has deliberately no fuzzy step (crates/tui/src/tools/registry.rs:313-316 — "a hallucinated name must fail, never dispatch"). There is no replay path for them; a transcript that calls one will not re-execute.

Removed spelling Use instead
exec_shell, exec_shell_wait, exec_wait, exec_shell_interact, exec_interact, exec_shell_cancel Bash: run, wait, interact, cancel
read_file, list_dir, grep_files, file_search, write_file, edit_file File: read, list, search_content, search_name, write, edit
git_status, git_diff, git_log, git_show, git_blame Git: matching action
run_tests, run_verifiers Run: tests, verifiers
web_search, fetch_url, wait_for_dev_server Web: search, fetch, wait

Enforced by shell_surface_contains_only_the_canonical_bash_tool (registry.rs:2290, "{alias} must be removed") and the retired-name loop at registry.rs:2066-2088 ("{retired} must stay removed" / "{retired} must not be advertised").

Replay-only aliases

These legacy names are still registered so saved transcripts, sessions, and recorded automation replay without migration. They are hidden from the model catalog and from tool_search; new prompts and docs must use the canonical action tools.

Replay-only spellings Canonical action
apply_patch File: patch (also DeepSeek Responses' one custom tool)
task_create, task_list, task_read tasks: matching action
github_issue_context, github_pr_context, github_comment github: matching action
automation_* automation: matching action
rlm_open, rlm_eval, rlm_configure, rlm_close rlm: open, eval, configure, close
checklist_*, todo_* work_update

Replay compatibility does not make an alias a supported spelling for new model calls. Alias execution must stay behaviorally equivalent to its canonical action and must not add the alias back to the advertised catalog.

Long-running work

Use Bash with action: "run" for bounded commands. Set its background option for work that may outlive a normal foreground wait, then use wait, interact, or cancel against the returned process id. Live shell jobs are also visible in /jobs; process-local jobs must be marked stale after restart rather than shown as reattached processes.

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 116 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 as ToolSurfaceFacts::active_tool_catalog_sha256 and /tools reports 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_name is 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 from default_synthetic_catalog_tool_names, which is asserted against the engine's own is_synthetic_catalog_tool predicate. A transmitted tool with no registry entry reports capabilities: 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 is unavailable even when the registry is full.
  • Unknown shrinks, it does not vanish. unavailable_for_this_request always contains provider_wire_payload: nothing on this path observes what the provider adapter finally transmits. It additionally contains provider and model without a resolved client, and provenance / capabilities / approval when 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 unknown with 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_status stays unknown.

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 --bin codewhale-tui --locked shell_surface_contains_only_the_canonical_bash_tool
cargo test -p codewhale-tui --bin codewhale-tui --locked runtime_task_families_expose_only_canonical_tools
cargo test --locked -p codewhale-tui --bin codewhale-tui print_mode_tool_catalog_metrics -- --ignored --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 full-policy receipt enables built-in memory and must report the nine default-active names listed above. A memory-disabled receipt truthfully omits remember and reports eight. A separate repository-wide tool count may include deferred, dynamic, feature-gated, and replay-only registrations; it is not the number of tools placed in the first-turn model catalog.