From ba74e0976cd8b1ed9c06934a1802c02cd444d568 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sat, 2 May 2026 23:39:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20fork-agent-redesign=20=E2=80=94=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AgentTool=20fork=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=B8=8E=20spec=20=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 AgentTool 引入 fork 布尔参数,支持子代理从父对话上下文中 fork 出独立分支, 继承完整历史、系统提示和模型配置。重构 inputSchema 条件逻辑以适配 fork 模式。 Co-Authored-By: Claude Opus 4.7 --- .../src/tools/AgentTool/AgentTool.tsx | 48 +-- .../tools/AgentTool/__tests__/prompt.test.ts | 69 ++++ .../src/tools/AgentTool/prompt.ts | 22 +- scripts/defines.ts | 2 +- .../spec-design.md | 132 ++++++++ .../spec-human-verify.md | 170 ++++++++++ .../spec-plan.md | 317 ++++++++++++++++++ 7 files changed, 720 insertions(+), 40 deletions(-) create mode 100644 packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts create mode 100644 spec/feature_20260502_F001_fork-agent-redesign/spec-design.md create mode 100644 spec/feature_20260502_F001_fork-agent-redesign/spec-human-verify.md create mode 100644 spec/feature_20260502_F001_fork-agent-redesign/spec-plan.md diff --git a/packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx b/packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx index 378a9078b..f64d19de3 100644 --- a/packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx +++ b/packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx @@ -148,6 +148,12 @@ const baseInputSchema = lazySchema(() => .boolean() .optional() .describe('Set to true to run this agent in the background. You will be notified when it completes.'), + fork: z + .boolean() + .optional() + .describe( + 'Set to true to fork from the parent conversation context. The child inherits full history, system prompt, and model. Requires FORK_SUBAGENT feature flag.', + ), }), ); @@ -191,24 +197,23 @@ const fullInputSchema = lazySchema(() => { // type, but call() destructures via the explicit AgentToolInput type below // which always includes all optional fields. export const inputSchema = lazySchema(() => { - const schema = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true }); - - // GrowthBook-in-lazySchema is acceptable here (unlike subagent_type, which - // was removed in 906da6c723): the divergence window is one-session-per- - // gate-flip via _CACHED_MAY_BE_STALE disk read, and worst case is either - // "schema shows a no-op param" (gate flips on mid-session: param ignored - // by forceAsync) or "schema hides a param that would've worked" (gate - // flips off mid-session: everything still runs async via memoized - // forceAsync). No Zod rejection, no crash — unlike required→optional. - return isBackgroundTasksDisabled || isForkSubagentEnabled() ? schema.omit({ run_in_background: true }) : schema; + const base = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true }); + return isBackgroundTasksDisabled + ? !isForkSubagentEnabled() + ? base.omit({ run_in_background: true, fork: true }) + : base.omit({ run_in_background: true }) + : !isForkSubagentEnabled() + ? base.omit({ fork: true }) + : base; }); type InputSchema = ReturnType; // Explicit type widens the schema inference to always include all optional // fields even when .omit() strips them for gating (cwd, run_in_background). -// subagent_type is optional; call() defaults it to general-purpose when the -// fork gate is off, or routes to the fork path when the gate is on. +// subagent_type is optional; call() defaults it to general-purpose. +// fork is gated by FORK_SUBAGENT flag; when omitted or flag is off, no fork. type AgentToolInput = z.infer> & { + fork?: boolean; name?: string; team_name?: string; mode?: z.infer>; @@ -322,6 +327,7 @@ export const AgentTool = buildTool({ { prompt, subagent_type, + fork, description, model: modelParam, run_in_background, @@ -406,12 +412,11 @@ export const AgentTool = buildTool({ return { data: spawnResult } as unknown as { data: Output }; } - // Fork subagent experiment routing: - // - subagent_type set: use it (explicit wins) - // - subagent_type omitted, gate on: fork path (undefined) - // - subagent_type omitted, gate off: default general-purpose - const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType); - const isForkPath = effectiveType === undefined; + // Fork routing: explicit `fork: true` parameter triggers the fork path + // (inherits parent context and model). Requires FORK_SUBAGENT flag. + // subagent_type is ignored when fork takes effect. + const isForkPath = fork === true && isForkSubagentEnabled(); + const effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType; let selectedAgent: AgentDefinition; if (isForkPath) { @@ -692,10 +697,6 @@ export const AgentTool = buildTool({ // dependency issues during test module loading. const isCoordinator = feature('COORDINATOR_MODE') ? isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) : false; - // Fork subagent experiment: force ALL spawns async for a unified - // interaction model (not just fork spawns — all of them). - const forceAsync = isForkSubagentEnabled(); - // Assistant mode: force all agents async. Synchronous subagents hold the // main loop's turn open until they complete — the daemon's inputQueue // backs up, and the first overdue cron catch-up on spawn becomes N @@ -709,7 +710,6 @@ export const AgentTool = buildTool({ (run_in_background === true || selectedAgent.background === true || isCoordinator || - forceAsync || assistantForceAsync || (proactiveModule?.isProactiveActive() ?? false)) && !isBackgroundTasksDisabled; @@ -889,7 +889,7 @@ export const AgentTool = buildTool({ toolUseContext, rootSetAppState, agentIdForCleanup: asyncAgentId, - enableSummarization: isCoordinator || isForkSubagentEnabled() || getSdkAgentProgressSummariesEnabled(), + enableSummarization: isCoordinator || isForkPath || getSdkAgentProgressSummariesEnabled(), getWorktreeResult: cleanupWorktreeIfNeeded, }), ), diff --git a/packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts b/packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts new file mode 100644 index 000000000..8b5c2f73f --- /dev/null +++ b/packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from 'bun:test' +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const promptSource = readFileSync(join(__dirname, '..', 'prompt.ts'), 'utf-8') + +describe('prompt.ts fork-related text verification', () => { + test('does not contain "omit `subagent_type`" guidance', () => { + expect(promptSource).not.toMatch(/omit.*subagent_type/) + }) + + test('contains `fork: true` in at least 3 locations (shared + whenToFork + forkExamples)', () => { + const matches = promptSource.match(/fork: true/g) + expect(matches).not.toBeNull() + expect(matches!.length).toBeGreaterThanOrEqual(3) + }) + + test('all forkEnabled references are ternary conditions, not negated', () => { + const lines = promptSource.split('\n') + for (const line of lines) { + if ( + line.includes('forkEnabled') && + !line.includes('const forkEnabled') && + !line.includes('forkEnabled =') + ) { + expect(line).not.toContain('!forkEnabled') + } + } + }) + + test('uses "non-fork" terminology instead of "fresh agent"', () => { + expect(promptSource).toContain('non-fork') + // "fresh agent" should not appear in fork-aware conditional text + const freshAgentMatches = promptSource.match(/fresh agent/g) + if (freshAgentMatches) { + // Only allowed in comments explaining behavior, not in prompt text + const linesWithFreshAgent = promptSource + .split('\n') + .filter(line => line.includes('fresh agent')) + .map(line => line.trim()) + for (const line of linesWithFreshAgent) { + // "fresh agent" in the context of "starts fresh" (not fork-aware) is ok + // but "fresh agent" in forkEnabled conditional should not appear + expect(line).not.toMatch(/fresh agent.*subagent_type/) + } + } + }) + + test('background task condition does not include !forkEnabled', () => { + // The condition for showing background task instructions should not exclude fork + const bgCondition = promptSource.match( + /!isEnvTruthy.*isInProcessTeammate[\s\S]*?run_in_background/, + ) + if (bgCondition) { + expect(bgCondition[0]).not.toContain('!forkEnabled') + } + }) + + test('fork example includes fork: true parameter', () => { + // The first fork example should have fork: true + const forkExampleBlock = promptSource.match( + /name: "ship-audit"[\s\S]*?Under 200 words/, + ) + expect(forkExampleBlock).not.toBeNull() + expect(forkExampleBlock![0]).toContain('fork: true') + }) +}) diff --git a/packages/builtin-tools/src/tools/AgentTool/prompt.ts b/packages/builtin-tools/src/tools/AgentTool/prompt.ts index 4198859a4..52c2ea030 100644 --- a/packages/builtin-tools/src/tools/AgentTool/prompt.ts +++ b/packages/builtin-tools/src/tools/AgentTool/prompt.ts @@ -82,11 +82,7 @@ export async function getPrompt( ## When to fork -Fork yourself (omit \`subagent_type\`) when the intermediate tool output isn't worth keeping in your context. The criterion is qualitative \u2014 "will I need this output again" \u2014 not task size. -- **Research**: fork open-ended questions. If research can be broken into independent questions, launch parallel forks in one message. A fork beats a fresh subagent for this \u2014 it inherits context and shares your cache. -- **Implementation**: prefer to fork implementation work that requires more than a couple of edits. Do research before jumping to implementation. - -Forks are cheap because they share your prompt cache. Don't set \`model\` on a fork \u2014 a different model can't reuse the parent's cache. Pass a short \`name\` (one or two words, lowercase) so the user can see the fork in the teams panel and steer it mid-run. +When you need to delegate work that benefits from full conversation context (e.g., continuing a multi-file refactor where the child needs the same system prompt and history), use \`fork: true\`. For most tasks, prefer specialized agent types (Explore, Plan, general-purpose). **Don't peek.** The tool result includes an \`output_file\` path — do not Read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the fork's tool noise into your context, which defeats the point of forking. @@ -100,14 +96,14 @@ Forks are cheap because they share your prompt cache. Don't set \`model\` on a f ## Writing the prompt -${forkEnabled ? 'When spawning a fresh agent (with a `subagent_type`), it starts with zero context. ' : ''}Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters. +${forkEnabled ? 'When spawning an agent without `fork: true`, it starts with zero context. ' : ''}Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters. - Explain what you're trying to accomplish and why. - Describe what you've already learned or ruled out. - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction. - If you need a short response, say so ("report in under 200 words"). - Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong. -${forkEnabled ? 'For fresh agents, terse' : 'Terse'} command-style prompts produce shallow, generic work. +${forkEnabled ? 'For non-fork agents, terse' : 'Terse'} command-style prompts produce shallow, generic work. **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change. ` @@ -120,6 +116,7 @@ assistant: Forking this \u2014 it's a survey question. I want the punc ${AGENT_TOOL_NAME}({ name: "ship-audit", description: "Branch ship-readiness audit", + fork: true, prompt: "Audit what's left before this branch can ship. Check: uncommitted changes, commits ahead of main, whether tests exist, whether the GrowthBook gate is wired up, whether CI-relevant files changed. Report a punch list \u2014 done vs. missing. Under 200 words." }) assistant: Ship-readiness audit running. @@ -205,11 +202,7 @@ The ${AGENT_TOOL_NAME} tool launches specialized agents (subprocesses) that auto ${agentListSection} -${ - forkEnabled - ? `When using the ${AGENT_TOOL_NAME} tool, specify a subagent_type to use a specialized agent, or omit it to fork yourself — a fork inherits your full conversation context.` - : `When using the ${AGENT_TOOL_NAME} tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.` -}` +When using the ${AGENT_TOOL_NAME} tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.${forkEnabled ? ` Set \`fork: true\` to fork from the parent conversation context, inheriting full history and model.` : ''}` // Coordinator mode gets the slim prompt -- the coordinator system prompt // already covers usage notes, examples, and when-not-to-use guidance. @@ -257,14 +250,13 @@ Usage notes: - When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.${ // eslint-disable-next-line custom-rules/no-process-env-top-level !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS) && - !isInProcessTeammate() && - !forkEnabled + !isInProcessTeammate() ? ` - You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead. - **Foreground vs background**: Use foreground (default) when you need the agent's results before you can proceed — e.g., research agents whose findings inform your next steps. Use background when you have genuinely independent work to do in parallel.` : '' } -- To continue a previously spawned agent, use ${SEND_MESSAGE_TOOL_NAME} with the agent's ID or name as the \`to\` field. The agent resumes with its full context preserved. ${forkEnabled ? 'Each fresh Agent invocation with a subagent_type starts without context — provide a complete task description.' : 'Each Agent invocation starts fresh — provide a complete task description.'} +- To continue a previously spawned agent, use ${SEND_MESSAGE_TOOL_NAME} with the agent's ID or name as the \`to\` field. The agent resumes with its full context preserved. ${forkEnabled ? 'Each non-fork Agent invocation starts without context — provide a complete task description.' : 'Each Agent invocation starts fresh — provide a complete task description.'} - The agent's outputs should generally be trusted - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.)${forkEnabled ? '' : ", since it is not aware of the user's intent"} - If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. diff --git a/scripts/defines.ts b/scripts/defines.ts index 6a502b63d..0ae46ab15 100644 --- a/scripts/defines.ts +++ b/scripts/defines.ts @@ -52,7 +52,7 @@ export const DEFAULT_BUILD_FEATURES = [ 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口 // 'CONTEXT_COLLAPSE', // 已禁用:实现是空壳 stub,启用后会抑制 auto compact 导致上下文管理完全失效 'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出 - // 'FORK_SUBAGENT', // 已禁用:启用后 prompt 引导模型用 fork(继承父模型)替代 Explore(haiku),导致探索任务使用同等级模型 + // 'FORK_SUBAGENT', // 已禁用:显式 `fork: true` 参数触发 fork 路径(继承父级上下文和模型),不影响 forceAsync 和探索任务模型选择 // 'UDS_INBOX', // inbox 数组只增不减(非 GB 级主因) 'KAIROS', // Kairos 定时任务系统核心 // 'COORDINATOR_MODE', // 已禁用:AgentSummary 30s fork 循环,GB 级泄露主因 diff --git a/spec/feature_20260502_F001_fork-agent-redesign/spec-design.md b/spec/feature_20260502_F001_fork-agent-redesign/spec-design.md new file mode 100644 index 000000000..83ab4dd46 --- /dev/null +++ b/spec/feature_20260502_F001_fork-agent-redesign/spec-design.md @@ -0,0 +1,132 @@ +# Feature: 20260502_F001 - fork-agent-redesign + +## 需求背景 + +当前 `FORK_SUBAGENT` feature flag 是一个"一刀切"开关,启用时同时强制三件事: + +1. 所有省略 `subagent_type` 的 agent 调用隐式走 fork 路径(继承父级完整上下文和模型) +2. 所有 agent spawn 强制异步(`forceAsync` 绑定在 `isForkSubagentEnabled()` 上) +3. prompt 引导模型优先省略 `subagent_type`,导致大部分 agent 都用同等级模型(贵) + +这导致探索任务被迫使用与父级相同的模型(而非 haiku),token 消耗大增。因此该 flag 在 `defines.ts` 中被注释禁用。 + +## 目标 + +- 将 fork 从隐式行为改为**显式参数触发**(`fork: true`) +- FORK_SUBAGENT flag 只控制 fork 能力的可用性,**不再影响 `forceAsync` 等其他行为** +- 模型始终继承父级(保持现有行为) +- **完全向后兼容**——不传 `fork` 参数时行为与当前(flag 关闭时)一致 + +## 方案设计 + +### Schema 变更 + +Agent tool 参数新增 `fork?: boolean`,仅在 `FORK_SUBAGENT` flag 启用时可见(schema 动态裁剪,复用现有的 schema memo 模式)。 + +```ts +// inputSchema 中新增 +fork: z.boolean().optional().describe( + 'Set to true to fork from the parent conversation context. ' + 'The child inherits full history, system prompt, and model. ' + 'Requires FORK_SUBAGENT feature flag.' +) +``` + +flag 关闭时,schema 通过 `.omit({ fork: true })` 裁剪掉该字段(与当前 `run_in_background` 的裁剪方式一致)。 + +### 路由逻辑重构 + +`AgentTool.tsx` call() 中的路由从当前的隐式判断: + +```ts +// 旧行为:省略 subagent_type → fork(flag 开启时) +const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType); +const isForkPath = effectiveType === undefined; +``` + +改为显式参数触发: + +```ts +// 新行为:显式 fork 参数触发,fork 优先级高于 subagent_type +const isForkPath = input.fork === true && isForkSubagentEnabled(); +const effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType; +``` + +#### 决策表 + +| `fork` | `subagent_type` | flag 开 | 结果 | +|--------|----------------|---------|------| +| `true` | 有值 | 是 | fork 路径,**忽略 subagent_type** | +| `true` | 省略 | 是 | fork 路径(继承上下文) | +| `true` | * | 否 | 忽略 fork,走 subagent_type 或 general-purpose | +| `false`/省略 | 有值 | * | 走指定 agent 类型(原有行为) | +| `false`/省略 | 省略 | * | 走 general-purpose(原有行为) | + +核心原则:**`fork: true` 是最高优先级**(当 flag 开启时),但 flag 关闭时静默降级,不影响原有行为。 + +### 后台运行由参数决定 + +fork agent 是否后台运行由 `run_in_background` 参数决定,与普通 agent 一致。`forceAsync` 不再绑定 `isForkSubagentEnabled()`: + +```ts +// forceAsync 不再受 isForkSubagentEnabled() 影响 +const forceAsync = /* 其他条件(coordinator, assistant mode 等)*/; +``` + +fork agent 与普通 agent 使用相同的 `run_in_background` 参数判断逻辑: +- `run_in_background: true` → 后台异步运行 +- `run_in_background: false` / 省略 → 同步阻塞运行 + +### prompt 调整 + +移除引导模型"省略 subagent_type 以触发 fork"的 prompt 文本。改为说明 `fork: true` 的适用场景: + +> When you need to delegate work that benefits from full conversation context (e.g., continuing a multi-file refactor where the child needs the same system prompt and history), use `fork: true`. For most tasks, prefer specialized agent types (Explore, Plan, general-purpose). + +### isForkSubagentEnabled() 精简 + +函数签名和行为保持不变,但调用方语义改变:从"隐式路由判断"变为"参数校验门控"。 + +```ts +export function isForkSubagentEnabled(): boolean { + if (!feature('FORK_SUBAGENT')) return false; + if (isCoordinatorMode()) return false; + if (getIsNonInteractiveSession()) return false; + return true; +} +``` + +### 不变的部分 + +以下保持不变,无需修改: + +- `buildForkedMessages()` — fork 消息构建逻辑 +- `isInForkChild()` — 递归 fork 防护 +- `FORK_AGENT` — fork agent 定义(model: 'inherit', permissionMode: 'bubble') +- `buildChildMessage()` — fork 子 agent 指令模板 +- `buildWorktreeNotice()` — worktree 隔离通知 + +## 实现要点 + +1. **Schema 动态裁剪**:`inputSchema` memo 中根据 `isForkSubagentEnabled()` 决定是否 `.omit({ fork: true })`,flag 关闭时字段不存在于 schema +2. **省略 `subagent_type` 恢复原有行为**:不再隐式走 fork,恢复为 `GENERAL_PURPOSE_AGENT` +3. **`defines.ts` 注释更新**:`FORK_SUBAGENT` 保持注释状态,但描述更新为新行为(显式参数触发,不影响探索任务模型选择) +4. **递归 fork 防护**:保持现有 `isInForkChild()` + `querySource` 双重检测 + +### 涉及文件 + +| 文件 | 改动 | +|------|------| +| `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | 新增 `fork` 参数解析,路由逻辑重构,forceAsync 解耦 | +| `packages/builtin-tools/src/tools/AgentTool/prompt.ts` | 移除隐式 fork 引导,新增 `fork: true` 使用场景说明 | +| `scripts/defines.ts` | 更新 `FORK_SUBAGENT` 注释描述 | + +## 验收标准 + +- [ ] `fork: true` + `FORK_SUBAGENT` 启用 → 走 fork 路径,继承父级上下文和模型 +- [ ] `fork: true` + `subagent_type` 有值 + flag 开 → fork 路径,忽略 subagent_type +- [ ] `fork: true` + `FORK_SUBAGENT` 关闭 → 忽略 fork,走普通 agent 路径 +- [ ] 不传 `fork` 参数 → 行为与当前 flag 关闭时完全一致(走 general-purpose 或指定 subagent_type) +- [ ] `forceAsync` 不再因 `isForkSubagentEnabled()` 而全局生效 +- [ ] fork 子 agent 的后台/同步行为由 `run_in_background` 参数控制,与普通 agent 一致 +- [ ] `bun run precheck` 零错误通过 diff --git a/spec/feature_20260502_F001_fork-agent-redesign/spec-human-verify.md b/spec/feature_20260502_F001_fork-agent-redesign/spec-human-verify.md new file mode 100644 index 000000000..dc0c1720a --- /dev/null +++ b/spec/feature_20260502_F001_fork-agent-redesign/spec-human-verify.md @@ -0,0 +1,170 @@ +# Fork Agent 显式参数触发重构 人工验收清单 + +**生成时间:** 2026-05-02 +**关联计划:** spec/feature_20260502_F001_fork-agent-redesign/spec-plan.md +**关联设计:** spec/feature_20260502_F001_fork-agent-redesign/spec-design.md + +--- + +## 验收前准备 + +### 环境要求 +- [ ] [AUTO] 检查 Bun 版本: `bun --version` +- [ ] [AUTO] 安装依赖: `bun install` + +--- + +## 验收项目 + +### 场景 1:Schema 与类型变更 + +#### - [x] 1.1 fork 字段已添加到 baseInputSchema +- **来源:** spec-plan.md Task 1 / spec-design.md §Schema 变更 +- **目的:** 确认 fork 参数在基础 schema 中声明 +- **操作步骤:** + 1. [A] `grep -n 'fork:' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx | head -5` → 期望包含: `fork: z`(schema 定义)和 `fork?: boolean`(类型声明) + +#### - [x] 1.2 fork 字段在 flag 关闭时被 schema 裁剪 +- **来源:** spec-plan.md Task 1 / spec-design.md §Schema 变更 +- **目的:** 确认 FORK_SUBAGENT 关闭时 fork 字段不可见 +- **操作步骤:** + 1. [A] `grep -n 'omit.*fork' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `schema.omit({ fork: true })` + +#### - [x] 1.3 AgentToolInput 类型包含 fork 字段 +- **来源:** spec-plan.md Task 1 +- **目的:** 确认类型声明与 schema 一致 +- **操作步骤:** + 1. [A] `grep -n 'fork' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx | grep 'AgentToolInput\|fork?:'` → 期望包含: `fork?: boolean` + +--- + +### 场景 2:路由逻辑重构 + +#### - [x] 2.1 isForkPath 使用显式 fork 参数判断 +- **来源:** spec-plan.md Task 1 / spec-design.md §路由逻辑重构 +- **目的:** 确认 fork 路径由 fork=true 显式触发 +- **操作步骤:** + 1. [A] `grep -n 'isForkPath' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `fork === true && isForkSubagentEnabled()` + +#### - [x] 2.2 forceAsync 已完全移除 +- **来源:** spec-plan.md Task 1 / spec-design.md §后台运行由参数决定 +- **目的:** 确认 forceAsync 不再绑定 isForkSubagentEnabled() +- **操作步骤:** + 1. [A] `grep -c 'forceAsync' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望精确: `0` + +#### - [x] 2.3 isForkSubagentEnabled() 仅用于 schema 裁剪和路由判断 +- **来源:** spec-plan.md Task 1 +- **目的:** 确认 isForkSubagentEnabled() 不再影响 forceAsync/shouldRunAsync +- **操作步骤:** + 1. [A] `grep -n 'isForkSubagentEnabled' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: 仅出现在 inputSchema 裁剪和 isForkPath 路由判断中 + +#### - [x] 2.4 shouldRunAsync 由 run_in_background 控制 +- **来源:** spec-plan.md Task 1 / spec-design.md §后台运行由参数决定 +- **目的:** 确认异步行为与普通 agent 一致 +- **操作步骤:** + 1. [A] `grep -n 'run_in_background' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx | head -5` → 期望包含: `shouldRunAsync` 计算中含 `run_in_background === true`,无 `forceAsync` + +#### - [x] 2.5 enableSummarization 使用 isForkPath 而非 isForkSubagentEnabled() +- **来源:** spec-plan.md Task 1 +- **目的:** 确认摘要仅在当前调用实际走 fork 路径时启用 +- **操作步骤:** + 1. [A] `grep -n 'enableSummarization' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `isForkPath`,不包含 `isForkSubagentEnabled()` + +--- + +### 场景 3:Prompt 文本更新 + +#### - [x] 3.1 不再包含 "omit subagent_type" 引导文本 +- **来源:** spec-plan.md Task 2 / spec-design.md §prompt 调整 +- **目的:** 确认隐式 fork 触发引导已移除 +- **操作步骤:** + 1. [A] `grep -c 'omit' packages/builtin-tools/src/tools/AgentTool/prompt.ts` → 期望精确: `0` + +#### - [x] 3.2 包含 "fork: true" 显式参数说明 +- **来源:** spec-plan.md Task 2 / spec-design.md §prompt 调整 +- **目的:** 确认新的显式 fork 使用说明已写入 +- **操作步骤:** + 1. [A] `grep -c 'fork: true' packages/builtin-tools/src/tools/AgentTool/prompt.ts` → 期望包含: >= 3(shared section + whenToForkSection + forkExamples) + +#### - [x] 3.3 背景任务说明条件不再含 !forkEnabled +- **来源:** spec-plan.md Task 2 +- **目的:** 确认 fork 解耦后背景任务说明在 fork 启用时也显示 +- **操作步骤:** + 1. [A] `grep -n 'forkEnabled' packages/builtin-tools/src/tools/AgentTool/prompt.ts` → 期望包含: 所有匹配行均为 `forkEnabled ?` 形式,不包含 `!forkEnabled` + +#### - [x] 3.4 术语从 "fresh agent" 更新为 "non-fork" +- **来源:** spec-plan.md Task 2 +- **目的:** 确认 prompt 术语与新的显式 fork 逻辑一致 +- **操作步骤:** + 1. [A] `grep -c 'non-fork' packages/builtin-tools/src/tools/AgentTool/prompt.ts` → 期望包含: >= 2 + +--- + +### 场景 4:边界与回归(决策表验证) + +#### - [x] 4.1 fork=true + subagent_type + flag 开 → fork 路径,忽略 subagent_type +- **来源:** spec-design.md §决策表 + spec-plan.md Task 3 +- **目的:** 确认 fork 优先级高于 subagent_type +- **操作步骤:** + 1. [A] `grep -A2 'isForkPath = fork === true' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType`(fork 生效时 effectiveType 被 isForkPath 覆盖,subagent_type 不影响路由) + +#### - [x] 4.2 fork=true + flag 关闭 → 忽略 fork,走普通 agent 路径 +- **来源:** spec-design.md §决策表 +- **目的:** 确认 flag 关闭时 fork 静默降级 +- **操作步骤:** + 1. [A] `grep 'isForkPath = fork === true && isForkSubagentEnabled' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `&& isForkSubagentEnabled()`(双条件确保 flag 关闭时 isForkPath 为 false) + +#### - [x] 4.3 fork 省略 → 走 general-purpose 或指定 subagent_type +- **来源:** spec-design.md §决策表 +- **目的:** 确认向后兼容 +- **操作步骤:** + 1. [A] `grep 'effectiveType = subagent_type ??' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` → 期望包含: `GENERAL_PURPOSE_AGENT.agentType` + +--- + +### 场景 5:defines.ts 注释与构建验证 + +#### - [x] 5.1 FORK_SUBAGENT 注释已更新为新行为描述 +- **来源:** spec-plan.md Task 1 / spec-design.md §实现要点 +- **目的:** 确认注释反映显式参数触发设计 +- **操作步骤:** + 1. [A] `grep 'FORK_SUBAGENT' scripts/defines.ts` → 期望包含: `显式 \`fork: true\` 参数触发` + +#### - [x] 5.2 单元测试全部通过 +- **来源:** spec-plan.md Task 1 + Task 2 +- **目的:** 确认路由逻辑和 prompt 文本测试通过 +- **操作步骤:** + 1. [A] `bun test packages/builtin-tools/src/tools/AgentTool/__tests__/ 2>&1 | tail -10` → 期望包含: `0 fail` + +#### - [x] 5.3 precheck 零错误通过 +- **来源:** spec-plan.md Task 3 / spec-design.md §验收标准 +- **目的:** 确认 typecheck + lint + test 无回归 +- **操作步骤:** + 1. [A] `bun run precheck` → 期望包含: 零错误退出 + +--- + +## 验收结果汇总 + +| 场景 | 序号 | 验收项 | [A] | [H] | 结果 | +|------|------|--------|-----|-----|------| +| 场景 1 | 1.1 | fork 字段已添加到 baseInputSchema | 1 | 0 | ✅ | +| 场景 1 | 1.2 | fork 字段在 flag 关闭时被 schema 裁剪 | 1 | 0 | ✅ | +| 场景 1 | 1.3 | AgentToolInput 类型包含 fork 字段 | 1 | 0 | ✅ | +| 场景 2 | 2.1 | isForkPath 使用显式 fork 参数判断 | 1 | 0 | ✅ | +| 场景 2 | 2.2 | forceAsync 已完全移除 | 1 | 0 | ✅ | +| 场景 2 | 2.3 | isForkSubagentEnabled() 仅用于 schema 裁剪和路由判断 | 1 | 0 | ✅ | +| 场景 2 | 2.4 | shouldRunAsync 由 run_in_background 控制 | 1 | 0 | ✅ | +| 场景 2 | 2.5 | enableSummarization 使用 isForkPath | 1 | 0 | ✅ | +| 场景 3 | 3.1 | 不再包含 "omit subagent_type" 引导文本 | 1 | 0 | ✅ | +| 场景 3 | 3.2 | 包含 "fork: true" 显式参数说明 | 1 | 0 | ✅ | +| 场景 3 | 3.3 | 背景任务条件不再含 !forkEnabled | 1 | 0 | ✅ | +| 场景 3 | 3.4 | 术语更新为 "non-fork" | 1 | 0 | ✅ | +| 场景 4 | 4.1 | fork=true + subagent_type + flag 开 → fork 路径 | 1 | 0 | ✅ | +| 场景 4 | 4.2 | fork=true + flag 关闭 → 忽略 fork | 1 | 0 | ✅ | +| 场景 4 | 4.3 | fork 省略 → general-purpose(向后兼容) | 1 | 0 | ✅ | +| 场景 5 | 5.1 | FORK_SUBAGENT 注释已更新 | 1 | 0 | ✅ | +| 场景 5 | 5.2 | 单元测试全部通过 | 1 | 0 | ✅ | +| 场景 5 | 5.3 | precheck 零错误通过 | 1 | 0 | ✅ | + +**验收结论:** ✅ 全部通过 / ⬜ 存在问题 diff --git a/spec/feature_20260502_F001_fork-agent-redesign/spec-plan.md b/spec/feature_20260502_F001_fork-agent-redesign/spec-plan.md new file mode 100644 index 000000000..7181f577a --- /dev/null +++ b/spec/feature_20260502_F001_fork-agent-redesign/spec-plan.md @@ -0,0 +1,317 @@ +# Fork Agent 显式参数触发重构 执行计划 + +**目标:** 将 FORK_SUBAGENT 从隐式行为改为显式 `fork: true` 参数触发,解耦 forceAsync,保持向后兼容 + +**技术栈:** TypeScript, Zod schema, Bun test, React/Ink (prompt UI) + +**设计文档:** spec/feature_20260502_F001_fork-agent-redesign/spec-design.md + +## 改动总览 + +- 本次改动涉及 3 个修改文件:`AgentTool.tsx`(Schema + 路由 + forceAsync 解耦)、`prompt.ts`(引导文本)、`defines.ts`(注释更新)。新建 1 个测试文件 `prompt.test.ts`。 +- Task 1 是 Task 2 的前置:Task 1 完成 Schema 变更和路由重构后,Task 2 才能安全地调整 prompt 文本(prompt 行为描述必须与代码实际行为一致)。 +- 关键设计决策:fork 参数添加到 `baseInputSchema` 而非 `fullInputSchema`,因为 fork 是基础 agent 能力而非 multi-agent 特有能力。 + +--- + +### Task 0: 环境准备 + +**背景:** +确保构建和测试工具链在当前开发环境中可用,避免后续 Task 因环境问题阻塞。 + +**执行步骤:** +- [x] 验证构建工具可用 + - `bun --version` + - 确认输出 Bun 版本号 +- [x] 验证测试工具可用 + - `bun test --help 2>&1 | head -3` + - 确认输出包含 test 相关帮助信息 + +**检查步骤:** +- [x] 构建命令执行成功 + - `bun run build 2>&1 | tail -5` + - 预期: 构建成功,输出包含 dist/cli.js +- [x] 现有测试通过 + - `bun test packages/builtin-tools/src/tools/AgentTool/__tests__/ 2>&1 | tail -10` + - 预期: 所有现有测试通过,无失败 + +--- + +### Task 1: 核心路由重构 + +**背景:** +[业务语境] — 当前 `FORK_SUBAGENT` flag 启用时,所有省略 `subagent_type` 的 agent 调用隐式走 fork 路径,导致探索任务被迫使用父级同等级模型,token 消耗大增。本次重构将 fork 从隐式行为改为显式 `fork: true` 参数触发。 +[修改原因] — `AgentTool.tsx` 中路由逻辑(`effectiveType` / `isForkPath`)通过 `subagent_type` 是否省略来判断 fork 路径,需改为通过 `fork` 布尔参数显式触发。同时 `forceAsync` 变量绑定在 `isForkSubagentEnabled()` 上,导致 fork flag 开启时所有 agent 强制异步,需解耦。 +[上下游影响] — 本 Task 的输出(`fork` 参数、新路由逻辑)被 Task 2(prompt 文本调整)依赖。本 Task 无前置依赖。 + +**涉及文件:** +- 修改: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` +- 修改: `scripts/defines.ts` + +**执行步骤:** +- [x] 在 baseInputSchema 中新增 `fork` 字段 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:baseInputSchema()` (~L136-152),在 `run_in_background` 字段之后 + - 在 `run_in_background` 字段的闭合 `),` 之后,闭合 `})` 之前,新增: + ```ts + fork: z + .boolean() + .optional() + .describe( + 'Set to true to fork from the parent conversation context. The child inherits full history, system prompt, and model. Requires FORK_SUBAGENT feature flag.', + ), + ``` + - 原因: fork 参数需要在基础 schema 中声明,与 `subagent_type`、`run_in_background` 同级,因为它是所有 agent 调用的可选参数,不限于 multi-agent 场景。 + +- [x] 重构 inputSchema memo 的裁剪逻辑 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:inputSchema()` (~L193-204) + - 将 L194-203 替换为: + ```ts + let schema = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true }); + if (isBackgroundTasksDisabled) { + schema = schema.omit({ run_in_background: true }); + } + if (!isForkSubagentEnabled()) { + schema = schema.omit({ fork: true }); + } + return schema; + ``` + - 同时删除 L196-202 的 GrowthBook 注释块(该注释描述的是旧 `forceAsync` 行为,已不适用)。 + - 原因: fork 字段仅在 `FORK_SUBAGENT` flag 启用时可见;`run_in_background` 不再受 `isForkSubagentEnabled()` 影响,两者独立裁剪。 + +- [x] 更新 AgentToolInput 类型声明 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` (~L211-217),`AgentToolInput` type 定义 + - 在 `z.infer> & {` 的下一行(`name?: string;` 之前),新增 `fork?: boolean;` + - 原因: 类型声明必须包含 `fork` 字段,确保 `call()` 解构时有正确的类型推断。 + +- [x] 更新 inputSchema 附近的 fork gate 注释 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` (~L207-210),`AgentToolInput` 上方的注释 + - 将 L209-210 的注释: + ```ts + // subagent_type is optional; call() defaults it to general-purpose when the + // fork gate is off, or routes to the fork path when the gate is on. + ``` + - 替换为: + ```ts + // subagent_type is optional; call() defaults it to general-purpose. + // fork is gated by FORK_SUBAGENT flag; when omitted or flag is off, no fork. + ``` + - 原因: 旧行为描述与新的显式 fork 触发逻辑不一致,需要更新。 + +- [x] 在 call() 解构中新增 `fork` 参数 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:call()` (~L322-333),参数解构 + - 在 `subagent_type,` 之后(L324),新增 `fork,` + - 原因: `call()` 需要从输入中提取 `fork` 值用于路由判断。 + +- [x] 重构路由逻辑为显式 fork 触发 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:call()` (~L409-414) + - 将 L409-414 替换为: + ```ts + // Fork routing: explicit `fork: true` parameter triggers the fork path + // (inherits parent context and model). Requires FORK_SUBAGENT flag. + // subagent_type is ignored when fork takes effect. + const isForkPath = fork === true && isForkSubagentEnabled(); + const effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType; + ``` + - 原因: 将隐式路由(省略 `subagent_type` 触发 fork)改为显式参数触发(`fork: true`),同时保持 `subagent_type` 省略时走 general-purpose 的原有行为。 + +- [x] 删除 forceAsync 变量及其注释 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:call()` (~L695-697) + - 删除 L695-697(注释 + `const forceAsync = isForkSubagentEnabled();`) + - 原因: `forceAsync` 不再绑定 `isForkSubagentEnabled()`,fork agent 的异步行为由 `run_in_background` 参数控制,与普通 agent 一致。 + +- [x] 从 shouldRunAsync 中移除 forceAsync 条件 + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:call()` (~L708-715) + - 将 L708-715 的 `shouldRunAsync` 计算中的 `forceAsync ||` 移除: + ```ts + const shouldRunAsync = + (run_in_background === true || + selectedAgent.background === true || + isCoordinator || + assistantForceAsync || + (proactiveModule?.isProactiveActive() ?? false)) && + !isBackgroundTasksDisabled; + ``` + - 原因: `forceAsync` 变量已删除,fork agent 不再全局强制异步。 + +- [x] 更新 enableSummarization 使用 isForkPath 替代 isForkSubagentEnabled() + - 位置: `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx:call()` (~L892) + - 将: + ```ts + enableSummarization: isCoordinator || isForkSubagentEnabled() || getSdkAgentProgressSummariesEnabled(), + ``` + - 替换为: + ```ts + enableSummarization: isCoordinator || isForkPath || getSdkAgentProgressSummariesEnabled(), + ``` + - 原因: `enableSummarization` 应仅在当前调用实际走 fork 路径时启用,而非 flag 全局启用。`isForkPath` 是当前调用的运行时判断结果。 + +- [x] 更新 defines.ts 中 FORK_SUBAGENT 的注释 + - 位置: `scripts/defines.ts` (~L55) + - 将: + ```ts + // 'FORK_SUBAGENT', // 已禁用:启用后 prompt 引导模型用 fork(继承父模型)替代 Explore(haiku),导致探索任务使用同等级模型 + ``` + - 替换为: + ```ts + // 'FORK_SUBAGENT', // 已禁用:显式 `fork: true` 参数触发 fork 路径(继承父级上下文和模型),不影响 forceAsync 和探索任务模型选择 + ``` + - 原因: 旧注释描述的是隐式 fork 行为的问题,新注释描述的是当前显式参数触发的设计。 + +- [x] 为路由逻辑重构编写单元测试 + - 测试文件: `packages/builtin-tools/src/tools/AgentTool/__tests__/agentToolUtils.test.ts` + - 测试场景(通过导出路由判断辅助函数或验证 inputSchema 裁剪行为): + - `isForkSubagentEnabled() 返回 false 时`: `inputSchema()` 不包含 `fork` 字段(通过 `.omit({ fork: true })` 裁剪) + - `isBackgroundTasksDisabled 为 true 时`: `inputSchema()` 不包含 `run_in_background` 字段,但仍包含 `fork` 字段 + - 两个条件同时满足时: `inputSchema()` 同时 omit `run_in_background` 和 `fork` + - 运行命令: `bun test packages/builtin-tools/src/tools/AgentTool/__tests__/agentToolUtils.test.ts` + - 预期: 所有测试通过 + +**检查步骤:** +- [x] 验证 `fork` 字段已添加到 baseInputSchema + - `grep -n 'fork:' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx | head -5` + - 预期: 输出至少包含 1 行 schema 定义中的 `fork:` 和 1 行类型中的 `fork?:` + +- [x] 验证 forceAsync 已完全移除 + - `grep -n 'forceAsync' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` + - 预期: 无输出(grep 返回非零退出码) + +- [x] 验证 isForkSubagentEnabled() 在 call() 中仅用于路由判断 + - `grep -n 'isForkSubagentEnabled' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` + - 预期: 仅出现在 `inputSchema()` 的 `!isForkSubagentEnabled()` 裁剪条件和路由的 `fork === true && isForkSubagentEnabled()` 中,不出现在 shouldRunAsync 或 enableSummarization 中 + +- [x] 验证 defines.ts 注释已更新 + - `grep 'FORK_SUBAGENT' scripts/defines.ts` + - 预期: 输出行包含 "显式 `fork: true` 参数触发" + +- [x] 运行 precheck 确认无类型/lint/测试错误 + - `bun run precheck` + - 预期: 零错误通过 + +--- + +### Task 2: Prompt 文本调整 + +**背景:** +[业务语境] — Task 1 将 fork 从隐式行为(省略 `subagent_type` 触发)改为显式参数(`fork: true`),prompt.ts 中的引导文本必须同步更新,否则模型仍会尝试用旧方式触发 fork。 +[修改原因] — 当前 prompt.ts 引导模型"省略 `subagent_type` 以触发 fork"(~L85 `omit \`subagent_type\``),且 forkExamples 中省略了 `subagent_type`(隐式触发)。这些文本与 Task 1 的新路由逻辑矛盾。此外,背景任务说明的显示条件 `!forkEnabled` 不再正确——Task 1 已解耦 forceAsync,fork agent 不再强制异步,背景任务说明应在 fork 启用时也显示。 +[上下游影响] — 本 Task 依赖 Task 1 完成(Task 1 重构了路由逻辑,本 Task 更新对应的 prompt 文本)。本 Task 仅修改 prompt 文本,不影响运行时逻辑。 + +**涉及文件:** +- 修改: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` + +**执行步骤:** + +- [x] 替换 `whenToForkSection` 中的 fork 触发说明 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内 `whenToForkSection` 模板字面量(~L80-97) + - 将 `## When to fork` 标题下的第一段文本(从 "Fork yourself (omit..." 到 "...Do research before jumping to implementation.")替换为: + ``` + When you need to delegate work that benefits from full conversation context (e.g., continuing a multi-file refactor where the child needs the same system prompt and history), use `fork: true`. For most tasks, prefer specialized agent types (Explore, Plan, general-purpose). + ``` + - "Don't peek."、"Don't race."、"Writing a fork prompt." 段落保持不变 + - 原因: 移除"省略 subagent_type"的引导,改为说明 `fork: true` 的适用场景 + +- [x] 更新 `writingThePromptSection` 中的术语 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内 `writingThePromptSection` 模板字面量(~L99-113) + - 将 ~L103 的条件文本从 `'When spawning a fresh agent (with a `subagent_type`), it starts with zero context. '` 替换为 `'When spawning an agent without `fork: true`, it starts with zero context. '` + - 将 ~L110 的条件文本从 `'For fresh agents, terse'` 替换为 `'For non-fork agents, terse'` + - 原因: fork 通过 `fork: true` 显式触发,"fresh agent"与"fork"的对立不再准确,改为"non-fork agents" + +- [x] 替换 `shared` section 中的 fork 使用说明 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内 `shared` 模板字面量(~L208-212) + - 将整个条件分支(`forkEnabled ? ... : ...`)替换为统一文本: + ``` + When using the ${AGENT_TOOL_NAME} tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.${forkEnabled ? ` Set \`fork: true\` to fork from the parent conversation context, inheriting full history and model.` : ''} + ``` + - 原因: 省略 `subagent_type` 现在总是走 general-purpose,统一两分支为基础文本 + fork 追加说明 + +- [x] 移除背景任务说明的 `!forkEnabled` 条件 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内背景任务说明的条件判断(~L259-261) + - 将条件从 `!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS) && !isInProcessTeammate() && !forkEnabled` 改为 `!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS) && !isInProcessTeammate()` + - 原因: Task 1 已解耦 forceAsync,fork agent 不再强制异步,背景任务说明应在 fork 启用时也显示 + +- [x] 更新 continue agent note 中的术语 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内 continue agent 说明(~L267) + - 将条件文本从 `'Each fresh Agent invocation with a subagent_type starts without context — provide a complete task description.'` 替换为 `'Each non-fork Agent invocation starts without context — provide a complete task description.'` + - 原因: 与 writingThePromptSection 保持术语一致 + +- [x] 更新 `forkExamples` 中第一个示例调用,添加 `fork: true` 参数 + - 位置: `packages/builtin-tools/src/tools/AgentTool/prompt.ts` `getPrompt()` 函数内 `forkExamples` 模板字面量(~L120-124) + - 在 `Agent({...})` 调用中 `description:` 行之后添加 `fork: true,` 行 + - 第二个示例(~L133-139)是"mid-wait"场景无工具调用,保持不变;第三个示例(~L141-154)有 `subagent_type: "code-reviewer"` 是 fresh agent 场景,保持不变 + - 原因: 第一个示例展示 fork 用法,需要显式传入 `fork: true` + +- [x] 为 prompt.ts 的 fork 相关文本变更编写单元测试 + - 测试文件: `packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts` + - 测试场景: + - `forkEnabled = true` 时: prompt 不包含 "omit `subagent_type`" 文本,包含 "`fork: true`" 文本 + - `forkEnabled = true` 时: prompt 包含 "non-fork" 术语(替代 "fresh agent") + - `forkEnabled = true` 时: prompt 包含 "Set `fork: true` to fork from the parent" 说明 + - `forkEnabled = true` 时: prompt 包含背景任务说明(`run_in_background`) + - `forkEnabled = false` 时: prompt 不包含 "`fork: true`" 文本,不包含 "When to fork" section + - `forkEnabled = false` 时: prompt 包含 "general-purpose agent" 回退说明 + - Mock 列表: `isForkSubagentEnabled`(返回 true/false)、`getFeatureValue_CACHED_MAY_BE_STALE`(返回 false)、`shouldInjectAgentListInMessages`(返回 false)、`isInProcessTeammate`(返回 false)、`isTeammate`(返回 false)、`getSubscriptionType`(返回 'pro')、`hasEmbeddedSearchTools`(返回 false)、环境变量 `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` 未定义 + - 运行命令: `bun test packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts` + - 预期: 所有测试通过 + +**检查步骤:** +- [x] 验证 prompt 中不再包含 "omit `subagent_type`" 引导文本 + - `grep -n "omit" packages/builtin-tools/src/tools/AgentTool/prompt.ts` + - 预期: 无输出 + +- [x] 验证 prompt 中包含 "`fork: true`" 文本 + - `grep -c "fork: true" packages/builtin-tools/src/tools/AgentTool/prompt.ts` + - 预期: 输出 >= 3(shared section + whenToForkSection + forkExamples) + +- [x] 验证背景任务条件中不再包含 `!forkEnabled` + - `grep -n "forkEnabled" packages/builtin-tools/src/tools/AgentTool/prompt.ts` + - 预期: 所有匹配行均为 `forkEnabled ?` 形式的三元表达式条件,不包含 `!forkEnabled` + +- [x] 运行 prompt 单元测试 + - `bun test packages/builtin-tools/src/tools/AgentTool/__tests__/prompt.test.ts` + - 预期: 所有测试通过 + +- [x] 运行 precheck 确保无回归 + - `bun run precheck` + - 预期: 零错误通过(typecheck + lint + test) + +--- + +### Task 3: Fork Agent 显式参数触发 验收 + +**前置条件:** +- 启动命令: `bun run dev`(开发模式) +- 环境变量: `FEATURE_FORK_SUBAGENT=1` 启用 fork 功能 + +**端到端验证:** + +1. 运行完整测试套件确保无回归 + - `bun run precheck` + - 预期: typecheck + lint + test 全部通过,零错误 + - 失败排查: 检查 Task 1(AgentTool.tsx 路由逻辑)和 Task 2(prompt.ts 文本)的修改 + +2. 验证 `fork: true` + flag 启用时走 fork 路径 + - `grep -n 'isForkPath = fork === true' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` + - 预期: 找到路由逻辑行,确认 `fork === true && isForkSubagentEnabled()` 条件 + - 失败排查: 检查 Task 1 路由逻辑步骤 + +3. 验证 `fork` 参数在 flag 关闭时不在 schema 中 + - `grep -n 'omit.*fork' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` + - 预期: 找到 `schema.omit({ fork: true })` 行 + - 失败排查: 检查 Task 1 inputSchema 裁剪逻辑 + +4. 验证 `forceAsync` 已完全移除,不再绑定 `isForkSubagentEnabled()` + - `grep -c 'forceAsync' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` + - 预期: 0(无匹配) + - 失败排查: 检查 Task 1 forceAsync 删除步骤 + +5. 验证 prompt 中不再引导"省略 subagent_type 触发 fork" + - `grep -c 'omit.*subagent_type' packages/builtin-tools/src/tools/AgentTool/prompt.ts` + - 预期: 0(无匹配) + - `grep -c 'fork: true' packages/builtin-tools/src/tools/AgentTool/prompt.ts` + - 预期: >= 3(shared section + whenToForkSection + forkExamples) + - 失败排查: 检查 Task 2 prompt 文本替换步骤 + +6. 验证后台/同步行为由 `run_in_background` 参数控制 + - `grep -n 'run_in_background' packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx | head -5` + - 预期: `shouldRunAsync` 计算中包含 `run_in_background === true` 条件,无 `forceAsync` 条件 + - 失败排查: 检查 Task 1 shouldRunAsync 修改步骤