@deepseek-ai/dsh-plan-mode 0.1.1-rc.2 → 0.1.2-alpha.2

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.
@@ -6,12 +6,11 @@
6
6
  * policy enforce restrictions independently and do not read or write plan
7
7
  * state.
8
8
  *
9
- * The state in force is folded from the session log (`plan/mode`, last one
10
- * wins), so resume and fork restore it without a live mirror. User selections
11
- * remain pending until the next accepted in-turn pre-step. The service includes
12
- * the selected state in the proposed step assembly, then appends `plan/mode`
13
- * from `agent/pre-step` only when the step is accepted. Same-step request
14
- * retries reuse their assembly.
9
+ * The `plan` projection folds the session log, so resume and fork restore the
10
+ * state. User selections remain pending until the next accepted in-turn
11
+ * pre-step. The service includes the selected state in the proposed step
12
+ * assembly, then appends `plan/mode` from `agent/pre-step` only when the step
13
+ * is accepted. Same-step request retries reuse their assembly.
15
14
  *
16
15
  * The exit tool remains registered while plan mode is inactive, so entering
17
16
  * or leaving plan mode changes only the prompt section, not the request tool
@@ -72,26 +71,6 @@ export function resolveConfig(config) {
72
71
  }
73
72
  return { section };
74
73
  }
75
- /**
76
- * Whether plan mode is active after the first `end` events. The last
77
- * `plan/mode` wins; a prefix with none is inactive.
78
- *
79
- * @param events The session log or any prefix of it.
80
- * @param end Fold `events[0, end)`; defaults to the whole log.
81
- * @returns Whether plan mode is active.
82
- */
83
- export function foldPlanMode(events, end = events.length) {
84
- let active = false;
85
- let index = 0;
86
- for (const event of events) {
87
- if (index >= end)
88
- break;
89
- index++;
90
- if (event.type === 'plan/mode')
91
- active = event.data.active;
92
- }
93
- return active;
94
- }
95
74
  const planUnitStateSchema = zod.object({
96
75
  active: zod.boolean(),
97
76
  wanted: zod.boolean().nullable(),
@@ -99,43 +78,55 @@ const planUnitStateSchema = zod.object({
99
78
  commandId: zod.string(),
100
79
  wanted: zod.boolean(),
101
80
  }).strict().nullable(),
81
+ activeAtLastHeader: zod.boolean().nullable(),
102
82
  }).strict();
103
83
  /** Wire payload schema of the `plan` projection. */
104
84
  const planProjectionSchema = zod.object({
105
85
  active: zod.boolean(),
106
86
  pending: zod.boolean(),
107
87
  });
108
- /** Whether the log holds an opened turn without its closing `turn/end`. */
109
- function hasOpenTurn(events) {
110
- let open = false;
111
- for (const event of events) {
112
- if (event.type === 'turn/start')
113
- open = true;
114
- else if (event.type === 'turn/end')
115
- open = false;
116
- }
117
- return open;
118
- }
119
- /** Plan state at the last logged request header, or `undefined` before the first header. */
120
- function planModeAtLastHeader(events) {
121
- let lastHeader = -1;
122
- let index = 0;
123
- for (const event of events) {
124
- if (event.type === 'request/header')
125
- lastHeader = index;
126
- index++;
127
- }
128
- if (lastHeader < 0)
129
- return undefined;
130
- return foldPlanMode(events, lastHeader + 1);
131
- }
88
+ /** Projection of logged plan selections and committed mode. */
89
+ export const planProjectionDefinition = {
90
+ key: 'plan',
91
+ stateVersion: 3,
92
+ stateSchema: planUnitStateSchema,
93
+ init: () => ({ active: false, wanted: null, running: null, activeAtLastHeader: null }),
94
+ apply: (state, event) => {
95
+ if (event.type === 'command/run' && event.data.name === 'plan') {
96
+ if (event.data.args === undefined)
97
+ return state;
98
+ const wanted = event.data.args.trim() !== 'off';
99
+ return { ...state, running: { commandId: event.data.commandId, wanted } };
100
+ }
101
+ if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
102
+ const wanted = event.data.kind === 'success' && state.running.wanted !== state.active
103
+ ? state.running.wanted
104
+ : null;
105
+ return { ...state, wanted, running: null };
106
+ }
107
+ if (event.type === 'plan/mode') {
108
+ return { ...state, active: event.data.active, wanted: null };
109
+ }
110
+ if (event.type === 'request/header') {
111
+ return { ...state, activeAtLastHeader: state.active };
112
+ }
113
+ return state;
114
+ },
115
+ wire: {
116
+ viewSchema: planProjectionSchema,
117
+ view: (state) => {
118
+ const wanted = state.running?.wanted ?? state.wanted;
119
+ return { active: state.active, pending: wanted !== null && wanted !== state.active };
120
+ },
121
+ },
122
+ };
132
123
  /**
133
124
  * `ctx.planMode`: owns logged plan state, applies and narrates selected state at step start,
134
125
  * the `plan:policy` section, the `/plan` command, and the stable exit tool.
135
- * UIs observe committed flips through `session/event`; there is no live mirror.
126
+ * Client carriers expose the projection's cropped `{ active, pending }` view.
136
127
  */
137
128
  export class PlanModeController extends Service {
138
- static inject = ['tools', 'systemPrompt'];
129
+ static inject = ['tools', 'systemPrompt', 'sessionProjections'];
139
130
  /** Validated deployment-owned guidance. */
140
131
  section;
141
132
  /**
@@ -172,55 +163,15 @@ export class PlanModeController extends Service {
172
163
  ctx.effect(() => () => { disposed = true; }, 'dsh-plan-mode: close service lifetime');
173
164
  ctx.systemPrompt.section({
174
165
  name: 'plan:policy',
175
- order: 50,
166
+ order: ctx.systemPrompt.getSectionOrder('PLAN_POLICY'),
176
167
  text: (context) => {
177
168
  if (context.agent === undefined)
178
169
  return '';
179
170
  const pending = this.pendingIntents.get(context.agent.session);
180
- return (pending?.active ?? foldPlanMode(context.agent.session.events)) ? this.section : '';
171
+ return (pending?.active ?? this.loggedActive(context.agent.session)) ? this.section : '';
181
172
  },
182
173
  });
183
- // The plan projection unit (session-projection RFC): a pure event fold
184
- // serving clients the whole {active, pending} value. `command/run`
185
- // records the user's logged /plan selection, its paired `command/done`
186
- // keeps only successful selections, and `plan/mode` records that
187
- // selection and clears it. Pending is thereby a pure
188
- // replay quantity: host restarts, other tabs, and cold reads all recover
189
- // it from the log alone. The unit child activates only when a projection
190
- // registry is composed (headless assemblies stay unaffected).
191
- ctx.inject(['sessionProjections'], (projectionCtx) => {
192
- projectionCtx.sessionProjections.register({
193
- key: 'plan',
194
- stateSchema: planUnitStateSchema,
195
- init: () => ({ active: false, wanted: null, running: null }),
196
- apply: (state, event) => {
197
- if (event.type === 'command/run' && event.data.name === 'plan') {
198
- if (event.data.args === undefined)
199
- return state;
200
- const wanted = event.data.args.trim() !== 'off';
201
- return { ...state, running: { commandId: event.data.commandId, wanted } };
202
- }
203
- if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
204
- const wanted = event.data.kind === 'success' && state.running.wanted !== state.active
205
- ? state.running.wanted
206
- : null;
207
- return { ...state, wanted, running: null };
208
- }
209
- if (event.type === 'plan/mode') {
210
- return { ...state, active: event.data.active, wanted: null };
211
- }
212
- return state;
213
- },
214
- wire: {
215
- viewSchema: planProjectionSchema,
216
- view: (state) => {
217
- const wanted = state.running?.wanted ?? state.wanted;
218
- return { active: state.active, pending: wanted !== null && wanted !== state.active };
219
- },
220
- },
221
- stateVersion: 2,
222
- });
223
- });
174
+ ctx.sessionProjections.register(planProjectionDefinition);
224
175
  // The command child activates only when a command registry is composed.
225
176
  ctx.inject(['commands'], (commandCtx) => {
226
177
  commandCtx.commands.register({
@@ -244,7 +195,7 @@ export class PlanModeController extends Service {
244
195
  // Repeat the queued wording while an exit still awaits the
245
196
  // next accepted pre-step; only a truly inactive session reads
246
197
  // idempotent.
247
- return foldPlanMode(agent.session.events)
198
+ return this.loggedActive(agent.session)
248
199
  ? { kind: 'success', text: 'Leaving plan mode (applies from the next step).' }
249
200
  : { kind: 'success', text: 'Plan mode is already inactive.' };
250
201
  }
@@ -288,7 +239,7 @@ export class PlanModeController extends Service {
288
239
  const agent = exec.agent;
289
240
  if (agent === undefined)
290
241
  throw new Error(`${EXIT_PLAN_MODE} requires a calling agent (no session to switch)`);
291
- if (!foldPlanMode(agent.session.events)) {
242
+ if (!this.loggedActive(agent.session)) {
292
243
  throw new Error(`${EXIT_PLAN_MODE} is only available in plan mode`);
293
244
  }
294
245
  if (!/^#\s+\S/.test(args.plan.trim())) {
@@ -359,6 +310,25 @@ export class PlanModeController extends Service {
359
310
  }),
360
311
  }));
361
312
  }
313
+ loggedActive(session) {
314
+ return this.planState(session).active;
315
+ }
316
+ hasOpenTurn(session) {
317
+ const state = this.ctx.sessionProjections.stateOf(session, 'turnBoundary');
318
+ if (state === undefined)
319
+ throw new Error('plan-mode requires the turnBoundary session projection');
320
+ return state.openTurnStartSeq !== null;
321
+ }
322
+ loggedActiveAtLastHeader(session) {
323
+ return this.planState(session).activeAtLastHeader ?? undefined;
324
+ }
325
+ /** Read the required plan projection state or fail at the first service access. */
326
+ planState(session) {
327
+ const state = this.ctx.sessionProjections.stateOf(session, 'plan');
328
+ if (state === undefined)
329
+ throw new Error('plan-mode requires the plan session projection');
330
+ return state;
331
+ }
362
332
  /**
363
333
  * Read the logged plan state and any selected state awaiting the next
364
334
  * accepted in-turn pre-step.
@@ -367,7 +337,7 @@ export class PlanModeController extends Service {
367
337
  * @returns Current logged state plus a pending selection, when present.
368
338
  */
369
339
  get(agent) {
370
- const active = foldPlanMode(agent.session.events);
340
+ const active = this.loggedActive(agent.session);
371
341
  const pending = this.pendingIntents.get(agent.session);
372
342
  return pending === undefined ? { active } : { active, pending: pending.active };
373
343
  }
@@ -390,16 +360,16 @@ export class PlanModeController extends Service {
390
360
  set(agent, active) {
391
361
  const session = agent.session;
392
362
  const pending = this.pendingIntents.get(session);
393
- const target = pending?.active ?? foldPlanMode(session.events);
363
+ const target = pending?.active ?? this.loggedActive(session);
394
364
  if (active === target)
395
365
  return 'noop';
396
- if (hasOpenTurn(session.events)) {
366
+ if (this.hasOpenTurn(session)) {
397
367
  this.pendingIntents.set(session, { active, narrate: true });
398
- return foldPlanMode(session.events) === active ? 'cancelled' : 'queued';
368
+ return this.loggedActive(session) === active ? 'cancelled' : 'queued';
399
369
  }
400
370
  // No open turn: commit now. Delete only after append succeeds so a
401
371
  // failed durable write leaves the selection retryable, not dropped.
402
- if (active === foldPlanMode(session.events)) {
372
+ if (active === this.loggedActive(session)) {
403
373
  this.pendingIntents.delete(session);
404
374
  return 'cancelled';
405
375
  }
@@ -416,7 +386,7 @@ export class PlanModeController extends Service {
416
386
  if (pending === undefined)
417
387
  return;
418
388
  const target = pending.active;
419
- if (target === foldPlanMode(session.events)) {
389
+ if (target === this.loggedActive(session)) {
420
390
  this.pendingIntents.delete(session);
421
391
  return;
422
392
  }
@@ -427,7 +397,7 @@ export class PlanModeController extends Service {
427
397
  }
428
398
  /** Build a user-switch notice when the last logged header described the other mode. */
429
399
  narration(session, target) {
430
- const told = planModeAtLastHeader(session.events);
400
+ const told = this.loggedActiveAtLastHeader(session);
431
401
  if (told === undefined || told === target)
432
402
  return;
433
403
  const text = target
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Pure types of the plan domain: the ONE home of the `plan` projection-key
3
- * declaration, free of this package's host-side value imports (cordis
4
- * service, dsh-tools, dsh-agent). Two namespace projections serve it —
5
- * `./types` for host consumers, `./client` for client aggregates — with zero
6
- * content duplication.
3
+ * declaration, free of this package's host-side value imports (cordis,
4
+ * dsh-tools, dsh-agent). Two namespace projections serve it — `./types` for
5
+ * host consumers and `./client` for client aggregates — with zero content
6
+ * duplication.
7
7
  *
8
8
  * @module @deepseek-ai/dsh-plan-mode/types
9
9
  */
10
+ import type { CommandId } from '@deepseek-ai/dsh-commands/brand';
10
11
  /**
11
12
  * The plan projection's wire value. `active` is the logged state in force
12
13
  * (the last `plan/mode`, inactive before the first); `pending` is true while
@@ -19,7 +20,25 @@ export interface PlanProjection {
19
20
  active: boolean;
20
21
  pending: boolean;
21
22
  }
23
+ /** Host state used to derive {@link PlanProjection}. */
24
+ export interface PlanUnitState {
25
+ /** Logged plan mode. */
26
+ active: boolean;
27
+ /** The selection's target mode; null when no selection is outstanding. */
28
+ wanted: boolean | null;
29
+ /** The latest plan command awaiting its paired settlement. */
30
+ running: {
31
+ commandId: CommandId;
32
+ wanted: boolean;
33
+ } | null;
34
+ /** Active state recorded by the latest `request/header`, or null. */
35
+ activeAtLastHeader: boolean | null;
36
+ }
22
37
  declare module '@deepseek-ai/dsh-session-projection/types' {
38
+ interface SessionProjectionStateMap {
39
+ /** Host plan-mode fold state. */
40
+ plan: PlanUnitState;
41
+ }
23
42
  interface SessionProjectionMap {
24
43
  /** Plan collaboration state folded from the plan command lifecycle and `plan/mode` events. */
25
44
  plan: PlanProjection;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Pure types of the plan domain: the ONE home of the `plan` projection-key
3
- * declaration, free of this package's host-side value imports (cordis
4
- * service, dsh-tools, dsh-agent). Two namespace projections serve it —
5
- * `./types` for host consumers, `./client` for client aggregates — with zero
6
- * content duplication.
3
+ * declaration, free of this package's host-side value imports (cordis,
4
+ * dsh-tools, dsh-agent). Two namespace projections serve it — `./types` for
5
+ * host consumers and `./client` for client aggregates — with zero content
6
+ * duplication.
7
7
  *
8
8
  * @module @deepseek-ai/dsh-plan-mode/types
9
9
  */
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.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
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.1-rc.2",
45
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
46
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
47
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
48
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
49
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
50
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
51
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
52
- "@deepseek-ai/dsh-user-questions": "^0.1.1-rc.2",
53
- "@deepseek-ai/cordis": "^4.0.1"
44
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.2",
46
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
47
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
48
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
49
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
50
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
51
+ "@deepseek-ai/dsh-user-questions": "^0.1.2-alpha.2",
52
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
53
+ "@deepseek-ai/cordis": "^4.0.2"
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.1-rc.2",
65
- "@deepseek-ai/dsh-agent-loop": "^0.1.1-rc.2",
66
- "@deepseek-ai/dsh-code-runtime": "^0.1.1-rc.2",
67
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
68
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
69
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
70
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
71
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
73
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
74
- "@deepseek-ai/dsh-user-questions": "^0.1.1-rc.2",
75
- "@deepseek-ai/cordis": "^4.0.1"
64
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
65
+ "@deepseek-ai/dsh-code-runtime": "^0.1.2-alpha.2",
66
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.2",
68
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
69
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
70
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
71
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
72
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
73
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
74
+ "@deepseek-ai/cordis": "^4.0.2",
75
+ "@deepseek-ai/dsh-user-questions": "^0.1.2-alpha.2"
76
76
  }
77
77
  }