Files
wehub-resource-sync d68f003000
CI / Change detection (push) Has been cancelled
CI / Version drift (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Workflow RLM cache (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / npm wrapper smoke (push) Has been cancelled
CI / Mobile runtime smoke (push) Has been cancelled
CI / Workflow lint (push) Has been cancelled
CI / Documentation (push) Has been cancelled
Web Frontend / Lint & Type Check (push) Failing after 1s
Auto-close harvested PRs / close (push) Failing after 1s
cargo-deny / cargo-deny (bans licenses sources) (push) Failing after 1s
Security audit / cargo-audit (push) Failing after 1s
Sync to CNB / sync (push) Failing after 1s
cargo-deny / cargo-deny (advisories) (push) Failing after 3s
Web Frontend / Deploy to Cloudflare (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:08:23 +08:00

380 lines
19 KiB
Markdown

# Tool-Surface Lifecycle Policy (v0.8.53)
**Status:** Design doc / policy. No catalog code lands in this cycle — the code
work is **deferred**. This document is the umbrella policy for GitHub **#2681**,
with **#2682** and **#2683** as concrete instances of the planned diet. It
describes *what will be done* and the invariants any future diet PR must hold.
**Scope of related open work (do not contradict):**
- PR **#2684** — subagent role vocabulary, lifecycle signals, eval ergonomics.
Legacy subagent-name cleanup + guardrail tests in this policy rebase on #2684.
- PR **#2685** — git-history active + RLM/field errors.
All file:line citations are against the verified tree at the current CodeWhale
checkout as of v0.8.52/0.8.53.
---
## 1. Purpose and the weaker-model problem
CodeWhale ships a large native tool surface. The first-turn *active* partition
of that surface is what every model sees before it has run a single
`tool_search_*` call. Today that active set contains several **near-duplicate
tools** that map to the *same* implementation under different names:
- `exec_wait` and `exec_shell_wait` are both `ShellWaitTool`
(`crates/tui/src/tools/registry.rs:526,529`).
- `exec_interact` and `exec_shell_interact` are both `ShellInteractTool`
(`registry.rs:527,530`).
- `tts` and `speech` are both `SpeechTool`
(`registry.rs:787-792`, both deferred).
- `work_update`, `checklist_*`, and `todo_*` are the *same*
`TodoWriteTool` surface, with only `work_update` visible to models.
For a strong model, redundant names are harmless noise. For **weaker / smaller
models** (the Arcee Trinity lane, `deepseek-v4-flash` child executors, and any
non-thinking executor), every additional near-duplicate in the visible set is a
real cost:
- It widens the choice space with options that do *nothing distinct*, increasing
wrong-tool selection and oscillation between synonyms.
- It spends scarce first-turn catalog budget (Section 5) on zero-information
entries.
- It dilutes the "one name = one thing" contract that lets a small model reason
about the surface at all.
The lifecycle policy exists to **shrink and discipline the model-visible
surface** without ever breaking the ability to replay an old transcript that
referenced a now-retired name.
### Canonical work-tracking surface for v0.8.68
The model-visible progress surface is a single tool: `work_update` (#4132).
Agents and Fleet workers use it for concrete To-do / Work progress under the
active runtime thread or durable task.
`task_*` and the Fleet/Workflow ledger remain the durable lifecycle owners.
Checklist metadata is the model-visible projection of progress:
`task_updates.checklist` carries the current items, completion percentage, and
in-progress item. `update_plan` is optional Strategy metadata/context/route for
complex initiatives; it must not duplicate To-do items or become a parallel
progress store.
The legacy `checklist_*` and older `todo_*` names are hidden compatibility
aliases. They remain registered and dispatchable against the same To-do state
so old transcripts replay without data loss, but they are not advertised to the
model catalog.
---
## 2. The five lifecycle states
Every native tool name occupies exactly one lifecycle state.
| State | Meaning | Visible on first turn? | In `tool_search_*`? | Executes if called? | When used |
|---|---|---|---|---|---|
| **active** | Canonical, in the first-turn catalog head | **Yes** | n/a (already active) | Yes | The tool a model should reach for by default |
| **deferred** | Registered + discoverable, hydrated on demand | No | **Yes** | Yes | Real, useful tools that don't earn a first-turn slot |
| **hidden-compatibility** | Registered + dispatchable, but removed from active **and** from search | No | **No** | **Yes — identical behavior, silent** | Old synonym kept only so old transcripts replay; no model should newly discover it |
| **deprecated** | Like hidden-compat, but execution **appends a replacement notice to result metadata** | No | **No** | **Yes — works, plus a "use X instead" notice** | A retired name we actively steer callers off of, still safe to replay |
| **removed** | Not registered at all | No | No | **No — hard error** | Only after `planned_removal_version`, once replay support is formally dropped |
### hidden-compatibility vs deprecated — be precise
Both states are **invisible** (not active, not in tool search) and both remain
**dispatchable** (calling them still works). The *only* difference is the
caller-facing signal:
- **hidden-compatibility:** completely silent. The tool behaves byte-for-byte
like its canonical twin. We use this when there is *no behavioral or naming
lesson to teach* — the name was a pure alias and we simply don't want models
re-learning it. (Example: `exec_wait` is literally `exec_shell_wait`.)
- **deprecated:** behaves identically *and succeeds*, but the tool result's
**metadata** carries an appended notice like
`"deprecated: use <replacement> instead"`. The notice goes **only in the
result metadata returned for that call** — never in the cached tool catalog
prefix (see Section 8). We use this when there is a canonical replacement we
want the caller (and any human reading the transcript) nudged toward.
Neither state ever changes the *behavior* of the call. Replay always works.
---
## 3. Representation in code
The lifecycle is represented as **const name-sets plus an alias/manifest table**
in `crates/tui/src/core/engine/tool_catalog.rs`, alongside the existing
`DEFAULT_ACTIVE_NATIVE_TOOLS` (`tool_catalog.rs:37-64`) and
`ARCEE_FIRST_TURN_NATIVE_TOOLS` (`tool_catalog.rs:106-115`).
### 3a. Name-sets and the manifest (sketch)
```rust
// crates/tui/src/core/engine/tool_catalog.rs (planned)
/// Tools removed from the active set AND from tool-search, but still
/// registered and dispatchable with byte-identical behavior. Silent.
pub(super) const HIDDEN_COMPATIBILITY_TOOLS: &[&str] = &[
"exec_wait", // == exec_shell_wait (ShellWaitTool)
"exec_interact", // == exec_shell_interact (ShellInteractTool)
"tts", // == speech (SpeechTool)
"checklist_write", // == work_update (TodoWriteTool)
"checklist_add", // == work_update single-item add
"checklist_update", // == work_update single-item update
"checklist_list", // == work_update list
"todo_write", // == work_update
"todo_add", // == work_update single-item add
"todo_update", // == work_update single-item update
"todo_list", // == work_update list
];
/// Deprecated aliases: invisible + dispatchable, with a replacement notice
/// appended to RESULT METADATA only (never the cached prefix).
pub(super) struct DeprecatedAlias {
pub name: &'static str,
pub replacement: &'static str,
pub note: &'static str,
}
pub(super) const DEPRECATED_ALIASES: &[DeprecatedAlias] = &[
// Empty in the #4132 work-surface cutover: checklist_* and todo_* are
// silent hidden-compatibility aliases of work_update for transcript replay.
];
#[inline]
pub(super) fn is_hidden_or_deprecated(name: &str) -> bool {
HIDDEN_COMPATIBILITY_TOOLS.contains(&name)
|| DEPRECATED_ALIASES.iter().any(|d| d.name == name)
}
```
### 3b. The two filter points
1. **Catalog / tool-search exclusion (tool_catalog.rs).**
Deferral is decided by `should_default_defer_tool` (`tool_catalog.rs:66-82`),
and the active set is the head built by `build_model_tool_catalog`
(`tool_catalog.rs:178-196`). Hidden-compat and deprecated tools must be
forced *out of the active head* and *out of the tool-search-discoverable
pool*. Concretely, the deferral predicate gains a short-circuit so these
names are never active, and the tool-search index builder skips any name for
which `is_hidden_or_deprecated(name)` is true. Arcee's narrowed first-turn
path (`apply_provider_tool_policy`, `tool_catalog.rs:134-149`) already
excludes them by construction since they aren't in
`ARCEE_FIRST_TURN_NATIVE_TOOLS`.
2. **Result-notice append (tool_routing.rs).**
Dispatch already routes by tool name in
`crates/tui/src/tui/tool_routing.rs` (e.g. the wait/interact unification at
`tool_routing.rs:1139-1140`). After a successful dispatch, if the called name
is in `DEPRECATED_ALIASES`, the router appends the matching `note` to the
**result metadata only**. Hidden-compat names append nothing.
### 3c. Why name-sets, not a per-`ToolSpec` enum field
A per-`ToolSpec` `lifecycle: Lifecycle` field was rejected for three reasons:
- **Prefix-cache safety.** The tool catalog array is part of DeepSeek's
immutable KV prefix (`tool_catalog.rs:169-177`). A per-spec field invites
serializing lifecycle state *into* each tool's schema, which is exactly the
kind of head mutation that forces a full re-prefill. Name-sets live entirely
in the catalog-build logic and never touch the emitted tool JSON.
- **Single source of truth + diffability.** The diet for a release is one small,
reviewable edit to two or three const arrays in one file, instead of scattered
field flips across many tool modules.
- **Registration stays orthogonal.** Tools remain registered exactly as today
(e.g. `with_shell_tools`, `registry.rs:523-531`). Lifecycle is a *catalog
policy* layered on top of registration, not a property baked into the tool.
---
## 4. Deprecation manifest (the #2681 acceptance-criteria table)
This is the authoritative manifest. Columns are the #2681 AC columns. No entry
is "removed" in 0.8.53; replay is supported for everything listed.
| Alias | Replacement (canonical) | Lifecycle state | first_deprecated_version | planned_removal_version | replay_supported |
|---|---|---|---|---|---|
| `exec_wait` | `exec_shell_wait` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
| `exec_interact` | `exec_shell_interact` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
| `tts` | `speech` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
| `checklist_write` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `checklist_add` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `checklist_update` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `checklist_list` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `todo_write` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `todo_add` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `todo_update` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
| `todo_list` | `work_update` | hidden-compatibility | 0.8.68 | TBD (≥ 0.9.x) | Yes |
**Legacy subagent names — removed, no manifest entry needed.**
The model-visible subagent surface is only `agent`. The old lifecycle names and
the experimental tool-agent lane were removed rather than kept as hidden
compatibility tools.
`planned_removal_version` is intentionally `TBD`: a name only moves to **removed**
once we formally drop replay for transcripts old enough to contain it, which is a
separate, deliberate decision per name.
---
## 5. Active-catalog budget (per mode, per provider)
The active set is the first-turn cost. Do not duplicate the exact
`DEFAULT_ACTIVE_NATIVE_TOOLS` count here: adjacent PRs in the v0.8.53 batch may
add or remove active tools, and the source of truth is always
`tool_catalog.rs`. This document defines the diet policy and invariants, not a
second catalog snapshot.
### Per provider
| Provider | First-turn active source | Budget policy |
|---|---|---|
| Default (DeepSeek et al.) | `DEFAULT_ACTIVE_NATIVE_TOOLS` | Remove duplicate aliases from the active head when their canonical twins stay active; any net growth needs an explicit budget decision. |
| Arcee (Trinity) | `ARCEE_FIRST_TURN_NATIVE_TOOLS` | Provider-specific read-only WAF workaround; unchanged by the default diet unless explicitly reviewed. |
The default diet removes `exec_wait` and `exec_interact` from the active head
(they become hidden-compat; their canonical twins `exec_shell_wait` /
`exec_shell_interact` stay). `tts` and `todo_*` are *already not* in the active
set, so they do not change the active budget in this diet. The net effect of
this specific diet is to remove two duplicate active aliases from whatever
default active head is current after the surrounding v0.8.53 PR batch.
### Per mode (Plan / Agent / YOLO)
The native active head is the **same set across modes** by design — mode does not
add or remove native tools from `DEFAULT_ACTIVE_NATIVE_TOOLS`
(`should_default_defer_tool` ignores `_mode` for native tools,
`tool_catalog.rs:66-68`). Mode affects **MCP** deferral instead:
`apply_mcp_tool_deferral` keeps MCP tools deferred unless `mode == Yolo`
(`tool_catalog.rs:162-167`).
| Mode | Native active budget | MCP tools active? |
|---|---|---|
| Plan | same native head | No (deferred) |
| Agent | same native head | No (deferred) |
| YOLO | same native head | Yes (a known, intentional widening) |
**Budget rule:** the native active head must stay byte-identical across Plan ↔
Agent ↔ YOLO (Section 8). Any growth of the head requires retiring something
else or an explicit budget bump in this doc.
---
## 6. The canonical-surface rule
> **Every model-visible (active or deferred-discoverable) tool must have one
> clear niche. If a tool is superseded, it gets a named replacement and moves to
> hidden-compatibility or deprecated — it does not stay visible.**
### Canonical vs compatibility summary for the confusing clusters
| Cluster | Canonical (keep visible) | Compatibility / retired | Notes |
|---|---|---|---|
| **Shell wait** | `exec_shell_wait` | `exec_wait` → hidden-compat | Same `ShellWaitTool` (`registry.rs:526,529`); router already unifies (`tool_routing.rs:1139`) |
| **Shell interact** | `exec_shell_interact` | `exec_interact` → hidden-compat | Same `ShellInteractTool` (`registry.rs:527,530`) |
| **Work progress / checklist / todo** | `work_update` | `checklist_write/add/update/list`, `todo_write/add/update/list` → hidden-compat | Same `TodoWriteTool`; compatibility names replay old transcripts only |
| **Speech / tts** | `speech` | `tts` → hidden-compat | Same `SpeechTool` (`registry.rs:787-792`) |
| **Subagent lifecycle** | `agent` | old lifecycle names and tool-agent lane removed | Single async launcher; child agents are leaf workers |
| **Edit family** | `apply_patch`, `edit_file`, `write_file`, `fim_edit` | none — **all distinct niches** | NOT touched (per #2681 non-goals); doc-only canonical guidance |
| **Search family** | `grep_files` (content), `file_search` (filename), `project_map` (structure) | none — **distinct niches** | NOT touched; no FTS5/BM25/semantic index exists today |
**Non-goals (explicitly NOT diet targets in this cycle, per #2681):**
`apply_patch` / `edit_file` / `write_file` / `fim_edit`;
`grep_files` / `file_search` / `project_map`;
`fetch_url` / `web.run` / `web_search`;
`task_shell_*`; `handle_read` / `retrieve_tool_result`. These have distinct
niches and receive **canonical guidance only** — no lifecycle change.
The RLM surface (`rlm_open` / `rlm_eval` / `rlm_configure` / `rlm_close` /
`rlm_session_objects`, `crates/tui/src/tools/rlm.rs`) is likewise out of scope;
`handle_read` retrieves var handles, and `finalize` / `FINAL` is an in-kernel
Python function, **not a tool** — so there is nothing to retire there.
---
## 7. Subagent cutover decision: one visible launcher
The old lifecycle trio and tool-agent lane are removed, not hidden compatibility
tools.
**Decision: expose only `agent`.**
- `agent` starts one focused background child and returns the agent id plus
transcript handle.
- Child results arrive as completion events. The parent should keep working
instead of polling a lifecycle tool.
- Child tool catalogs exclude subagent lifecycle tools, so children are leaf
workers and cannot recursively summon more agents.
- Detailed inspection goes through `handle_read` on the returned transcript
handle.
This is a lifecycle simplification, not a provider gate.
---
## 8. Prefix-cache safety + replay guarantee
### Prefix-cache rules every diet PR MUST follow
The tools array is part of DeepSeek's immutable KV prefix. The catalog-head
byte-stability invariant (`tool_catalog.rs:169-196`) is binding:
1. **Never mutate the active head non-deterministically.** The first-turn active
block must be **byte-identical run-to-run** and across Plan ↔ Agent ↔ YOLO.
2. **A diet is a one-time deterministic edit.** Removing a name from
`DEFAULT_ACTIVE_NATIVE_TOOLS` shifts the head exactly once; after that it must
be stable. Land such edits as their own focused change.
3. **Notices live in result metadata, never the prefix.** Deprecated replacement
notes are appended at dispatch time in `tool_routing.rs` to the *call result*
only. **Nothing** about hidden/deprecated state may be serialized into a tool
schema, description, or the catalog array.
4. **Preserve ordering and partitioning.** `build_model_tool_catalog` sorts each
partition by name and keeps built-ins as a contiguous prefix ahead of MCP
tools (`tool_catalog.rs:186-194`). Diet edits must not break this.
5. **Hidden/deprecated tools are excluded *before* the head is built**, so their
removal is the only head change — they do not appear in the prefix at all.
### Old-transcript replay guarantee
> For every name in the deprecation manifest with `replay_supported = Yes`, the
> tool stays **registered and dispatchable with identical behavior**. Replaying
> an old transcript that calls `exec_wait`, `exec_interact`, `tts`, or any
> `todo_*` produces the same result it always did. Deprecated names additionally
> attach a result-metadata notice; hidden-compat names are silent. A name is only
> ever made non-dispatchable (**removed**) after a deliberate, per-name decision
> to drop replay support at `planned_removal_version`.
---
## 9. Required tests
Any diet PR (and the umbrella #2681 work) must add/keep:
1. **Duplicate-active-alias guard.** A test asserting that no name in
`HIDDEN_COMPATIBILITY_TOOLS` or `DEPRECATED_ALIASES` appears in
`DEFAULT_ACTIVE_NATIVE_TOOLS` or `ARCEE_FIRST_TURN_NATIVE_TOOLS`, and that no
two active entries resolve to the same underlying tool implementation.
2. **Tool-search exclusion test.** Assert that hidden-compat and deprecated names
are absent from the tool-search-discoverable pool while remaining present in
the registry (dispatchable).
3. **Replay / dispatch tests.** For each manifest name, calling it still
executes and returns the same result as its canonical twin. Deprecated names
additionally assert the replacement note is present **in result metadata** and
absent from the catalog/prefix. Hidden-compat names assert **no** added
notice.
4. **Golden active-block byte test.** A snapshot test pinning the byte
serialization of the first-turn active tool block, asserting it is identical
across Plan / Agent / YOLO (native head) and stable run-to-run — enforcing the
`tool_catalog.rs:169-196` invariant. The golden updates **only** as a
reviewed, deliberate one-time edit when the diet lands.
5. **Subagent guardrail test.** Assert only `agent` is registered as a
model-visible subagent tool and that hidden/legacy names from
`subagent/mod.rs` are not advertised.
6. **Leaf-worker test.** Assert subagent tool catalogs exclude `agent` and
retired legacy lifecycle names.