dsh-advisor 0.4.0 → 0.5.0

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/lib/commands.d.ts CHANGED
@@ -1,23 +1,29 @@
1
1
  /**
2
- * T7 — slash commands (spec §2 S5, §6 status surface, §8.5 KD-5 seed-on-enable).
2
+ * T7 — slash commands (spec §2 S5, §6 status surface, §8.5 KD-5 seed-on-enable,
3
+ * §5.3 session model override).
3
4
  *
4
5
  * One `/advisor` command is registered (through {@link registerAdvisorCommands})
5
- * with four forms:
6
+ * with these forms:
6
7
  *
7
8
  * - `/advisor` — toggle the per-session override (on ↔ off);
8
9
  * - `/advisor on` — enable the advisor for this session;
9
10
  * - `/advisor off` — disable the advisor for this session;
10
11
  * - `/advisor status` — report the per-session status surface;
11
- * - `/advisor config` — show the composed advisor config (settings readback);
12
+ * - `/advisor config` — show the composed advisor config (global defaults);
13
+ * - `/advisor model` — show the effective reviewer route + source;
14
+ * - `/advisor model set <provider> <model>` — pin an atomic pair for the
15
+ * invoking session only (validated via `resolveModelInfo`, 60 s, no retry);
16
+ * - `/advisor model reset` — re-inherit the current global defaults;
12
17
  * - anything else — usage text.
13
18
  *
14
- * Toggle/on/off are **session-scoped and ephemeral**: they drive a per-session
15
- * override flag (`AdvisorSessionOverrides`) that the runtime gate consults as
16
- * `override ?? config.enabled`, so no command ever touches the persisted
17
- * config (spec §4 mapping — matches omp `/advisor` semantics). Enabling a
18
- * session whose config has no provider/model starts no model call: the S4
19
- * explicit gate (spec §5.2) still applies, and the status/on text explains
20
- * the disabled-with-reason.
19
+ * All forms are **session-scoped and ephemeral**: they drive per-session
20
+ * overrides ({@link AdvisorSessionOverrides} — the enable flag and the
21
+ * runtime-only atomic model pair, fenced by per-session generations) that the
22
+ * runtime gate and the effective resolver consult, so no command ever touches
23
+ * the persisted config (spec §4 mapping, §5.3 — matches omp `/advisor`
24
+ * semantics). Enabling a session whose effective route has no complete pair
25
+ * starts no model call: the S4 explicit gate (spec §5.2) applies AFTER session
26
+ * resolution, and the status/on text explains the disabled-with-reason.
21
27
  *
22
28
  * The module is cordis-free (pure parse + render + registration contract), so
23
29
  * it is unit-testable with a fake command registry and a fake controller;
@@ -28,6 +34,7 @@
28
34
  * @module dsh-advisor/commands
29
35
  */
30
36
  import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
37
+ import type { Agent } from '@deepseek-ai/dsh-agent';
31
38
  import type { AdvisorRuntimeStatus } from './advisor-runtime.js';
32
39
  /** The parsed form of the exact text following `/advisor`. */
33
40
  export type AdvisorCommand = {
@@ -40,9 +47,64 @@ export type AdvisorCommand = {
40
47
  readonly kind: 'status';
41
48
  } | {
42
49
  readonly kind: 'config';
50
+ } | {
51
+ readonly kind: 'model';
52
+ readonly sub: AdvisorModelCommand;
53
+ } | {
54
+ readonly kind: 'usage';
55
+ };
56
+ /**
57
+ * The parsed form of the text following `/advisor model` (spec §5.3 B1
58
+ * surface): bare → show, `set <provider> <model>` → set (parsed by
59
+ * {@link parseModelSetArgs}), `reset` → reset, anything else → model usage.
60
+ */
61
+ export type AdvisorModelCommand = {
62
+ readonly kind: 'show';
63
+ } | {
64
+ readonly kind: 'set';
65
+ readonly args: string;
66
+ } | {
67
+ readonly kind: 'reset';
43
68
  } | {
44
69
  readonly kind: 'usage';
45
70
  };
71
+ /**
72
+ * Parse the text following `/advisor model`. `set` keeps its RAW remainder —
73
+ * the atomic-pair validation ({@link parseModelSetArgs}) owns it, because the
74
+ * set flow reports a precise rejection reason in the command reply.
75
+ */
76
+ export declare function parseAdvisorModelCommand(rawInput: string): AdvisorModelCommand;
77
+ /**
78
+ * The result of parsing `/advisor model set` arguments into the atomic pair
79
+ * (spec §5.3 pair validation).
80
+ */
81
+ export type ModelSetArgs = {
82
+ readonly ok: true;
83
+ readonly provider: string;
84
+ readonly model: string;
85
+ } | {
86
+ readonly ok: false;
87
+ readonly reason: string;
88
+ };
89
+ /**
90
+ * Validate one structured `{provider, model}` pair (spec §5.3 pair validation):
91
+ * both values must be strings that are non-empty after outer trim, with no
92
+ * whitespace INSIDE either identifier (the same rules `parseModelSetArgs`
93
+ * enforces for the CLI form — one validation source shared by the command face
94
+ * and the web gateway face, so the two surfaces can never diverge). Outer
95
+ * whitespace is trimmed; case is retained; model ids containing `/` are fine
96
+ * (`/` is not whitespace). Never throws — every rejection carries a
97
+ * user-facing reason.
98
+ */
99
+ export declare function parseModelPair(provider: unknown, model: unknown): ModelSetArgs;
100
+ /**
101
+ * Validate `/advisor model set <provider> <model>` arguments (spec §5.3):
102
+ * exactly two arguments; outer whitespace trimmed; case retained; blank or
103
+ * partial pairs, extra fields (a third argument), and whitespace-containing
104
+ * identifiers rejected. Separate args are what allow model ids containing
105
+ * `/`. Never throws — every rejection carries a user-facing reason.
106
+ */
107
+ export declare function parseModelSetArgs(rawArgs: string): ModelSetArgs;
46
108
  /**
47
109
  * Parse the text following `/advisor` (the dsh `parseCommand` split already
48
110
  * yields `rawInput` including the separator whitespace, e.g. `' on'` for
@@ -51,15 +113,45 @@ export type AdvisorCommand = {
51
113
  */
52
114
  export declare function parseAdvisorCommand(rawInput: string): AdvisorCommand;
53
115
  /**
54
- * The per-session override consulted by the runtime gate as
55
- * `override ?? config.enabled`. `/advisor on|off|toggle` write here — the
56
- * persisted config is never modified. The map is keyed by session id and
57
- * entries live for the session lifetime (`index.ts` clears them on
58
- * `agent/disposed` / `session/disposed`).
116
+ * The runtime-only reviewer model override for one session (spec §5.3) — an
117
+ * **atomic** `{ provider, model }` pair. It never merges with the global
118
+ * pair (no half-pairs) and is never persisted: it lives in memory for the
119
+ * live-session lifetime and is cleared by reset, dispose, owner teardown,
120
+ * cold resume, or restart.
121
+ */
122
+ export interface AdvisorModelPair {
123
+ readonly provider: string;
124
+ readonly model: string;
125
+ }
126
+ /**
127
+ * Where a session's effective reviewer route comes from (spec §5.3):
128
+ * `session` = a pinned `AdvisorModelPair`, `global` = the composed global
129
+ * advisor pair.
130
+ */
131
+ export type AdvisorModelSource = 'session' | 'global';
132
+ /**
133
+ * The per-session overrides consulted by the runtime gate and the effective
134
+ * resolver (`src/index.ts`):
135
+ *
136
+ * - enable: `override ?? config.enabled` (`/advisor on|off|toggle`);
137
+ * - model: the complete session pair ?? the composed global pair — the pair
138
+ * is atomic, so the two levels are never merged (spec §5.3);
139
+ * - generation: a per-session counter that fences async model work — a newer
140
+ * set/reset supersedes unresolved older work, and dispose/owner teardown
141
+ * (which wipe the counter) can never be undone by a delayed completion.
142
+ *
143
+ * Nothing here touches the persisted config. All maps are keyed by session id
144
+ * and hold only LIVE sessions' state: `clear` runs on `agent/disposed` /
145
+ * `session/disposed`, `clearModel` runs on reset (an emptied entry is
146
+ * removed), and `disposeAll` runs on owner teardown — state stays
147
+ * O(live overrides), with no historical SessionId accumulation and no
148
+ * TTL/GC job.
59
149
  */
60
150
  export declare class AdvisorSessionOverrides {
61
151
  private configEnabled;
62
- private readonly overrides;
152
+ private readonly enables;
153
+ private readonly models;
154
+ private readonly generations;
63
155
  constructor(configEnabled: boolean);
64
156
  /** Effective switch for one session: `override ?? config.enabled`. */
65
157
  effective(sessionId: string): boolean;
@@ -70,10 +162,41 @@ export declare class AdvisorSessionOverrides {
70
162
  * takes effect for new sessions without touching the override mechanism.
71
163
  */
72
164
  setConfigEnabled(enabled: boolean): void;
73
- /** Set the override for one session. */
165
+ /** Set the enable override for one session. */
74
166
  set(sessionId: string, enabled: boolean): void;
75
- /** Remove a session's override, falling back to the config switch. */
167
+ /** The session's pinned model pair, or `undefined` when it inherits. */
168
+ model(sessionId: string): AdvisorModelPair | undefined;
169
+ /** Commit the (already validated) atomic pair for one session. */
170
+ setModel(sessionId: string, pair: AdvisorModelPair): void;
171
+ /**
172
+ * Remove a session's model pin (reset-to-inherit). @returns `true` when a
173
+ * pin was actually removed — `false` means the session was already
174
+ * inheriting (a reset no-op).
175
+ */
176
+ clearModel(sessionId: string): boolean;
177
+ /**
178
+ * Bump and return the session's model-work generation — the fence token an
179
+ * async validation captures; any later `beginModelGeneration` (a newer
180
+ * set/reset) or any state wipe makes the captured value stale.
181
+ */
182
+ beginModelGeneration(sessionId: string): number;
183
+ /** The session's current model-work generation (0 before the first). */
184
+ modelGeneration(sessionId: string): number;
185
+ /** Sessions holding ANY per-session override state (enable and/or pair). */
186
+ overrideSessionIds(): IterableIterator<string>;
187
+ /**
188
+ * Drop ALL per-session state for one session (enable + pair + generation) —
189
+ * the `agent/disposed` / `session/disposed` cleanup. Wiping the generation
190
+ * invalidates any in-flight validation for the session (captured values are
191
+ * ≥ 1 and can never again match the post-clear default 0).
192
+ */
76
193
  clear(sessionId: string): void;
194
+ /**
195
+ * Owner teardown: wipe every session's state. Pending validations for any
196
+ * session become stale exactly like a per-session `clear` — a delayed
197
+ * completion can never commit after the owner is gone.
198
+ */
199
+ disposeAll(): void;
77
200
  }
78
201
  /**
79
202
  * Per-session status snapshot consumed by `/advisor status`. Built by the
@@ -93,6 +216,12 @@ export interface AdvisorSessionStatus {
93
216
  readonly provider?: string;
94
217
  /** Configured model id (shown even while disabled — spec §5.2). */
95
218
  readonly model?: string;
219
+ /**
220
+ * Where the reported route comes from (spec §5.3) — present iff
221
+ * `provider`/`model` are: `session` = the session's pinned pair,
222
+ * `global` = the composed global advisor pair.
223
+ */
224
+ readonly modelSource?: AdvisorModelSource;
96
225
  /** The session's runtime status; `disabled` when no runtime exists. */
97
226
  readonly runtimeStatus: AdvisorRuntimeStatus;
98
227
  /** Deltas waiting to be drained (bounded backlog, spec §6). */
@@ -106,13 +235,59 @@ export interface AdvisorSessionStatus {
106
235
  * the pending count, and the last accepted-note activity (ISO, or `never`).
107
236
  */
108
237
  export declare function advisorStatusText(status: AdvisorSessionStatus): string;
238
+ /** Validation deadline for one `/advisor model set` lookup (60 s — matches
239
+ * the runtime's whole-call deadline default, `src/advisor-runtime.ts`). */
240
+ export declare const ADVISOR_MODEL_VALIDATION_TIMEOUT_MS = 60000;
241
+ /** The effective reviewer route for one session (spec §5.3). */
242
+ export interface AdvisorModelRoute {
243
+ readonly provider?: string;
244
+ readonly model?: string;
245
+ /** `session` when the effective pair is the session's pin, else `global`. */
246
+ readonly source: AdvisorModelSource;
247
+ }
248
+ /** Outcome of one `/advisor model set` attempt (spec §5.3 validation + fencing). */
249
+ export type AdvisorSetModelOutcome = {
250
+ readonly kind: 'committed';
251
+ readonly status: AdvisorSessionStatus;
252
+ } | {
253
+ readonly kind: 'unchanged';
254
+ readonly status: AdvisorSessionStatus;
255
+ } | {
256
+ readonly kind: 'rejected';
257
+ readonly reason: string;
258
+ } | {
259
+ readonly kind: 'failed';
260
+ readonly reason: string;
261
+ } | {
262
+ readonly kind: 'cancelled';
263
+ } | {
264
+ readonly kind: 'superseded';
265
+ } | {
266
+ readonly kind: 'gone';
267
+ };
268
+ /** Outcome of one `/advisor model reset`. */
269
+ export type AdvisorResetModelOutcome = {
270
+ readonly kind: 'reset';
271
+ readonly status: AdvisorSessionStatus;
272
+ } | {
273
+ readonly kind: 'noop';
274
+ };
275
+ /**
276
+ * Render the `/advisor model` surface: the effective pair, its source, and
277
+ * the live-session lifetime (spec §5.3 — the reply must name the lifetime).
278
+ */
279
+ export declare function advisorModelText(route: AdvisorModelRoute): string;
280
+ /** Render one `/advisor model set` outcome. */
281
+ export declare function modelSetText(outcome: AdvisorSetModelOutcome): string;
282
+ /** Render one `/advisor model reset` outcome. */
283
+ export declare function modelResetText(outcome: AdvisorResetModelOutcome): string;
109
284
  /**
110
285
  * Composed-config surface consumed by `/advisor config`. **Session-less by
111
286
  * design**: the wiring builds it from the same resolved config the web card
112
- * reads (`/api/advisor/get` — schema defaults → plugin-row base → settings
113
- * user layer, with the hard gate applied), so a per-session `/advisor off`
114
- * override can never misreport settings.yaml. Runtime state stays owned by
115
- * the status surface (`AdvisorSessionStatus`); config and status are separate.
287
+ * reads (`/api/advisor/get` — the live entry config, with the hard gate
288
+ * applied), so a per-session `/advisor off` override can never misreport the
289
+ * persisted config. Runtime state stays owned by the status surface
290
+ * (`AdvisorSessionStatus`); config and status are separate.
116
291
  */
117
292
  export interface AdvisorComposedConfig {
118
293
  /** Config-level composed switch — NOT the per-session override. */
@@ -141,8 +316,7 @@ export interface AdvisorComposedConfig {
141
316
  * dsh-advisor-tui-settings-n9 T2). An environment signal, never derived
142
317
  * from the per-session override; it does not change the resolved-config
143
318
  * read. When true the hint lists the TUI `/settings` screen as a write
144
- * path; when false the n8 hint (profile patch layer + settings.yaml) is
145
- * shown unchanged.
319
+ * path; when false the profile patch layer hint is shown unchanged.
146
320
  */
147
321
  readonly tuiSettingsAvailable: boolean;
148
322
  }
@@ -157,9 +331,9 @@ export declare function summarizeSystemPrompt(prompt: string): string;
157
331
  * Render the composed config surface. Mirrors the status renderer's minimal
158
332
  * line style; the edit hint points at the operator edit paths — when the TUI
159
333
  * `tuiSettingsSections` seam is mounted (dsh-tui ≥ v0.8.0) the TUI `/settings`
160
- * Advisor section is listed FIRST, followed by the profile patch layer + the
161
- * shared `$DSH_HOME/settings.yaml` `advisor:` section the web card writes;
162
- * otherwise only the two file paths (n8 text, byte-identical).
334
+ * Advisor section is listed FIRST, followed by the profile patch layer
335
+ * (`cordis.patch.yml`, whose `advisor` plugin row carries the volatile live
336
+ * fields); otherwise only the file path.
163
337
  */
164
338
  export declare function advisorConfigText(config: AdvisorComposedConfig): string;
165
339
  /**
@@ -180,6 +354,23 @@ export interface AdvisorCommandController {
180
354
  getStatus(sessionId: string): AdvisorSessionStatus;
181
355
  /** Snapshot the composed config surface (session-less settings readback). */
182
356
  getConfig(): AdvisorComposedConfig;
357
+ /** The session's effective reviewer route (pair + source — spec §5.3). */
358
+ getModelRoute(sessionId: string): AdvisorModelRoute;
359
+ /**
360
+ * Validate and commit an atomic pair for the INVOKING session (spec §5.3):
361
+ * resolve through the LLM service before commit (60 s deadline, fused with
362
+ * `signal`, cancellable, NO automatic retry — failure leaves the previous
363
+ * selection untouched), fenced by the per-session generation. Never toggles
364
+ * the enable override; never starts a generation call.
365
+ */
366
+ setModel(sessionId: string, provider: string, model: string, agent: Agent, signal?: AbortSignal): Promise<AdvisorSetModelOutcome>;
367
+ /**
368
+ * Drop the session's pin and re-inherit the CURRENT global defaults (never
369
+ * touches the enable override). Synchronous — no validation needed.
370
+ * @param sessionLength - current transcript length, used for the re-seed
371
+ * when the effective route actually changes.
372
+ */
373
+ resetModel(sessionId: string, sessionLength?: number): AdvisorResetModelOutcome;
183
374
  }
184
375
  /** Minimal command registry surface (satisfied by the dsh `CommandService`). */
185
376
  export interface AdvisorCommandRegistry {
@@ -187,6 +378,8 @@ export interface AdvisorCommandRegistry {
187
378
  }
188
379
  /** Usage text for an unknown `/advisor` subcommand. */
189
380
  export declare const USAGE: string;
381
+ /** Usage text for an unknown `/advisor model` subcommand. */
382
+ export declare const MODEL_USAGE: string;
190
383
  /**
191
384
  * Register the `/advisor` command with a command registry (the dsh
192
385
  * `CommandService`, or a fake in tests). Called from the plugin's conditional