Skip to content

feat(core): auto-reveal exit_plan_mode tool when entering plan mode - #5311

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
aspnmy:feat/exit-plan-mode-auto-reveal
Jun 18, 2026
Merged

feat(core): auto-reveal exit_plan_mode tool when entering plan mode#5311
wenshao merged 1 commit into
QwenLM:mainfrom
aspnmy:feat/exit-plan-mode-auto-reveal

Conversation

@aspnmy

@aspnmy aspnmy commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does

When the model calls enter_plan_mode, this PR automatically reveals the exit_plan_mode deferred tool so the model can invoke it without needing to search for it first. The implementation mirrors the existing pattern in ToolSearch's select path: reveal the deferred tool, sync the tool declaration set via setTools(), and roll back the reveal if the sync fails to keep the registry consistent with the chat's declaration list.

Why it's needed

Currently, after entering plan mode, the model must call ToolSearch to discover exit_plan_mode before it can transition back to implementation. This adds an unnecessary round-trip. The reveal-on-enter pattern eliminates that friction while preserving correctness through the rollback-on-failure guard.

Reviewer Test Plan

How to verify

  1. Start a session and enter plan mode (/plan or have the model call enter_plan_mode).
  2. Observe that exit_plan_mode appears in the available tool set without the model calling ToolSearch.
  3. Confirm plan mode exit works correctly by calling exit_plan_mode.
  4. Optionally: simulate a setTools failure by injecting a network error after reveal, verify the reveal is rolled back and the model can still use ToolSearch as fallback.

Evidence (Before & After)

N/A (non-UI, tool registry internal change).

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows
🐧 Linux
  • Linux: container build + typecheck + lint + esbuild all pass.
  • Windows: standalone build + smoke test pass.

Risk & Scope

  • Main risk or tradeoff: setTools() is an async IPC round-trip; if it fails, the reveal is rolled back so the model can still discover exit_plan_mode via ToolSearch. No hard dependency introduced.
  • Not validated / out of scope: macOS not tested (no darwin environment available).
  • Breaking changes / migration notes: None. This is a purely additive change to an existing tool invocation.

Linked Issues

N/A (self-discovered improvement, not from an upstream issue).

中文说明

本 PR 做了什么

当模型调用 enter_plan_mode 进入计划模式时,自动揭示 exit_plan_mode 延迟工具,使模型无需先调用 ToolSearch 即可直接退出计划模式。实现方式与 ToolSearch 的选择路径一致:揭示延迟工具 → 通过 setTools() 同步工具声明 → 同步失败时回滚揭示,保持注册表与聊天声明列表一致。

为什么需要

当前进入计划模式后,模型必须先调用 ToolSearch 才能发现 exit_plan_mode,多了一轮不必要的往返。进入时自动揭示消除了这个摩擦,同时通过回滚保护保证正确性。

审查者测试计划

验证方法

  1. 启动会话,进入计划模式(/plan 或让模型调用 enter_plan_mode)。
  2. 观察 exit_plan_mode 出现在可用工具列表中,无需模型调用 ToolSearch
  3. 确认调用 exit_plan_mode 能正确退出计划模式。
  4. 可选:在揭示后注入网络错误模拟 setTools 失败,验证揭示被回滚,模型仍可通过 ToolSearch 作为回退方案。

测试环境

系统 状态
🍏 macOS ⚠️
🪟 Windows
🐧 Linux
  • Linux:容器内 typecheck + lint + esbuild 全部通过。
  • Windows:standalone 构建 + 冒烟测试通过。

风险与范围

  • 主要风险:setTools() 是异步 IPC 往返;失败时揭示被回滚,模型仍可通过 ToolSearch 发现 exit_plan_mode。无硬依赖。
  • 未验证:macOS(无可用的 darwin 环境)。
  • 破坏性变更:无。纯增量修改。

关联 Issue

无(本地自行挖掘的改进,非来自上游 Issue)。

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a genuine friction point — after entering plan mode, the model has to make an unnecessary ToolSearch round-trip just to find exit_plan_mode. Auto-revealing it on entry is the obvious fix. Claude Code's CHANGELOG is full of plan mode refinements (permission handling, session resume, compaction), so this area is clearly a shared concern across agent CLIs. Aligned.

On approach: 32 lines, one file, zero scope creep. The implementation mirrors the existing ToolSearch reveal pattern exactly. I don't see a simpler path — this is already the minimal change. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个真实的摩擦点——进入计划模式后,模型必须额外调用一次 ToolSearch 才能找到 exit_plan_mode。进入时自动揭示是显而易见的修复。Claude Code 的 CHANGELOG 中有大量计划模式的改进(权限处理、会话恢复、压缩等),说明这个领域是各 agent CLI 共同关注的重点。方向对齐。

方案:32 行,一个文件,零范围蔓延。实现完全复用了现有的 ToolSearch 揭示模式。没有更简路径了——这已经是最小改动。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The implementation mirrors ToolSearch's reveal pattern faithfully — revealDeferredToolsetTools → rollback on failure. I compared it against my own proposal (which was essentially the same approach) and the PR adds one detail I hadn't thought of: the isDeferredToolRevealed guard for idempotency when plan mode is re-entered. Good catch.

Two observations, neither blocking:

Missing refreshStartupContextReminder(): ToolSearch calls this after setTools() succeeds. The PR skips it. Not a bug — refreshStartupContextReminder re-prepends the workspace/env prelude in chat history, which has nothing to do with tool declarations. The API-level tool sync via setTools() is the important part and it's there. Calling refreshStartupContextReminder would be belt-and-suspenders for consistency with ToolSearch, but it's not needed for correctness.

No unit tests: ToolSearch has 33 tests covering the reveal/rollback paths. The PR adds no tests for the new reveal logic in enterPlanMode.ts. The behavior is simple enough that the integration test (actually entering plan mode and observing the tool appears) is the most meaningful verification, but a unit test covering the happy path and the setTools-failure rollback would be a good safety net. Not blocking, but worth adding in a follow-up.

Build & Test Results

$ npm run build
✅ Build successful, 0 errors

$ npm run lint
✅ Lint clean (0 errors)

$ cd packages/core && npx vitest run src/tools/tool-search.test.ts
✓ 33 tests passed (reveal, rollback, ensureTool failure paths all green)

Real-Scenario Test (tmux)

$ node dist/cli.js -p 'enter plan mode'
Unable to enter plan mode — the current session is running in non-interactive mode,
which doesn't support the interactive gate required by `enter_plan_mode`.

$ node dist/cli.js -p 'hello' -d
Debug mode enabled
Logging to: /home/runner/.qwen/debug/fc427ecb-8da9-4f26-89d0-5164f789397e.txt
Hello! How can I help you today?

No API keys available in this CI environment, so interactive plan mode (where the auto-reveal actually triggers) can't be exercised. The non-interactive guard works correctly — plan mode is properly blocked before the reveal code is reached, which is the expected behavior. The CLI starts and responds to prompts without issues.

Verdict on testing: build/lint pass, related unit tests pass, CLI smoke test passes. The actual auto-reveal behavior requires an interactive session with API access to verify end-to-end.

中文说明

代码审查

实现忠实地复用了 ToolSearch 的揭示模式——revealDeferredToolsetTools → 失败回滚。与我自己的方案对比后,PR 多了一个我没想到的细节:isDeferredToolRevealed 保护确保重复进入计划模式时的幂等性。

两个观察,均不构成阻塞:

缺少 refreshStartupContextReminder() ToolSearch 在 setTools() 成功后会调用它。PR 跳过了。不是 bug——该函数刷新的是聊天历史中的工作区/环境前言,与工具声明无关。API 级别的工具同步通过 setTools() 已经完成。为与 ToolSearch 保持一致可以加上,但不影响正确性。

没有单元测试: ToolSearch 有 33 个测试覆盖揭示/回滚路径。PR 没有为 enterPlanMode.ts 的新逻辑添加测试。行为足够简单,集成测试(实际进入计划模式观察工具是否出现)是最有意义的验证方式,但覆盖正常路径和 setTools 失败回滚的单元测试会是很好的安全网。不阻塞,建议后续补充。

测试

构建、Lint、相关单元测试全部通过。无 API 密钥,无法在 CI 中测试交互式计划模式的实际揭示行为。非交互模式正确阻止了计划模式进入。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stepping back: this is a clean, focused PR that solves a real friction point. The model shouldn't need to search for a tool it's about to need — auto-revealing exit_plan_mode on plan mode entry is the kind of small improvement that compounds over a session.

The implementation is exactly what I would have written (and then some — the idempotency guard is a nice touch). 32 lines in one file, mirrors the existing ToolSearch pattern, proper error handling with rollback, and non-fatal on failure so plan mode entry always succeeds.

Build, lint, and all 33 related unit tests pass. I couldn't exercise the interactive plan mode path in CI (no API keys), but the code path is straightforward and well-guarded.

The missing unit tests are a fair observation but not a blocker — the behavior is simple and the integration path is the meaningful verification. Worth following up, not worth blocking on.

LGTM ✅

中文说明

总结:这是一个干净、聚焦的 PR,解决了真实的摩擦点。模型不应该需要搜索一个即将用到的工具——进入计划模式时自动揭示 exit_plan_mode 是那种在会话中不断累积的小改进。

实现与我自己的方案一致(甚至更好——幂等保护是加分项)。32 行代码,一个文件,复用现有 ToolSearch 模式,有完善的错误处理和回滚,揭示失败时计划模式进入仍然成功。

构建、Lint、33 个相关单元测试全部通过。CI 环境中无 API 密钥无法测试交互式计划模式路径,但代码路径简单且有充分的保护。

缺少单元测试是合理的观察但不构成阻塞——行为简单,集成测试是有意义的验证方式。建议后续补充。

可以合并 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Real-build verification — behavior correct; one ask (add a test)

I built the actual qwen core from this PR's head (a72f89c5f1) in an isolated worktree and drove the real compiled EnterPlanModeTool.execute() against the real ToolRegistry reveal methods, in a tmux pane, A/B vs a pre-fix build (same tree, only enterPlanMode.ts reverted to base). This PR is an internal tool-registry change with no UI surface, so the "real run" is the shipped execute() on the real reveal Set — not keystrokes.

What I checked

1. CI / existing tests — green. All checks pass (Lint, CodeQL, Test ×3 OS); the existing enterPlanMode.test.ts still passes. ⚠️ But this PR adds no test for the new auto-reveal behavior (see the ask at the end).

2. API & pattern correctness. The PR calls real registry API — revealDeferredTool / unrevealDeferredTool / isDeferredToolRevealed (tool-registry.ts) and ToolNames.EXIT_PLAN_MODE — and faithfully mirrors tool-search.ts's select path (reveal → setTools() → roll back on throw), including the same "orphaned-reveal" reasoning the rollback guards against.

3. Behavioral harness — A/B over 4 scenarios (real execute(), real reveal Set):

scenario PRE-FIX FIXED (this PR)
A normal enter exit_plan_mode not revealed (revealed=false, setTools not called) revealed=true, setTools synced once
B setTools() throws — (no reveal code) reveal rolled backrevealed=false; plan mode still entered (non-fatal)
C already revealed setTools not called again — idempotent !revealedBefore guard
D non-interactive, no ACP guarded — no reveal guarded — no reveal

Scenario A is the headline: the PR is precisely what flips exit_plan_mode to revealed on plan-mode entry; without it, the model still has to ToolSearch for it. B confirms the rollback actually fires and doesn't break plan-mode entry. C/D confirm no redundant re-sync and correct gating.

raw tmux capture (fixed vs pre-fix)
BUILD=fixed  realRegistry=true
  A reveal-success               revealed=true  setToolsCalls=1 approval=plan    | Entered plan mode.
  B rollback-on-setTools-throw   revealed=false setToolsCalls=1 approval=plan    | Entered plan mode.
  C idempotent-already-revealed  revealed=true  setToolsCalls=0 approval=plan    | Entered plan mode.
  D non-interactive-guard        revealed=false setToolsCalls=0 approval=default | Plan mode unavailable...

BUILD=prefix realRegistry=true
  A reveal-success               revealed=false setToolsCalls=0 approval=plan    | Entered plan mode.
  B rollback-on-setTools-throw   revealed=false setToolsCalls=0 approval=plan    | Entered plan mode.
  C idempotent-already-revealed  revealed=true  setToolsCalls=0 approval=plan    | Entered plan mode.
  D non-interactive-guard        revealed=false setToolsCalls=0 approval=default | Plan mode unavailable...

Notes

  • Please add a regression test. A change to the plan-mode flow should have one, and there's already an enterPlanMode.test.ts to extend — the 4 scenarios above (especially A reveal + B rollback) are a ready template. Non-blocking for correctness, but worth it before this silently regresses later.
  • Minor (non-blocking): if getGeminiClient() were ever null, the tool is revealed without a setTools() sync and that path isn't rolled back — but the client is always present at plan-mode entry in a live session, so this is defensive only and self-heals on the next setTools().

Verdict: behavior is correct, matches the proven ToolSearch pattern, no regression — LGTM to merge. Recommend adding the unit test the PR currently omits. 👍

中文说明(完整对应)

✅ 真实构建验证 —— 行为正确;一个请求(补测试)

我在隔离 worktree 里从本 PR 的 head(a72f89c5f1)构建了真实 core,并在 tmux 中用真实编译EnterPlanModeTool.execute()真实 ToolRegistry 的 reveal 方法做了 A/B(同一棵树,只把 enterPlanMode.ts 回退到 base)。本 PR 是无 UI 的工具注册表内部改动,所以"真实运行"是直接跑发布的 execute() 及真实 reveal Set,而非击键。

我检查了什么

1. CI / 既有测试 —— 全绿。 所有检查通过(Lint、CodeQL、三平台 Test);既有的 enterPlanMode.test.ts 仍通过。⚠️ 但本 PR 没有为新增的自动 reveal 行为加任何测试(见末尾请求)。

2. API 与模式正确性。 PR 调用的是真实注册表 API —— revealDeferredTool / unrevealDeferredTool / isDeferredToolRevealed(tool-registry.ts)与 ToolNames.EXIT_PLAN_MODE —— 并忠实复刻了 tool-search.ts 的 select 路径(reveal → setTools() → 抛错时回滚),连"孤儿 reveal"的处理理由都一致。

3. 行为 harness —— 4 个场景的 A/B(真实 execute()、真实 reveal Set):

场景 PRE-FIX FIXED(本 PR)
A 正常进入 exit_plan_mode reveal(revealed=false,未调用 setTools) revealed=true,setTools 同步一次
B setTools() 抛错 ——(无 reveal 代码) reveal 回滚revealed=false;仍成功进入 plan mode(非致命)
C 已 reveal —— 不再调用 setTools —— !revealedBefore 幂等守卫
D 非交互、无 ACP 守卫 —— 不 reveal 守卫 —— 不 reveal

场景 A 是核心:本 PR 正是进入 plan mode 时把 exit_plan_mode 翻成 revealed 的原因;没有它,模型仍需 ToolSearch 才能找到。B 证明回滚确实触发且不破坏进入 plan mode。C/D 证明无冗余同步、门控正确。

备注

  • 请补一个回归测试。 改动 plan-mode 流程理应有测试,而且已有 enterPlanMode.test.ts 可扩展 —— 上面 4 个场景(尤其 A reveal + B 回滚)就是现成模板。对正确性非阻塞,但值得在它将来被悄悄改坏之前补上。
  • 次要(非阻塞):若 getGeminiClient() 返回 null,工具会被 reveal 但不做 setTools() 同步,且该路径不回滚 —— 但实时会话进入 plan mode 时 client 必然存在,所以这只是防御性分支,且会在下次 setTools() 时自愈。

结论:行为正确、与成熟的 ToolSearch 模式一致、无回归 —— LGTM 可合并。建议补上本 PR 目前缺失的单元测试。 👍

@wenshao
wenshao merged commit f3803ed into QwenLM:main Jun 18, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants