---
date: 2026-07-02
type: fix
title: 修复私董会 transient state 残留 + ReAct 工具调用引导不足
status: in-progress
---

## Summary

收尾两个独立 bug：(1) 前端 store-level transient state（`boardState` / `debateState` / `collaborationState`）在 `createConversation` / `selectConversation` / `deleteConversation` 三个动作下的重置口径不一致，导致新建对话后私董会顶部标题残留、跨会话状态泄漏；(2) ReAct 引擎 `_build_tool_use_prompt` 规则 3 "如果不需要工具就能回答，直接回答即可" 给 LLM 留出偷懒窗口，且工具调用提示被后置的 tool section 覆盖，导致复杂需求（涉及外部数据 / 多步分析）下 LLM 倾向于直接回答而非调用 `web_search` / `baidu_search`。Bug 1 覆盖前端 3 个 action 路径对称重置；Bug 2 仅做 L0（提示规则调整），L1（工具描述扩展）与 L2（PLAN_EXEC 启用）按用户决策拆为独立 plan。

## Progress

| Unit | 状态 | 验证 | 提交 |
|------|------|------|------|
| U1 createConversation 重置 | done | 5 前端单测 pass | 7376005 |
| U2 selectConversation 条件重置 | done | 5 前端单测 pass | 7376005 |
| U3 deleteConversation 补全 | done | 5 前端单测 pass | 7376005 |
| U4 ReAct prompt 规则重排 | done | 6 后端单测 pass | 7376005 |
| U5 端到端验证测试 | done | 11 单测全 pass | 7376005 |
| U6 Bug 2 L4 真实 LLM smoke test | done | 3/4 tool-call + 1/1 direct pass | (本 commit) |
| U7 工作树未提交变更清理 | done | git status 干净 + vitest 138/139 pass | 9e2ccf5..44f4f1c |

Bug 2 状态：L4 verified（L0 规则重排在真实 LLM 调用下生效）

L4 smoke test 结果（2026-07-02，bailian-coding/qwen3.7-plus）：
- Probe #1 external_info: PASS（8 次 web_search 调用，99.9s）
- Probe #2 realtime_data: ERROR（120s 超时，非 LLM 不调用工具）
- Probe #3 multi_step: PASS（8 次 web_search 调用，62.6s）
- Probe #4 realtime_data_simple: PASS（3 次 web_search 调用，23.8s）
- Probe #5 no_tool_escape_hatch: PASS（0 次工具调用，直接回答，4.2s）
- 判定：3/4 tool-call pass（达阈值 ≥3/4）+ 1/1 direct pass → L4 verified

PR: http://8.153.107.96/gitea/fischer/fischer-agentkit/pulls/17
ce-code-review: mode:agent, 无 actionable findings
ce-test-browser: agent-browser 已安装（U6 用脚本直接验证，未走前端）

## Problem Frame

**Bug 1：私董会顶部标题在新对话后残留**

根因（[chatStore.ts:333-345](src/agentkit/server/frontend/src/stores/chatStore.ts#L333-L345)）`createConversation()` 仅清空 `streamingSteps`，**未重置** `stream.boardState.value` / `debateState.value` / `collaborationState.value`。`StickyModeHeader.vue:113-117` 的 `mode` computed 依赖 `chatStore.boardState` 渲染"私董会"模式（带旧专家头像行），新对话切换时状态未清空 → 旧私董会标题持续显示。

同源问题散落三处：
- `createConversation` (chatStore.ts:333-345) — 三个 state 全漏
- `selectConversation` (chatStore.ts:219-330) — 仅在 404 分支（line 266-267）重置 board/debate，主流程 line 222 仅重置 collaboration；正常切换不重置 board/debate
- `deleteConversation` (chatStore.ts:348-372) — line 364-365 重置 board/debate，但**漏**了 `collaborationState`

**Bug 2：Agent 面对复杂需求时倾向直接回答**

根因（[react.py:1605-1616](src/agentkit/core/react.py#L1605-L1616)）`_build_tool_use_prompt` 拼接的规则 3：

```
3. 如果不需要工具就能回答，直接回答即可
```

此规则是 LLM 偷懒的合法依据。`base_prompt`（[server/app.py:200-206](src/agentkit/server/app.py#L200-L206)）已有正向引导"当你不确定事实信息、时事新闻或任何你不确信的话题时，你必须先使用搜索工具"，但位置在 system prompt **前部**。**假设**：后注入的 tool section 可能在注意力分配上弱化了 base_prompt 的正向引导（此假设未经 ablation 验证，但规则重排本身的风险可控 — 见 KTD）。`web_search` / `baidu_search` 工具描述也无"何时使用"的触发条件。

**预测（Bug 1）**：若在 `createConversation` 末尾加 `stream.boardState.value = null; stream.debateState.value = null; stream.collaborationState.value = null;`，`StickyModeHeader` 的 `mode` 返回 `null`，`v-if="mode"` 不渲染 → 标题消失；同源问题在 `selectConversation` / `deleteConversation` 也应统一口径。

**预测（Bug 2）**：若规则 3 措辞改为"涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具；仅在确实无需工具时可直接回答"，且**重排为规则 1**（先说"何时必须"，再列"何时不必"），LLM 面对"GitHub Trending + 商业价值分析"类需求时调用 `web_search` 的概率显著提升（实证需 L4 真实 LLM smoke test 验证）。

## Requirements

**R1**（Bug 1）：`createConversation` 末尾重置 `stream.boardState.value = null; stream.debateState.value = null; stream.collaborationState.value = null;`（与现有 `stream.clearConvSteps` 顺序一致，先 stream-owned transient 后 streaming 步骤清理，避免响应式 watcher 误触发）

**R2**（Bug 1）：`selectConversation` 在切换到不同 conversation 时（即非 404 分支、亦非 `selectConversation(sameId)`），替换 line 222 的无条件 `collaborationState` 重置为顶部条件性三 state 重置（仅当 `prevConvId !== id` 时触发 `boardState` / `debateState` / `collaborationState` 置 null），避免 force-reload 同一会话时误清空状态

**R3**（Bug 1）：`deleteConversation` 删除后切换到下一个会话时，若当前会话非 `currentConversationId`，三个 state 不动；若当前会话被删，三个 state 全部置 null（包括 `collaborationState`），与 `selectConversation` 口径一致

**R4**（Bug 2 L0）：`_build_tool_use_prompt` 规则重排为：
```
1. 涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具
2. 每次只调用一个工具
3. 等待工具返回结果后再决定下一步
4. 仅在确实无需工具时可直接回答
5. 不要在回答中重复工具的输出，而是基于结果给出有用的总结
```

**R5**（Bug 2 L0）：保持 base_prompt 不动（按用户决策 L1/L2 拆为独立 plan）

**R6**（Bug 2 L0）：不在本次改 web_search / baidu_search 工具 description

## Key Technical Decisions

- **三个 transient state 的重置时机选择**：选在 `createConversation` / `selectConversation` / `deleteConversation` action 内统一重置（而非在 `useChatStream` 初始化时按 conversationId 拆分 ref），理由是当前 `useChatStream` 是 store-level 单例，stream-owned state 全部是单一 ref。重构为"按 conversationId 拆分的 reactive map"是更大的架构变更（影响所有读取 `stream.boardState` 的组件），不在本 plan 范围内
- **selectConversation 切换检测**：以 `currentConversationId` 是否变化为重置条件（即 `id !== currentConversationId.value` 才重置），避免 `selectConversation(sameId, true)` 这种 force-reload 误清空
- **createConversation 中重置顺序**：先 transient state（board/debate/collaboration），再 `clearConvSteps`。理由：transient state 派生渲染（StickyModeHeader、CollaborationGraph），清空它会触发对应组件卸载；`clearConvSteps` 删除 streaming 步骤，触发 streaming UI 收尾。先 transient 后 streaming 保证 UI 状态先稳定再清理流
- **R4 规则重排而非删除**：保留原规则 3 的语义（"无需工具时可直接回答"）作为规则 4，仅位置后移 + 措辞收敛。**理论选择**：本 plan 的修复理论是"位置优先" — 在同一 prompt block 内，靠前的规则获得更多注意力权重。此理论未经 ablation 验证，但规则重排的回归风险可控（trivial 输入走 DIRECT_CHAT 不进 ReAct）。保留 rule 4 的真实理由：非 trivial 但无需外部工具的输入（如"总结这段文字"、"改写这段话"）会进入 ReAct 循环，此类输入不应被强制工具化，需要一个 escape hatch。"你好/介绍下自己"类 L1 trivial 输入已走 DIRECT_CHAT，不构成保留理由
- **不动 web_search / baidu_search description**：按用户决策推迟到独立 plan。L0 调整后若真实 LLM 行为改善有限再决定 L1
- **不在 L0 引入 PhasePolicy / PLAN_EXEC**：L2（启用 PLAN_EXEC 让复杂需求先输出计划再执行）按用户决策拆为独立 plan
- **测试策略**：Bug 1 用 vitest 单元测试覆盖三个 action 的状态重置矩阵；Bug 2 L0 用 pytest 单元测试断言 `_build_tool_use_prompt` 输出文本包含新规则 + 不包含旧规则 3 措辞；不强制跑真实 LLM（依赖 API key 与网络，且不稳定），但写一个 mock-based test 验证 web_search 描述出现在 prompt 中
- **Bug 2 验收门槛**：L0 文本断言通过后，Bug 2 状态标记为"hypothesis applied, pending L4 verification"（非"fixed"）。在 L1/L2 独立 plan 中包含真实 LLM smoke test：用 5 个 probe query（如"GitHub Trending 分析"、"最新 AI 新闻"等），对比 fix 前后 web_search 调用率，目标 ≥4/5 触发工具调用。L0 plan 不包含此 smoke test 但在 Verification 中显式记录此降级状态

## Implementation Units

### U1. createConversation 补全 transient state 重置

**Goal:** 修复 Bug 1 的第一处泄漏点 — `createConversation` 创建新会话时同步重置 `boardState` / `debateState` / `collaborationState` 三个 stream-owned ref。

**Requirements:** R1

**Dependencies:** 无

**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts（修改 line 333-345 `createConversation`）
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts（追加测试）

**Approach:**
- 在 `createConversation` 的 `stream.clearConvSteps(newConversation.id)` 之前插入三行：
  ```ts
  stream.boardState.value = null;
  stream.debateState.value = null;
  stream.collaborationState.value = null;
  ```
- 不动 `stream.collaborationState` 已有逻辑（line 222 在 `selectConversation` 顶部重置）
- 不动 `pendingConversations` / `pendingLastUsedAt`（与本 bug 无关）

**Patterns to follow:**
- `chatStore.ts:264-281` 的 404 fallback 中 `stream.boardState.value = null; stream.debateState.value = null;` 的写法（line 266-267）— 同样模式
- `chatStore.ts:222` 的 `stream.collaborationState.value = null;` — 单行重置

**Test scenarios:**
- Happy path：调 `createConversation()` 后，`chatStore.boardState === null` 且 `chatStore.debateState === null` 且 `chatStore.collaborationState === null`
- Edge case：先 `selectConversation` 一个含 board_started 的旧会话（boardState 非 null），再 `createConversation()` → 三个 state 全为 null
- Edge case：`createConversation()` 后 `currentConversationId` 指向新会话 ID，且新会话的 streamingStepsByConv entry 被清空（已有 `stream.clearConvSteps` 行为，回归测试）

**Verification:** `cd src/agentkit/server/frontend && npm run test:unit -- --reporter=verbose 2>&1 | grep -E "chatStore|transient"` 通过；手动验证：先开 @board，再点新建对话，StickyModeHeader 不再显示私董会专家头像行

### U2. selectConversation 统一 transient state 重置口径

**Goal:** 修复 Bug 1 的第二处泄漏点 — `selectConversation` 从带 boardState 的会话切到其他会话时残留旧 boardState；同时与 `createConversation` / `deleteConversation` 口径对齐。

**Requirements:** R2

**Dependencies:** 无（与 U1 并行可合入）

**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts（修改 line 219-330 `selectConversation`）
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts（追加测试）

**Approach:**
- 在 `selectConversation` 顶部 line 220 `currentConversationId.value = id` 之后，判断是否切换到不同会话：
  ```ts
  const isSwitching = currentConversationId.value !== id; // 注意：line 220 已写入 id，此处判断为切换条件需对比旧值
  ```
  注：line 220 已先写入新 id，所以需要先把旧 id 缓存到临时变量，再 line 220 写入新 id，再判断
- 若 isSwitching 为 true 且新会话不含 board_started（由 `restoreBoardStateFromMessages` 返回 null），三个 state 置 null：
  - 由于后续 line 307 `stream.boardState.value = restoreBoardStateFromMessages(...)` 会覆盖，所以"先置 null 再被覆盖"是安全的
  - 但若旧会话的 boardState 包含 stream-derived 数据（如 liveColorByName）可能丢失 — 实际上 boardState.value 被整体覆盖为新对象，旧 stream-derived map 在 `allExperts` computed 中会基于新 boardState 重新构建（[StickyModeHeader.vue:160-184](src/agentkit/server/frontend/src/components/chat/StickyModeHeader.vue#L160-L184)），所以无副作用
- 简化方案：直接删除 line 222 的 `stream.collaborationState.value = null;` 无条件重置，替换为 line 219 顶部统一三行：
  ```ts
  const prevConvId = currentConversationId.value;
  currentConversationId.value = id;
  if (prevConvId !== id) {
    stream.boardState.value = null;
    stream.debateState.value = null;
    stream.collaborationState.value = null;
  }
  ```
- 保留 404 分支（line 258-281）的现有逻辑，404 时 `boardState` / `debateState` 也置 null（line 266-267），与主流程口径一致

**Patterns to follow:**
- `chatStore.ts:222` 已有无条件 `collaborationState` 重置 — 升级为条件性三 state 重置
- 404 分支 line 264-281 的多 state 重置 + 切换到下一个会话模式

**Test scenarios:**
- Happy path：从有 boardState 的会话 A 切到会话 B（无 boardState）→ `boardState === null` `debateState === null` `collaborationState === null`
- Edge case：从会话 A 切回 A（force-reload 同一 id）→ 三个 state 保持原值（不被无脑清空）
- Edge case：从会话 A 切到会话 B（也无 boardState）→ 三个 state 保持 null（无变化也无副作用）
- Edge case：404 后 `createConversation()` 流程（已有 fallback 测试）— 三个 state 全 null

**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` 通过；手动验证：开 @board，再点另一普通会话，StickyModeHeader 切到普通模式（不显示私董会头像）

### U3. deleteConversation 补全 collaborationState 重置

**Goal:** 修复 Bug 1 的第三处泄漏点 — `deleteConversation` 删除当前会话时漏了 `collaborationState` 重置，与其他两个 action 口径对齐。

**Requirements:** R3

**Dependencies:** 无（与 U1/U2 并行可合入）

**Files:**
- src/agentkit/server/frontend/src/stores/chatStore.ts（修改 line 362-371 `deleteConversation` 分支）
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts（追加测试）

**Approach:**
- 在 `deleteConversation` 的 `if (currentConversationId.value === id)` 分支 line 364 之后追加一行：
  ```ts
  stream.collaborationState.value = null;
  ```
- 与现有 line 364-365 的 board/debate 重置并列
- 不影响"删除非当前会话"分支（line 357 仅从列表移除，三个 state 不变 — 这是正确行为，因为当前会话不切换）

**Patterns to follow:**
- `chatStore.ts:364-365` 的 `stream.boardState.value = null; stream.debateState.value = null;` 已有模式

**Test scenarios:**
- Happy path：当前会话有 collaborationState（来自 collaboration_graph 消息）→ `deleteConversation(currentId)` → `collaborationState === null`
- Edge case：删除非当前会话 → 当前会话的三个 state 不变（无副作用）
- Edge case：删除当前会话后自动 `createConversation()` → 三个 state 全 null（与 U1 联动）

**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` 通过

### U4. ReAct _build_tool_use_prompt 规则重排 + 措辞调整

**Goal:** 修复 Bug 2 L0 — 重排 `_build_tool_use_prompt` 规则列表，让"何时必须使用工具"排在"何时可以不用工具"之前，并收窄规则 3 的措辞，去除"偷懒窗口"。

**Requirements:** R4, R5, R6

**Dependencies:** 无

**Files:**
- src/agentkit/core/react.py（修改 line 1605-1616 `_build_tool_use_prompt` 返回的 rules 字符串）
- tests/unit/test_react_engine.py（追加测试 `TestReActToolUsePromptRules`）

**Approach:**
- 修改 `return (` 起的多行字符串中规则部分：
  - 现有规则 3 改为规则 4，措辞从"如果不需要工具就能回答，直接回答即可"改为"仅在确实无需工具时可直接回答"
  - 现有规则 1 改为规则 2（语义不变："每次只调用一个工具"）
  - 现有规则 2 改为规则 3（语义不变："等待工具返回结果后再决定下一步"）
  - 现有规则 4 改为规则 5（语义不变："不要在回答中重复工具的输出"）
  - **新增规则 1**（在最前）："涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具"
- 不动 `core_tools` / `extended_tools` 渲染逻辑
- 不动 `_render_core_tools` / `_render_extended_tools` / `_maybe_add_tool_search`
- 不动 `system_prompt` 拼接（line 608-611）— `_build_tool_use_prompt` 仍以同样方式被追加

**Patterns to follow:**
- `react.py:1605-1616` 现有规则结构 — 替换为 5 条而非删除

**Test scenarios:**
- Happy path：调用 `_build_tool_use_prompt([web_search_tool, read_file_tool])` → 输出包含"必须使用工具"且规则序号正确（1 在 2 前）
- Edge case：tools 列表为空 → 走 fast-path（不调用 `_build_tool_use_prompt`），无变化
- 文本断言：输出不包含"如果不需要工具就能回答，直接回答即可"（旧规则 3）
- 文本断言：输出包含"涉及外部信息、实时数据、多步骤分析或你不确定的事实时必须使用工具"（新规则 1）
- 文本断言：输出包含 `<tool_use>` XML 格式示例（保持向后兼容）

**Verification:** `pytest tests/unit/test_react_engine.py -k ToolUsePromptRules` 通过；`pytest tests/unit/test_react_engine.py` 全套通过（不破坏现有 200+ 测试）

### U5. 端到端验证测试（Bug 1 + Bug 2 联动）

**Goal:** 写一个端到端测试覆盖 Bug 1 的前端 store 行为链 + Bug 2 的后端 prompt 文本，验证两个 fix 在测试套件中都被回归保护。

**Requirements:** 全部 R1-R6

**Dependencies:** U1, U2, U3, U4

**Files:**
- src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts（追加 `describe('transient state reset matrix')` 块）
- tests/unit/test_react_engine.py（追加 `describe('Bug 2 L0 prompt rules')` 块）

**Approach:**
- **Bug 1 联动测试**：在 chatStore.test.ts 写一个"建私董会 → 切新对话 → 切回旧私董会"三步流程，断言中间步骤的三个 state 全为 null，最终回到旧私董会时通过 `restoreBoardStateFromMessages` 重建（注意：此 case 测的是 store-level 状态切换，不依赖后端响应）
- **Bug 2 联动测试**：在 test_react_engine.py 写一个"注册 web_search 工具 → 调 `_build_tool_use_prompt` → 断言 prompt 文本"测试，验证新规则 1 出现在 prompt 头部、web_search 工具描述完整（包含 description + parameters）
- 不跑真实 LLM（依赖 API key），仅文本层断言

**Patterns to follow:**
- `chatStore.test.ts:18-81` 的 `boardStartedMsg` / `speechMsg` / `conclusionMsg` fixture 模式
- `test_react_engine.py:402-420` 的 `TestReActSystemPrompt` 模式（mock gateway + 调 execute + 断言 messages）

**Test scenarios:**
- Bug 1 三步流程：建私董会（注入 board_started fixture）→ `createConversation()` → 断言三 state null → `selectConversation(originalId)` → 断言 boardState 重建
- Bug 1 跨 session：建会话 A 含 boardState → `selectConversation(B)`（B 无 board）→ 断言三 state null → `selectConversation(A)` 重新触发 restore → 断言 boardState 重建
- Bug 2 规则顺序：调 `_build_tool_use_prompt` → 用 regex `r'1\.[^2]*2\.'` 断言规则 1 出现在规则 2 之前
- Bug 2 web_search 描述：调 `_build_tool_use_prompt([web_search_tool])` → 断言输出包含 "搜索互联网信息"（description 内容）

**Verification:** `cd src/agentkit/server/frontend && npm run test:unit` + `pytest tests/unit/test_react_engine.py` 全部通过

### U6. Bug 2 L4 真实 LLM smoke test 验证

**Goal:** 验证 U4 的 L0 规则重排在真实 LLM 调用中是否生效 — Agent 面对复杂需求（外部信息 / 实时数据 / 多步分析）时是否调用 `web_search` 而非直接回答。将 Bug 2 状态从 "hypothesis applied, pending L4 verification" 升级为 "verified" 或回退并触发 L1/L2。

**Requirements:** R4 验证闭环

**Dependencies:** U4（已 done），PR #17 合入后执行

**Files:**
- tests/manual/test_react_l4_smoke.py（新建手动 smoke test 脚本，不进 CI）

**Approach:**
- 准备 5 个 probe query，覆盖外部信息 / 实时数据 / 多步分析 / 不确定事实 / 混合类型：
  1. "收集 GitHub Trending 前 10 个项目信息并分析商业价值"（原 bug 复现 query）
  2. "最新 AI 领域有什么重要新闻？"（实时数据）
  3. "对比 React 和 Vue 3 在大型项目中的性能差异"（多步分析 + 外部信息）
  4. "今天上海天气怎么样？"（实时数据，简单）
  5. "请帮我总结这段文字：..."（无需工具，验证 escape hatch 规则 4 仍有效）
- 对每个 query 跑 `ReActEngine.execute()`，记录是否触发 `web_search` tool call
- query 1-4 期望触发工具调用（≥3/4 pass 算通过），query 5 期望不触发
- 如通过率 < 3/4，触发 L1（工具描述扩展）并创建独立 plan
- 使用 `agent-browser` 打开 http://localhost:15173 进行前端层验证（可选）：在 chat 中输入 probe query，观察是否出现 tool_call step

**Test scenarios:**
- probe query 1-4：ReAct 循环中至少出现一次 `web_search` tool call
- probe query 5：ReAct 循环中无 tool call，直接回答
- 回归断言：U4 单测仍 pass（L0 文本未变）

**Verification:** `python3 tests/manual/test_react_l4_smoke.py` 输出报告：5 个 query 的 tool call 统计 + pass/fail 判定。通过后更新本 plan Progress 表 U6 状态为 done，Bug 2 状态升级为 "L4 verified"

### U7. 工作树未提交变更清理

**Goal:** 清理工作树中 32 个来自前序 session 的未提交变更，按 concern 分组提交，使工作树恢复干净状态。避免后续开发时 diff 噪声干扰 review。

**Requirements:** 无（工程治理）

**Dependencies:** 无（可与 U6 并行）

**Files (按 concern 分组):**

| 分组 | 文件 | 来源 session | 建议提交方式 |
|------|------|-------------|-------------|
| A. expert avatar emoji 移除 | configs/experts/*.yaml (15 个) | emoji 移除 plan (2026-07-02-001) | 单独 commit `refactor: expert avatar 改为首字符` |
| B. dev 环境配置 | docker-compose.yaml, scripts/dev-start.sh, docker-compose.dev.yml, .env.dev, src/agentkit/server/config.py | dev 环境修复 session | 单独 commit `fix: dev 环境配置 + 端口隔离` |
| C. board 元数据持久化 | src/agentkit/experts/board_orchestrator.py, src/agentkit/server/routes/chat.py | 私董会持久化修复 session | 单独 commit `fix: board_speech/round_summary 持久化 avatar/color 元数据` |
| D. 前端方案B + board UI | StickyModeHeader.vue, expertIdentity.ts, useMessageRenderer.ts, BoardRoundCard.vue, MessageShell.vue, Scene4BoardDiscussion.vue, chatStream.ts, types.ts, LoginView.vue | 方案B + 私董会限制 session | 需 review 后 commit，部分可能已通过 PR #15 合入 |
| E. .understand-anything | .understand-anything/* (3 tracked + untracked) | knowledge graph 工具 | 加入 .gitignore 或单独 commit |
| F. 未跟踪 plan/brainstorm 文档 | docs/brainstorms/2026-07-02-*.md, docs/plans/2026-07-02-001-*.md | ce-plan/ce-brainstorm 产物 | commit 为决策记录 |

**Approach:**
- 对每组 `git diff HEAD -- <files>` 检查变更内容，确认无冲突
- 分组 commit，每组 commit message 遵循 conventional commits 格式
- D 组需特别注意：检查是否与已合入的 PR #15 内容重叠，避免重复提交
- E 组（.understand-anything）：建议加入 .gitignore 而非 commit（是工具生成的本地索引）
- 如某组变更属于未完成的 feature（如方案B），则 stash 而非 commit，待对应 plan 完成后再提交

**Test scenarios:**
- 每组 commit 后 `git status --short` 该组文件不再出现
- 全部完成后 `git status --short` 仅显示 untracked（.understand-anything 等 gitignored 项）
- `npm run typecheck` + `ruff check src/` 仍通过（确认无回归）

**Verification:** `git status --short` 输出为空（或仅 gitignored 项）；`npm run test:unit` + `pytest tests/unit/ -x -q` 全套通过

## Out of Scope

- **L1（工具描述扩展）**：web_search / baidu_search / web_crawl 工具 description 添加"何时使用"触发关键词（如"需要最新互联网信息、新闻、Trending、股价时使用 web_search"）。按用户决策推迟为独立 plan
- **L2（PLAN_EXEC 启用）**：在 default agent 上注入 `PhasePolicy` 让复杂需求先输出 Plan 再执行。涉及 phase 配置、auto-advance 阈值、违规处理、phase event WS 协议。影响面较大，按用户决策拆为独立 plan
- **重构 stream-owned state 为按 conversationId 拆分**：当前是 store-level 单 ref，导致每次"切换会话"必须显式重置。改为 `Map<conversationId, BoardState>` 可从根上消除泄漏，但影响所有 `chatStore.boardState` 读取点（StickyModeHeader / useMessageRenderer 等），属于架构重构
- **base_prompt 调整**：保持原样，按用户决策
- **私董会生命周期 / Skill 路由策略 / tool registry 架构 / LLM gateway**：均不动

## Risks & Dependencies

- **R-U4-1**（低）：规则重排可能影响现有 LLM 行为 — 风险点在某些 LLM 训练分布下"正向规则 1"可能让 LLM **过度工具化**（trivial 输入也调工具）。缓解：测试覆盖 trivial 输入走 DIRECT_CHAT 不进 ReAct 循环（request_preprocessor 已保证）；pytest 单元测试断言新规则在 prompt 中但不验证 LLM 行为
- **R-U1/2/3-1**（低）：state 重置顺序在**同步代码路径**（createConversation、deleteConversation 重置块）中不会触发中间态渲染 — Vue 响应式批量更新在 microtask 中合并。但 `selectConversation` 有 async 路径：`await apiClient.getConversation(id)` 位于顶部 reset（boardState → null）和 post-fetch restore（line 307 `restoreBoardStateFromMessages`）之间。在 fetch 期间 Vue 会渲染一帧 boardState=null，导致 StickyModeHeader 卸载再重载。这是期望行为（"无旧数据残留"），非 race condition。切换两个私董会会话时 header 会短暂消失再出现 — 若需平滑过渡可在 follow-up 中加 skeleton placeholder
- **D-frontend-build**（低）：前端改动需要重新 build static（`npm run build:frontend`）才能被 backend 静态服务拾取。AGENTS.md 已记录此风险

## Deferred to Follow-Up Work

- L1：web_search / baidu_search 工具 description 扩展（独立 plan）— 仅在 U6 L4 smoke test 未通过时触发
- L2：启用 PLAN_EXEC phase policy 处理复杂需求（独立 plan）— 仅在 U6 通过但工具调用率仍不理想时触发
- 重构 stream-owned state 为按 conversationId 拆分（架构性，独立 plan）

## Verification (per unit, summary)

- U1/U2/U3：`cd src/agentkit/server/frontend && npm run test:unit` 全套通过；新增 3 个 `describe` 块共 8+ test cases
- U4：`pytest tests/unit/test_react_engine.py -k ToolUsePromptRules` 通过；新增 1 个 test class 4-5 个 test cases（已 done）。**Bug 2 状态声明**：L0 文本断言通过后 Bug 2 标记为 "hypothesis applied, pending L4 verification"（非 "fixed"），真实 LLM smoke test 在 U6 中执行
- U5：完整套件通过；端到端 4-5 个联动测试（已 done，11 单测全 pass）
- U6：`python3 tests/manual/test_react_l4_smoke.py` 输出 5 个 probe query 的 tool call 统计；≥3/4 外部信息 query 触发 web_search + query 5 不触发 → Bug 2 状态升级为 "L4 verified"
- U7：`git status --short` 为空（或仅 gitignored 项）；`npm run test:unit` + `pytest tests/unit/ -x -q` 全套通过
- 集成：`python3 -m pytest tests/unit/ -x -q`（AGENTS.md 硬约束） + `cd src/agentkit/server/frontend && npm run test:unit` 通过
- Lint：`ruff check src/ && ruff format src/`（AGENTS.md 硬约束）通过
- TypeScript：`cd src/agentkit/server/frontend && npm run typecheck` 通过
