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,242 @@
1
+ /**
2
+ * Per-session advisor runtime (spec §2 S2, §4 mapping rows, §6, §8.2 KD-2,
3
+ * §8.5 KD-5) — the "advisor model call + note extraction + drain/backlog"
4
+ * core.
5
+ *
6
+ * One {@link AdvisorRuntime} exists per session (created on `agent/created` or
7
+ * lazily on the first stepped `turn/end`, disposed on `agent/disposed` /
8
+ * `session/disposed` — wired in `index.ts`). It owns:
9
+ *
10
+ * - a FIFO queue of pending transcript deltas (bounded — spec §6 "bounded
11
+ * backlog"; drop-newest when full);
12
+ * - a serialized async drain loop: one `llm.stream` call per delta with
13
+ * `{ provider, model, system, messages: [user delta], maxTokens: 5120 }` and
14
+ * `purpose` left UNSET (KD-5 — an advisor call is an ordinary conversation
15
+ * request);
16
+ * - a call-level deadline on every `llm.stream` call (dsh-timeout `deadline`,
17
+ * fused with the dispose signal and raced per chunk): a hung provider stream
18
+ * times out instead of wedging the drain, and a timeout is a transient
19
+ * failure (KD-5 retry → drop);
20
+ * - KD-2 JSON-frame extraction: the first balanced `{…}` in the reply is
21
+ * parsed (tolerant of prose/fences), `note` must be non-empty (else
22
+ * drop+log), `severity` missing/invalid defaults to `nit`, no parse retry;
23
+ * - the KD-5 failure policy: transient → 1 retry with a short backoff → drop;
24
+ * 3 consecutive dropped deltas → flush the pending backlog (never stall);
25
+ * permanent errors (`invalid_request_error`, model-not-found, "is not
26
+ * supported when") → halt the session's advisor; quota/rate-limit → pause
27
+ * (`quota_exhausted`), batch retained, no auto-resume timer; the in-flight
28
+ * call is aborted on dispose via the `signal`. `halted` is terminal in
29
+ * place — the command layer rebuilds the runtime (dispose + recreate); a
30
+ * `quota_exhausted` runtime resumes via {@link AdvisorRuntime.resume}.
31
+ *
32
+ * The runtime never parks the primary loop: everything is fire-and-forget
33
+ * async and a failing advisor can only drop its own backlog.
34
+ *
35
+ * @module dsh-advisor/advisor-runtime
36
+ */
37
+ import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
38
+ import type { EmissionGuard } from './emission-guard.js';
39
+ import type { Delta } from './transcript.js';
40
+ /** Severity vocabulary (spec §6) carried by every delivered advice note. */
41
+ export type AdviceSeverity = 'nit' | 'concern' | 'blocker';
42
+ /** One extracted advice note, fed to the T5 emission guard for delivery. */
43
+ export interface AdviceNote {
44
+ readonly note: string;
45
+ readonly severity: AdviceSeverity;
46
+ }
47
+ /**
48
+ * Per-session runtime status surface for T7 `/advisor status`.
49
+ *
50
+ * T4 sets `running` | `quota_exhausted` | `halted`; `paused` is reserved for
51
+ * explicit pause semantics and `disabled` for the config gate — the runtime is
52
+ * never constructed in either state (`index.ts` returns early when the resolved
53
+ * config is disabled).
54
+ */
55
+ export type AdvisorRuntimeStatus = 'running' | 'paused' | 'quota_exhausted' | 'halted' | 'disabled';
56
+ /** Minimal `ctx.llm` surface the runtime drives (satisfied by `LlmRuntime`). */
57
+ export interface AdvisorLlm {
58
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
59
+ /**
60
+ * Optional capability query (LlmRuntime exposes it; injected fakes may not).
61
+ * Used to capability-gate `reasoningEffort` (qc2 W-1 / qc1 W-1 / qc3 F-3):
62
+ * the dsh LlmRuntime rejects an explicit effort for any model whose adapter
63
+ * does not declare it, so the runtime resolves the model's declared efforts
64
+ * and passes `'off'` only when the model supports it. Absent → the option
65
+ * is omitted and `resolveCallFor` materializes the adapter default.
66
+ */
67
+ resolveModelInfo?(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
68
+ }
69
+ /** Logger seam (cordis `ctx.logger('advisor')` satisfies it; console works too). */
70
+ export interface AdvisorRuntimeLogger {
71
+ debug(message: string, ...args: unknown[]): void;
72
+ warn(message: string, ...args: unknown[]): void;
73
+ }
74
+ /** Options for one per-session {@link AdvisorRuntime}. */
75
+ export interface AdvisorRuntimeOptions {
76
+ /** Resolved provider route (the explicit gate guarantees it when enabled). */
77
+ readonly provider: string;
78
+ /** Resolved model id (the explicit gate guarantees it when enabled). */
79
+ readonly model: string;
80
+ /** System prompt sent with every advisor call (config override or KD-2 default). */
81
+ readonly systemPrompt: string;
82
+ /** Output-token cap (KD-2); default 256. */
83
+ readonly maxTokens?: number;
84
+ /** Backoff for the single transient retry (KD-5); default 1000ms. */
85
+ readonly retryBackoffMs?: number;
86
+ /**
87
+ * Call-level deadline for one `llm.stream` call (qc2 W-4 / qc3 W-1): a hung
88
+ * provider stream (no chunk, no end, no error) times out instead of wedging
89
+ * this session's drain. A timeout is classified as a transient failure —
90
+ * KD-5 retry(1) → drop. Default 60000ms.
91
+ */
92
+ readonly callTimeoutMs?: number;
93
+ /** Bounded backlog (spec §6); default 32 — drop-newest with a log when full. */
94
+ readonly maxQueued?: number;
95
+ /** The llm service (`ctx.llm`); injectable for tests. */
96
+ readonly llm: AdvisorLlm;
97
+ /**
98
+ * The T5 emission guard gating extracted notes before delivery. Defaults to
99
+ * a fresh {@link createEmissionGuard} (per-runtime lifetime — a new guard
100
+ * per session); injectable for tests.
101
+ */
102
+ readonly guard?: EmissionGuard;
103
+ /**
104
+ * Invoked once per extracted note that passes the T5 emission guard
105
+ * (accepted = delivered to T6; suppressed notes are dropped silently).
106
+ */
107
+ readonly onNote: (note: AdviceNote) => void;
108
+ readonly logger?: AdvisorRuntimeLogger;
109
+ }
110
+ /**
111
+ * n4 user direction: the advisor call runs with a 20x token budget
112
+ * (256 -> 5120) so even a reasoning-heavy reply cannot starve the JSON frame.
113
+ * Exported so the test suites assert the pinned value instead of a magic
114
+ * literal.
115
+ *
116
+ * Supersession note (qc2 S-2 / qc1 S-2 / qc3 F-2): the frozen spec §8.2
117
+ * (KD-2) pins `maxTokens: 256` ("so a runaway reply cannot blow the budget").
118
+ * This 5120 value is the USER-DIRECTED supersession of that pin — a 20x
119
+ * worst-case per-call ceiling, adopted together with `reasoningEffort: 'off'`
120
+ * (capability-gated, see `resolveReasoningEffort`) so the raised budget goes
121
+ * to the JSON frame rather than reasoning output. The looser runaway-reply
122
+ * guard is re-bounded downstream: `extractAdviceNote` caps the note at
123
+ * `ADVISOR_NOTE_MAX_CHARS` and `buildAdvisorMessage` bounds the notice
124
+ * summary via `boundContextSummary`.
125
+ */
126
+ export declare const ADVISOR_MAX_TOKENS = 5120;
127
+ /**
128
+ * One extracted note's length cap (qc3 F-2 / qc2 S-1): a verbose/rogue advisor
129
+ * reply with the 20x token budget must not inject an unbounded user-role
130
+ * message into the primary session. Truncated with a '…' marker.
131
+ */
132
+ export declare const ADVISOR_NOTE_MAX_CHARS = 1000;
133
+ /**
134
+ * Extract one {@link AdviceNote} from the advisor's reply (KD-2).
135
+ *
136
+ * Locates the first balanced `{…}` object (tolerant of surrounding prose and
137
+ * markdown fences), parses it, and validates: `note` must be a non-empty
138
+ * string after trim (else drop), `severity` missing/invalid defaults to
139
+ * `nit`. A reply with no parseable frame returns `undefined` — the caller
140
+ * drops + logs, and there is NO retry for parse failures (the retry budget is
141
+ * reserved for transport errors, KD-2). The note is capped at
142
+ * {@link ADVISOR_NOTE_MAX_CHARS} with a '…' marker (qc3 F-2 — the 20x token
143
+ * budget must not translate into an unbounded injection into the primary
144
+ * session). Never throws.
145
+ */
146
+ export declare function extractAdviceNote(reply: string): AdviceNote | undefined;
147
+ /**
148
+ * Per-session advisor runtime: queue deltas, async drain, `llm.stream` call,
149
+ * JSON-frame note extraction, and the KD-5 failure policy. All async work is
150
+ * fire-and-forget — the primary loop is never parked.
151
+ */
152
+ export declare class AdvisorRuntime {
153
+ private readonly provider;
154
+ private readonly model;
155
+ private readonly systemPrompt;
156
+ private readonly maxTokens;
157
+ private readonly retryBackoffMs;
158
+ private readonly callTimeoutMs;
159
+ private readonly maxQueued;
160
+ private readonly llm;
161
+ private readonly guard;
162
+ private readonly onNote;
163
+ private readonly logger;
164
+ private readonly controller;
165
+ /**
166
+ * Per-(provider, model) reasoning-effort capability cache (qc2 W-1 / qc1
167
+ * W-1 / qc3 F-3): resolving the model's declared efforts on EVERY advisor
168
+ * call would add an adapter round-trip per delta; one resolution per
169
+ * (provider, model) per runtime suffices — the route is pinned for the
170
+ * runtime's lifetime.
171
+ */
172
+ private readonly reasoningEffortCache;
173
+ private readonly queue;
174
+ private state;
175
+ private draining;
176
+ private disposed;
177
+ private consecutiveDrops;
178
+ private drainPromise;
179
+ /** Epoch-ms of the last note accepted by the emission guard (T7 status). */
180
+ private lastActivityAt;
181
+ constructor(options: AdvisorRuntimeOptions);
182
+ /** Current per-session status (T7 `/advisor status` surface). */
183
+ status(): AdvisorRuntimeStatus;
184
+ /** Number of deltas waiting to be drained (bounded by `maxQueued`). */
185
+ get pendingCount(): number;
186
+ /**
187
+ * T7 `/advisor status` surface — epoch-ms of the last note accepted by the
188
+ * emission guard, or `undefined` before the first accepted note.
189
+ */
190
+ get lastActivity(): number | undefined;
191
+ /**
192
+ * Queue one rendered transcript delta (from the T3 observer's `onDelta`).
193
+ * While `quota_exhausted`, deltas queue up (bounded) but the drain is never
194
+ * auto-restarted (KD-5 — no auto-resume timer); while `halted`/disposed they
195
+ * are dropped with a log. Never throws, never parks the caller.
196
+ */
197
+ enqueue(delta: Delta): void;
198
+ /** Abort the in-flight call and stop the drain (wiring: session/agent disposed). */
199
+ dispose(): void;
200
+ /**
201
+ * Manual resume after a quota pause (T7 `/advisor on`); no-op when halted/
202
+ * disposed — a halted runtime is terminal in place and is recovered by the
203
+ * command layer via dispose-and-recreate (qc1/qc2/qc3 W-1/I-4), never
204
+ * resumed here.
205
+ */
206
+ resume(): void;
207
+ /**
208
+ * KD-5 reset trigger: a compaction / surface rewrite clears the emission
209
+ * guard's dedupe history and per-update latch — the session state is being
210
+ * rewritten, so the old note history no longer applies (a note already
211
+ * advised before the rewrite may legitimately be advised again). The wiring
212
+ * (`index.ts` onRewrite) calls this alongside the delivery cooldown reset.
213
+ */
214
+ resetGuard(): void;
215
+ /** Resolve once the current drain run settles (test/integration hook). */
216
+ waitForDrain(): Promise<void>;
217
+ private kickDrain;
218
+ /** Serialized drain loop: process the queue one delta at a time. */
219
+ private drain;
220
+ /** KD-5: clear the pending backlog after consecutive failures (never stall). */
221
+ private flushBacklog;
222
+ /** Process one delta: attempt + single retry (transient), classify terminal failures. */
223
+ private processDelta;
224
+ /** One model call: build options, stream text, extract the note (KD-2). */
225
+ private callModel;
226
+ private buildOptions;
227
+ /**
228
+ * Capability-gate the `reasoningEffort: 'off'` option (qc2 W-1 / qc1 W-1 /
229
+ * qc3 F-3): pass the branded 'off' effort only when the resolved model's
230
+ * `reasoning.efforts` includes it, else omit the option (`resolveCallFor`
231
+ * materializes the adapter default). Cached per (provider, model) — one
232
+ * resolution per runtime, never per call. A resolution failure (unknown
233
+ * route, adapter throw, or a deadline abort — n4 QC N-5) is advisory: the
234
+ * call proceeds WITHOUT the option, matching the pre-n4 behavior for every
235
+ * model that does not declare 'off'. The optional `signal` (the call's
236
+ * deadline signal) is threaded into `resolveModelInfo`, whose contract
237
+ * allows adapter-owned asynchronous lookup with cancellation — a hung
238
+ * lookup that honors the signal aborts with the call deadline instead of
239
+ * wedging the drain.
240
+ */
241
+ private resolveReasoningEffort;
242
+ }