小橙子 e6332810ed
feat(coordinator): research-fork opt-in — let coordinator mode use a single fork path for cache-sharing fan-out (#22)
Resolves the only path that could plausibly cut coordinator-mode token
cost without changing architecture. Coordinator mode normally rejects
implicit forks (the orchestrator is supposed to delegate to typed
workers); this opt-in carves out a narrow window for cache-sharing
research fan-out only.

Why
===
Coordinator-mode parallel research dispatches several workers of the
same type. Each worker prefills its own system prompt + full tool
schema, easily 10-30k tokens × N workers. Fork's byte-identical API
prefix bypasses that entirely — the parent's prompt cache is fully
reused, marginal cost is just the directive bytes. On a 3-worker
research fan-out we expect ~10x prefill savings for the research
phase.

Out of scope: implementation/verification spawns still go through
typed workers (independent context, independent cache). This PR is
about research only.

What's in this PR
=================
- forkSubagent.ts
  - New helper: isCoordinatorResearchForkEnabled() — gated on
    feature('FORK_SUBAGENT') + isCoordinatorMode() + non-interactive
    + CLAUDE_CODE_COORDINATOR_RESEARCH_FORK=1.
  - New constant: COORDINATOR_RESEARCH_FORK_SUBAGENT_TYPE = 'fork'
    (single source of truth for what callers spell as subagent_type).
  - New ForkMode union: 'normal' | 'coordinator-research'.
  - buildForkedMessages / buildChildMessage now take an optional mode
    parameter (default 'normal'). 'coordinator-research' adds rule 11
    to the boilerplate: "RESEARCH FORK: do not modify files; report
    findings instead". The first 10 rules are byte-identical to the
    normal mode (cache-friendly), and the framing tag at the top is
    unchanged so isInForkChild's recursive-fork guard fires for both
    modes.
- AgentTool.tsx
  - Adds a coord-research-fork branch in the agent-resolution code:
    `subagent_type === 'fork'` + isCoordinatorResearchForkEnabled() →
    routes through the same fork path as an omitted subagent_type.
  - The omitted-subagent_type path in coordinator mode is unchanged
    (still requires explicit type, with the existing helpful error).
  - Computed forkMode threads through to buildForkedMessages.
- invocationLimiter.ts
  - 'fork' default cap = 10 (between verification's 5 and the 8
    fallback). Generous for legitimate parallel research, tight enough
    that pathological loops show up before the budget runs out.
- forkSubagent.test.ts (new, 11 cases)
  - Env-flag gating across all combinations.
  - Boilerplate rule-11 injection in research mode + byte-identical
    first 10 rules across modes (cache contract).
  - Recursive-fork guard fires for both mode variants.
  - Subagent type constant matches the literal "fork".
- invocationLimiter.test.ts
  - One new case: default cap for 'fork' is 10.
- scripts/dev-coordinator-e2e.ps1
  - Adds 8th e2e step: type constant, boilerplate variants, recursion
    guard cross-mode, env-flag gate.

Why opt-in (and double-gated)
=============================
- Lazy-delegation lint and specialist redirect are blacklists: specific
  anti-patterns, near-zero false positives, safe to default on.
- Coordinator research fork is a positive-space affordance: "the
  coordinator MAY use a fork for research." Defaulting it on would
  encourage forks for spawns that legitimately want a clean context
  (e.g. adversarial verification), which conflicts with coordinator's
  independent-worker philosophy.
- Two gates needed: feature('FORK_SUBAGENT') is the bundle-time
  feature flag (we share its plumbing), CLAUDE_CODE_COORDINATOR_RESEARCH_FORK=1
  is the user-level opt-in. Both must be on.

Risks considered
================
- Boilerplate divergence between modes would defeat the parent prompt
  cache. Mitigated: research-mode boilerplate IS the normal-mode
  boilerplate plus rule 11 at the end; first 10 rules are byte-identical
  (covered by a test asserting that property explicitly).
- Recursive-fork guard could miss the new boilerplate. Mitigated: the
  framing tag at the top is unchanged across modes (covered by tests).
- Model abuses 'fork' for non-research spawns. Mitigated: rule 11 in
  the boilerplate forbids file modifications + invocationLimiter cap
  10 contains pathological loops + opt-in flag means nothing changes
  for users who don't turn it on.

Verification
============
- src/tools/AgentTool/forkSubagent.test.ts ............. 11/11 pass
- src/tools/AgentTool/invocationLimiter.test.ts ........ 14/14 pass
- All B1+B2+B3+B4 unit tests .......................... 144/144 pass
- scripts/dev-coordinator-e2e.ps1 ..................... GREEN (8 steps)
- get_diagnostics on changed files .................... clean

Tested: bun test src/tools/AgentTool/forkSubagent.test.ts (11/11); all coordinator unit tests (144/144); scripts/dev-coordinator-e2e.ps1 step 8 GREEN
Not-tested: live coordinator session with the flag on producing actual cache savings (telemetry-only follow-up; the flag is gated and the boilerplate contract is enforced by test)
Confidence: high
Scope-risk: narrow
Constraint: must NOT change buildChildMessage's first 10 rules (cache identity); must NOT change the boilerplate framing tag (recursion guard); must NOT widen the gate to non-coordinator paths (fork stays mutex with normal coordinator delegation by default)
Rejected: defaulting the gate on (positive-space affordance, false-positive cost too high); adding `fork_research` as a new AgentTool input parameter (schema change rejected on B2 by the same logic — too much blast radius); changing fork's tool pool to forbid Edit/Write in research mode (would break useExactTools cache identity; soft-constrained by rule 11 instead)
Directive: when telemetry shows enough adoption to validate the cache savings, consider exposing a /coord-fork slash command alongside the env flag for easier experimentation; do NOT promote to default-on without that data

Co-authored-by: 你的姓名 <you@example.com>
2026-06-12 05:31:50 +08:00
..