@tangle-network/agent-eval 0.120.2 → 0.120.4
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/CHANGELOG.md +13 -0
- package/dist/analyst/index.d.ts +3111 -0
- package/dist/analyst/index.js +403 -0
- package/dist/analyst/index.js.map +1 -0
- package/dist/authenticity/index.d.ts +161 -0
- package/dist/authenticity/index.js +215 -0
- package/dist/authenticity/index.js.map +1 -0
- package/dist/belief-state/index.d.ts +1301 -0
- package/dist/belief-state/index.js +2152 -0
- package/dist/belief-state/index.js.map +1 -0
- package/dist/benchmarks/index.d.ts +974 -0
- package/dist/benchmarks/index.js +60 -0
- package/dist/benchmarks/index.js.map +1 -0
- package/dist/builder-eval/index.d.ts +695 -0
- package/dist/builder-eval/index.js +366 -0
- package/dist/builder-eval/index.js.map +1 -0
- package/dist/campaign/index.d.ts +7469 -0
- package/dist/campaign/index.js +272 -0
- package/dist/campaign/index.js.map +1 -0
- package/dist/chunk-3RF76KTD.js +84 -0
- package/dist/chunk-3RF76KTD.js.map +1 -0
- package/dist/chunk-3XH4Y2SS.js +750 -0
- package/dist/chunk-3XH4Y2SS.js.map +1 -0
- package/dist/chunk-3YYRZDON.js +45 -0
- package/dist/chunk-3YYRZDON.js.map +1 -0
- package/dist/chunk-5BYTIDZ7.js +550 -0
- package/dist/chunk-5BYTIDZ7.js.map +1 -0
- package/dist/chunk-5CVUPHJ4.js +2668 -0
- package/dist/chunk-5CVUPHJ4.js.map +1 -0
- package/dist/chunk-7QBFOJ3E.js +3920 -0
- package/dist/chunk-7QBFOJ3E.js.map +1 -0
- package/dist/chunk-ARU2PZFM.js +312 -0
- package/dist/chunk-ARU2PZFM.js.map +1 -0
- package/dist/chunk-BOD4O7OF.js +40 -0
- package/dist/chunk-BOD4O7OF.js.map +1 -0
- package/dist/chunk-CVJP5TMD.js +766 -0
- package/dist/chunk-CVJP5TMD.js.map +1 -0
- package/dist/chunk-DPZAEKA6.js +880 -0
- package/dist/chunk-DPZAEKA6.js.map +1 -0
- package/dist/chunk-DTJ6QUQB.js +131 -0
- package/dist/chunk-DTJ6QUQB.js.map +1 -0
- package/dist/chunk-GGE4NNQT.js +65 -0
- package/dist/chunk-GGE4NNQT.js.map +1 -0
- package/dist/chunk-H5UD2323.js +286 -0
- package/dist/chunk-H5UD2323.js.map +1 -0
- package/dist/chunk-HHWE3POT.js +94 -0
- package/dist/chunk-HHWE3POT.js.map +1 -0
- package/dist/chunk-HKUCJ437.js +787 -0
- package/dist/chunk-HKUCJ437.js.map +1 -0
- package/dist/chunk-JHCHEVET.js +274 -0
- package/dist/chunk-JHCHEVET.js.map +1 -0
- package/dist/chunk-K4DBDHLK.js +158 -0
- package/dist/chunk-K4DBDHLK.js.map +1 -0
- package/dist/chunk-K6N6XJJX.js +306 -0
- package/dist/chunk-K6N6XJJX.js.map +1 -0
- package/dist/chunk-MA6HLL3S.js +65 -0
- package/dist/chunk-MA6HLL3S.js.map +1 -0
- package/dist/chunk-MAZ26DC7.js +99 -0
- package/dist/chunk-MAZ26DC7.js.map +1 -0
- package/dist/chunk-MOXWMGPC.js +577 -0
- package/dist/chunk-MOXWMGPC.js.map +1 -0
- package/dist/chunk-NJC7U437.js +626 -0
- package/dist/chunk-NJC7U437.js.map +1 -0
- package/dist/chunk-NMN4WGSJ.js +1030 -0
- package/dist/chunk-NMN4WGSJ.js.map +1 -0
- package/dist/chunk-NPCTHQIO.js +91 -0
- package/dist/chunk-NPCTHQIO.js.map +1 -0
- package/dist/chunk-ONWEPEDO.js +57 -0
- package/dist/chunk-ONWEPEDO.js.map +1 -0
- package/dist/chunk-OYZAPX5G.js +1526 -0
- package/dist/chunk-OYZAPX5G.js.map +1 -0
- package/dist/chunk-PC4UYEBM.js +166 -0
- package/dist/chunk-PC4UYEBM.js.map +1 -0
- package/dist/chunk-PJQFMIOX.js +1182 -0
- package/dist/chunk-PJQFMIOX.js.map +1 -0
- package/dist/chunk-PXD6ZFNY.js +1107 -0
- package/dist/chunk-PXD6ZFNY.js.map +1 -0
- package/dist/chunk-PXE2VKMX.js +140 -0
- package/dist/chunk-PXE2VKMX.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QBRSJK47.js +622 -0
- package/dist/chunk-QBRSJK47.js.map +1 -0
- package/dist/chunk-S3UZOQ5Y.js +328 -0
- package/dist/chunk-S3UZOQ5Y.js.map +1 -0
- package/dist/chunk-SQQED7ZH.js +998 -0
- package/dist/chunk-SQQED7ZH.js.map +1 -0
- package/dist/chunk-SYV364BL.js +1266 -0
- package/dist/chunk-SYV364BL.js.map +1 -0
- package/dist/chunk-T4SQEITX.js +95 -0
- package/dist/chunk-T4SQEITX.js.map +1 -0
- package/dist/chunk-TT4KNT67.js +124 -0
- package/dist/chunk-TT4KNT67.js.map +1 -0
- package/dist/chunk-U5CHZ5M3.js +357 -0
- package/dist/chunk-U5CHZ5M3.js.map +1 -0
- package/dist/chunk-ULOKLHIQ.js +1937 -0
- package/dist/chunk-ULOKLHIQ.js.map +1 -0
- package/dist/chunk-VI2UW6B6.js +162 -0
- package/dist/chunk-VI2UW6B6.js.map +1 -0
- package/dist/chunk-VQMK5FMP.js +247 -0
- package/dist/chunk-VQMK5FMP.js.map +1 -0
- package/dist/chunk-VSMTAMNK.js +53 -0
- package/dist/chunk-VSMTAMNK.js.map +1 -0
- package/dist/chunk-VZSRQ272.js +149 -0
- package/dist/chunk-VZSRQ272.js.map +1 -0
- package/dist/chunk-WW2A73HW.js +159 -0
- package/dist/chunk-WW2A73HW.js.map +1 -0
- package/dist/chunk-X4UCIOTZ.js +136 -0
- package/dist/chunk-X4UCIOTZ.js.map +1 -0
- package/dist/chunk-XJYR7XFV.js +317 -0
- package/dist/chunk-XJYR7XFV.js.map +1 -0
- package/dist/chunk-ZET2UAYW.js +89 -0
- package/dist/chunk-ZET2UAYW.js.map +1 -0
- package/dist/chunk-ZMXDQ4K7.js +870 -0
- package/dist/chunk-ZMXDQ4K7.js.map +1 -0
- package/dist/chunk-ZTJPIIVI.js +7958 -0
- package/dist/chunk-ZTJPIIVI.js.map +1 -0
- package/dist/chunk-ZZUXHH3R.js +99 -0
- package/dist/chunk-ZZUXHH3R.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract/index.d.ts +4987 -0
- package/dist/contract/index.js +1654 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/control.d.ts +1013 -0
- package/dist/control.js +34 -0
- package/dist/control.js.map +1 -0
- package/dist/fuzz.d.ts +759 -0
- package/dist/fuzz.js +714 -0
- package/dist/fuzz.js.map +1 -0
- package/dist/hosted/index.d.ts +730 -0
- package/dist/hosted/index.js +14 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/index.d.ts +16780 -0
- package/dist/index.js +12168 -0
- package/dist/index.js.map +1 -0
- package/dist/matrix/index.d.ts +155 -0
- package/dist/matrix/index.js +8 -0
- package/dist/matrix/index.js.map +1 -0
- package/dist/meta-eval/index.d.ts +1030 -0
- package/dist/meta-eval/index.js +417 -0
- package/dist/meta-eval/index.js.map +1 -0
- package/dist/multishot/index.d.ts +579 -0
- package/dist/multishot/index.js +589 -0
- package/dist/multishot/index.js.map +1 -0
- package/dist/openapi.json +992 -0
- package/dist/pipelines/index.d.ts +567 -0
- package/dist/pipelines/index.js +515 -0
- package/dist/pipelines/index.js.map +1 -0
- package/dist/reporting.d.ts +1277 -0
- package/dist/reporting.js +48 -0
- package/dist/reporting.js.map +1 -0
- package/dist/rl.d.ts +4092 -0
- package/dist/rl.js +1724 -0
- package/dist/rl.js.map +1 -0
- package/dist/run-campaign-75RTPVV5.js +14 -0
- package/dist/run-campaign-75RTPVV5.js.map +1 -0
- package/dist/storyboard/index.d.ts +279 -0
- package/dist/storyboard/index.js +767 -0
- package/dist/storyboard/index.js.map +1 -0
- package/dist/trace-attributes.d.ts +52 -0
- package/dist/trace-attributes.js +62 -0
- package/dist/trace-attributes.js.map +1 -0
- package/dist/traces.d.ts +2343 -0
- package/dist/traces.js +249 -0
- package/dist/traces.js.map +1 -0
- package/dist/wire/index.d.ts +1252 -0
- package/dist/wire/index.js +81 -0
- package/dist/wire/index.js.map +1 -0
- package/docs/eval-surface-map.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/trace/emitter.ts
|
|
2
|
+
var TraceEmitter = class {
|
|
3
|
+
store;
|
|
4
|
+
stack = [];
|
|
5
|
+
_runId;
|
|
6
|
+
now;
|
|
7
|
+
id;
|
|
8
|
+
hooks;
|
|
9
|
+
hookErrors;
|
|
10
|
+
constructor(store, options = {}) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
this.now = options.now ?? (() => Date.now());
|
|
13
|
+
this.id = options.id ?? (() => cryptoRandomId());
|
|
14
|
+
this._runId = options.runId ?? this.id();
|
|
15
|
+
this.hooks = options.onRunComplete ?? [];
|
|
16
|
+
this.hookErrors = options.hookErrors ?? "swallow";
|
|
17
|
+
}
|
|
18
|
+
get runId() {
|
|
19
|
+
return this._runId;
|
|
20
|
+
}
|
|
21
|
+
get traceStore() {
|
|
22
|
+
return this.store;
|
|
23
|
+
}
|
|
24
|
+
/** Append a hook after construction (e.g. attach the trace analyst). */
|
|
25
|
+
addRunCompleteHook(hook) {
|
|
26
|
+
this.hooks.push(hook);
|
|
27
|
+
}
|
|
28
|
+
// ── Run lifecycle ──────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Begin a Run.
|
|
31
|
+
*
|
|
32
|
+
* `scenarioId` is required on the persisted Run shape — every Run downstream
|
|
33
|
+
* gets a non-empty scenarioId so filters and aggregations stay simple — but
|
|
34
|
+
* the INPUT here accepts it as optional. When omitted, startRun substitutes
|
|
35
|
+
* a sensible default (`run.layer ?? run.tags?.['kind'] ?? 'runtime'`) so
|
|
36
|
+
* runtime / operator / meta-eval runs that have no curated-scenario corpus
|
|
37
|
+
* to anchor to don't have to invent placeholder strings at the call site.
|
|
38
|
+
*/
|
|
39
|
+
async startRun(run) {
|
|
40
|
+
const scenarioId = run.scenarioId ?? run.layer ?? run.tags?.kind ?? "runtime";
|
|
41
|
+
const full = {
|
|
42
|
+
...run,
|
|
43
|
+
scenarioId,
|
|
44
|
+
runId: this._runId,
|
|
45
|
+
startedAt: this.now(),
|
|
46
|
+
status: "running"
|
|
47
|
+
};
|
|
48
|
+
await this.store.appendRun(full);
|
|
49
|
+
return full;
|
|
50
|
+
}
|
|
51
|
+
async endRun(outcome) {
|
|
52
|
+
const status = outcome?.pass === false ? "failed" : "completed";
|
|
53
|
+
await this.store.updateRun(this._runId, { endedAt: this.now(), status, outcome });
|
|
54
|
+
await this.runHooks({ runId: this._runId, emitter: this, store: this.store, outcome, status });
|
|
55
|
+
}
|
|
56
|
+
async abortRun(reason) {
|
|
57
|
+
const outcome = { pass: false, notes: reason };
|
|
58
|
+
await this.store.updateRun(this._runId, {
|
|
59
|
+
endedAt: this.now(),
|
|
60
|
+
status: "aborted",
|
|
61
|
+
outcome
|
|
62
|
+
});
|
|
63
|
+
await this.runHooks({
|
|
64
|
+
runId: this._runId,
|
|
65
|
+
emitter: this,
|
|
66
|
+
store: this.store,
|
|
67
|
+
outcome,
|
|
68
|
+
status: "aborted"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async runHooks(ctx) {
|
|
72
|
+
for (const hook of this.hooks) {
|
|
73
|
+
try {
|
|
74
|
+
await hook(ctx);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (this.hookErrors === "throw") throw err;
|
|
77
|
+
try {
|
|
78
|
+
await this.store.appendEvent({
|
|
79
|
+
eventId: this.id(),
|
|
80
|
+
runId: this._runId,
|
|
81
|
+
kind: "log",
|
|
82
|
+
timestamp: this.now(),
|
|
83
|
+
payload: {
|
|
84
|
+
source: "run_complete_hook",
|
|
85
|
+
error: err instanceof Error ? err.message : String(err)
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Generic span ───────────────────────────────────────────────────
|
|
94
|
+
async span(init) {
|
|
95
|
+
const spanId = this.id();
|
|
96
|
+
const parent = init.parentSpanId ?? this.stack[this.stack.length - 1];
|
|
97
|
+
const span = {
|
|
98
|
+
spanId,
|
|
99
|
+
parentSpanId: parent,
|
|
100
|
+
runId: this._runId,
|
|
101
|
+
startedAt: this.now(),
|
|
102
|
+
...init
|
|
103
|
+
};
|
|
104
|
+
await this.store.appendSpan(span);
|
|
105
|
+
this.stack.push(spanId);
|
|
106
|
+
return this.handle(span);
|
|
107
|
+
}
|
|
108
|
+
handle(span) {
|
|
109
|
+
return {
|
|
110
|
+
span,
|
|
111
|
+
end: async (patch) => {
|
|
112
|
+
const endedAt = this.now();
|
|
113
|
+
await this.store.updateSpan(span.spanId, {
|
|
114
|
+
endedAt,
|
|
115
|
+
status: "ok",
|
|
116
|
+
...patch
|
|
117
|
+
});
|
|
118
|
+
this.pop(span.spanId);
|
|
119
|
+
},
|
|
120
|
+
fail: async (error, patch) => {
|
|
121
|
+
const endedAt = this.now();
|
|
122
|
+
const errStr = error instanceof Error ? error.message : error;
|
|
123
|
+
await this.store.updateSpan(span.spanId, {
|
|
124
|
+
endedAt,
|
|
125
|
+
status: "error",
|
|
126
|
+
error: errStr,
|
|
127
|
+
...patch
|
|
128
|
+
});
|
|
129
|
+
this.pop(span.spanId);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
pop(spanId) {
|
|
134
|
+
const idx = this.stack.lastIndexOf(spanId);
|
|
135
|
+
if (idx >= 0) this.stack.splice(idx, 1);
|
|
136
|
+
}
|
|
137
|
+
// ── Typed span conveniences ────────────────────────────────────────
|
|
138
|
+
llm(init) {
|
|
139
|
+
return this.span({ kind: "llm", ...init });
|
|
140
|
+
}
|
|
141
|
+
tool(init) {
|
|
142
|
+
return this.span({ kind: "tool", ...init });
|
|
143
|
+
}
|
|
144
|
+
retrieval(init) {
|
|
145
|
+
return this.span({ kind: "retrieval", ...init });
|
|
146
|
+
}
|
|
147
|
+
async recordJudge(verdict) {
|
|
148
|
+
const spanId = this.id();
|
|
149
|
+
const now = this.now();
|
|
150
|
+
const full = {
|
|
151
|
+
spanId,
|
|
152
|
+
runId: this._runId,
|
|
153
|
+
kind: "judge",
|
|
154
|
+
startedAt: now,
|
|
155
|
+
endedAt: now,
|
|
156
|
+
status: "ok",
|
|
157
|
+
...verdict
|
|
158
|
+
};
|
|
159
|
+
await this.store.appendSpan(full);
|
|
160
|
+
return full;
|
|
161
|
+
}
|
|
162
|
+
sandbox(init) {
|
|
163
|
+
return this.span({ kind: "sandbox", ...init });
|
|
164
|
+
}
|
|
165
|
+
// ── Events ─────────────────────────────────────────────────────────
|
|
166
|
+
async emit(event) {
|
|
167
|
+
const full = {
|
|
168
|
+
eventId: this.id(),
|
|
169
|
+
runId: this._runId,
|
|
170
|
+
spanId: event.spanId ?? this.stack[this.stack.length - 1],
|
|
171
|
+
kind: event.kind,
|
|
172
|
+
timestamp: this.now(),
|
|
173
|
+
payload: event.payload ?? {}
|
|
174
|
+
};
|
|
175
|
+
await this.store.appendEvent(full);
|
|
176
|
+
return full;
|
|
177
|
+
}
|
|
178
|
+
// ── Budget ledger ──────────────────────────────────────────────────
|
|
179
|
+
async recordBudget(entry) {
|
|
180
|
+
const full = {
|
|
181
|
+
runId: this._runId,
|
|
182
|
+
timestamp: entry.timestamp ?? this.now(),
|
|
183
|
+
dimension: entry.dimension,
|
|
184
|
+
limit: entry.limit,
|
|
185
|
+
consumed: entry.consumed,
|
|
186
|
+
remaining: entry.remaining,
|
|
187
|
+
breached: entry.breached,
|
|
188
|
+
spanId: entry.spanId ?? this.stack[this.stack.length - 1]
|
|
189
|
+
};
|
|
190
|
+
await this.store.appendBudgetEntry(full);
|
|
191
|
+
if (full.breached) {
|
|
192
|
+
await this.emit({
|
|
193
|
+
kind: "budget_breach",
|
|
194
|
+
spanId: full.spanId,
|
|
195
|
+
payload: { dimension: full.dimension, limit: full.limit, consumed: full.consumed }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return full;
|
|
199
|
+
}
|
|
200
|
+
// ── Artifacts ──────────────────────────────────────────────────────
|
|
201
|
+
async recordArtifact(artifact) {
|
|
202
|
+
const full = { artifactId: this.id(), runId: this._runId, ...artifact };
|
|
203
|
+
await this.store.appendArtifact(full);
|
|
204
|
+
return full;
|
|
205
|
+
}
|
|
206
|
+
// ── Nested composition ─────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Runs `fn` inside a span; auto-ends on success, auto-fails on throw.
|
|
209
|
+
* Returns the fn's return value. Use this for the 95% case.
|
|
210
|
+
*/
|
|
211
|
+
async within(init, fn) {
|
|
212
|
+
const handle = await this.span(init);
|
|
213
|
+
try {
|
|
214
|
+
const result = await fn(handle);
|
|
215
|
+
await handle.end();
|
|
216
|
+
return result;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
await handle.fail(err instanceof Error ? err : String(err));
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
function cryptoRandomId() {
|
|
224
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
225
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
226
|
+
}
|
|
227
|
+
function llmSpanFromProvider(args) {
|
|
228
|
+
return {
|
|
229
|
+
name: args.name ?? args.model,
|
|
230
|
+
model: args.model,
|
|
231
|
+
messages: args.messages,
|
|
232
|
+
output: args.output,
|
|
233
|
+
inputTokens: args.usage?.inputTokens,
|
|
234
|
+
outputTokens: args.usage?.outputTokens,
|
|
235
|
+
cachedTokens: args.usage?.cachedTokens,
|
|
236
|
+
cacheWriteTokens: args.usage?.cacheWriteTokens,
|
|
237
|
+
reasoningTokens: args.usage?.reasoningTokens,
|
|
238
|
+
costUsd: args.costUsd,
|
|
239
|
+
finishReason: args.finishReason
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export {
|
|
244
|
+
TraceEmitter,
|
|
245
|
+
llmSpanFromProvider
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=chunk-VQMK5FMP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/trace/emitter.ts"],"sourcesContent":["/**\n * TraceEmitter — hierarchical span builder that auto-parents using an\n * internal stack. One emitter per Run; emitters do NOT share state.\n *\n * Convenience methods (`llm`, `tool`, `retrieval`, `judge`, `sandbox`)\n * return a `SpanHandle` with `.end()` / `.fail()` so callers don't\n * have to thread spanIds manually. For async workflows that can't use\n * the stack (e.g. fan-out parallel calls), pass `parentSpanId`\n * explicitly.\n */\n\nimport type {\n Artifact,\n BudgetLedgerEntry,\n EventKind,\n JudgeSpan,\n LlmSpan,\n Message,\n RetrievalSpan,\n Run,\n RunOutcome,\n SandboxSpan,\n Span,\n SpanKind,\n ToolSpan,\n TraceEvent,\n} from './schema'\nimport type { TraceStore } from './store'\n\nexport interface SpanHandle<S extends Span = Span> {\n span: S\n end(patch?: Partial<S>): Promise<void>\n fail(error: string | Error, patch?: Partial<S>): Promise<void>\n}\n\nexport interface RunCompleteHookContext {\n runId: string\n emitter: TraceEmitter\n store: TraceStore\n /** Outcome the caller passed to `endRun` (undefined for `abortRun`). */\n outcome?: RunOutcome\n /** Final run status. */\n status: 'completed' | 'failed' | 'aborted'\n}\n\nexport type RunCompleteHook = (ctx: RunCompleteHookContext) => Promise<void> | void\n\nexport interface TraceEmitterOptions {\n runId?: string\n /** Inject a clock for deterministic tests. */\n now?: () => number\n /** Inject an id generator for deterministic tests. */\n id?: () => string\n /**\n * Hooks fired after `endRun` / `abortRun` writes the final run state.\n * Designed for trace-analyst auto-execution, integrity assertions, and\n * outbound notifications. Hooks run sequentially in the order supplied.\n *\n * By default a hook that throws is swallowed and logged as a `note` event\n * on the run — auto-orchestration must not crash the underlying flow.\n * Set `hookErrors: 'throw'` to propagate.\n */\n onRunComplete?: RunCompleteHook[]\n /** `'swallow'` (default) | `'throw'`. */\n hookErrors?: 'swallow' | 'throw'\n}\n\nexport class TraceEmitter {\n private store: TraceStore\n private stack: string[] = []\n private _runId: string\n private now: () => number\n private id: () => string\n private hooks: RunCompleteHook[]\n private hookErrors: 'swallow' | 'throw'\n\n constructor(store: TraceStore, options: TraceEmitterOptions = {}) {\n this.store = store\n this.now = options.now ?? (() => Date.now())\n this.id = options.id ?? (() => cryptoRandomId())\n this._runId = options.runId ?? this.id()\n this.hooks = options.onRunComplete ?? []\n this.hookErrors = options.hookErrors ?? 'swallow'\n }\n\n get runId(): string {\n return this._runId\n }\n\n get traceStore(): TraceStore {\n return this.store\n }\n\n /** Append a hook after construction (e.g. attach the trace analyst). */\n addRunCompleteHook(hook: RunCompleteHook): void {\n this.hooks.push(hook)\n }\n\n // ── Run lifecycle ──────────────────────────────────────────────────\n\n /**\n * Begin a Run.\n *\n * `scenarioId` is required on the persisted Run shape — every Run downstream\n * gets a non-empty scenarioId so filters and aggregations stay simple — but\n * the INPUT here accepts it as optional. When omitted, startRun substitutes\n * a sensible default (`run.layer ?? run.tags?.['kind'] ?? 'runtime'`) so\n * runtime / operator / meta-eval runs that have no curated-scenario corpus\n * to anchor to don't have to invent placeholder strings at the call site.\n */\n async startRun(\n run: Omit<Run, 'runId' | 'scenarioId' | 'startedAt' | 'status'> & { scenarioId?: string },\n ): Promise<Run> {\n const scenarioId = run.scenarioId ?? run.layer ?? run.tags?.kind ?? 'runtime'\n const full: Run = {\n ...run,\n scenarioId,\n runId: this._runId,\n startedAt: this.now(),\n status: 'running',\n }\n await this.store.appendRun(full)\n return full\n }\n\n async endRun(outcome?: RunOutcome): Promise<void> {\n const status: 'completed' | 'failed' = outcome?.pass === false ? 'failed' : 'completed'\n await this.store.updateRun(this._runId, { endedAt: this.now(), status, outcome })\n await this.runHooks({ runId: this._runId, emitter: this, store: this.store, outcome, status })\n }\n\n async abortRun(reason: string): Promise<void> {\n const outcome = { pass: false, notes: reason }\n await this.store.updateRun(this._runId, {\n endedAt: this.now(),\n status: 'aborted',\n outcome,\n })\n await this.runHooks({\n runId: this._runId,\n emitter: this,\n store: this.store,\n outcome,\n status: 'aborted',\n })\n }\n\n private async runHooks(ctx: RunCompleteHookContext): Promise<void> {\n for (const hook of this.hooks) {\n try {\n await hook(ctx)\n } catch (err) {\n if (this.hookErrors === 'throw') throw err\n try {\n await this.store.appendEvent({\n eventId: this.id(),\n runId: this._runId,\n kind: 'log',\n timestamp: this.now(),\n payload: {\n source: 'run_complete_hook',\n error: err instanceof Error ? err.message : String(err),\n },\n })\n } catch {\n // best-effort\n }\n }\n }\n }\n\n // ── Generic span ───────────────────────────────────────────────────\n\n async span<S extends Span = Span>(\n init: {\n kind: SpanKind\n name: string\n parentSpanId?: string\n attributes?: Record<string, unknown>\n } & Partial<Omit<S, 'spanId' | 'runId' | 'startedAt' | 'kind' | 'name'>>,\n ): Promise<SpanHandle<S>> {\n const spanId = this.id()\n const parent = init.parentSpanId ?? this.stack[this.stack.length - 1]\n const span = {\n spanId,\n parentSpanId: parent,\n runId: this._runId,\n startedAt: this.now(),\n ...init,\n } as unknown as S\n await this.store.appendSpan(span)\n this.stack.push(spanId)\n return this.handle<S>(span)\n }\n\n private handle<S extends Span>(span: S): SpanHandle<S> {\n return {\n span,\n end: async (patch?: Partial<S>) => {\n const endedAt = this.now()\n await this.store.updateSpan(span.spanId, {\n endedAt,\n status: 'ok',\n ...patch,\n } as Partial<Span>)\n this.pop(span.spanId)\n },\n fail: async (error: string | Error, patch?: Partial<S>) => {\n const endedAt = this.now()\n const errStr = error instanceof Error ? error.message : error\n await this.store.updateSpan(span.spanId, {\n endedAt,\n status: 'error',\n error: errStr,\n ...patch,\n } as Partial<Span>)\n this.pop(span.spanId)\n },\n }\n }\n\n private pop(spanId: string): void {\n const idx = this.stack.lastIndexOf(spanId)\n if (idx >= 0) this.stack.splice(idx, 1)\n }\n\n // ── Typed span conveniences ────────────────────────────────────────\n\n llm(\n init: Omit<LlmSpan, 'spanId' | 'runId' | 'kind' | 'startedAt'>,\n ): Promise<SpanHandle<LlmSpan>> {\n return this.span<LlmSpan>({ kind: 'llm', ...init })\n }\n\n tool(\n init: Omit<ToolSpan, 'spanId' | 'runId' | 'kind' | 'startedAt'>,\n ): Promise<SpanHandle<ToolSpan>> {\n return this.span<ToolSpan>({ kind: 'tool', ...init })\n }\n\n retrieval(\n init: Omit<RetrievalSpan, 'spanId' | 'runId' | 'kind' | 'startedAt'>,\n ): Promise<SpanHandle<RetrievalSpan>> {\n return this.span<RetrievalSpan>({ kind: 'retrieval', ...init })\n }\n\n async recordJudge(\n verdict: Omit<JudgeSpan, 'spanId' | 'runId' | 'kind' | 'startedAt' | 'endedAt'>,\n ): Promise<JudgeSpan> {\n const spanId = this.id()\n const now = this.now()\n const full: JudgeSpan = {\n spanId,\n runId: this._runId,\n kind: 'judge',\n startedAt: now,\n endedAt: now,\n status: 'ok',\n ...verdict,\n }\n await this.store.appendSpan(full)\n return full\n }\n\n sandbox(\n init: Omit<SandboxSpan, 'spanId' | 'runId' | 'kind' | 'startedAt'>,\n ): Promise<SpanHandle<SandboxSpan>> {\n return this.span<SandboxSpan>({ kind: 'sandbox', ...init })\n }\n\n // ── Events ─────────────────────────────────────────────────────────\n\n async emit(event: {\n kind: EventKind\n spanId?: string\n payload?: Record<string, unknown>\n }): Promise<TraceEvent> {\n const full: TraceEvent = {\n eventId: this.id(),\n runId: this._runId,\n spanId: event.spanId ?? this.stack[this.stack.length - 1],\n kind: event.kind,\n timestamp: this.now(),\n payload: event.payload ?? {},\n }\n await this.store.appendEvent(full)\n return full\n }\n\n // ── Budget ledger ──────────────────────────────────────────────────\n\n async recordBudget(\n entry: Omit<BudgetLedgerEntry, 'runId' | 'timestamp'> & { timestamp?: number },\n ): Promise<BudgetLedgerEntry> {\n const full: BudgetLedgerEntry = {\n runId: this._runId,\n timestamp: entry.timestamp ?? this.now(),\n dimension: entry.dimension,\n limit: entry.limit,\n consumed: entry.consumed,\n remaining: entry.remaining,\n breached: entry.breached,\n spanId: entry.spanId ?? this.stack[this.stack.length - 1],\n }\n await this.store.appendBudgetEntry(full)\n if (full.breached) {\n await this.emit({\n kind: 'budget_breach',\n spanId: full.spanId,\n payload: { dimension: full.dimension, limit: full.limit, consumed: full.consumed },\n })\n }\n return full\n }\n\n // ── Artifacts ──────────────────────────────────────────────────────\n\n async recordArtifact(artifact: Omit<Artifact, 'artifactId' | 'runId'>): Promise<Artifact> {\n const full: Artifact = { artifactId: this.id(), runId: this._runId, ...artifact }\n await this.store.appendArtifact(full)\n return full\n }\n\n // ── Nested composition ─────────────────────────────────────────────\n\n /**\n * Runs `fn` inside a span; auto-ends on success, auto-fails on throw.\n * Returns the fn's return value. Use this for the 95% case.\n */\n async within<T>(\n init: Parameters<TraceEmitter['span']>[0],\n fn: (handle: SpanHandle) => Promise<T>,\n ): Promise<T> {\n const handle = await this.span(init)\n try {\n const result = await fn(handle)\n await handle.end()\n return result\n } catch (err) {\n await handle.fail(err instanceof Error ? err : String(err))\n throw err\n }\n }\n}\n\n// Helpers -------------------------------------------------------------\n\nfunction cryptoRandomId(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID()\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n}\n\n/** Helper to build an LLM span handle args object from a provider-shaped response. */\nexport function llmSpanFromProvider(args: {\n name?: string\n model: string\n messages: Message[]\n output: string\n usage?: {\n inputTokens?: number\n outputTokens?: number\n cachedTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n }\n costUsd?: number\n finishReason?: string\n}): Omit<LlmSpan, 'spanId' | 'runId' | 'kind' | 'startedAt'> {\n return {\n name: args.name ?? args.model,\n model: args.model,\n messages: args.messages,\n output: args.output,\n inputTokens: args.usage?.inputTokens,\n outputTokens: args.usage?.outputTokens,\n cachedTokens: args.usage?.cachedTokens,\n cacheWriteTokens: args.usage?.cacheWriteTokens,\n reasoningTokens: args.usage?.reasoningTokens,\n costUsd: args.costUsd,\n finishReason: args.finishReason,\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/pre-registration.ts
|
|
2
|
+
function canonicalize(v) {
|
|
3
|
+
if (v === null || typeof v !== "object") return v;
|
|
4
|
+
if (Array.isArray(v)) return v.map(canonicalize);
|
|
5
|
+
const keys = Object.keys(v).sort();
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const k of keys) out[k] = canonicalize(v[k]);
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
async function hashJson(obj) {
|
|
11
|
+
const canonical = canonicalize(obj);
|
|
12
|
+
const bytes = new TextEncoder().encode(JSON.stringify(canonical));
|
|
13
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
14
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
}
|
|
16
|
+
async function signManifest(m) {
|
|
17
|
+
const hash = await hashJson(m);
|
|
18
|
+
return { ...m, contentHash: hash, algo: "sha256-content" };
|
|
19
|
+
}
|
|
20
|
+
async function verifyManifest(m) {
|
|
21
|
+
const { contentHash, algo: _algo, ...rest } = m;
|
|
22
|
+
void _algo;
|
|
23
|
+
const resigned = await signManifest(rest);
|
|
24
|
+
return resigned.contentHash === contentHash;
|
|
25
|
+
}
|
|
26
|
+
async function evaluateHypothesis(manifest, observed) {
|
|
27
|
+
if (!await verifyManifest(manifest)) {
|
|
28
|
+
throw new Error("evaluateHypothesis: manifest content hash mismatch (tampered)");
|
|
29
|
+
}
|
|
30
|
+
const reasons = [];
|
|
31
|
+
const directionOk = manifest.direction === "increase" ? observed.effect > 0 : observed.effect < 0;
|
|
32
|
+
if (!directionOk) reasons.push("wrong_direction");
|
|
33
|
+
if (Math.abs(observed.effect) < manifest.minEffect) reasons.push("effect_too_small");
|
|
34
|
+
if (observed.pValue >= manifest.alpha) reasons.push("not_significant");
|
|
35
|
+
if (observed.n < manifest.preRegisteredN) reasons.push("undersampled");
|
|
36
|
+
return {
|
|
37
|
+
manifest,
|
|
38
|
+
observedN: observed.n,
|
|
39
|
+
observedEffect: observed.effect,
|
|
40
|
+
observedPValue: observed.pValue,
|
|
41
|
+
confirmed: reasons.length === 0,
|
|
42
|
+
rejectionReasons: reasons
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
canonicalize,
|
|
48
|
+
hashJson,
|
|
49
|
+
signManifest,
|
|
50
|
+
verifyManifest,
|
|
51
|
+
evaluateHypothesis
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=chunk-VSMTAMNK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pre-registration.ts"],"sourcesContent":["/**\n * Pre-registered hypotheses — declare what you're testing BEFORE the\n * run, check it AFTER. Prevents p-hacking, optional stopping, and the\n * \"we ran until it looked good\" failure mode.\n *\n * Manifest is a plain JSON-friendly object. Sign it with a content hash\n * + timestamp; the registered record becomes immutable. Post-run,\n * evaluate the manifest against observed results — the library refuses\n * to let you re-interpret a different metric as the declared one.\n */\n\nexport interface HypothesisManifest {\n id: string\n /** Human prose — goes into the audit trail. */\n hypothesis: string\n /** Metric the hypothesis claims to move. */\n metric: string\n /** 'increase' = candidate should score higher than baseline; 'decrease' = lower. */\n direction: 'increase' | 'decrease'\n /** Minimum effect size to count (same units as the metric). */\n minEffect: number\n /** Alpha threshold. */\n alpha: number\n /** Target statistical power at which sample size was pre-computed. */\n power: number\n /** Declared N per arm before running. */\n preRegisteredN: number\n /** ISO8601 timestamp the manifest was registered. */\n registeredAt: string\n /** Optional identifiers to tie into the trace corpus. */\n baselineLabel?: string\n candidateLabel?: string\n}\n\n/**\n * Identifier for the hashing scheme used to produce `contentHash`.\n *\n * `'sha256-content'` — sha256 hex over the canonicalized manifest with\n * the `contentHash` and `algo` fields stripped. Held as a string union\n * so future schemes can be added without breaking parsers; SignedManifest\n * values without `algo` deserialize cleanly because the field is optional.\n */\nexport type SignedManifestAlgo = 'sha256-content'\n\nexport interface SignedManifest extends HypothesisManifest {\n /** sha256 hex of canonicalized manifest (everything except contentHash and algo). */\n contentHash: string\n /**\n * Algorithm string describing how `contentHash` was produced.\n *\n * Optional on the type so serialized manifests without it still parse,\n * but ALWAYS populated by {@link signManifest}. Consumers that want to\n * enforce a known algorithm should reject manifests where this field\n * is missing or unrecognized.\n */\n algo?: SignedManifestAlgo\n}\n\nexport interface HypothesisResult {\n manifest: SignedManifest\n observedN: number\n observedEffect: number\n observedPValue: number\n /** True iff the observed effect hits the pre-declared direction with\n * magnitude ≥ minEffect AND p < alpha. */\n confirmed: boolean\n /** Enumerated reasons the hypothesis was rejected (each a machine-tag). */\n rejectionReasons: Array<\n 'wrong_direction' | 'effect_too_small' | 'not_significant' | 'undersampled'\n >\n notes?: string\n}\n\n/**\n * Deterministic JSON canonicalization — sort object keys recursively.\n *\n * Two semantically-equal objects produce byte-identical canonicalized output;\n * this is what makes a content-hash stable across encoders, key insertion\n * orders, and runtime versions. Exported for any consumer that needs the same\n * canonicalization guarantee outside the manifest-signing path (e.g., signing\n * an artifact bundle, hashing a dataset version, etc.).\n */\nexport function canonicalize(v: unknown): unknown {\n if (v === null || typeof v !== 'object') return v\n if (Array.isArray(v)) return v.map(canonicalize)\n const keys = Object.keys(v as Record<string, unknown>).sort()\n const out: Record<string, unknown> = {}\n for (const k of keys) out[k] = canonicalize((v as Record<string, unknown>)[k])\n return out\n}\n\n/**\n * SHA-256 hex (full 64 chars) over the canonicalized JSON encoding of `obj`.\n *\n * The same primitive `signManifest` and `verifyManifest` are built on, exposed\n * directly so consumers signing arbitrary structured content (artifact bundles,\n * production packets, dataset manifests, etc.) don't have to re-derive\n * canonicalize+sha256 from scratch.\n *\n * Stable across:\n * - object key insertion order (canonicalization sorts keys recursively)\n * - encoder choice (UTF-8 via TextEncoder, fixed)\n * - runtime (uses the Web Crypto subtle digest, present in Node ≥18 and browsers)\n *\n * Named `hashJson` to disambiguate from `prompt-registry.ts`'s `hashContent`,\n * which takes a string input and returns a truncated 12-char prompt id.\n * Use `hashJson` when you mean \"canonicalize then hash.\"\n *\n * @example\n * const hash = await hashJson({ id: '1', kind: 'spec' })\n * // 'a3f1...' (64 hex chars)\n */\nexport async function hashJson<T>(obj: T): Promise<string> {\n const canonical = canonicalize(obj)\n const bytes = new TextEncoder().encode(JSON.stringify(canonical))\n const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)\n return Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Sign a manifest with a SHA-256 content hash.\n *\n * The hash covers the canonicalized manifest with the `contentHash`\n * and `algo` fields stripped; this lets verifiers re-sign the rest and\n * compare. Returned manifest always carries `algo: 'sha256-content'`\n * so downstream consumers can identify the scheme; manifests without\n * `algo` still verify because it is stripped before hashing on both sides.\n */\nexport async function signManifest(m: HypothesisManifest): Promise<SignedManifest> {\n const hash = await hashJson(m)\n return { ...m, contentHash: hash, algo: 'sha256-content' }\n}\n\n/**\n * Verify that a signed manifest has not been tampered with.\n *\n * Strips `contentHash` and `algo` before re-signing so manifests without\n * `algo` verify identically to ones that carry it.\n */\nexport async function verifyManifest(m: SignedManifest): Promise<boolean> {\n const { contentHash, algo: _algo, ...rest } = m\n void _algo\n const resigned = await signManifest(rest)\n return resigned.contentHash === contentHash\n}\n\n/**\n * Evaluate a pre-registered hypothesis against observed results.\n * Mechanical — no re-interpretation permitted.\n */\nexport async function evaluateHypothesis(\n manifest: SignedManifest,\n observed: { n: number; effect: number; pValue: number },\n): Promise<HypothesisResult> {\n if (!(await verifyManifest(manifest))) {\n throw new Error('evaluateHypothesis: manifest content hash mismatch (tampered)')\n }\n const reasons: HypothesisResult['rejectionReasons'] = []\n const directionOk = manifest.direction === 'increase' ? observed.effect > 0 : observed.effect < 0\n if (!directionOk) reasons.push('wrong_direction')\n if (Math.abs(observed.effect) < manifest.minEffect) reasons.push('effect_too_small')\n if (observed.pValue >= manifest.alpha) reasons.push('not_significant')\n if (observed.n < manifest.preRegisteredN) reasons.push('undersampled')\n return {\n manifest,\n observedN: observed.n,\n observedEffect: observed.effect,\n observedPValue: observed.pValue,\n confirmed: reasons.length === 0,\n rejectionReasons: reasons,\n }\n}\n"],"mappings":";AAkFO,SAAS,aAAa,GAAqB;AAChD,MAAI,MAAM,QAAQ,OAAO,MAAM,SAAU,QAAO;AAChD,MAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,IAAI,YAAY;AAC/C,QAAM,OAAO,OAAO,KAAK,CAA4B,EAAE,KAAK;AAC5D,QAAM,MAA+B,CAAC;AACtC,aAAW,KAAK,KAAM,KAAI,CAAC,IAAI,aAAc,EAA8B,CAAC,CAAC;AAC7E,SAAO;AACT;AAuBA,eAAsB,SAAY,KAAyB;AACzD,QAAM,YAAY,aAAa,GAAG;AAClC,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,SAAS,CAAC;AAChE,QAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,KAAK;AACrE,SAAO,MAAM,KAAK,IAAI,WAAW,MAAM,CAAC,EACrC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAC1C,KAAK,EAAE;AACZ;AAWA,eAAsB,aAAa,GAAgD;AACjF,QAAM,OAAO,MAAM,SAAS,CAAC;AAC7B,SAAO,EAAE,GAAG,GAAG,aAAa,MAAM,MAAM,iBAAiB;AAC3D;AAQA,eAAsB,eAAe,GAAqC;AACxE,QAAM,EAAE,aAAa,MAAM,OAAO,GAAG,KAAK,IAAI;AAC9C,OAAK;AACL,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,SAAO,SAAS,gBAAgB;AAClC;AAMA,eAAsB,mBACpB,UACA,UAC2B;AAC3B,MAAI,CAAE,MAAM,eAAe,QAAQ,GAAI;AACrC,UAAM,IAAI,MAAM,+DAA+D;AAAA,EACjF;AACA,QAAM,UAAgD,CAAC;AACvD,QAAM,cAAc,SAAS,cAAc,aAAa,SAAS,SAAS,IAAI,SAAS,SAAS;AAChG,MAAI,CAAC,YAAa,SAAQ,KAAK,iBAAiB;AAChD,MAAI,KAAK,IAAI,SAAS,MAAM,IAAI,SAAS,UAAW,SAAQ,KAAK,kBAAkB;AACnF,MAAI,SAAS,UAAU,SAAS,MAAO,SAAQ,KAAK,iBAAiB;AACrE,MAAI,SAAS,IAAI,SAAS,eAAgB,SAAQ,KAAK,cAAc;AACrE,SAAO;AAAA,IACL;AAAA,IACA,WAAW,SAAS;AAAA,IACpB,gBAAgB,SAAS;AAAA,IACzB,gBAAgB,SAAS;AAAA,IACzB,WAAW,QAAQ,WAAW;AAAA,IAC9B,kBAAkB;AAAA,EACpB;AACF;","names":[]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/rl/active-curriculum.ts
|
|
2
|
+
function varianceBasedCurriculum(observations, candidateCells, opts) {
|
|
3
|
+
const variancePrior = opts.variancePrior ?? 0.05;
|
|
4
|
+
const floor = opts.floorPerCell ?? 1;
|
|
5
|
+
const budget = opts.budget;
|
|
6
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const o of observations) {
|
|
8
|
+
const k = `${o.variantId}::${o.scenarioId}`;
|
|
9
|
+
const arr = grouped.get(k) ?? [];
|
|
10
|
+
arr.push(o.score);
|
|
11
|
+
grouped.set(k, arr);
|
|
12
|
+
}
|
|
13
|
+
const cellStats = candidateCells.map((c) => {
|
|
14
|
+
const k = `${c.variantId}::${c.scenarioId}`;
|
|
15
|
+
const samples = grouped.get(k) ?? [];
|
|
16
|
+
const n = samples.length;
|
|
17
|
+
const mean = n === 0 ? 0.5 : samples.reduce((s, v) => s + v, 0) / n;
|
|
18
|
+
const variance = n < 2 ? variancePrior : samples.reduce((s, v) => s + (v - mean) ** 2, 0) / (n - 1) + variancePrior;
|
|
19
|
+
const weight = Math.sqrt(variance) + 1 / Math.sqrt(Math.max(1, n));
|
|
20
|
+
return { variantId: c.variantId, scenarioId: c.scenarioId, n, mean, variance, weight };
|
|
21
|
+
});
|
|
22
|
+
const floorTotal = floor * cellStats.length;
|
|
23
|
+
if (floorTotal >= budget) {
|
|
24
|
+
const each = Math.max(1, Math.floor(budget / Math.max(1, cellStats.length)));
|
|
25
|
+
return cellStats.map((c) => ({
|
|
26
|
+
variantId: c.variantId,
|
|
27
|
+
scenarioId: c.scenarioId,
|
|
28
|
+
count: each,
|
|
29
|
+
reason: `floor allocation (budget tight; n=${c.n})`
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
const remaining = budget - floorTotal;
|
|
33
|
+
const totalWeight = cellStats.reduce((s, c) => s + c.weight, 0);
|
|
34
|
+
return cellStats.map((c) => {
|
|
35
|
+
const proportional = totalWeight === 0 ? 0 : Math.round(c.weight / totalWeight * remaining);
|
|
36
|
+
return {
|
|
37
|
+
variantId: c.variantId,
|
|
38
|
+
scenarioId: c.scenarioId,
|
|
39
|
+
count: floor + proportional,
|
|
40
|
+
reason: `variance ${c.variance.toFixed(3)} (n=${c.n}, mean=${c.mean.toFixed(3)})`
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function thompsonCurriculum(observations, candidateCells, opts) {
|
|
45
|
+
const threshold = opts.decisionThreshold ?? 0.5;
|
|
46
|
+
const alpha0 = opts.priorAlpha ?? 1;
|
|
47
|
+
const beta0 = opts.priorBeta ?? 1;
|
|
48
|
+
const rng = makeRng(opts.seed);
|
|
49
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const o of observations) {
|
|
51
|
+
const k = `${o.variantId}::${o.scenarioId}`;
|
|
52
|
+
const cur = grouped.get(k) ?? { passes: 0, failures: 0 };
|
|
53
|
+
const pass = o.pass ?? o.score >= threshold;
|
|
54
|
+
if (pass) cur.passes += 1;
|
|
55
|
+
else cur.failures += 1;
|
|
56
|
+
grouped.set(k, cur);
|
|
57
|
+
}
|
|
58
|
+
const stats = candidateCells.map((c) => {
|
|
59
|
+
const k = `${c.variantId}::${c.scenarioId}`;
|
|
60
|
+
const cur = grouped.get(k) ?? { passes: 0, failures: 0 };
|
|
61
|
+
const a = alpha0 + cur.passes;
|
|
62
|
+
const b = beta0 + cur.failures;
|
|
63
|
+
const sampled = sampleBeta(a, b, rng);
|
|
64
|
+
const distance = Math.abs(sampled - threshold);
|
|
65
|
+
const variance = a * b / ((a + b) ** 2 * (a + b + 1));
|
|
66
|
+
const sigma = Math.max(0.05, Math.sqrt(variance));
|
|
67
|
+
const weight = Math.exp(-((distance / sigma) ** 2));
|
|
68
|
+
return {
|
|
69
|
+
variantId: c.variantId,
|
|
70
|
+
scenarioId: c.scenarioId,
|
|
71
|
+
n: cur.passes + cur.failures,
|
|
72
|
+
sampled,
|
|
73
|
+
sigma,
|
|
74
|
+
weight,
|
|
75
|
+
a,
|
|
76
|
+
b
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const totalWeight = stats.reduce((s, c) => s + c.weight, 0);
|
|
80
|
+
return stats.map((c) => {
|
|
81
|
+
const proportional = totalWeight === 0 ? 0 : Math.round(c.weight / totalWeight * opts.budget);
|
|
82
|
+
return {
|
|
83
|
+
variantId: c.variantId,
|
|
84
|
+
scenarioId: c.scenarioId,
|
|
85
|
+
count: Math.max(0, proportional),
|
|
86
|
+
reason: `Beta(${c.a.toFixed(1)},${c.b.toFixed(1)}) sample=${c.sampled.toFixed(3)} (target ${threshold})`
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function observationsFromRunRecords(runs, opts = {}) {
|
|
91
|
+
const threshold = opts.passThreshold ?? 0.5;
|
|
92
|
+
const useHoldout = opts.useHoldout ?? true;
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const r of runs) {
|
|
95
|
+
if (!r.scenarioId) continue;
|
|
96
|
+
const score = useHoldout ? r.outcome.holdoutScore ?? r.outcome.searchScore : r.outcome.searchScore ?? r.outcome.holdoutScore;
|
|
97
|
+
if (typeof score !== "number" || !Number.isFinite(score)) continue;
|
|
98
|
+
out.push({
|
|
99
|
+
variantId: r.candidateId,
|
|
100
|
+
scenarioId: r.scenarioId,
|
|
101
|
+
score,
|
|
102
|
+
pass: score >= threshold
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function makeRng(seed) {
|
|
108
|
+
if (seed === void 0) return Math.random;
|
|
109
|
+
let s = seed >>> 0;
|
|
110
|
+
return () => {
|
|
111
|
+
s = s + 1831565813 >>> 0;
|
|
112
|
+
let t = s;
|
|
113
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
114
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
115
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function sampleBeta(alpha, beta, rng) {
|
|
119
|
+
const a = Math.max(1, alpha);
|
|
120
|
+
const b = Math.max(1, beta);
|
|
121
|
+
const x = sampleGamma(a, rng);
|
|
122
|
+
const y = sampleGamma(b, rng);
|
|
123
|
+
return x / (x + y);
|
|
124
|
+
}
|
|
125
|
+
function sampleGamma(shape, rng) {
|
|
126
|
+
const d = shape - 1 / 3;
|
|
127
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
128
|
+
while (true) {
|
|
129
|
+
let x;
|
|
130
|
+
let v;
|
|
131
|
+
do {
|
|
132
|
+
const u1 = rng() || 1e-12;
|
|
133
|
+
const u2 = rng() || 1e-12;
|
|
134
|
+
x = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
135
|
+
v = 1 + c * x;
|
|
136
|
+
} while (v <= 0);
|
|
137
|
+
v = v * v * v;
|
|
138
|
+
const u = rng();
|
|
139
|
+
if (u < 1 - 0.0331 * x ** 4) return d * v;
|
|
140
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
varianceBasedCurriculum,
|
|
146
|
+
thompsonCurriculum,
|
|
147
|
+
observationsFromRunRecords
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=chunk-VZSRQ272.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rl/active-curriculum.ts"],"sourcesContent":["/**\n * Adaptive curriculum / active scenario selection.\n *\n * Fixed scenario sets waste sample budget on cells the policy already\n * passes (no information left) and cells the policy never passes (no\n * gradient available either). Active learning over scenarios fixes this\n * by allocating the next sample budget to cells where the policy's\n * outcome is *uncertain* — those carry the most decision-relevant signal.\n *\n * This module ships two complementary strategies:\n *\n * 1. **Variance-based** — score each (variant, scenario) cell by the\n * empirical variance of past observations. Allocate next-round budget\n * proportional to variance. Standard active-learning-by-uncertainty\n * heuristic; works well when the policy is non-deterministic and\n * cells differ in observation noise.\n *\n * 2. **Bandit-based (Thompson sampling)** — model each (variant,\n * scenario) cell as a Beta-Bernoulli arm; sample a posterior; pick\n * cells whose posterior mean is closest to the per-scenario decision\n * threshold. The right primitive when scenarios are\n * \"pass/fail\" rather than continuous, and when promotion gates fire\n * at a known threshold (e.g., 0.5).\n *\n * The output is a *next-round budget allocation* — a list of (variant,\n * scenario, count) triples. The consumer's matrix runner consumes the\n * allocation, runs those cells, feeds the new observations back. Loop.\n *\n * Out of scope (deliberate): scenario *generation* — that's the\n * adversarial primitive's job. This module allocates over an existing\n * scenario pool.\n */\n\nimport type { RunRecord } from '../run-record'\n\nexport interface CellObservation {\n variantId: string\n scenarioId: string\n /** Observed score in [0, 1]. */\n score: number\n /** For Bernoulli arms — derive from the score with a threshold if needed. */\n pass?: boolean\n}\n\nexport interface CurriculumAllocation {\n variantId: string\n scenarioId: string\n /** How many additional reps to run on this cell. */\n count: number\n /** Strategy-specific reason for the allocation. */\n reason: string\n}\n\nexport interface VarianceCurriculumOptions {\n /** Total reps to allocate across all cells. */\n budget: number\n /**\n * Smoothing prior on variance — keeps the allocator from concentrating\n * on a cell with one observation just because its 1-sample variance is\n * 0. Default 0.05.\n */\n variancePrior?: number\n /**\n * Minimum reps per cell — even when the variance estimate is low, give\n * every cell at least this many. Default 1.\n */\n floorPerCell?: number\n}\n\n/**\n * Variance-proportional allocation. For each cell, estimate variance from\n * past observations + a prior, then allocate the budget proportional to\n * (sqrt(variance) + 1/sqrt(n)) — a classical optimal-allocation rule\n * (Neyman 1934) that balances \"explore noisy cells\" with \"explore\n * under-sampled cells.\"\n */\nexport function varianceBasedCurriculum(\n observations: CellObservation[],\n candidateCells: Array<{ variantId: string; scenarioId: string }>,\n opts: VarianceCurriculumOptions,\n): CurriculumAllocation[] {\n const variancePrior = opts.variancePrior ?? 0.05\n const floor = opts.floorPerCell ?? 1\n const budget = opts.budget\n\n const grouped = new Map<string, number[]>()\n for (const o of observations) {\n const k = `${o.variantId}::${o.scenarioId}`\n const arr = grouped.get(k) ?? []\n arr.push(o.score)\n grouped.set(k, arr)\n }\n\n const cellStats = candidateCells.map((c) => {\n const k = `${c.variantId}::${c.scenarioId}`\n const samples = grouped.get(k) ?? []\n const n = samples.length\n const mean = n === 0 ? 0.5 : samples.reduce((s, v) => s + v, 0) / n\n const variance =\n n < 2\n ? variancePrior\n : samples.reduce((s, v) => s + (v - mean) ** 2, 0) / (n - 1) + variancePrior\n // Neyman optimal allocation: weight ∝ √variance; add √(1/n) to break\n // ties toward under-sampled cells.\n const weight = Math.sqrt(variance) + 1 / Math.sqrt(Math.max(1, n))\n return { variantId: c.variantId, scenarioId: c.scenarioId, n, mean, variance, weight }\n })\n\n // Reserve floor*N for the floor; allocate the rest proportional to weight.\n const floorTotal = floor * cellStats.length\n if (floorTotal >= budget) {\n const each = Math.max(1, Math.floor(budget / Math.max(1, cellStats.length)))\n return cellStats.map((c) => ({\n variantId: c.variantId,\n scenarioId: c.scenarioId,\n count: each,\n reason: `floor allocation (budget tight; n=${c.n})`,\n }))\n }\n const remaining = budget - floorTotal\n const totalWeight = cellStats.reduce((s, c) => s + c.weight, 0)\n return cellStats.map((c) => {\n const proportional = totalWeight === 0 ? 0 : Math.round((c.weight / totalWeight) * remaining)\n return {\n variantId: c.variantId,\n scenarioId: c.scenarioId,\n count: floor + proportional,\n reason: `variance ${c.variance.toFixed(3)} (n=${c.n}, mean=${c.mean.toFixed(3)})`,\n }\n })\n}\n\nexport interface ThompsonCurriculumOptions {\n budget: number\n /**\n * The per-scenario decision threshold. Cells whose posterior mean is\n * closest to this get the most budget — that's where the next observation\n * has the highest information value for the gate decision. Default 0.5.\n */\n decisionThreshold?: number\n /** Beta prior parameters. Default α=β=1 (uniform). */\n priorAlpha?: number\n priorBeta?: number\n /** Seed the Thompson sampler. Default unset (Math.random). */\n seed?: number\n}\n\n/**\n * Thompson-sampling-style allocation for pass/fail cells. For each cell:\n *\n * - Maintain Beta(α + passes, β + failures) posterior on pass-rate\n * - Allocation weight ∝ exp(-((sampledMean - threshold) / σ)^2):\n * cells whose sampled posterior straddles the decision boundary get\n * the most weight; cells already clearly above or below get less.\n *\n * This is the right primitive when promotion gates fire at a known\n * threshold and you want to sharpen the posterior near the boundary.\n */\nexport function thompsonCurriculum(\n observations: CellObservation[],\n candidateCells: Array<{ variantId: string; scenarioId: string }>,\n opts: ThompsonCurriculumOptions,\n): CurriculumAllocation[] {\n const threshold = opts.decisionThreshold ?? 0.5\n const alpha0 = opts.priorAlpha ?? 1\n const beta0 = opts.priorBeta ?? 1\n const rng = makeRng(opts.seed)\n\n const grouped = new Map<string, { passes: number; failures: number }>()\n for (const o of observations) {\n const k = `${o.variantId}::${o.scenarioId}`\n const cur = grouped.get(k) ?? { passes: 0, failures: 0 }\n const pass = o.pass ?? o.score >= threshold\n if (pass) cur.passes += 1\n else cur.failures += 1\n grouped.set(k, cur)\n }\n\n const stats = candidateCells.map((c) => {\n const k = `${c.variantId}::${c.scenarioId}`\n const cur = grouped.get(k) ?? { passes: 0, failures: 0 }\n const a = alpha0 + cur.passes\n const b = beta0 + cur.failures\n // Sample a single Beta draw — the Thompson signal.\n const sampled = sampleBeta(a, b, rng)\n const distance = Math.abs(sampled - threshold)\n // Information-near-threshold weight: closer = higher.\n // Use Gaussian-shaped kernel with σ tuned to posterior std.\n const variance = (a * b) / ((a + b) ** 2 * (a + b + 1))\n const sigma = Math.max(0.05, Math.sqrt(variance))\n const weight = Math.exp(-((distance / sigma) ** 2))\n return {\n variantId: c.variantId,\n scenarioId: c.scenarioId,\n n: cur.passes + cur.failures,\n sampled,\n sigma,\n weight,\n a,\n b,\n }\n })\n\n const totalWeight = stats.reduce((s, c) => s + c.weight, 0)\n return stats.map((c) => {\n const proportional = totalWeight === 0 ? 0 : Math.round((c.weight / totalWeight) * opts.budget)\n return {\n variantId: c.variantId,\n scenarioId: c.scenarioId,\n count: Math.max(0, proportional),\n reason: `Beta(${c.a.toFixed(1)},${c.b.toFixed(1)}) sample=${c.sampled.toFixed(3)} (target ${threshold})`,\n }\n })\n}\n\n/** Convenience: extract `CellObservation[]` directly from `RunRecord[]`. */\nexport function observationsFromRunRecords(\n runs: RunRecord[],\n opts: { passThreshold?: number; useHoldout?: boolean } = {},\n): CellObservation[] {\n const threshold = opts.passThreshold ?? 0.5\n const useHoldout = opts.useHoldout ?? true\n const out: CellObservation[] = []\n for (const r of runs) {\n if (!r.scenarioId) continue\n const score = useHoldout\n ? (r.outcome.holdoutScore ?? r.outcome.searchScore)\n : (r.outcome.searchScore ?? r.outcome.holdoutScore)\n if (typeof score !== 'number' || !Number.isFinite(score)) continue\n out.push({\n variantId: r.candidateId,\n scenarioId: r.scenarioId,\n score,\n pass: score >= threshold,\n })\n }\n return out\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\nfunction makeRng(seed?: number): () => number {\n if (seed === undefined) return Math.random\n let s = seed >>> 0\n return () => {\n s = (s + 0x6d2b79f5) >>> 0\n let t = s\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/**\n * Sample from Beta(α, β) via the Marsaglia–Tsang method using two Gamma\n * variates. Accuracy is good for α, β > 1; we floor the parameters at 1\n * to avoid degenerate cases.\n */\nfunction sampleBeta(alpha: number, beta: number, rng: () => number): number {\n const a = Math.max(1, alpha)\n const b = Math.max(1, beta)\n const x = sampleGamma(a, rng)\n const y = sampleGamma(b, rng)\n return x / (x + y)\n}\n\nfunction sampleGamma(shape: number, rng: () => number): number {\n // Marsaglia–Tsang for shape ≥ 1.\n const d = shape - 1 / 3\n const c = 1 / Math.sqrt(9 * d)\n while (true) {\n let x: number\n let v: number\n do {\n const u1 = rng() || 1e-12\n const u2 = rng() || 1e-12\n // Box-Muller for a normal sample.\n x = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)\n v = 1 + c * x\n } while (v <= 0)\n v = v * v * v\n const u = rng()\n if (u < 1 - 0.0331 * x ** 4) return d * v\n if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v\n }\n}\n"],"mappings":"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","names":[]}
|