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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +7 -0
- package/README.md +303 -0
- package/README.zh.md +166 -0
- package/cordis.patch.yml +6 -0
- package/lib/advisor-runtime.d.ts +242 -0
- package/lib/advisor-runtime.js +662 -0
- package/lib/advisor-runtime.js.map +1 -0
- package/lib/client/advisor-card.d.ts +90 -0
- package/lib/client/advisor-store.d.ts +310 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/locales.d.ts +40 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.js +840 -0
- package/lib/commands.d.ts +136 -0
- package/lib/commands.js +185 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +74 -0
- package/lib/config.js +93 -0
- package/lib/config.js.map +1 -0
- package/lib/delivery.d.ts +129 -0
- package/lib/delivery.js +169 -0
- package/lib/delivery.js.map +1 -0
- package/lib/emission-guard.d.ts +99 -0
- package/lib/emission-guard.js +155 -0
- package/lib/emission-guard.js.map +1 -0
- package/lib/gateway.d.ts +116 -0
- package/lib/gateway.js +214 -0
- package/lib/gateway.js.map +1 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.js +485 -0
- package/lib/index.js.map +1 -0
- package/lib/kinds.d.ts +38 -0
- package/lib/kinds.js +24 -0
- package/lib/kinds.js.map +1 -0
- package/lib/prompts.d.ts +22 -0
- package/lib/prompts.js +38 -0
- package/lib/prompts.js.map +1 -0
- package/lib/settings.d.ts +96 -0
- package/lib/settings.js +141 -0
- package/lib/settings.js.map +1 -0
- package/lib/transcript.d.ts +257 -0
- package/lib/transcript.js +530 -0
- package/lib/transcript.js.map +1 -0
- package/package.json +90 -0
- 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;
|
package/lib/commands.js
ADDED
|
@@ -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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"}
|
package/lib/config.d.ts
ADDED
|
@@ -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
|
+
}
|