dsh-advisor 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +7 -0
  3. package/README.md +303 -0
  4. package/README.zh.md +166 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/advisor-runtime.d.ts +242 -0
  7. package/lib/advisor-runtime.js +662 -0
  8. package/lib/advisor-runtime.js.map +1 -0
  9. package/lib/client/advisor-card.d.ts +90 -0
  10. package/lib/client/advisor-store.d.ts +310 -0
  11. package/lib/client/index.d.ts +39 -0
  12. package/lib/client/locales.d.ts +40 -0
  13. package/lib/client.d.ts +1 -0
  14. package/lib/client.js +840 -0
  15. package/lib/commands.d.ts +136 -0
  16. package/lib/commands.js +185 -0
  17. package/lib/commands.js.map +1 -0
  18. package/lib/config.d.ts +74 -0
  19. package/lib/config.js +93 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/delivery.d.ts +129 -0
  22. package/lib/delivery.js +169 -0
  23. package/lib/delivery.js.map +1 -0
  24. package/lib/emission-guard.d.ts +99 -0
  25. package/lib/emission-guard.js +155 -0
  26. package/lib/emission-guard.js.map +1 -0
  27. package/lib/gateway.d.ts +116 -0
  28. package/lib/gateway.js +214 -0
  29. package/lib/gateway.js.map +1 -0
  30. package/lib/index.d.ts +48 -0
  31. package/lib/index.js +485 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/kinds.d.ts +38 -0
  34. package/lib/kinds.js +24 -0
  35. package/lib/kinds.js.map +1 -0
  36. package/lib/prompts.d.ts +22 -0
  37. package/lib/prompts.js +38 -0
  38. package/lib/prompts.js.map +1 -0
  39. package/lib/settings.d.ts +96 -0
  40. package/lib/settings.js +141 -0
  41. package/lib/settings.js.map +1 -0
  42. package/lib/transcript.d.ts +257 -0
  43. package/lib/transcript.js +530 -0
  44. package/lib/transcript.js.map +1 -0
  45. package/package.json +90 -0
  46. package/scripts/build-client.mjs +268 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * T7 — slash commands (spec §2 S5, §6 status surface, §8.5 KD-5 seed-on-enable).
3
+ *
4
+ * One `/advisor` command is registered (through {@link registerAdvisorCommands})
5
+ * with four forms:
6
+ *
7
+ * - `/advisor` — toggle the per-session override (on ↔ off);
8
+ * - `/advisor on` — enable the advisor for this session;
9
+ * - `/advisor off` — disable the advisor for this session;
10
+ * - `/advisor status` — report the per-session status surface;
11
+ * - anything else — usage text.
12
+ *
13
+ * Toggle/on/off are **session-scoped and ephemeral**: they drive a per-session
14
+ * override flag (`AdvisorSessionOverrides`) that the runtime gate consults as
15
+ * `override ?? config.enabled`, so no command ever touches the persisted
16
+ * config (spec §4 mapping — matches omp `/advisor` semantics). Enabling a
17
+ * session whose config has no provider/model starts no model call: the S4
18
+ * explicit gate (spec §5.2) still applies, and the status/on text explains
19
+ * the disabled-with-reason.
20
+ *
21
+ * The module is cordis-free (pure parse + render + registration contract), so
22
+ * it is unit-testable with a fake command registry and a fake controller;
23
+ * `index.ts` binds it into the plugin through the conditional
24
+ * `ctx.inject(['commands'], ...)` child (commands must NOT join the top-level
25
+ * inject list — T1 fix).
26
+ *
27
+ * @module dsh-advisor/commands
28
+ */
29
+ import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
30
+ import type { AdvisorRuntimeStatus } from './advisor-runtime.js';
31
+ /** The parsed form of the exact text following `/advisor`. */
32
+ export type AdvisorCommand = {
33
+ readonly kind: 'toggle';
34
+ } | {
35
+ readonly kind: 'on';
36
+ } | {
37
+ readonly kind: 'off';
38
+ } | {
39
+ readonly kind: 'status';
40
+ } | {
41
+ readonly kind: 'usage';
42
+ };
43
+ /**
44
+ * Parse the text following `/advisor` (the dsh `parseCommand` split already
45
+ * yields `rawInput` including the separator whitespace, e.g. `' on'` for
46
+ * `/advisor on`). Subcommands match exactly after trimming — same
47
+ * case-sensitivity as dsh command names; anything else is a usage error.
48
+ */
49
+ export declare function parseAdvisorCommand(rawInput: string): AdvisorCommand;
50
+ /**
51
+ * The per-session override consulted by the runtime gate as
52
+ * `override ?? config.enabled`. `/advisor on|off|toggle` write here — the
53
+ * persisted config is never modified. The map is keyed by session id and
54
+ * entries live for the session lifetime (`index.ts` clears them on
55
+ * `agent/disposed` / `session/disposed`).
56
+ */
57
+ export declare class AdvisorSessionOverrides {
58
+ private configEnabled;
59
+ private readonly overrides;
60
+ constructor(configEnabled: boolean);
61
+ /** Effective switch for one session: `override ?? config.enabled`. */
62
+ effective(sessionId: string): boolean;
63
+ /**
64
+ * Update the config-level fallback switch (live config — settings onChange,
65
+ * plan dsh-advisor-settings-n2 T1). Sessions with an explicit override keep
66
+ * it; every other session follows the new switch, so a Settings-page edit
67
+ * takes effect for new sessions without touching the override mechanism.
68
+ */
69
+ setConfigEnabled(enabled: boolean): void;
70
+ /** Set the override for one session. */
71
+ set(sessionId: string, enabled: boolean): void;
72
+ /** Remove a session's override, falling back to the config switch. */
73
+ clear(sessionId: string): void;
74
+ }
75
+ /**
76
+ * Per-session status snapshot consumed by `/advisor status`. Built by the
77
+ * wiring (`index.ts`) from the resolved config, the session's runtime, and
78
+ * the override state.
79
+ */
80
+ export interface AdvisorSessionStatus {
81
+ /** Effective switch for this session (`override ?? config.enabled`). */
82
+ readonly enabled: boolean;
83
+ /**
84
+ * Present iff the session is effectively enabled but the S4 explicit gate
85
+ * blocks model calls (provider/model missing or empty) — disabled-with-
86
+ * reason (spec §5.2).
87
+ */
88
+ readonly disabledReason?: string;
89
+ /** Configured provider route (shown even while disabled — spec §5.2). */
90
+ readonly provider?: string;
91
+ /** Configured model id (shown even while disabled — spec §5.2). */
92
+ readonly model?: string;
93
+ /** The session's runtime status; `disabled` when no runtime exists. */
94
+ readonly runtimeStatus: AdvisorRuntimeStatus;
95
+ /** Deltas waiting to be drained (bounded backlog, spec §6). */
96
+ readonly pendingCount: number;
97
+ /** Epoch-ms of the last accepted note; undefined before the first (T4). */
98
+ readonly lastActivityAt?: number;
99
+ }
100
+ /**
101
+ * Render the status surface. Kept minimal and truthful: state, the S4 reason
102
+ * when the gate blocks, the resolved provider/model, the runtime status with
103
+ * the pending count, and the last accepted-note activity (ISO, or `never`).
104
+ */
105
+ export declare function advisorStatusText(status: AdvisorSessionStatus): string;
106
+ /**
107
+ * The session-scoped operations the `/advisor` handler drives. Implemented by
108
+ * the wiring (`index.ts`) against the observer, the per-session runtimes, and
109
+ * the resolved config; faked in unit tests.
110
+ */
111
+ export interface AdvisorCommandController {
112
+ /**
113
+ * Apply the session override and start/stop the session's runtime:
114
+ * enabling seeds the observer cursor to the current transcript length
115
+ * (KD-5 — no full-history replay) and resumes/creates the runtime; disabling
116
+ * disposes the runtime (aborts the in-flight call, drops the backlog).
117
+ * @param sessionLength - current transcript length, used for the KD-5 seed.
118
+ */
119
+ setEnabled(sessionId: string, enabled: boolean, sessionLength?: number): void;
120
+ /** Snapshot the per-session status surface. */
121
+ getStatus(sessionId: string): AdvisorSessionStatus;
122
+ }
123
+ /** Minimal command registry surface (satisfied by the dsh `CommandService`). */
124
+ export interface AdvisorCommandRegistry {
125
+ register(definition: CommandDefinition): () => void;
126
+ }
127
+ /** Usage text for an unknown `/advisor` subcommand. */
128
+ export declare const USAGE: string;
129
+ /**
130
+ * Register the `/advisor` command with a command registry (the dsh
131
+ * `CommandService`, or a fake in tests). Called from the plugin's conditional
132
+ * `ctx.inject(['commands'], ...)` child — the command exists only when a
133
+ * registry is composed.
134
+ * @returns the registry disposer (the inject child owns its lifetime).
135
+ */
136
+ export declare function registerAdvisorCommands(registry: AdvisorCommandRegistry, controller: AdvisorCommandController): () => void;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * T7 — slash commands (spec §2 S5, §6 status surface, §8.5 KD-5 seed-on-enable).
3
+ *
4
+ * One `/advisor` command is registered (through {@link registerAdvisorCommands})
5
+ * with four forms:
6
+ *
7
+ * - `/advisor` — toggle the per-session override (on ↔ off);
8
+ * - `/advisor on` — enable the advisor for this session;
9
+ * - `/advisor off` — disable the advisor for this session;
10
+ * - `/advisor status` — report the per-session status surface;
11
+ * - anything else — usage text.
12
+ *
13
+ * Toggle/on/off are **session-scoped and ephemeral**: they drive a per-session
14
+ * override flag (`AdvisorSessionOverrides`) that the runtime gate consults as
15
+ * `override ?? config.enabled`, so no command ever touches the persisted
16
+ * config (spec §4 mapping — matches omp `/advisor` semantics). Enabling a
17
+ * session whose config has no provider/model starts no model call: the S4
18
+ * explicit gate (spec §5.2) still applies, and the status/on text explains
19
+ * the disabled-with-reason.
20
+ *
21
+ * The module is cordis-free (pure parse + render + registration contract), so
22
+ * it is unit-testable with a fake command registry and a fake controller;
23
+ * `index.ts` binds it into the plugin through the conditional
24
+ * `ctx.inject(['commands'], ...)` child (commands must NOT join the top-level
25
+ * inject list — T1 fix).
26
+ *
27
+ * @module dsh-advisor/commands
28
+ */
29
+ /**
30
+ * Parse the text following `/advisor` (the dsh `parseCommand` split already
31
+ * yields `rawInput` including the separator whitespace, e.g. `' on'` for
32
+ * `/advisor on`). Subcommands match exactly after trimming — same
33
+ * case-sensitivity as dsh command names; anything else is a usage error.
34
+ */
35
+ export function parseAdvisorCommand(rawInput) {
36
+ const argument = rawInput.trim();
37
+ if (argument === '')
38
+ return { kind: 'toggle' };
39
+ if (argument === 'on')
40
+ return { kind: 'on' };
41
+ if (argument === 'off')
42
+ return { kind: 'off' };
43
+ if (argument === 'status')
44
+ return { kind: 'status' };
45
+ return { kind: 'usage' };
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // Per-session override mechanism (session-scoped, ephemeral)
49
+ // ---------------------------------------------------------------------------
50
+ /**
51
+ * The per-session override consulted by the runtime gate as
52
+ * `override ?? config.enabled`. `/advisor on|off|toggle` write here — the
53
+ * persisted config is never modified. The map is keyed by session id and
54
+ * entries live for the session lifetime (`index.ts` clears them on
55
+ * `agent/disposed` / `session/disposed`).
56
+ */
57
+ export class AdvisorSessionOverrides {
58
+ configEnabled;
59
+ overrides = new Map();
60
+ constructor(configEnabled) {
61
+ this.configEnabled = configEnabled;
62
+ }
63
+ /** Effective switch for one session: `override ?? config.enabled`. */
64
+ effective(sessionId) {
65
+ return this.overrides.get(sessionId) ?? this.configEnabled;
66
+ }
67
+ /**
68
+ * Update the config-level fallback switch (live config — settings onChange,
69
+ * plan dsh-advisor-settings-n2 T1). Sessions with an explicit override keep
70
+ * it; every other session follows the new switch, so a Settings-page edit
71
+ * takes effect for new sessions without touching the override mechanism.
72
+ */
73
+ setConfigEnabled(enabled) {
74
+ this.configEnabled = enabled;
75
+ }
76
+ /** Set the override for one session. */
77
+ set(sessionId, enabled) {
78
+ this.overrides.set(sessionId, enabled);
79
+ }
80
+ /** Remove a session's override, falling back to the config switch. */
81
+ clear(sessionId) {
82
+ this.overrides.delete(sessionId);
83
+ }
84
+ }
85
+ /**
86
+ * Render the status surface. Kept minimal and truthful: state, the S4 reason
87
+ * when the gate blocks, the resolved provider/model, the runtime status with
88
+ * the pending count, and the last accepted-note activity (ISO, or `never`).
89
+ */
90
+ export function advisorStatusText(status) {
91
+ const lines = [];
92
+ lines.push(status.enabled ? 'Advisor: enabled' : 'Advisor: disabled');
93
+ if (status.disabledReason !== undefined)
94
+ lines.push(`Reason: ${status.disabledReason}`);
95
+ if (status.provider && status.model) {
96
+ lines.push(`Model: ${status.provider}/${status.model}`);
97
+ }
98
+ const pending = status.pendingCount > 0 ? ` (${status.pendingCount} pending)` : '';
99
+ lines.push(`Runtime: ${status.runtimeStatus}${pending}`);
100
+ lines.push(`Last activity: ${status.lastActivityAt === undefined ? 'never' : new Date(status.lastActivityAt).toISOString()}`);
101
+ return lines.join('\n');
102
+ }
103
+ /** Usage text for an unknown `/advisor` subcommand. */
104
+ export const USAGE = [
105
+ 'Usage: /advisor [on|off|status]',
106
+ ' /advisor toggle the advisor for this session',
107
+ ' /advisor on enable the advisor for this session',
108
+ ' /advisor off disable the advisor for this session',
109
+ ' /advisor status show per-session advisor status (state, model, runtime, pending, last activity)',
110
+ ].join('\n');
111
+ /**
112
+ * "Enabled" outcome text — mentions the S4 gate when it blocks model calls.
113
+ * Callers pass the status AFTER the override flip, so the caveat appears when
114
+ * the flip itself is what trips the gate (qc2 W-2 / qc3 I-2 — the pre-flip
115
+ * status cannot know the gate yet: the gate only fires when enabled).
116
+ */
117
+ function enableText(status) {
118
+ if (status.disabledReason === undefined)
119
+ return 'Advisor on for this session.';
120
+ return `Advisor on for this session — but no model call can start: ${status.disabledReason}`;
121
+ }
122
+ /** Build the `/advisor` handler bound to one controller. */
123
+ function createAdvisorCommandHandler(controller) {
124
+ return (invocation) => {
125
+ const sessionId = invocation.agent.session.id;
126
+ switch (parseAdvisorCommand(invocation.rawInput).kind) {
127
+ case 'toggle': {
128
+ const before = controller.getStatus(sessionId);
129
+ const next = !before.enabled;
130
+ // The KD-5 seed length is only meaningful when enabling.
131
+ controller.setEnabled(sessionId, next, next ? invocation.agent.session.events.length : undefined);
132
+ if (!next)
133
+ return { kind: 'success', text: 'Advisor off for this session.' };
134
+ // Post-flip status: the reply carries the S4 gate caveat when the
135
+ // toggle-to-on flip trips the gate (qc2 W-2 / qc3 I-2).
136
+ return { kind: 'success', text: enableText(controller.getStatus(sessionId)) };
137
+ }
138
+ case 'on': {
139
+ const before = controller.getStatus(sessionId);
140
+ // Recovery routing (qc1/qc2/qc3 W-1/I-4): an effectively-enabled
141
+ // session whose runtime is halted/quota-paused must reach `setEnabled`
142
+ // (which resumes/rebuilds it) — a plain "already on" would be a dead
143
+ // end, since the only resume call site sits behind the enable path.
144
+ const needsRecovery = before.enabled
145
+ && (before.runtimeStatus === 'halted' || before.runtimeStatus === 'quota_exhausted');
146
+ if (before.enabled && !needsRecovery) {
147
+ return { kind: 'success', text: 'Advisor is already on for this session.' };
148
+ }
149
+ controller.setEnabled(sessionId, true, invocation.agent.session.events.length);
150
+ // Reply from the POST-flip status: when the override flip trips the
151
+ // S4 gate (config-off + missing provider/model), the reply must say
152
+ // the advisor did not start and why, not a bare "Advisor on" (qc2
153
+ // W-2 / qc3 I-2).
154
+ return { kind: 'success', text: enableText(controller.getStatus(sessionId)) };
155
+ }
156
+ case 'off': {
157
+ const before = controller.getStatus(sessionId);
158
+ if (!before.enabled)
159
+ return { kind: 'success', text: 'Advisor is already off for this session.' };
160
+ controller.setEnabled(sessionId, false);
161
+ return { kind: 'success', text: 'Advisor off for this session.' };
162
+ }
163
+ case 'status':
164
+ return { kind: 'success', text: advisorStatusText(controller.getStatus(sessionId)) };
165
+ case 'usage':
166
+ return { kind: 'success', text: USAGE };
167
+ }
168
+ };
169
+ }
170
+ /**
171
+ * Register the `/advisor` command with a command registry (the dsh
172
+ * `CommandService`, or a fake in tests). Called from the plugin's conditional
173
+ * `ctx.inject(['commands'], ...)` child — the command exists only when a
174
+ * registry is composed.
175
+ * @returns the registry disposer (the inject child owns its lifetime).
176
+ */
177
+ export function registerAdvisorCommands(registry, controller) {
178
+ return registry.register({
179
+ name: 'advisor',
180
+ description: 'Toggle, enable, disable, or inspect the per-session advisor',
181
+ input: { hint: '[on|off|status]' },
182
+ handler: createAdvisorCommandHandler(controller),
183
+ });
184
+ }
185
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * dsh-advisor plugin configuration contract (spec §5 / S4).
3
+ *
4
+ * The exported schemastery `Config` schema is what the cordis Loader uses to
5
+ * validate the plugin row config: it applies defaults (`enabled` false,
6
+ * `immuneTurns` 3, `maxDeltaMessages` 60, `systemPrompt` "") and enforces
7
+ * types/bounds (integers ≥ 0). `resolveAdvisorConfig(raw)` additionally
8
+ * enforces the explicit model gate: when `enabled` is true but `provider` or
9
+ * `model` is missing or empty, it resolves to a disabled-with-reason config —
10
+ * the advisor never starts a model call (hard gate, not a warning).
11
+ *
12
+ * @module dsh-advisor/config
13
+ */
14
+ import z from 'schemastery';
15
+ /** Raw plugin row config after Loader defaults — spec §5.1. */
16
+ export interface AdvisorConfig {
17
+ /** Master switch; default false. */
18
+ readonly enabled: boolean;
19
+ /** Provider route; REQUIRED (non-empty) when enabled. */
20
+ readonly provider?: string;
21
+ /** Model id; REQUIRED (non-empty) when enabled. */
22
+ readonly model?: string;
23
+ /** Optional system prompt override; "" = built-in reviewer prompt (T4). */
24
+ readonly systemPrompt: string;
25
+ /** Cooldown after a delivered interrupt; integer ≥ 0, default 3. */
26
+ readonly immuneTurns: number;
27
+ /** Delta window; integer ≥ 0, default 60, 0 = unbounded (KD-3). */
28
+ readonly maxDeltaMessages: number;
29
+ }
30
+ /** Config after the explicit model gate (spec §5.2) — consumed by T4/T6. */
31
+ export interface ResolvedAdvisorConfig {
32
+ readonly enabled: boolean;
33
+ readonly provider?: string;
34
+ readonly model?: string;
35
+ readonly systemPrompt: string;
36
+ readonly immuneTurns: number;
37
+ readonly maxDeltaMessages: number;
38
+ /** Present iff the advisor is disabled by the explicit model gate. */
39
+ readonly disabledReason?: string;
40
+ }
41
+ /**
42
+ * Loader schema (strict): defaults + type/bounds validation for the plugin
43
+ * row config. The explicit gate is intentionally NOT here — `provider`/`model`
44
+ * stay optional so an enabled-without-pair config validates and then resolves
45
+ * to disabled-with-reason instead of failing to load.
46
+ *
47
+ * Type note: left to inference (`Schema<ObjectS, ObjectT>`), so calling the
48
+ * schema accepts partial input (each key optional, `| null`) and yields the
49
+ * fully-defaulted output — matching schemastery's runtime semantics.
50
+ */
51
+ export declare const Config: z<Schemastery.ObjectS<{
52
+ enabled: z<boolean, boolean>;
53
+ provider: z<string, string>;
54
+ model: z<string, string>;
55
+ systemPrompt: z<string, string>;
56
+ immuneTurns: z<number, number>;
57
+ maxDeltaMessages: z<number, number>;
58
+ }>, Schemastery.ObjectT<{
59
+ enabled: z<boolean, boolean>;
60
+ provider: z<string, string>;
61
+ model: z<string, string>;
62
+ systemPrompt: z<string, string>;
63
+ immuneTurns: z<number, number>;
64
+ maxDeltaMessages: z<number, number>;
65
+ }>>;
66
+ /**
67
+ * Resolve the raw config into the runtime contract.
68
+ *
69
+ * - Rejects unknown keys (strict schema, spec §5.2) and non-object input.
70
+ * - Applies the explicit model gate (S4): `enabled: true` with `provider` or
71
+ * `model` missing/empty → disabled-with-reason, never throws, no model call.
72
+ * - `provider`/`model` are ignored while disabled.
73
+ */
74
+ export declare function resolveAdvisorConfig(raw: unknown): ResolvedAdvisorConfig;
package/lib/config.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * dsh-advisor plugin configuration contract (spec §5 / S4).
3
+ *
4
+ * The exported schemastery `Config` schema is what the cordis Loader uses to
5
+ * validate the plugin row config: it applies defaults (`enabled` false,
6
+ * `immuneTurns` 3, `maxDeltaMessages` 60, `systemPrompt` "") and enforces
7
+ * types/bounds (integers ≥ 0). `resolveAdvisorConfig(raw)` additionally
8
+ * enforces the explicit model gate: when `enabled` is true but `provider` or
9
+ * `model` is missing or empty, it resolves to a disabled-with-reason config —
10
+ * the advisor never starts a model call (hard gate, not a warning).
11
+ *
12
+ * @module dsh-advisor/config
13
+ */
14
+ import z from 'schemastery';
15
+ /**
16
+ * Complete configuration key set for strict unknown-key rejection. The
17
+ * schemastery object resolver merges unknown keys by default (strict flag is
18
+ * never passed by the cordis Loader), so the resolver rejects them explicitly
19
+ * — same pattern as `resolveSessionTitleLlmConfig` in the dsh repo.
20
+ */
21
+ const CONFIG_KEYS = new Set([
22
+ 'enabled',
23
+ 'provider',
24
+ 'model',
25
+ 'systemPrompt',
26
+ 'immuneTurns',
27
+ 'maxDeltaMessages',
28
+ ]);
29
+ /**
30
+ * Loader schema (strict): defaults + type/bounds validation for the plugin
31
+ * row config. The explicit gate is intentionally NOT here — `provider`/`model`
32
+ * stay optional so an enabled-without-pair config validates and then resolves
33
+ * to disabled-with-reason instead of failing to load.
34
+ *
35
+ * Type note: left to inference (`Schema<ObjectS, ObjectT>`), so calling the
36
+ * schema accepts partial input (each key optional, `| null`) and yields the
37
+ * fully-defaulted output — matching schemastery's runtime semantics.
38
+ */
39
+ export const Config = z.object({
40
+ enabled: z.boolean().default(false),
41
+ provider: z.string(),
42
+ model: z.string(),
43
+ systemPrompt: z.string().default(''),
44
+ immuneTurns: z.number().step(1).min(0).default(3),
45
+ maxDeltaMessages: z.number().step(1).min(0).default(60),
46
+ });
47
+ function isNonEmptyString(value) {
48
+ // Trim before checking: a whitespace-only value (" ") is empty in effect
49
+ // and must trip the explicit gate (spec §5.2 "missing or empty"; qc2 W-3 /
50
+ // qc3 I-3 — a strict superset of dsh's own `length === 0` check).
51
+ return typeof value === 'string' && value.trim().length > 0;
52
+ }
53
+ /**
54
+ * Resolve the raw config into the runtime contract.
55
+ *
56
+ * - Rejects unknown keys (strict schema, spec §5.2) and non-object input.
57
+ * - Applies the explicit model gate (S4): `enabled: true` with `provider` or
58
+ * `model` missing/empty → disabled-with-reason, never throws, no model call.
59
+ * - `provider`/`model` are ignored while disabled.
60
+ */
61
+ export function resolveAdvisorConfig(raw) {
62
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
63
+ throw new TypeError('dsh-advisor: configuration must be a plain object');
64
+ }
65
+ for (const key of Object.keys(raw)) {
66
+ if (!CONFIG_KEYS.has(key)) {
67
+ throw new Error(`dsh-advisor: unknown config key "${key}"`);
68
+ }
69
+ }
70
+ const config = Config(raw);
71
+ // schemastery passes nullable input (null) through for fields without a
72
+ // default — normalize to undefined so the resolved contract is null-free
73
+ // and the gate treats null exactly like a missing value.
74
+ const normalized = {
75
+ ...config,
76
+ provider: config.provider ?? undefined,
77
+ model: config.model ?? undefined,
78
+ };
79
+ if (!normalized.enabled)
80
+ return normalized;
81
+ const missing = [];
82
+ if (!isNonEmptyString(normalized.provider))
83
+ missing.push('provider');
84
+ if (!isNonEmptyString(normalized.model))
85
+ missing.push('model');
86
+ if (missing.length === 0)
87
+ return normalized;
88
+ const disabledReason = missing.length === 2
89
+ ? 'enabled but provider and model are missing — configure both to enable the advisor'
90
+ : `enabled but ${missing[0]} is missing or empty — configure provider and model`;
91
+ return { ...normalized, enabled: false, disabledReason };
92
+ }
93
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,CAAC,MAAM,aAAa,CAAA;AA8B3B;;;;;GAKG;AACH,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC;IAC/C,SAAS;IACT,UAAU;IACV,OAAO;IACP,cAAc;IACd,aAAa;IACb,kBAAkB;CACnB,CAAC,CAAA;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACpC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACxD,CAAC,CAAA;AAEF,SAAS,gBAAgB,CAAC,KAAyB;IACjD,2EAA2E;IAC3E,2EAA2E;IAC3E,kEAAkE;IAClE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;AAC7D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAA;IAC1E,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,GAAG,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,wEAAwE;IACxE,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,UAAU,GAAkB;QAChC,GAAG,MAAM;QACT,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,SAAS;QACtC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,SAAS;KACjC,CAAA;IACD,IAAI,CAAC,UAAU,CAAC,OAAO;QAAE,OAAO,UAAU,CAAA;IAC1C,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACpE,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC9D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,UAAU,CAAA;IAC3C,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC;QACzC,CAAC,CAAC,mFAAmF;QACrF,CAAC,CAAC,eAAe,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAA;IAClF,OAAO,EAAE,GAAG,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,CAAA;AAC1D,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Delivery routing (spec §2 S3, §4 mapping row, §6 delivery semantics,
3
+ * §8.4 KD-4) — the advice delivery channel into the primary agent.
4
+ *
5
+ * One {@link AdvisorDelivery} exists per plugin load and owns:
6
+ *
7
+ * - **The KD-4 per-session agent map**: keyed by `agent.id` (=== `session.id`),
8
+ * maintained by `index.ts` on `agent/created` / `agent/disposed`, with a
9
+ * registry fallback (`ctx.agents.get(session.id)`, injected as
10
+ * `lookupAgent`) that covers agents published before this plugin loaded.
11
+ * A missing agent at delivery time drops the note with a log — advisory
12
+ * only, never throw, never stall.
13
+ * - **Severity routing (spec §6)**: nit → `agent.inject` (non-waking, consumed
14
+ * at the next pre-step boundary); concern/blocker → `agent.steer` (waking —
15
+ * an idle driver starts a turn, a running driver consumes at its next step
16
+ * boundary).
17
+ * - **The immuneTurns cooldown (spec §6)**: after a concern/blocker is actually
18
+ * steered, the next `immuneTurns` stepped primary turns must complete before
19
+ * another interrupting note may steer; interrupting notes inside the window
20
+ * downgrade to inject. The fence arms only on a real steer delivery; the
21
+ * observer's `onSteppedTurnEnd` / `onRewrite` hooks (T3 wiring) drive the
22
+ * countdown and the KD-5 reset.
23
+ *
24
+ * Message shape (spec §6): a user-role message via `createUserMessage` whose
25
+ * source carries the distinct `kind === 'advisor'` (the plugin's
26
+ * `MessageSourceMap` merge extension, src/kinds.ts) and whose content is
27
+ * self-describing `[advisor:{severity}] {note}` — the only cue the primary
28
+ * model gets about how to treat it ("weigh, don't blindly obey" spirit).
29
+ *
30
+ * Delivery is synchronous and fire-and-forget; the runtime path (T4 F1) is
31
+ * what contains a throwing `inject`/`steer` — this module lets agent-method
32
+ * throws propagate to that containment seam.
33
+ *
34
+ * @module dsh-advisor/delivery
35
+ */
36
+ import type { UserMessage } from '@deepseek-ai/dsh-llm';
37
+ import type { AdviceNote } from './advisor-runtime.js';
38
+ /** The channel one accepted note is delivered on (spec §6). */
39
+ export type DeliveryChannel = 'inject' | 'steer';
40
+ /** Minimal `Agent` surface the delivery router drives (spec §4, KD-4). */
41
+ export interface AdvisorDeliveryAgent {
42
+ /** `Agent.id` — equals the session id by construction (KD-4). */
43
+ readonly id: string;
44
+ /** Non-waking: queue model-facing context for the next pre-step (spec §6). */
45
+ inject(message: UserMessage): void;
46
+ /** Waking: submit steering for the nearest step (spec §6). */
47
+ steer(message: UserMessage): void;
48
+ }
49
+ /** Logger seam (cordis `ctx.logger('advisor')` satisfies it; console works too). */
50
+ export interface AdvisorDeliveryLogger {
51
+ debug(message: string, ...args: unknown[]): void;
52
+ warn(message: string, ...args: unknown[]): void;
53
+ }
54
+ /** Options for one {@link AdvisorDelivery}. */
55
+ export interface AdvisorDeliveryOptions {
56
+ /** immuneTurns cooldown length (config, default 3, ≥ 0). */
57
+ readonly immuneTurns: number;
58
+ /**
59
+ * KD-4 registry fallback (`ctx.agents.get(sessionId)`) — resolves agents
60
+ * published before this plugin loaded, whose `agent/created` was never
61
+ * observed. Absent → the map alone is authoritative.
62
+ */
63
+ readonly lookupAgent?: (sessionId: string) => AdvisorDeliveryAgent | undefined;
64
+ readonly logger?: AdvisorDeliveryLogger;
65
+ }
66
+ /**
67
+ * Build the advisor message for one note (spec §6): a user-role message whose
68
+ * source carries the distinct advisor kind and whose content is self-describing
69
+ * `[advisor:{severity}] {note}`.
70
+ *
71
+ * Bounds (qc3 F-2 / qc2 S-1): the note itself is already capped at
72
+ * `ADVISOR_NOTE_MAX_CHARS` by extraction; the collapsed-row summary is
73
+ * additionally bounded via `boundContextSummary` (120 chars — the platform
74
+ * `CONTEXT_SUMMARY_MAX_CHARS` convention), so the durable log and the
75
+ * collapsed context row never carry an unbounded account.
76
+ */
77
+ export declare function buildAdvisorMessage(note: AdviceNote): UserMessage;
78
+ /**
79
+ * Per-plugin delivery router: severity → channel, the KD-4 agent map, and the
80
+ * immuneTurns cooldown. Cordis-free, so the routing logic is unit-testable.
81
+ */
82
+ export declare class AdvisorDelivery {
83
+ private immuneTurns;
84
+ private readonly lookupAgent;
85
+ private readonly logger;
86
+ /** KD-4 per-session agent map, keyed by `agent.id` (=== session.id). */
87
+ private readonly agents;
88
+ /**
89
+ * immuneTurns latch: remaining stepped primary turns before an interrupting
90
+ * note may steer again (spec §6). A present entry > 0 means armed; the entry
91
+ * is removed when the countdown exhausts.
92
+ */
93
+ private readonly cooldown;
94
+ constructor(options: AdvisorDeliveryOptions);
95
+ /** KD-4: register an agent on `agent/created` (keyed by `agent.id`). */
96
+ registerAgent(agent: AdvisorDeliveryAgent): void;
97
+ /** KD-4: drop an agent — and its cooldown with the session — on `agent/disposed`. */
98
+ unregisterAgent(sessionId: string): void;
99
+ /**
100
+ * Update the immuneTurns cooldown length (live config — settings onChange,
101
+ * plan dsh-advisor-settings-n2 T1). The fence is re-armed with the new
102
+ * length on the next real steer; the per-session cooldown countdown itself
103
+ * is untouched, so the delivery semantics (spec §6) never change mid-window.
104
+ */
105
+ setImmuneTurns(value: number): void;
106
+ /**
107
+ * One completed stepped primary turn (observer `onSteppedTurnEnd`): decrement
108
+ * the immuneTurns countdown. The latch is removed at zero, so the next
109
+ * interrupting note steers again. Total — never throws.
110
+ */
111
+ onSteppedTurnEnd(sessionId: string): void;
112
+ /**
113
+ * KD-5 reset trigger: a compaction / surface rewrite clears the immuneTurns
114
+ * latch — the session state is being rewritten, so the cooldown's turn-count
115
+ * basis no longer applies. Total — never throws.
116
+ */
117
+ reset(sessionId: string): void;
118
+ /**
119
+ * Route one accepted advice note (spec §6, KD-4).
120
+ *
121
+ * Resolves the primary agent via the map, falling back to the registry; a
122
+ * missing agent drops the note with a log (advisory only — never throw,
123
+ * never stall). nit → inject; concern/blocker → steer, unless the
124
+ * immuneTurns fence is armed, in which case they downgrade to inject.
125
+ *
126
+ * @returns the channel delivered on, or `undefined` when dropped (no agent).
127
+ */
128
+ route(sessionId: string, note: AdviceNote): DeliveryChannel | undefined;
129
+ }