@deepseek-ai/dsh-plan-mode 0.1.0-rc.7 → 0.1.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/plan/plan-mode/README.md
5
- README.md: 7171997406ea43487762d9947d07426df400c78a
6
- README.zh.md: 5c04cdabc293c9abda6bb5e77f596715ee137c6f
5
+ README.md: 3eabe2cb3f04b434b7f908f7beca869f1022a59e
6
+ README.zh.md: f7d6a1f8e9f5ba95f8aad9457f3dde5fc415fdcf
package/README.md CHANGED
@@ -16,13 +16,13 @@ While active, `plan:policy` renders the configured `section`. The plugin always
16
16
 
17
17
  The review question declares the `plan-review` presentation intent, naming `Approve` as the label that approves it, so a capable UI presents the plan as a decision instead of a generic question; the answer the tool reads is the same either way. A dismissed review — the user closing the request to speak instead — is reported to the model as such, telling it to stay in plan mode and wait for the message; every other review failure keeps the seam's own message.
18
18
 
19
- When `ctx.commands` is composed, the package registers `/plan [message]` and reserves the exact argument `off` for direct exit. Bare `/plan` selects plan mode; any other non-empty argument selects it first and is then submitted through `agent.steer()`, so it becomes the next step's ordinary logged user message under plan guidance. `/plan off` selects inactive without sending model input; it also cancels a pending entry before plan mode reaches a request.
19
+ When `ctx.commands` is composed, the package registers `/plan [message]` and reserves the exact argument `off` for direct exit. Bare `/plan` selects plan mode; any other non-empty argument selects it first and is then submitted through `agent.steer()`, so it becomes the next step's ordinary logged user message under plan guidance. `/plan off` selects inactive without sending model input; it also cancels a pending entry before plan mode reaches a request. The command declares `input.images`: composer image attachments ride the steered message ahead of its text block. Bare `/plan` with images steers an image-only user message, while `/plan off` with images returns a direct error before any mode change so the composer keeps them.
20
20
 
21
21
  The Web client consumes the plugin-owned `/plan` command; other entry points may drive the same service directly without defining a second mode vocabulary.
22
22
 
23
23
  ## Session projection
24
24
 
25
- When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md)), this package registers the `plan` projection unit under an injected child. The unit folds two event kinds: a `command/run` record named `plan` with recorded `args` sets the wanted target (`off` → inactive, anything else → active), and `plan/mode` commits the logged state and clears it; every other event returns the same state reference. `view` derives `{ active, pending }`, where `pending` is true only while an outstanding selection differs from the logged state — a pure replay quantity, so host restarts, other tabs, and cold reads all recover it from the log alone (the `/plan` handler calls `set()` before any failing path, so a failed handler cannot leave a recorded command without its plan selection). The key merges into `SessionProjectionMap` from `src/types.ts` (served to host consumers via `./types` and client aggregates via `./client`); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected.
25
+ When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md)), this package registers the `plan` projection unit under an injected child. A `command/run` record named `plan` with recorded `args` starts a candidate target (`off` → inactive, anything else → active); its paired `command/done` retains a successful selection and drops an error; `plan/mode` commits the logged state and clears the retained selection. Every other event returns the same state reference. `view` derives `{ active, pending }`, where `pending` is true only while an unsettled or successful selection differs from the logged state. This remains a pure replay quantity, so host restarts, other tabs, and cold reads recover it from the log alone, and a rejected `/plan off` with images cannot leave a pending exit. The key merges into `SessionProjectionMap` from `src/types.ts` (served to host consumers via `./types` and client aggregates via `./client`); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected.
26
26
 
27
27
  ## Configuration
28
28
 
@@ -65,11 +65,11 @@ The section is stable within plan mode, but entering or leaving changes the syst
65
65
 
66
66
  #### What the model sees
67
67
 
68
- `/plan`, `/plan off`, and their terminal results stay outside model history. A non-empty suffix other than the exact `off` argument becomes one trimmed user text block through `agent.steer()` after plan mode is selected. An active `/plan off` selection contributes the standard logged user-switch notice only when the last request header described plan mode; cancelling a pending entry contributes none because no request observed it.
68
+ `/plan`, `/plan off`, and their terminal results stay outside model history. A non-empty suffix other than the exact `off` argument becomes one user message through `agent.steer()` after plan mode is selected: any admitted image attachments as leading image blocks, then the trimmed text block. Bare `/plan` with admitted images steers one user message containing only those image blocks. An active `/plan off` selection contributes the standard logged user-switch notice only when the last request header described plan mode; cancelling a pending entry contributes none because no request observed it.
69
69
 
70
70
  #### Token effect
71
71
 
72
- The optional message costs the same history tokens as submitting that text separately; bare `/plan` and `/plan off` add none. A narrated active exit adds the small retained switch notice.
72
+ The optional message costs the same history tokens as submitting that content separately. Bare `/plan` without images and `/plan off` add none; bare `/plan` with images has the normal image-prompt cost. A narrated active exit adds the small retained switch notice.
73
73
 
74
74
  #### KV Cache effect
75
75
 
package/README.zh.md CHANGED
@@ -16,13 +16,13 @@
16
16
 
17
17
  评审问题声明 `plan-review` 呈现意图,并指名 `Approve` 为表示批准的标签,因此有能力的 UI 会把计划呈现为一次决定而非通用问题;两种情况下该工具读到的回答完全相同。放弃审阅——用户关闭请求,转而发言——会如实报告给模型,要求它留在 plan mode 中等待那条消息;其余每一种评审失败都保留 seam 自身的消息。
18
18
 
19
- 组合 `ctx.commands` 时,该包会注册 `/plan [message]`,并将参数恰好为 `off` 的情况保留给直接退出。不带参数的 `/plan` 会启用 plan mode;任何其他非空参数都会先启用 plan mode,再通过 `agent.steer()` 提交,因此它会在 plan 引导下成为下一步骤的常规已记录用户消息。`/plan off` 会选择停用状态,不发送模型输入;它还可以在启用 plan mode 的待处理选择由轮内 pre-step 追加之前将其取消。
19
+ 组合 `ctx.commands` 时,该包会注册 `/plan [message]`,并将参数恰好为 `off` 的情况保留给直接退出。不带参数的 `/plan` 会启用 plan mode;任何其他非空参数都会先启用 plan mode,再通过 `agent.steer()` 提交,因此它会在 plan 引导下成为下一步骤的常规已记录用户消息。`/plan off` 会选择停用状态,不发送模型输入;它还可以在启用 plan mode 的待处理选择由轮内 pre-step 追加之前将其取消。该命令声明了 `input.images`:composer 图片附件会随被 steer 的消息一起提交,位于文本块之前。不带参数的 `/plan` 若附有图片,会 steer 一条只含图片的用户消息;`/plan off` 若附有图片,会在任何模式变更前直接返回错误,composer 保留图片。
20
20
 
21
21
  Web 客户端使用该插件提供的 `/plan` 命令;其他入口可以直接驱动同一服务,无需定义第二套 mode 词汇。
22
22
 
23
23
  ## 会话投影
24
24
 
25
- 当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。该单元折叠两类事件:名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会设置目标状态(`off` → 未激活,其余 → 激活),`plan/mode` 会提交已记录状态并清除该目标;其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在尚未落实的选择与已记录状态不同时为 true。该值完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它。`/plan` 处理器会在任何可能失败的路径之前调用 `set()`,因此处理器失败时不会留下缺少对应 plan 选择的已记录命令。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。
25
+ 当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会开始一个候选目标(`off` → 未激活,其余 → 激活);与它配对的 `command/done` 保留成功选择并丢弃错误选择;`plan/mode` 提交已记录状态并清除已保留的选择。其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true。该值仍完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它,被拒绝的带图 `/plan off` 也不会留下待退出状态。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。
26
26
 
27
27
  ## 配置
28
28
 
@@ -65,11 +65,11 @@ You are in plan mode. Explore and design before presenting the complete plan thr
65
65
 
66
66
  #### 模型所见内容
67
67
 
68
- `/plan`、`/plan off` 及其终端结果留在模型历史之外。除恰好为 `off` 以外的非空后缀会在选择 plan mode 后,通过 `agent.steer()` 成为一个已去除首尾空白的用户文本块。plan mode 已激活时,选择 `/plan off` 只会在最后一个请求头描述了 plan mode 的情况下追加标准的已记录用户切换通知;取消待生效进入不会贡献通知,因为没有请求观测到它。
68
+ `/plan`、`/plan off` 及其终端结果留在模型历史之外。除恰好为 `off` 以外的非空后缀会在选择 plan mode 后,通过 `agent.steer()` 成为一条用户消息:任何已准入的图片附件作为前置图片块,之后是已去除首尾空白的文本块。不带参数的 `/plan` 若带有已准入图片,会 steer 一条只含这些图片块的用户消息。plan mode 已激活时,选择 `/plan off` 只会在最后一个请求头描述了 plan mode 的情况下追加标准的已记录用户切换通知;取消待生效进入不会贡献通知,因为没有请求观测到它。
69
69
 
70
70
  #### Token 影响
71
71
 
72
- 可选消息的历史 token 成本与单独提交该文本相同;不带参数的 `/plan` 和 `/plan off` 不增加 token。一次带有切换通知的已激活状态退出会追加一条简短且会保留的通知。
72
+ 可选消息的历史 token 成本与单独提交该内容相同。不带图片和参数的 `/plan` 与 `/plan off` 不增加 token;不带参数但带图的 `/plan` 产生常规图片提示词成本。一次带有切换通知的已激活状态退出会追加一条简短且会保留的通知。
73
73
 
74
74
  #### KV Cache 影响
75
75
 
package/lib/index.js CHANGED
@@ -155,37 +155,60 @@ var PlanModeController = class extends Service {
155
155
  schema: planProjectionSchema,
156
156
  init: () => ({
157
157
  active: false,
158
- wanted: null
158
+ wanted: null,
159
+ running: null
159
160
  }),
160
161
  apply: (state, event) => {
161
162
  if (event.type === "command/run" && event.data.name === "plan") {
162
163
  if (event.data.args === void 0) return state;
163
164
  const wanted = event.data.args.trim() !== "off";
164
- return wanted === state.wanted ? state : {
165
- active: state.active,
166
- wanted
165
+ return {
166
+ ...state,
167
+ running: {
168
+ commandId: event.data.commandId,
169
+ wanted
170
+ }
171
+ };
172
+ }
173
+ if (event.type === "command/done" && event.data.commandId === state.running?.commandId) {
174
+ const wanted = event.data.kind === "success" && state.running.wanted !== state.active ? state.running.wanted : null;
175
+ return {
176
+ ...state,
177
+ wanted,
178
+ running: null
167
179
  };
168
180
  }
169
181
  if (event.type === "plan/mode") return {
182
+ ...state,
170
183
  active: event.data.active,
171
184
  wanted: null
172
185
  };
173
186
  return state;
174
187
  },
175
- view: (state) => ({
176
- active: state.active,
177
- pending: state.wanted !== null && state.wanted !== state.active
178
- }),
179
- stateVersion: 1
188
+ view: (state) => {
189
+ const wanted = state.running?.wanted ?? state.wanted;
190
+ return {
191
+ active: state.active,
192
+ pending: wanted !== null && wanted !== state.active
193
+ };
194
+ },
195
+ stateVersion: 2
180
196
  });
181
197
  });
182
198
  ctx.inject(["commands"], (commandCtx) => {
183
199
  commandCtx.commands.register({
184
200
  name: "plan",
185
201
  description: "Enter or leave plan mode",
186
- input: { hint: "[off|message]" },
187
- handler: ({ agent, rawInput }) => {
202
+ input: {
203
+ hint: "[off|message]",
204
+ images: true
205
+ },
206
+ handler: ({ agent, rawInput, attachments }) => {
188
207
  const message = rawInput.trim();
208
+ if (message === "off" && attachments.length > 0) return {
209
+ kind: "error",
210
+ text: "Image attachments cannot accompany /plan off."
211
+ };
189
212
  if (message === "off") switch (this.set(agent, false)) {
190
213
  case "committed": return {
191
214
  kind: "success",
@@ -208,11 +231,11 @@ var PlanModeController = class extends Service {
208
231
  };
209
232
  }
210
233
  const outcome = this.set(agent, true);
211
- if (message !== "") agent.steer(createUserMessage({
212
- content: [{
234
+ if (message !== "" || attachments.length > 0) agent.steer(createUserMessage({
235
+ content: [...attachments, ...message === "" ? [] : [{
213
236
  type: "text",
214
237
  text: message
215
- }],
238
+ }]],
216
239
  source: { kind: "user" }
217
240
  }));
218
241
  return {
@@ -172,12 +172,11 @@ export class PlanModeController extends Service {
172
172
  return (pending?.active ?? foldPlanMode(context.agent.session.events)) ? this.section : '';
173
173
  },
174
174
  });
175
- // The plan projection unit (session-projection RFC): a pure double-event
176
- // fold serving clients the whole {active, pending} value. `command/run`
177
- // records the user's logged /plan selection (the handler calls `set()`
178
- // before any failing path, so a failed handler cannot leave the recorded
179
- // command without its plan selection); `plan/mode` records that selection
180
- // and clears it. Pending is thereby a pure
175
+ // The plan projection unit (session-projection RFC): a pure event fold
176
+ // serving clients the whole {active, pending} value. `command/run`
177
+ // records the user's logged /plan selection, its paired `command/done`
178
+ // keeps only successful selections, and `plan/mode` records that
179
+ // selection and clears it. Pending is thereby a pure
181
180
  // replay quantity: host restarts, other tabs, and cold reads all recover
182
181
  // it from the log alone. The unit child activates only when a projection
183
182
  // registry is composed (headless assemblies stay unaffected).
@@ -185,24 +184,30 @@ export class PlanModeController extends Service {
185
184
  projectionCtx.sessionProjections.register({
186
185
  key: 'plan',
187
186
  schema: planProjectionSchema,
188
- init: () => ({ active: false, wanted: null }),
187
+ init: () => ({ active: false, wanted: null, running: null }),
189
188
  apply: (state, event) => {
190
189
  if (event.type === 'command/run' && event.data.name === 'plan') {
191
190
  if (event.data.args === undefined)
192
191
  return state;
193
192
  const wanted = event.data.args.trim() !== 'off';
194
- return wanted === state.wanted ? state : { active: state.active, wanted };
193
+ return { ...state, running: { commandId: event.data.commandId, wanted } };
194
+ }
195
+ if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
196
+ const wanted = event.data.kind === 'success' && state.running.wanted !== state.active
197
+ ? state.running.wanted
198
+ : null;
199
+ return { ...state, wanted, running: null };
195
200
  }
196
201
  if (event.type === 'plan/mode') {
197
- return { active: event.data.active, wanted: null };
202
+ return { ...state, active: event.data.active, wanted: null };
198
203
  }
199
204
  return state;
200
205
  },
201
- view: state => ({
202
- active: state.active,
203
- pending: state.wanted !== null && state.wanted !== state.active,
204
- }),
205
- stateVersion: 1,
206
+ view: (state) => {
207
+ const wanted = state.running?.wanted ?? state.wanted;
208
+ return { active: state.active, pending: wanted !== null && wanted !== state.active };
209
+ },
210
+ stateVersion: 2,
206
211
  });
207
212
  });
208
213
  // The command child activates only when a command registry is composed.
@@ -210,9 +215,12 @@ export class PlanModeController extends Service {
210
215
  commandCtx.commands.register({
211
216
  name: 'plan',
212
217
  description: 'Enter or leave plan mode',
213
- input: { hint: '[off|message]' },
214
- handler: ({ agent, rawInput }) => {
218
+ input: { hint: '[off|message]', images: true },
219
+ handler: ({ agent, rawInput, attachments }) => {
215
220
  const message = rawInput.trim();
221
+ if (message === 'off' && attachments.length > 0) {
222
+ return { kind: 'error', text: 'Image attachments cannot accompany /plan off.' };
223
+ }
216
224
  if (message === 'off') {
217
225
  switch (this.set(agent, false)) {
218
226
  case 'committed':
@@ -231,8 +239,15 @@ export class PlanModeController extends Service {
231
239
  }
232
240
  }
233
241
  const outcome = this.set(agent, true);
234
- if (message !== '')
235
- agent.steer(createUserMessage({ content: [{ type: 'text', text: message }], source: { kind: 'user' } }));
242
+ if (message !== '' || attachments.length > 0) {
243
+ agent.steer(createUserMessage({
244
+ content: [
245
+ ...attachments,
246
+ ...(message === '' ? [] : [{ type: 'text', text: message }]),
247
+ ],
248
+ source: { kind: 'user' },
249
+ }));
250
+ }
236
251
  return {
237
252
  kind: 'success',
238
253
  text: outcome === 'committed'
@@ -10,9 +10,10 @@
10
10
  /**
11
11
  * The plan projection's wire value. `active` is the logged state in force
12
12
  * (the last `plan/mode`, inactive before the first); `pending` is true while
13
- * a logged `/plan` selection (`command/run`) targets a state other than
14
- * `active` and no later `plan/mode` event has recorded that state. Capability
15
- * absence (plan-mode not composed) is the key's absence, never a value.
13
+ * a logged `/plan` selection targets a state other than `active`, has not
14
+ * failed through its paired `command/done`, and no later `plan/mode` event has
15
+ * recorded that state. Capability absence (plan-mode not composed) is the
16
+ * key's absence, never a value.
16
17
  */
17
18
  export interface PlanProjection {
18
19
  active: boolean;
@@ -20,7 +21,7 @@ export interface PlanProjection {
20
21
  }
21
22
  declare module '@deepseek-ai/dsh-session-projection/types' {
22
23
  interface SessionProjectionMap {
23
- /** Plan collaboration state folded from `command/run` (name `plan`) and `plan/mode` events. */
24
+ /** Plan collaboration state folded from the plan command lifecycle and `plan/mode` events. */
24
25
  plan: PlanProjection;
25
26
  }
26
27
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-plan-mode",
3
3
  "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit",
4
- "version": "0.1.0-rc.7",
4
+ "version": "0.1.0-rc.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -41,16 +41,16 @@
41
41
  ],
42
42
  "license": "MIT",
43
43
  "peerDependencies": {
44
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
45
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
46
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
47
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
48
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
49
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
50
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
51
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
52
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.7",
53
- "@deepseek-ai/cordis": "^4.0.1"
44
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
45
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
46
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
47
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
48
+ "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8",
49
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
50
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
51
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
52
+ "@deepseek-ai/cordis": "^4.0.1",
53
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@deepseek-ai/dsh-commands": {
@@ -61,17 +61,17 @@
61
61
  "zod": "^4.4.3"
62
62
  },
63
63
  "devDependencies": {
64
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
65
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.7",
66
- "@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.7",
67
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
68
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
69
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
70
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
71
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
72
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
73
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
64
+ "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.8",
65
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
66
+ "@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.8",
67
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
69
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
72
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
73
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
74
74
  "@deepseek-ai/cordis": "^4.0.1",
75
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.7"
75
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8"
76
76
  }
77
77
  }