@seanhogg/builderforce-memory 2026.9.29 → 2026.9.30
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/dist/bridges/AnthropicBridge.d.ts.map +1 -1
- package/dist/bridges/AnthropicBridge.js +24 -65
- package/dist/bridges/AnthropicBridge.js.map +1 -1
- package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
- package/dist/bridges/OpenAIBridge.js +12 -46
- package/dist/bridges/OpenAIBridge.js.map +1 -1
- package/dist/bridges/VertexAIBridge.d.ts.map +1 -1
- package/dist/bridges/VertexAIBridge.js +3 -34
- package/dist/bridges/VertexAIBridge.js.map +1 -1
- package/dist/distillation/DistillationEngine.d.ts +71 -54
- package/dist/distillation/DistillationEngine.d.ts.map +1 -1
- package/dist/distillation/DistillationEngine.js +112 -95
- package/dist/distillation/DistillationEngine.js.map +1 -1
- package/dist/distillation/index.d.ts +7 -1
- package/dist/distillation/index.d.ts.map +1 -1
- package/dist/distillation/index.js +5 -0
- package/dist/distillation/index.js.map +1 -1
- package/dist/distillation/ports.d.ts +43 -0
- package/dist/distillation/ports.d.ts.map +1 -0
- package/dist/distillation/ports.js +29 -0
- package/dist/distillation/ports.js.map +1 -0
- package/dist/eval/EvalHarness.d.ts +2 -1
- package/dist/eval/EvalHarness.d.ts.map +1 -1
- package/dist/eval/EvalHarness.js +13 -1
- package/dist/eval/EvalHarness.js.map +1 -1
- package/dist/eval/baseline.d.ts +33 -0
- package/dist/eval/baseline.d.ts.map +1 -0
- package/dist/eval/baseline.js +41 -0
- package/dist/eval/baseline.js.map +1 -0
- package/dist/eval/index.d.ts +1 -0
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +1 -0
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/types.d.ts +7 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/rag/EnterpriseRetriever.d.ts.map +1 -1
- package/dist/rag/EnterpriseRetriever.js +2 -1
- package/dist/rag/EnterpriseRetriever.js.map +1 -1
- package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
- package/dist/retrieval/HybridRetriever.js +2 -1
- package/dist/retrieval/HybridRetriever.js.map +1 -1
- package/dist/retrieval/decay.d.ts +24 -0
- package/dist/retrieval/decay.d.ts.map +1 -0
- package/dist/retrieval/decay.js +29 -0
- package/dist/retrieval/decay.js.map +1 -0
- package/dist/retrieval/fusion.d.ts +13 -19
- package/dist/retrieval/fusion.d.ts.map +1 -1
- package/dist/retrieval/fusion.js +17 -44
- package/dist/retrieval/fusion.js.map +1 -1
- package/dist/retrieval/index.d.ts +13 -6
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/index.js +10 -5
- package/dist/retrieval/index.js.map +1 -1
- package/dist/retrieval/mmr.d.ts +60 -0
- package/dist/retrieval/mmr.d.ts.map +1 -0
- package/dist/retrieval/mmr.js +94 -0
- package/dist/retrieval/mmr.js.map +1 -0
- package/dist/retrieval/weightedFusion.d.ts +64 -0
- package/dist/retrieval/weightedFusion.d.ts.map +1 -0
- package/dist/retrieval/weightedFusion.js +80 -0
- package/dist/retrieval/weightedFusion.js.map +1 -0
- package/dist/similarity/index.d.ts +6 -4
- package/dist/similarity/index.d.ts.map +1 -1
- package/dist/similarity/index.js +5 -3
- package/dist/similarity/index.js.map +1 -1
- package/dist/wire/index.d.ts +18 -0
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/wire/index.js +17 -0
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/sse.d.ts +29 -0
- package/dist/wire/sse.d.ts.map +1 -0
- package/dist/wire/sse.js +81 -0
- package/dist/wire/sse.js.map +1 -0
- package/dist/wire/usage.d.ts +46 -0
- package/dist/wire/usage.d.ts.map +1 -0
- package/dist/wire/usage.js +78 -0
- package/dist/wire/usage.js.map +1 -0
- package/package.json +11 -3
- package/src/bridges/AnthropicBridge.ts +24 -68
- package/src/bridges/OpenAIBridge.ts +11 -50
- package/src/bridges/VertexAIBridge.ts +3 -32
- package/src/distillation/DistillationEngine.ts +176 -141
- package/src/distillation/index.ts +15 -1
- package/src/distillation/ports.ts +55 -0
- package/src/eval/EvalHarness.ts +12 -1
- package/src/eval/baseline.ts +51 -0
- package/src/eval/index.ts +1 -0
- package/src/eval/types.ts +7 -0
- package/src/index.ts +5 -2
- package/src/rag/EnterpriseRetriever.ts +2 -1
- package/src/retrieval/HybridRetriever.ts +2 -1
- package/src/retrieval/decay.ts +29 -0
- package/src/retrieval/fusion.ts +19 -52
- package/src/retrieval/index.ts +24 -6
- package/src/retrieval/mmr.ts +126 -0
- package/src/retrieval/weightedFusion.ts +101 -0
- package/src/similarity/index.ts +6 -4
- package/src/wire/index.ts +18 -0
- package/src/wire/sse.ts +74 -0
- package/src/wire/usage.ts +95 -0
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DistillationEngine –
|
|
2
|
+
* DistillationEngine – online knowledge distillation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* This runs entirely in the browser with no Python or full-retraining required.
|
|
4
|
+
* A *teacher* (a frontier model) writes the ideal response; the *student* is adapted
|
|
5
|
+
* on it. Runs with no Python and no full retraining.
|
|
7
6
|
*
|
|
8
7
|
* Distillation flow:
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
8
|
+
* 1. teacher.generate(input) → exemplar
|
|
9
|
+
* 2. quality gate (length / already-learned) → skip, or fall back to the caller's text
|
|
10
|
+
* 3. student.adapt(input + exemplar, + rehearsed past exemplars)
|
|
11
|
+
* 4. log the outcome
|
|
12
|
+
*
|
|
13
|
+
* Teacher and student are ports ({@link ./ports}). Passing an `SSMRuntime` and a
|
|
14
|
+
* `TransformerBridge` — the original pairing — still works unchanged.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import type { AdaptOptions, AdaptResult } from '../session/index.js';
|
|
15
18
|
import type { SSMRuntime } from '../runtime/SSMRuntime.js';
|
|
16
|
-
import type {
|
|
19
|
+
import type { BridgeGenerateOptions } from '../bridges/TransformerBridge.js';
|
|
17
20
|
import { SSMError } from '../errors/SSMError.js';
|
|
21
|
+
import { isDistillationStudent, ssmRuntimeStudent, type DistillationStudent, type DistillationTeacher } from './ports.js';
|
|
18
22
|
|
|
19
23
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
20
24
|
|
|
@@ -25,25 +29,18 @@ export interface QualityGate {
|
|
|
25
29
|
*/
|
|
26
30
|
minLength? : number;
|
|
27
31
|
/**
|
|
28
|
-
* Maximum
|
|
29
|
-
* When the
|
|
32
|
+
* Maximum student perplexity threshold (needs a student with `evaluate`).
|
|
33
|
+
* When the student already achieves perplexity below this value on the teacher
|
|
30
34
|
* output, the content is considered already learned and adaptation is skipped.
|
|
31
35
|
*/
|
|
32
36
|
maxPerplexity? : number;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
export interface DistillOptions {
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
* Default: { wsla: true, epochs: 3 }
|
|
39
|
-
* WSLA is preferred because it is fast and targets the selective
|
|
40
|
-
* projection rows — exactly the parameters that encode token routing.
|
|
41
|
-
*/
|
|
42
|
-
adapt? : AdaptOptions;
|
|
39
|
+
export interface DistillOptions<O = AdaptOptions> {
|
|
40
|
+
/** Options forwarded to the student's `adapt()`. */
|
|
41
|
+
adapt? : O;
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* Options forwarded to `bridge.generate()`.
|
|
46
|
-
*/
|
|
43
|
+
/** Options forwarded to the teacher's `generate()` (system prompt, max tokens, …). */
|
|
47
44
|
generate? : BridgeGenerateOptions;
|
|
48
45
|
|
|
49
46
|
/**
|
|
@@ -51,14 +48,27 @@ export interface DistillOptions {
|
|
|
51
48
|
* already-learned inputs.
|
|
52
49
|
*/
|
|
53
50
|
qualityGate? : QualityGate;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What to learn when the teacher yields nothing usable — it threw, or its output
|
|
54
|
+
* failed the `minLength` gate. Without it such an input is skipped (a teacher
|
|
55
|
+
* error is thrown); with it the student learns this text instead, so the
|
|
56
|
+
* contribution is never lost, and the result says it was not distilled.
|
|
57
|
+
*/
|
|
58
|
+
fallbackText?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cap on the characters of `input` that prefix the exemplar in the training text,
|
|
62
|
+
* so a long input can never crowd the exemplar out of the student's window.
|
|
63
|
+
*/
|
|
64
|
+
contextChars?: number;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
|
-
* Catastrophic-forgetting guard (EVM-5). Online
|
|
58
|
-
* weights toward the newest exemplar, which can erode previously-learned
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* reinforced instead of overwritten.
|
|
68
|
+
* Catastrophic-forgetting guard (EVM-5). Online adaptation moves a narrow set of
|
|
69
|
+
* weights toward the newest exemplar, which can erode previously-learned knowledge.
|
|
70
|
+
* A rehearsal (experience-replay) buffer mitigates this: each adapt also trains on a
|
|
71
|
+
* sample of past exemplars, so old knowledge is continually reinforced.
|
|
62
72
|
*/
|
|
63
73
|
export interface RehearsalOptions {
|
|
64
74
|
/** Ring-buffer capacity of past exemplars. 0 disables rehearsal. Default 0. */
|
|
@@ -69,27 +79,33 @@ export interface RehearsalOptions {
|
|
|
69
79
|
seed?: number;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
/** Why an input was not learned from a teacher exemplar. */
|
|
83
|
+
export type DistillSkipReason = 'low_quality' | 'already_learned' | 'teacher_failed';
|
|
84
|
+
|
|
85
|
+
export interface DistillResult<R = AdaptResult> {
|
|
73
86
|
/** The input prompt that was distilled. */
|
|
74
87
|
input : string;
|
|
75
|
-
/** The teacher's
|
|
88
|
+
/** The teacher's response to the input ('' when the teacher failed). */
|
|
76
89
|
teacherOutput: string;
|
|
77
|
-
/** The
|
|
78
|
-
adaptResult :
|
|
79
|
-
/**
|
|
90
|
+
/** The student's result — its skipped result when nothing was adapted. */
|
|
91
|
+
adaptResult : R;
|
|
92
|
+
/** True when nothing was adapted at all. */
|
|
80
93
|
skipped? : boolean;
|
|
81
|
-
/**
|
|
82
|
-
|
|
94
|
+
/** True when the student learned the teacher's exemplar (not a fallback). */
|
|
95
|
+
distilled : boolean;
|
|
96
|
+
/** Why the exemplar was not learned, when it was not. */
|
|
97
|
+
skipReason? : DistillSkipReason;
|
|
98
|
+
/** What the teacher threw, when `skipReason` is `teacher_failed`. */
|
|
99
|
+
teacherError?: unknown;
|
|
83
100
|
/** Number of past exemplars rehearsed alongside this one (EVM-5). */
|
|
84
101
|
rehearsed? : number;
|
|
85
102
|
/** Student's pre-adapt perplexity on the teacher output, when the quality gate
|
|
86
|
-
* measured it — a "how novel was this exemplar" signal.
|
|
87
|
-
* exemplar was NOT skipped (previously discarded on the trained path). */
|
|
103
|
+
* measured it — a "how novel was this exemplar" signal. */
|
|
88
104
|
gatePerplexity?: number;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
export interface DistillBatchResult {
|
|
92
|
-
results : DistillResult[];
|
|
107
|
+
export interface DistillBatchResult<R = AdaptResult> {
|
|
108
|
+
results : DistillResult<R>[];
|
|
93
109
|
/** Total number of adapt epochs run across all inputs. */
|
|
94
110
|
totalEpochs: number;
|
|
95
111
|
/** Wall-clock time for the entire batch in milliseconds. */
|
|
@@ -101,7 +117,9 @@ export interface DistillationLog {
|
|
|
101
117
|
input : string;
|
|
102
118
|
teacherOutputLength: number;
|
|
103
119
|
skipped : boolean;
|
|
104
|
-
|
|
120
|
+
/** True when the exemplar (not a fallback) was learned. */
|
|
121
|
+
distilled : boolean;
|
|
122
|
+
skipReason? : DistillSkipReason;
|
|
105
123
|
finalLoss? : number;
|
|
106
124
|
epochs : number;
|
|
107
125
|
/** Pre-adapt perplexity on the teacher output (when the quality gate measured it). */
|
|
@@ -113,9 +131,9 @@ const MAX_LOG_ENTRIES = 200;
|
|
|
113
131
|
|
|
114
132
|
// ── DistillationEngine ────────────────────────────────────────────────────────
|
|
115
133
|
|
|
116
|
-
export class DistillationEngine {
|
|
117
|
-
private readonly
|
|
118
|
-
private readonly
|
|
134
|
+
export class DistillationEngine<R = AdaptResult, O = AdaptOptions> {
|
|
135
|
+
private readonly _student : DistillationStudent<R, O>;
|
|
136
|
+
private readonly _teacher : DistillationTeacher;
|
|
119
137
|
private readonly _log : DistillationLog[] = [];
|
|
120
138
|
|
|
121
139
|
// ── Rehearsal buffer (EVM-5 catastrophic-forgetting guard) ─────────────────
|
|
@@ -125,16 +143,16 @@ export class DistillationEngine {
|
|
|
125
143
|
private _rehearsalState : number;
|
|
126
144
|
|
|
127
145
|
/**
|
|
128
|
-
* @param
|
|
129
|
-
* @param
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* `bufferSize > 0`, each adapt also trains on a sample of past
|
|
133
|
-
* exemplars to guard against catastrophic forgetting.
|
|
146
|
+
* @param student What learns: an `SSMRuntime`, or any {@link DistillationStudent}.
|
|
147
|
+
* @param teacher What teaches: any `TransformerBridge`, or any {@link DistillationTeacher}.
|
|
148
|
+
* @param rehearsal Optional experience-replay config (EVM-5). When `bufferSize > 0`,
|
|
149
|
+
* each adapt also trains on a sample of past exemplars.
|
|
134
150
|
*/
|
|
135
|
-
constructor(
|
|
136
|
-
this.
|
|
137
|
-
|
|
151
|
+
constructor(student: SSMRuntime | DistillationStudent<R, O>, teacher: DistillationTeacher, rehearsal: RehearsalOptions = {}) {
|
|
152
|
+
this._student = isDistillationStudent<R, O>(student)
|
|
153
|
+
? student
|
|
154
|
+
: (ssmRuntimeStudent(student as SSMRuntime) as unknown as DistillationStudent<R, O>);
|
|
155
|
+
this._teacher = teacher;
|
|
138
156
|
this._rehearsalSize = Math.max(0, rehearsal.bufferSize ?? 0);
|
|
139
157
|
this._rehearsalK = Math.max(0, rehearsal.sampleK ?? 2);
|
|
140
158
|
this._rehearsalState = (rehearsal.seed ?? 1) >>> 0 || 1;
|
|
@@ -174,133 +192,88 @@ export class DistillationEngine {
|
|
|
174
192
|
* Runs a single distillation pass:
|
|
175
193
|
* 1. Teacher generates a response for `input`
|
|
176
194
|
* 2. Quality gate is evaluated (if configured)
|
|
177
|
-
* 3.
|
|
195
|
+
* 3. Student is adapted on `input` + the teacher's output
|
|
178
196
|
*
|
|
179
|
-
* The training signal is the teacher's full response — this teaches the
|
|
180
|
-
*
|
|
181
|
-
* labelled data or a loss function beyond the standard LM objective.
|
|
197
|
+
* The training signal is the teacher's full response — this teaches the student
|
|
198
|
+
* what a good response to that prompt looks like, without labelled data.
|
|
182
199
|
*/
|
|
183
|
-
async distill(input: string, opts: DistillOptions = {}): Promise<DistillResult
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
epochs : 3,
|
|
187
|
-
...opts.adapt,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
let teacherOutput: string;
|
|
200
|
+
async distill(input: string, opts: DistillOptions<O> = {}): Promise<DistillResult<R>> {
|
|
201
|
+
let teacherOutput = '';
|
|
202
|
+
let teacherError: unknown;
|
|
191
203
|
try {
|
|
192
|
-
teacherOutput = await this.
|
|
204
|
+
teacherOutput = await this._teacher.generate(input, opts.generate);
|
|
193
205
|
} catch (err) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
if (opts.fallbackText === undefined) {
|
|
207
|
+
throw new SSMError(
|
|
208
|
+
'DISTILL_FAILED',
|
|
209
|
+
`Teacher bridge failed to generate for distillation: ${err instanceof Error ? err.message : String(err)}`,
|
|
210
|
+
err,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
teacherError = err;
|
|
214
|
+
}
|
|
215
|
+
if (teacherError !== undefined) {
|
|
216
|
+
return this._learnFallback(input, '', 'teacher_failed', opts, teacherError);
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
// ── Quality gate ──────────────────────────────────────────────────────
|
|
202
220
|
|
|
203
|
-
// Pre-adapt novelty of this exemplar
|
|
204
|
-
// Measured by the gate below; hoisted so it survives to the TRAINED path instead
|
|
205
|
-
// of being dropped for exactly the exemplars we go on to learn from.
|
|
221
|
+
// Pre-adapt novelty of this exemplar, hoisted so it survives to the TRAINED path.
|
|
206
222
|
let gatePerplexity: number | undefined;
|
|
223
|
+
const gate = opts.qualityGate;
|
|
207
224
|
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const result: DistillResult = {
|
|
213
|
-
input,
|
|
214
|
-
teacherOutput,
|
|
215
|
-
adaptResult : { losses: [], epochCount: 0, durationMs: 0 },
|
|
216
|
-
skipped : true,
|
|
217
|
-
skipReason : 'low_quality',
|
|
218
|
-
};
|
|
219
|
-
this._appendLog({
|
|
220
|
-
input,
|
|
221
|
-
teacherOutputLength: teacherOutput.length,
|
|
222
|
-
skipped : true,
|
|
223
|
-
skipReason : 'low_quality',
|
|
224
|
-
epochs : 0,
|
|
225
|
-
});
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
225
|
+
if (gate?.minLength != null && teacherOutput.length < gate.minLength) {
|
|
226
|
+
if (opts.fallbackText !== undefined) return this._learnFallback(input, teacherOutput, 'low_quality', opts);
|
|
227
|
+
return this._skip(input, teacherOutput, 'low_quality');
|
|
228
|
+
}
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
input,
|
|
238
|
-
teacherOutput,
|
|
239
|
-
adaptResult : { losses: [], epochCount: 0, durationMs: 0 },
|
|
240
|
-
skipped : true,
|
|
241
|
-
skipReason : 'already_learned',
|
|
242
|
-
gatePerplexity,
|
|
243
|
-
};
|
|
244
|
-
this._appendLog({
|
|
245
|
-
input,
|
|
246
|
-
teacherOutputLength: teacherOutput.length,
|
|
247
|
-
skipped : true,
|
|
248
|
-
skipReason : 'already_learned',
|
|
249
|
-
epochs : 0,
|
|
250
|
-
gatePerplexity,
|
|
251
|
-
});
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
230
|
+
if (gate?.maxPerplexity != null && this._student.evaluate) {
|
|
231
|
+
try {
|
|
232
|
+
gatePerplexity = await this._student.evaluate(teacherOutput);
|
|
233
|
+
} catch {
|
|
234
|
+
// Evaluation failure is non-fatal — proceed with adaptation
|
|
235
|
+
}
|
|
236
|
+
if (gatePerplexity != null && gatePerplexity < gate.maxPerplexity) {
|
|
237
|
+
return this._skip(input, teacherOutput, 'already_learned', gatePerplexity);
|
|
254
238
|
}
|
|
255
239
|
}
|
|
256
240
|
|
|
257
|
-
// Train the
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// knowledge instead of overwriting it (catastrophic-forgetting guard).
|
|
241
|
+
// Train on the (prompt → response) mapping. EVM-5: interleave a sample of past
|
|
242
|
+
// exemplars (experience replay) so this adapt reinforces prior knowledge
|
|
243
|
+
// instead of overwriting it.
|
|
261
244
|
const rehearsed = this._sampleRehearsal();
|
|
262
|
-
const
|
|
263
|
-
const trainingText =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
adaptResult = await this._runtime.adapt(trainingText, adaptOpts);
|
|
268
|
-
} catch (err) {
|
|
269
|
-
throw new SSMError(
|
|
270
|
-
'DISTILL_FAILED',
|
|
271
|
-
`SSM adaptation failed during distillation: ${err instanceof Error ? err.message : String(err)}`,
|
|
272
|
-
err,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
245
|
+
const context = (i: string) => (opts.contextChars != null ? i.trim().slice(0, opts.contextChars) : i);
|
|
246
|
+
const trainingText = [...rehearsed, { input, teacherOutput }]
|
|
247
|
+
.map((p) => `${context(p.input)}\n${p.teacherOutput}`)
|
|
248
|
+
.join('\n\n');
|
|
275
249
|
|
|
276
|
-
|
|
250
|
+
const adaptResult = await this._adapt(trainingText, opts);
|
|
277
251
|
this._pushRehearsal(input, teacherOutput);
|
|
278
|
-
|
|
279
252
|
this._appendLog({
|
|
280
253
|
input,
|
|
281
254
|
teacherOutputLength: teacherOutput.length,
|
|
282
255
|
skipped : false,
|
|
283
|
-
|
|
284
|
-
|
|
256
|
+
distilled : true,
|
|
257
|
+
...this._student.describe(adaptResult),
|
|
285
258
|
gatePerplexity,
|
|
286
259
|
});
|
|
287
260
|
|
|
288
|
-
return { input, teacherOutput, adaptResult, skipped: false, rehearsed: rehearsed.length, gatePerplexity };
|
|
261
|
+
return { input, teacherOutput, adaptResult, skipped: false, distilled: true, rehearsed: rehearsed.length, gatePerplexity };
|
|
289
262
|
}
|
|
290
263
|
|
|
291
264
|
/**
|
|
292
265
|
* Runs distillation for each input in sequence.
|
|
293
266
|
* Aggregate statistics are returned alongside individual results.
|
|
294
267
|
*/
|
|
295
|
-
async distillBatch(inputs: string[], opts: DistillOptions = {}): Promise<DistillBatchResult
|
|
268
|
+
async distillBatch(inputs: string[], opts: DistillOptions<O> = {}): Promise<DistillBatchResult<R>> {
|
|
296
269
|
const startMs = Date.now();
|
|
297
|
-
const results: DistillResult[] = [];
|
|
270
|
+
const results: DistillResult<R>[] = [];
|
|
298
271
|
let totalEpochs = 0;
|
|
299
272
|
|
|
300
273
|
for (const input of inputs) {
|
|
301
274
|
const result = await this.distill(input, opts);
|
|
302
275
|
results.push(result);
|
|
303
|
-
totalEpochs += result.adaptResult.
|
|
276
|
+
totalEpochs += this._student.describe(result.adaptResult).epochs;
|
|
304
277
|
}
|
|
305
278
|
|
|
306
279
|
return {
|
|
@@ -319,6 +292,68 @@ export class DistillationEngine {
|
|
|
319
292
|
|
|
320
293
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
321
294
|
|
|
295
|
+
private async _adapt(trainingText: string, opts: DistillOptions<O>): Promise<R> {
|
|
296
|
+
try {
|
|
297
|
+
return await this._student.adapt(trainingText, opts.adapt);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw new SSMError(
|
|
300
|
+
'DISTILL_FAILED',
|
|
301
|
+
`SSM adaptation failed during distillation: ${err instanceof Error ? err.message : String(err)}`,
|
|
302
|
+
err,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** No exemplar worth learning: learn the caller's fallback text instead. */
|
|
308
|
+
private async _learnFallback(
|
|
309
|
+
input: string,
|
|
310
|
+
teacherOutput: string,
|
|
311
|
+
skipReason: DistillSkipReason,
|
|
312
|
+
opts: DistillOptions<O>,
|
|
313
|
+
teacherError?: unknown,
|
|
314
|
+
): Promise<DistillResult<R>> {
|
|
315
|
+
const adaptResult = await this._adapt(opts.fallbackText!, opts);
|
|
316
|
+
this._appendLog({
|
|
317
|
+
input,
|
|
318
|
+
teacherOutputLength: teacherOutput.length,
|
|
319
|
+
skipped : false,
|
|
320
|
+
distilled : false,
|
|
321
|
+
skipReason,
|
|
322
|
+
...this._student.describe(adaptResult),
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
input,
|
|
326
|
+
teacherOutput,
|
|
327
|
+
adaptResult,
|
|
328
|
+
skipped: false,
|
|
329
|
+
distilled: false,
|
|
330
|
+
skipReason,
|
|
331
|
+
...(teacherError !== undefined ? { teacherError } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Nothing adapted. */
|
|
336
|
+
private _skip(input: string, teacherOutput: string, skipReason: DistillSkipReason, gatePerplexity?: number): DistillResult<R> {
|
|
337
|
+
this._appendLog({
|
|
338
|
+
input,
|
|
339
|
+
teacherOutputLength: teacherOutput.length,
|
|
340
|
+
skipped : true,
|
|
341
|
+
distilled : false,
|
|
342
|
+
skipReason,
|
|
343
|
+
epochs : 0,
|
|
344
|
+
gatePerplexity,
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
input,
|
|
348
|
+
teacherOutput,
|
|
349
|
+
adaptResult: this._student.skippedResult(),
|
|
350
|
+
skipped: true,
|
|
351
|
+
distilled: false,
|
|
352
|
+
skipReason,
|
|
353
|
+
...(gatePerplexity !== undefined ? { gatePerplexity } : {}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
322
357
|
private _appendLog(entry: Omit<DistillationLog, 'timestamp'>): void {
|
|
323
358
|
this._log.push({ timestamp: Date.now(), ...entry });
|
|
324
359
|
if (this._log.length > MAX_LOG_ENTRIES) {
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distillation — a teacher's exemplars adapted into a student, behind two ports.
|
|
3
|
+
* Engine-free: the `@seanhogg/builderforce-memory/distillation` subpath.
|
|
4
|
+
*/
|
|
1
5
|
export { DistillationEngine } from './DistillationEngine.js';
|
|
2
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
DistillOptions,
|
|
8
|
+
DistillResult,
|
|
9
|
+
DistillBatchResult,
|
|
10
|
+
DistillationLog,
|
|
11
|
+
DistillSkipReason,
|
|
12
|
+
QualityGate,
|
|
13
|
+
RehearsalOptions,
|
|
14
|
+
} from './DistillationEngine.js';
|
|
15
|
+
export { ssmRuntimeStudent, SSM_STUDENT_DEFAULT_ADAPT } from './ports.js';
|
|
16
|
+
export type { DistillationTeacher, DistillationStudent } from './ports.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two ports a {@link DistillationEngine} runs between.
|
|
3
|
+
*
|
|
4
|
+
* Distillation is one recipe — teacher answers, gate, rehearse, adapt, log — over
|
|
5
|
+
* any teacher and any student. The package's own pairing (a `TransformerBridge`
|
|
6
|
+
* teaching an `SSMRuntime`) is one instance; a host that teaches through a metered
|
|
7
|
+
* gateway and adapts a packaged checkpoint into a mergeable diff is another. Both
|
|
8
|
+
* plug in here instead of restating the recipe.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AdaptOptions, AdaptResult } from '../session/index.js';
|
|
12
|
+
import type { SSMRuntime } from '../runtime/SSMRuntime.js';
|
|
13
|
+
import type { BridgeGenerateOptions } from '../bridges/TransformerBridge.js';
|
|
14
|
+
|
|
15
|
+
/** Produces the exemplar for an input. Any `TransformerBridge` is one. */
|
|
16
|
+
export interface DistillationTeacher {
|
|
17
|
+
/** The exemplar for `input`. Throws when there is none; the engine records the error. */
|
|
18
|
+
generate(input: string, opts?: BridgeGenerateOptions): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Learns from a training text. `R` is whatever one adaptation yields (an `AdaptResult`,
|
|
23
|
+
* a checkpoint diff, …); `O` the per-call options it takes.
|
|
24
|
+
*/
|
|
25
|
+
export interface DistillationStudent<R, O = undefined> {
|
|
26
|
+
adapt(trainingText: string, opts?: O): Promise<R>;
|
|
27
|
+
/** Perplexity on `text` — enables the `maxPerplexity` gate. Optional. */
|
|
28
|
+
evaluate?(text: string): Promise<number>;
|
|
29
|
+
/** The result recorded when nothing was adapted (the gate skipped the input). */
|
|
30
|
+
skippedResult(): R;
|
|
31
|
+
/** Final loss and epoch count of one result, for the log and batch totals. */
|
|
32
|
+
describe(result: R): { finalLoss?: number; epochs: number };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The adapt options an {@link SSMRuntime} student uses unless a call overrides them. */
|
|
36
|
+
export const SSM_STUDENT_DEFAULT_ADAPT: AdaptOptions = { wsla: true, epochs: 3 };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An {@link SSMRuntime} as a student. WSLA by default: it is fast and targets the
|
|
40
|
+
* selective-projection rows, exactly the parameters that encode token routing.
|
|
41
|
+
*/
|
|
42
|
+
export function ssmRuntimeStudent(runtime: SSMRuntime): DistillationStudent<AdaptResult, AdaptOptions> {
|
|
43
|
+
return {
|
|
44
|
+
adapt: (text, opts) => runtime.adapt(text, { ...SSM_STUDENT_DEFAULT_ADAPT, ...opts }),
|
|
45
|
+
evaluate: (text) => runtime.evaluate(text),
|
|
46
|
+
skippedResult: () => ({ losses: [], epochCount: 0, durationMs: 0 }),
|
|
47
|
+
describe: (r) => ({ finalLoss: r.losses.at(-1), epochs: r.epochCount }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when `x` already speaks the student port (an `SSMRuntime` does not: it has no `describe`). */
|
|
52
|
+
export function isDistillationStudent<R, O>(x: unknown): x is DistillationStudent<R, O> {
|
|
53
|
+
return !!x && typeof (x as { describe?: unknown }).describe === 'function'
|
|
54
|
+
&& typeof (x as { skippedResult?: unknown }).skippedResult === 'function';
|
|
55
|
+
}
|
package/src/eval/EvalHarness.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { mapWithConcurrency } from '../ingest/IngestionPipeline.js';
|
|
|
12
12
|
import { percentile } from '../telemetry/MetricsRegistry.js';
|
|
13
13
|
import type { Tracer } from '../telemetry/Tracer.js';
|
|
14
14
|
import { DEFAULT_RAG_GRADERS } from './graders.js';
|
|
15
|
+
import { baselineRatio, meetsBaselineRatio, sameEvalProblem, type EvalReportSummary } from './baseline.js';
|
|
15
16
|
import type {
|
|
16
17
|
CaseResult,
|
|
17
18
|
EvalDataset,
|
|
@@ -139,7 +140,7 @@ export class EvalHarness {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
/** Applies a gate to a report. Pure, so a build can re-gate an archived report. */
|
|
142
|
-
export function evaluateGate(report: EvalReport, gate: EvalGate): GateVerdict {
|
|
143
|
+
export function evaluateGate(report: EvalReport, gate: EvalGate, baseline?: EvalReportSummary): GateVerdict {
|
|
143
144
|
const failures: string[] = [];
|
|
144
145
|
const thresholds = Object.entries(gate).filter(([, v]) => v !== undefined);
|
|
145
146
|
|
|
@@ -158,6 +159,16 @@ export function evaluateGate(report: EvalReport, gate: EvalGate): GateVerdict {
|
|
|
158
159
|
if (gate.maxP95LatencyMs !== undefined && report.p95LatencyMs > gate.maxP95LatencyMs) {
|
|
159
160
|
failures.push(`p95 latency ${Math.round(report.p95LatencyMs)}ms > allowed ${gate.maxP95LatencyMs}ms`);
|
|
160
161
|
}
|
|
162
|
+
if (gate.minBaselineRatio !== undefined) {
|
|
163
|
+
const problem = baseline ? sameEvalProblem(report, baseline) : 'gate needs a baseline report to compare against';
|
|
164
|
+
const ratio = baseline ? baselineRatio(report.meanScore, baseline.meanScore) : null;
|
|
165
|
+
if (problem) failures.push(problem);
|
|
166
|
+
else if (!meetsBaselineRatio(ratio, gate.minBaselineRatio)) {
|
|
167
|
+
failures.push(ratio === null
|
|
168
|
+
? 'baseline scored 0, so no ratio can be taken'
|
|
169
|
+
: `score is ${pct(ratio)} of the baseline < required ${pct(gate.minBaselineRatio)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
161
172
|
for (const [grader, minimum] of Object.entries(gate.minGraderScore ?? {})) {
|
|
162
173
|
const actual = report.graderScores[grader];
|
|
163
174
|
if (actual === undefined) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eval/baseline.ts — gating a candidate RELATIVE to a baseline.
|
|
3
|
+
*
|
|
4
|
+
* "Good enough to launch" is often not an absolute score but a fraction of what an
|
|
5
|
+
* incumbent scores on the same suite: a small model may serve a task once it reaches
|
|
6
|
+
* 90% of the frontier model it replaces. That comparison only means something when
|
|
7
|
+
* both reports came from the SAME eval (same dataset, same cases), and when the
|
|
8
|
+
* baseline scored above zero. These are the rules for it, used by
|
|
9
|
+
* {@link evaluateGate}'s `minBaselineRatio` and by any caller that stores the two
|
|
10
|
+
* scores and re-checks the bar later without the reports.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The three facts of a report that a baseline comparison reads. */
|
|
14
|
+
export interface EvalReportSummary {
|
|
15
|
+
dataset: string;
|
|
16
|
+
meanScore: number;
|
|
17
|
+
/** The cases array, or just its length. */
|
|
18
|
+
cases: number | readonly unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Float tolerance for a ratio bar, so an exact 90% (0.81 / 0.9) meets a 0.9 bar. */
|
|
22
|
+
export const BASELINE_RATIO_EPSILON = 1e-9;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `score ÷ baselineScore`, or null when the pair cannot be compared — a non-finite or
|
|
26
|
+
* negative score, or a baseline at or below zero, proves nothing.
|
|
27
|
+
*/
|
|
28
|
+
export function baselineRatio(score: number, baselineScore: number): number | null {
|
|
29
|
+
if (!Number.isFinite(score) || !Number.isFinite(baselineScore) || score < 0 || baselineScore <= 0) return null;
|
|
30
|
+
return score / baselineScore;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when `ratio` reaches `minRatio` (within {@link BASELINE_RATIO_EPSILON}). A null ratio never does. */
|
|
34
|
+
export function meetsBaselineRatio(ratio: number | null, minRatio: number): boolean {
|
|
35
|
+
return ratio !== null && ratio + BASELINE_RATIO_EPSILON >= minRatio;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Why two reports are not the same eval, or null when they are. "The same eval" is
|
|
40
|
+
* enforced, not assumed: a ratio of scores from different suites compares nothing.
|
|
41
|
+
*/
|
|
42
|
+
export function sameEvalProblem(candidate: EvalReportSummary, baseline: EvalReportSummary): string | null {
|
|
43
|
+
const count = (c: EvalReportSummary['cases']) => (typeof c === 'number' ? c : c.length);
|
|
44
|
+
if (candidate.dataset !== baseline.dataset) {
|
|
45
|
+
return `reports are from different evals ('${candidate.dataset}' vs '${baseline.dataset}')`;
|
|
46
|
+
}
|
|
47
|
+
if (count(candidate.cases) !== count(baseline.cases)) {
|
|
48
|
+
return `reports scored different case counts (${count(candidate.cases)} vs ${count(baseline.cases)})`;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
package/src/eval/index.ts
CHANGED
package/src/eval/types.ts
CHANGED
|
@@ -80,6 +80,13 @@ export interface EvalGate {
|
|
|
80
80
|
maxP95LatencyMs?: number;
|
|
81
81
|
/** Maximum share of cases that may error outright. */
|
|
82
82
|
maxErrorRate?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Minimum `meanScore` as a fraction of a BASELINE report's on the same eval —
|
|
85
|
+
* "reach 90% of the model this replaces". Needs the baseline passed to
|
|
86
|
+
* {@link evaluateGate}; without one, or with a baseline from a different eval,
|
|
87
|
+
* the gate fails and says why.
|
|
88
|
+
*/
|
|
89
|
+
minBaselineRatio?: number;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface GateVerdict {
|
package/src/index.ts
CHANGED
|
@@ -473,15 +473,18 @@ export { publishToHuggingFace, writeExportToDir } from './publish/index.js';
|
|
|
473
473
|
export type { HuggingFaceTarget, PublishOutcome, HubClient, FsLike } from './publish/index.js';
|
|
474
474
|
|
|
475
475
|
// ── Distillation ──────────────────────────────────────────────────────────────
|
|
476
|
-
export { DistillationEngine } from './distillation/
|
|
476
|
+
export { DistillationEngine, ssmRuntimeStudent } from './distillation/index.js';
|
|
477
477
|
export type {
|
|
478
478
|
DistillOptions,
|
|
479
479
|
DistillResult,
|
|
480
480
|
DistillBatchResult,
|
|
481
481
|
DistillationLog,
|
|
482
|
+
DistillSkipReason,
|
|
482
483
|
QualityGate,
|
|
483
484
|
RehearsalOptions,
|
|
484
|
-
|
|
485
|
+
DistillationTeacher,
|
|
486
|
+
DistillationStudent,
|
|
487
|
+
} from './distillation/index.js';
|
|
485
488
|
|
|
486
489
|
// ── Agent ─────────────────────────────────────────────────────────────────────
|
|
487
490
|
export { SSMAgent } from './agent/SSMAgent.js';
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import type { TransformerBridge } from '../bridges/TransformerBridge.js';
|
|
26
|
-
import {
|
|
26
|
+
import { reciprocalRankFusion } from '../retrieval/fusion.js';
|
|
27
|
+
import { maximalMarginalRelevance } from '../retrieval/mmr.js';
|
|
27
28
|
import type { Span, Tracer } from '../telemetry/Tracer.js';
|
|
28
29
|
import {
|
|
29
30
|
SOURCE_ID_FIELD,
|