feat(plan): keep exit_plan_mode declared after the first plan-mode entry

Disclose the exit tool progressively instead of following every plan-mode
transition. The plan projection records whether a logged plan/mode ever
activated plan mode (stateVersion 4), and the tool's condition admits it
for a step whose selected state is active or once the Session has entered
plan mode. Leaving plan mode now changes only the guidance section, so an
addition-only route no longer drops a declaration from its cached prefix.

A call while plan mode is not committed still fails as unavailable outside
plan mode, and a Session that never entered plan mode does not list the
tool. The Web plan-review fixtures return to master's recording because the
request tool lists no longer change on exit.
This commit is contained in:
Yichen Jiang committed 2026-10-21 17:11:13 +08:00
1 parent 85b29bbcea
commit 752cc5c0c0
21 files changed
+98 -67

No files matched your search

+1 -1
View File
@@ -2465,7 +2465,7 @@ export interface Config {
## `@deepseek-ai/dsh-plan-mode`
- `inject`: `tools` · `systemPrompt` · `sessionProjections` · `agents`
- `source`: [`packages/plan/plan-mode/src/index.ts:66`](../packages/plan/plan-mode/src/index.ts)
- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts)
```ts config-catalog
/** Deployment-owned plan guidance. */
+1 -1
View File
@@ -2467,7 +2467,7 @@ export interface Config {
## `@deepseek-ai/dsh-plan-mode`
- `inject`: `tools` · `systemPrompt` · `sessionProjections` · `agents`
- `source`: [`packages/plan/plan-mode/src/index.ts:66`](../packages/plan/plan-mode/src/index.ts)
- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts)
```ts config-catalog
/** Deployment-owned plan guidance. */
+4 -4
View File
@@ -153,8 +153,8 @@
en: 4294f78d26ed985b
zh: 4294f78d26ed985b
/session-persistence-event-catalog/events/plan/plan-mode-log-only:
en: 862175bfe8692ef7
zh: a8ab123bf674cf77
en: 9ae5537eeae238de
zh: 6be761c7fd83fdce
/session-persistence-event-catalog/events/request:
en: a8df82440ebdf5f8
zh: a8df82440ebdf5f8
@@ -1398,8 +1398,8 @@
en: a74e3dfa51c987af
zh: 6c3c87974d7a7260
/session-persistence-event-catalog/resolved-persistence-types/active~2:
en: a3de6a6fe9672c7a
zh: a357481e27b3b81f
en: c861888d6239b700
zh: a9b2271d3c6b8737
/session-persistence-event-catalog/resolved-persistence-types/agentpreset:
en: 1afbe8196f161601
zh: a320a36a8928817c
+2 -2
View File
@@ -735,7 +735,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:52`](../packages/
'plan/mode': { active: boolean }
```
Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
### `request/*`
@@ -5769,7 +5769,7 @@ Array of `unknown` (opaque).
SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5`
Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
Sources: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
| Property | Presence | Type |
|---|---|---|
+2 -2
View File
@@ -737,7 +737,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'plan/mode': { active: boolean }
```
来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
### `request/*`
@@ -5771,7 +5771,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7`
SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5`
来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+1 -1
View File
@@ -153,7 +153,7 @@
{"digest":"1f1385dbc3e875c021e387c7096d671d606f22daec51b25c80348735b6bbfa10","names":[],"sources":[]},
{"digest":"1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0","names":["packages/core/session/src/types.ts#TurnEndCancelCause"],"sources":["packages/core/session/src/types.ts:197"]},
{"digest":"1fdad624334d354820dfb3a26e28d0bc9fc1b62a33f87bab63f715b3f4391bfd","names":[],"sources":[]},
{"digest":"20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5","names":[],"sources":["packages/plan/plan-mode/src/index.ts:52"]},
{"digest":"20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5","names":[],"sources":["packages/plan/plan-mode/src/index.ts:53"]},
{"digest":"2142e391b9ccb60d487afcf373ce74db98d952f69e8289eddb352a7e16e996c5","names":[],"sources":[]},
{"digest":"2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSource"],"sources":["packages/experimental/agent-team/src/types.ts:138"]},
{"digest":"21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d","names":[],"sources":[]},
+2 -2
View File
@@ -15,8 +15,8 @@
en: ee04468005fae8e1
zh: 1347d554102951f2
/plan-mode/the-exit-tool-and-the-plan-command:
en: 9f2a549447f8d374
zh: 8f3064e31d514702
en: 32ecfb7086dd1578
zh: 0aaeaa69bb63f255
/plan-mode/the-service:
en: f3e6d5a7fe0b67f0
zh: 774cc97e493991fa
+1 -1
View File
@@ -30,7 +30,7 @@ A missing, blank, or non-string `section` and any unknown key fail at plugin loa
## The exit tool and the `/plan` command
[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) is admitted only for steps whose selected plan state is active, so entering or leaving plan mode changes the request tool declarations and the plan guidance together; Native and PTC execution use the same admitted set, and a call outside plan mode returns `UNKNOWN_TOOL`. In plan mode it requires a complete markdown plan starting with a `#` heading and presents it for review through the [user-questions seam](user-questions.md). Approval returns `{ approved: true }` and records a silent (non-narrated) pending exit that is appended at the next accepted in-turn pre-step. Plan guidance therefore remains active for the rest of the assistant's current tool batch, and the tool result itself reports the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again. A dismissed review returns `{ approved: false }` and concludes the turn, so plan mode stays active until the user's next message. A missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode.
[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) is disclosed progressively: it is admitted from the first step whose selected plan state is active and stays admitted for the rest of the Session, so the first plan-mode entry adds it to the request tool declarations and later transitions change only the plan guidance. Native and PTC execution use the same admitted set; a call before the first entry returns `UNKNOWN_TOOL`, and a call while plan mode is not committed fails. In plan mode it requires a complete markdown plan starting with a `#` heading and presents it for review through the [user-questions seam](user-questions.md). Approval returns `{ approved: true }` and records a silent (non-narrated) pending exit that is appended at the next accepted in-turn pre-step. Plan guidance therefore remains active for the rest of the assistant's current tool batch, and the tool result itself reports the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again. A dismissed review returns `{ approved: false }` and concludes the turn, so plan mode stays active until the user's next message. A missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode.
When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off|message]`: bare `/plan` selects plan mode, any other non-empty message selects it and then submits the text through `agent.steer()` so it becomes the next step's ordinary logged user message under plan guidance, and the exact argument `off` selects inactive, which also cancels a pending entry before it is appended and becomes visible to a request.
+1 -1
View File
@@ -30,7 +30,7 @@ interface PlanModeConfig {
## 退出工具与 `/plan` 命令
[`exit_plan_mode`](../tool-catalog.zh.md#deepseek-aidsh-plan-mode) 只在已选计划状态为激活的步骤中准入,因此进入或离开计划模式会同时改变请求的工具声明与计划指引;原生与 PTC 执行使用同一准入集合,在计划模式之外调用会返回 `UNKNOWN_TOOL`。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-questions.zh.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待生效退出,由下一个被接受的轮内 pre-step 追加。因此,计划指引在 assistant 当前这批工具调用的剩余部分继续生效,而工具结果本身会报告这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交。放弃评审返回 `{ approved: false }` 并结束当前轮次,计划模式保持激活,直到用户发出下一条消息。评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。
[`exit_plan_mode`](../tool-catalog.zh.md#deepseek-aidsh-plan-mode) 采用渐进式披露:从已选计划状态首次为激活的步骤起准入,并在会话剩余部分保持准入,因此首次进入计划模式时它进入请求的工具声明,之后的切换只改变计划指引。原生与 PTC 执行使用同一准入集合;首次进入之前调用返回 `UNKNOWN_TOOL`,计划模式未提交时调用会失败。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-questions.zh.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待生效退出,由下一个被接受的轮内 pre-step 追加。因此,计划指引在 assistant 当前这批工具调用的剩余部分继续生效,而工具结果本身会报告这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交。放弃评审返回 `{ approved: false }` 并结束当前轮次,计划模式保持激活,直到用户发出下一条消息。评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。
当 [`ctx.commands`](commands.zh.md) 被组合时,插件注册 `/plan [off|message]`:单独的 `/plan` 选择计划模式;任何其他非空消息先选择计划模式,再通过 `agent.steer()` 提交该文本,使其在计划指引下成为下一步骤的普通已记录用户消息;确切参数 `off` 选择未激活,这还会在待生效条目被追加并对请求可见之前将其取消。
@@ -3,14 +3,14 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/conditional-tools/guide.md
/:
en: 759962a4bf2e454f
zh: df802b222d1a46f3
en: 1dc343c5aa10de02
zh: 59ac1c2052255919
/declare-conditional-tools-through-step-admission:
en: 4edb4c80b402de77
zh: 856160ec69c515ea
/declare-conditional-tools-through-step-admission/change:
en: ba47cb460364f666
zh: 1b5db25e4fb99ac2
en: 71aba6399833cd1d
zh: b820e301baf72e4e
/declare-conditional-tools-through-step-admission/migration:
en: 676839aab96dcd2d
zh: eae3c9a6795f8d6e
en: 943671c4348031bb
zh: 0e44d7b29eab0168
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
description: "Tool declarations come only from step-admitted registrations: `system-prompt/assemble` no longer carries tools, conditional tools need a resolved step set, and `exit_plan_mode` is declared only in plan mode."
description: "Tool declarations come only from step-admitted registrations: `system-prompt/assemble` no longer carries tools, conditional tools need a resolved step set, and `exit_plan_mode` is declared only after a Session first enters plan mode."
---
# Declare conditional tools through step admission
@@ -15,11 +15,11 @@ The `system-prompt/assemble` waterfall now receives sections, contexts, and vari
Model tool calls run only through their step's admitted set. A call to a conditional tool without a resolved set, a call after the set is disposed, and a pending call whose registration was replaced or removed during the step return `UNKNOWN_TOOL` instead of running.
`exit_plan_mode` is declared only for steps whose selected plan state is active, so the request tool list changes when plan mode starts or ends. Existing Sessions require no format migration.
`exit_plan_mode` is declared from the first step whose selected plan state is active and stays declared for the rest of the Session, so requests of a Session that never entered plan mode no longer list it. Existing Sessions require no format migration.
## Migration
1. Replace tool filtering in `system-prompt/assemble` listeners with a `when` condition on the registration; read the step's provider, model, and reasoning effort from the condition's `modelSelection`. Keep only section, context, and variable changes in the waterfall.
2. Make tool guidance follow the same admission: section callbacks pass `context.toolSet` to `ctx.tools.get()` or `ctx.tools.schemas()`.
3. For programmatic calls of conditional tools, call `ctx.tools.resolve({ agent, modelSelection, signal })`, pass the returned `toolSet` to `assemble()` and `ctx.tools.execute()`, and dispose it after the batch settles.
4. Update expectations that listed `exit_plan_mode` outside plan mode. Verify that a disabled tool is absent from the request, that calling its name returns `UNKNOWN_TOOL` without reaching the implementation, and that a failing condition ends the turn with `TOOL_ACTIVATION_FAILED`.
4. Update expectations that listed `exit_plan_mode` before a Session entered plan mode. Verify that a disabled tool is absent from the request, that calling its name returns `UNKNOWN_TOOL` without reaching the implementation, and that a failing condition ends the turn with `TOOL_ACTIVATION_FAILED`.
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
description: "工具声明只来自步骤准入的注册:`system-prompt/assemble` 不再携带工具,条件工具需要已解析的步骤集合,`exit_plan_mode` 只在计划模式下声明。"
description: "工具声明只来自步骤准入的注册:`system-prompt/assemble` 不再携带工具,条件工具需要已解析的步骤集合,`exit_plan_mode` 只在会话首次进入计划模式后声明。"
---
# 通过步骤准入声明条件工具
@@ -15,11 +15,11 @@ description: "工具声明只来自步骤准入的注册:`system-prompt/assemb
模型的工具调用只通过所属步骤的准入集合执行。没有已解析集合的条件工具调用、集合释放之后的调用,以及注册在步骤内被替换或移除时尚未开始的调用,都会返回 `UNKNOWN_TOOL`,不会执行。
`exit_plan_mode` 只在已选计划状态为激活的步骤中声明,因此进入或离开计划模式时请求的工具列表会变化。现有 Session 无需格式迁移。
`exit_plan_mode` 从已选计划状态首次为激活的步骤起声明,并在会话剩余部分保持声明,因此从未进入计划模式的会话,其请求不再列出它。现有 Session 无需格式迁移。
## 迁移
1. 把 `system-prompt/assemble` 监听器中的工具过滤改为注册时的 `when` 条件;从条件的 `modelSelection` 读取该步骤的提供方、模型与推理强度。waterfall 中只保留段落、上下文与变量的修改。
2. 让工具指引跟随同一准入:段落回调把 `context.toolSet` 传给 `ctx.tools.get()` 或 `ctx.tools.schemas()`。
3. 程序化调用条件工具时,调用 `ctx.tools.resolve({ agent, modelSelection, signal })`,把返回的 `toolSet` 传给 `assemble()` 与 `ctx.tools.execute()`,并在批次结束后释放。
4. 更新在计划模式之外仍列出 `exit_plan_mode` 的期望。验证被禁用的工具不出现在请求中,用其名称调用会返回 `UNKNOWN_TOOL` 且不进入实现,条件失败时轮次以 `TOOL_ACTIVATION_FAILED` 结束。
4. 更新在会话进入计划模式之前仍列出 `exit_plan_mode` 的期望。验证被禁用的工具不出现在请求中,用其名称调用会返回 `UNKNOWN_TOOL` 且不进入实现,条件失败时轮次以 `TOOL_ACTIVATION_FAILED` 结束。