@alisio/core 0.6.0 → 0.7.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 (43) hide show
  1. package/dist/changelog/data.js +16 -0
  2. package/dist/eval/canonical.d.ts +14 -0
  3. package/dist/eval/canonical.js +40 -0
  4. package/dist/eval/cassette.d.ts +180 -0
  5. package/dist/eval/cassette.js +424 -0
  6. package/dist/eval/compare.d.ts +147 -0
  7. package/dist/eval/compare.js +436 -0
  8. package/dist/eval/engine.d.ts +144 -0
  9. package/dist/eval/engine.js +728 -0
  10. package/dist/eval/export.d.ts +40 -0
  11. package/dist/eval/export.js +90 -0
  12. package/dist/eval/fingerprint.d.ts +88 -0
  13. package/dist/eval/fingerprint.js +186 -0
  14. package/dist/eval/normalize.d.ts +68 -0
  15. package/dist/eval/normalize.js +138 -0
  16. package/dist/eval/paths.d.ts +11 -0
  17. package/dist/eval/paths.js +50 -0
  18. package/dist/eval/plan.d.ts +26 -0
  19. package/dist/eval/plan.js +44 -0
  20. package/dist/eval/report.d.ts +53 -0
  21. package/dist/eval/report.js +258 -0
  22. package/dist/eval/run-suite.d.ts +68 -0
  23. package/dist/eval/run-suite.js +94 -0
  24. package/dist/eval/secrets.d.ts +12 -0
  25. package/dist/eval/secrets.js +45 -0
  26. package/dist/eval/stats.d.ts +17 -0
  27. package/dist/eval/stats.js +50 -0
  28. package/dist/eval/suite.d.ts +20 -0
  29. package/dist/eval/suite.js +165 -0
  30. package/dist/eval/variants.d.ts +35 -0
  31. package/dist/eval/variants.js +137 -0
  32. package/dist/eval/verifiers.d.ts +49 -0
  33. package/dist/eval/verifiers.js +264 -0
  34. package/dist/forge/javascript/hosts/worker-host.d.ts +9 -0
  35. package/dist/forge/javascript/hosts/worker-host.js +16 -1
  36. package/dist/forge/kernel.js +3 -2
  37. package/dist/index.d.ts +16 -1
  38. package/dist/index.js +16 -1
  39. package/dist/plan/exit-plan.js +2 -1
  40. package/dist/runtime/ids.d.ts +21 -0
  41. package/dist/runtime/ids.js +41 -1
  42. package/dist/runtime/store.js +3 -2
  43. package/package.json +2 -2
@@ -0,0 +1,26 @@
1
+ /** Deterministic run planning of the reliability lab (specs/010 D6, D15). Pure. */
2
+ /** Deterministic PRNG in [0, 1) (same algorithm as `scripts/bench/lib.ts`). */
3
+ export declare function mulberry32(seed: number): () => number;
4
+ export interface PlannedRun {
5
+ taskId: string;
6
+ repetition: number;
7
+ }
8
+ /**
9
+ * Every (task, repetition) pair exactly once, in a seeded order so provider drift over time is not
10
+ * correlated with one task. The same task ids, k and seed always give the same plan, whatever order
11
+ * the ids were listed in: the variants of a comparison run the same plan.
12
+ */
13
+ export declare function planRuns(taskIds: string[], k: number, seed: number): PlannedRun[];
14
+ export interface VariantStep extends PlannedRun {
15
+ variantId: string;
16
+ /** Index of the plan step this run belongs to (every variant runs each step). */
17
+ step: number;
18
+ }
19
+ /**
20
+ * Expands a plan so every variant runs EVERY plan step, one after another, instead of all of A and
21
+ * then all of B. The starting variant rotates with the step (A,B then B,A for two variants) so no
22
+ * variant systematically goes first. This spreads provider drift over time across the variants
23
+ * and keeps the same tasks, repetitions and seeds for each; it does not remove the drift (runs
24
+ * still happen at different moments) and nothing is paired.
25
+ */
26
+ export declare function interleaveVariants(plan: PlannedRun[], variantIds: string[]): VariantStep[];
@@ -0,0 +1,44 @@
1
+ /** Deterministic run planning of the reliability lab (specs/010 D6, D15). Pure. */
2
+ /** Deterministic PRNG in [0, 1) (same algorithm as `scripts/bench/lib.ts`). */
3
+ export function mulberry32(seed) {
4
+ let a = seed >>> 0;
5
+ return () => {
6
+ a = (a + 0x6d2b79f5) >>> 0;
7
+ let t = a;
8
+ t = Math.imul(t ^ (t >>> 15), t | 1);
9
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
10
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
11
+ };
12
+ }
13
+ /**
14
+ * Every (task, repetition) pair exactly once, in a seeded order so provider drift over time is not
15
+ * correlated with one task. The same task ids, k and seed always give the same plan, whatever order
16
+ * the ids were listed in: the variants of a comparison run the same plan.
17
+ */
18
+ export function planRuns(taskIds, k, seed) {
19
+ const plan = [...taskIds]
20
+ .sort()
21
+ .flatMap((taskId) => Array.from({ length: k }, (_, repetition) => ({ taskId, repetition })));
22
+ const random = mulberry32(seed);
23
+ for (let i = plan.length - 1; i > 0; i--) {
24
+ const j = Math.floor(random() * (i + 1));
25
+ [plan[i], plan[j]] = [plan[j], plan[i]];
26
+ }
27
+ return plan;
28
+ }
29
+ /**
30
+ * Expands a plan so every variant runs EVERY plan step, one after another, instead of all of A and
31
+ * then all of B. The starting variant rotates with the step (A,B then B,A for two variants) so no
32
+ * variant systematically goes first. This spreads provider drift over time across the variants
33
+ * and keeps the same tasks, repetitions and seeds for each; it does not remove the drift (runs
34
+ * still happen at different moments) and nothing is paired.
35
+ */
36
+ export function interleaveVariants(plan, variantIds) {
37
+ const n = variantIds.length;
38
+ return plan.flatMap((run, step) => variantIds.map((_, offset) => ({
39
+ variantId: variantIds[(step + offset) % n],
40
+ taskId: run.taskId,
41
+ repetition: run.repetition,
42
+ step,
43
+ })));
44
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Aggregation of run records into a reliability report (specs/010 5.2, D6, D7) and its Markdown
3
+ * rendering. Pure: no I/O.
4
+ *
5
+ * What the numbers mean (the review of Phase 3 starts here):
6
+ * - A run that ended `error` is infrastructure, not a repetition of the task. It is excluded from
7
+ * every rate and spread and counted in `errored`. `k` of a task is its number of JUDGED runs
8
+ * (`pass`, `fail`, `budget_exceeded`); `budget_exceeded` is a non-pass that stays in the rate.
9
+ * - `successRate` is passes / judged runs with a 95 % Wilson interval. `passAtK` is "at least one
10
+ * judged run passed", `passPowK` "every judged run passed" (false with no judged run).
11
+ * - Spreads (median, variance) use the judged runs; the sample variance has n - 1 in the divisor.
12
+ * - `tokensPerSuccess` / `wallMsPerSuccess` / `costUsdPerSuccess` are the TOTAL spent over all
13
+ * judged runs (failures included) divided by the number of successes. Token and cost totals are
14
+ * computed only when every judged run reported usage: a partial sum would understate them.
15
+ * - `measured` (provider usage, wall time) and `estimated` (money from caller-supplied rates)
16
+ * never share a field. Without rates no cost is computed and no `estimated` key exists.
17
+ */
18
+ import type { EvalRates, EvalReport, EvalRunRecord, EvalTaskMetrics } from "@alisio/sdk";
19
+ /** Fewest judged runs per task below which no improvement is claimed (specs/010 O3). */
20
+ export declare const MIN_POWER_K = 5;
21
+ /** Runs cannot be aggregated into one report (mixed conditions, duplicates, bad rates...). */
22
+ export declare class EvalReportError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ export interface BuildReportOptions {
26
+ /** USD per million tokens. Without them no cost is computed (nothing is guessed). */
27
+ rates?: EvalRates;
28
+ /** Seed the runs were planned from (default: the lowest `seed` among the records). */
29
+ seedBase?: number;
30
+ /** Repetitions requested per task (default: the most records any one task has). */
31
+ k?: number;
32
+ /** The variant these runs are (`eval variants`): kept in the report with its overrides. */
33
+ variant?: {
34
+ id: string;
35
+ overrides: Record<string, unknown>;
36
+ };
37
+ }
38
+ /** What the token figures are and are not (they feed `tokensPerSuccess`, spreads and cost). */
39
+ export declare const REPORT_NOTES: readonly ["Token figures are main-loop tokens (excludes compaction calls): the summarizer calls of context compaction are not counted.", "Estimated cost is derived from those tokens at flat input/output rates; it ignores cached-input and reasoning-token pricing."];
40
+ /** Estimated cost of one run from caller-supplied rates; undefined without reported usage. */
41
+ export declare function estimateRunCost(run: EvalRunRecord, rates: EvalRates): number | undefined;
42
+ /**
43
+ * Report over `records`, which must come from one suite version, one model, one mode and one
44
+ * provider/harness version. `configHash` also covers each task's own policy and budget, so it may
45
+ * differ between tasks but never between repetitions of one task; the report's
46
+ * `conditions.configHash` is then the hash of the sorted `taskId -> configHash` pairs (equal to the
47
+ * shared value when every task has the same one).
48
+ */
49
+ export declare function buildReport(records: EvalRunRecord[], options?: BuildReportOptions): EvalReport;
50
+ /** Tasks whose judged runs fall below the power threshold. */
51
+ export declare function lowPowerTasks(report: EvalReport): EvalTaskMetrics[];
52
+ /** Markdown of a report: conditions, warnings, then Measured and Estimated as separate sections. */
53
+ export declare function renderReportMarkdown(report: EvalReport): string;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Aggregation of run records into a reliability report (specs/010 5.2, D6, D7) and its Markdown
3
+ * rendering. Pure: no I/O.
4
+ *
5
+ * What the numbers mean (the review of Phase 3 starts here):
6
+ * - A run that ended `error` is infrastructure, not a repetition of the task. It is excluded from
7
+ * every rate and spread and counted in `errored`. `k` of a task is its number of JUDGED runs
8
+ * (`pass`, `fail`, `budget_exceeded`); `budget_exceeded` is a non-pass that stays in the rate.
9
+ * - `successRate` is passes / judged runs with a 95 % Wilson interval. `passAtK` is "at least one
10
+ * judged run passed", `passPowK` "every judged run passed" (false with no judged run).
11
+ * - Spreads (median, variance) use the judged runs; the sample variance has n - 1 in the divisor.
12
+ * - `tokensPerSuccess` / `wallMsPerSuccess` / `costUsdPerSuccess` are the TOTAL spent over all
13
+ * judged runs (failures included) divided by the number of successes. Token and cost totals are
14
+ * computed only when every judged run reported usage: a partial sum would understate them.
15
+ * - `measured` (provider usage, wall time) and `estimated` (money from caller-supplied rates)
16
+ * never share a field. Without rates no cost is computed and no `estimated` key exists.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { median, passAtK, passPowK, sampleVariance, wilsonInterval } from "./stats.js";
20
+ /** Fewest judged runs per task below which no improvement is claimed (specs/010 O3). */
21
+ export const MIN_POWER_K = 5;
22
+ /** Runs cannot be aggregated into one report (mixed conditions, duplicates, bad rates...). */
23
+ export class EvalReportError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "EvalReportError";
27
+ }
28
+ }
29
+ /** What the token figures are and are not (they feed `tokensPerSuccess`, spreads and cost). */
30
+ export const REPORT_NOTES = [
31
+ "Token figures are main-loop tokens (excludes compaction calls): the summarizer calls of context compaction are not counted.",
32
+ "Estimated cost is derived from those tokens at flat input/output rates; it ignores cached-input and reasoning-token pricing.",
33
+ ];
34
+ /** Main-loop tokens of a run; undefined when usage is missing or only some turns reported it. */
35
+ const tokensOf = (run) => run.measured.inputTokens !== undefined &&
36
+ run.measured.outputTokens !== undefined &&
37
+ !run.measured.usagePartial
38
+ ? run.measured.inputTokens + run.measured.outputTokens
39
+ : undefined;
40
+ /** Estimated cost of one run from caller-supplied rates; undefined without reported usage. */
41
+ export function estimateRunCost(run, rates) {
42
+ const { inputTokens, outputTokens, usagePartial } = run.measured;
43
+ if (inputTokens === undefined || outputTokens === undefined || usagePartial)
44
+ return undefined;
45
+ return (inputTokens * rates.inputPerMTokUsd + outputTokens * rates.outputPerMTokUsd) / 1_000_000;
46
+ }
47
+ const sum = (values) => values.reduce((a, b) => a + b, 0);
48
+ function assertRates(rates) {
49
+ for (const [name, value] of Object.entries(rates))
50
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
51
+ throw new EvalReportError(`Rate ${name} must be a finite number of USD per million tokens, not negative (got ${value}).`);
52
+ }
53
+ /** Fails when `field` takes more than one value across `records`. */
54
+ function assertUniform(records, field, hint) {
55
+ const values = [...new Set(records.map((r) => String(r[field])))];
56
+ if (values.length > 1)
57
+ throw new EvalReportError(`Cannot aggregate runs measured under different ${field}: ${values.map((v) => `"${v}"`).join(", ")}. ${hint}`);
58
+ }
59
+ function taskMetrics(taskId, runs, rates) {
60
+ const judged = runs.filter((r) => r.status !== "error");
61
+ const errored = runs.length - judged.length;
62
+ const successes = judged.filter((r) => r.status === "pass").length;
63
+ const wilson = wilsonInterval(successes, judged.length);
64
+ const wallTimes = judged.map((r) => r.measured.wallMs);
65
+ const tokenSamples = judged.flatMap((r) => {
66
+ const tokens = tokensOf(r);
67
+ return tokens === undefined ? [] : [tokens];
68
+ });
69
+ const allReportedUsage = judged.length > 0 && tokenSamples.length === judged.length;
70
+ const medianWallMs = median(wallTimes);
71
+ const varianceWallMs = sampleVariance(wallTimes);
72
+ // Same rule as tokensPerSuccess: a spread over the runs that happened to report would mislead.
73
+ const varianceTokens = allReportedUsage ? sampleVariance(tokenSamples) : null;
74
+ const measured = {
75
+ ...(medianWallMs === null ? {} : { medianWallMs }),
76
+ ...(varianceWallMs === null ? {} : { varianceWallMs }),
77
+ ...(varianceTokens === null ? {} : { varianceTokens }),
78
+ ...(successes > 0 && allReportedUsage
79
+ ? { tokensPerSuccess: sum(tokenSamples) / successes }
80
+ : {}),
81
+ ...(successes > 0 ? { wallMsPerSuccess: sum(wallTimes) / successes } : {}),
82
+ };
83
+ const costs = rates ? judged.map((r) => estimateRunCost(r, rates)) : [];
84
+ const costKnown = rates !== undefined && successes > 0 && costs.every((c) => c !== undefined);
85
+ const hashes = [...new Set(runs.map((r) => r.configHash))];
86
+ return {
87
+ taskId,
88
+ k: judged.length,
89
+ successRate: {
90
+ value: judged.length === 0 ? 0 : successes / judged.length,
91
+ wilsonLow: wilson.low,
92
+ wilsonHigh: wilson.high,
93
+ },
94
+ passAtK: passAtK(judged.map((r) => r.status === "pass")),
95
+ passPowK: passPowK(judged.map((r) => r.status === "pass")),
96
+ ...(errored > 0 ? { errored } : {}),
97
+ ...(hashes.length === 1 ? { configHash: hashes[0] } : {}),
98
+ measured,
99
+ ...(costKnown ? { estimated: { costUsdPerSuccess: sum(costs) / successes } } : {}),
100
+ };
101
+ }
102
+ /**
103
+ * Report over `records`, which must come from one suite version, one model, one mode and one
104
+ * provider/harness version. `configHash` also covers each task's own policy and budget, so it may
105
+ * differ between tasks but never between repetitions of one task; the report's
106
+ * `conditions.configHash` is then the hash of the sorted `taskId -> configHash` pairs (equal to the
107
+ * shared value when every task has the same one).
108
+ */
109
+ export function buildReport(records, options = {}) {
110
+ const first = records[0];
111
+ if (!first)
112
+ throw new EvalReportError("Cannot build a report from zero runs.");
113
+ if (options.rates)
114
+ assertRates(options.rates);
115
+ const ids = new Set();
116
+ for (const run of records) {
117
+ if (ids.has(run.runId))
118
+ throw new EvalReportError(`Duplicate run ${run.runId}: a run can be aggregated only once.`);
119
+ ids.add(run.runId);
120
+ }
121
+ const hint = "Aggregate runs of one configuration; compare two reports with `eval compare`.";
122
+ for (const field of [
123
+ "suiteId",
124
+ "suiteVersion",
125
+ "model",
126
+ "mode",
127
+ "provider",
128
+ "harnessVersion",
129
+ "compactionHash",
130
+ ])
131
+ assertUniform(records, field, hint);
132
+ const byTask = new Map();
133
+ for (const run of records) {
134
+ const group = byTask.get(run.taskId);
135
+ if (group)
136
+ group.push(run);
137
+ else
138
+ byTask.set(run.taskId, [run]);
139
+ }
140
+ for (const [taskId, runs] of byTask)
141
+ assertUniform(runs, "configHash", `Repetitions of task "${taskId}" must share one configuration.`);
142
+ const sorted = [...byTask].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
143
+ const tasks = sorted.map(([taskId, runs]) => taskMetrics(taskId, runs, options.rates));
144
+ const taskHashes = sorted.map(([taskId, runs]) => [taskId, runs[0].configHash]);
145
+ const distinct = new Set(taskHashes.map(([, hash]) => hash));
146
+ const configHash = distinct.size === 1
147
+ ? taskHashes[0][1]
148
+ : `sha256:${createHash("sha256").update(JSON.stringify(taskHashes)).digest("hex")}`;
149
+ const count = (status) => records.filter((r) => r.status === status).length;
150
+ return {
151
+ schemaVersion: 1,
152
+ suiteId: first.suiteId,
153
+ suiteVersion: first.suiteVersion,
154
+ conditions: {
155
+ harnessVersion: first.harnessVersion,
156
+ configHash,
157
+ provider: first.provider,
158
+ model: first.model,
159
+ mode: first.mode,
160
+ k: options.k ?? Math.max(...[...byTask.values()].map((runs) => runs.length)),
161
+ seedBase: options.seedBase ?? Math.min(...records.map((r) => r.seed)),
162
+ ...(first.compactionHash ? { compactionHash: first.compactionHash } : {}),
163
+ },
164
+ ...(options.variant
165
+ ? { variant: { id: options.variant.id, overrides: { ...options.variant.overrides } } }
166
+ : {}),
167
+ notes: [...REPORT_NOTES],
168
+ tasks,
169
+ runs: records.map((r) => r.runId),
170
+ summary: {
171
+ tasks: tasks.length,
172
+ passed: count("pass"),
173
+ errored: count("error"),
174
+ budgetExceeded: count("budget_exceeded"),
175
+ runs: records.length,
176
+ failed: count("fail"),
177
+ },
178
+ ...(options.rates ? { rates: { ...options.rates } } : {}),
179
+ };
180
+ }
181
+ // ---------------------------------------------------------------------------------------------
182
+ // Markdown
183
+ // ---------------------------------------------------------------------------------------------
184
+ const cell = (text) => text.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
185
+ const percent = (value) => `${Math.round(value * 1000) / 10}%`;
186
+ const seconds = (ms) => `${(Math.round(ms / 10) / 100).toFixed(2)} s`;
187
+ const count = (value) => Math.round(value).toLocaleString("en-US");
188
+ const optional = (value, format) => value === undefined ? "-" : format(value);
189
+ const sd = (variance, format) => variance === undefined ? "-" : format(Math.sqrt(variance));
190
+ function table(headers, rows) {
191
+ return [
192
+ `| ${headers.join(" | ")} |`,
193
+ `| ${headers.map(() => "---").join(" | ")} |`,
194
+ ...rows.map((row) => `| ${row.join(" | ")} |`),
195
+ ].join("\n");
196
+ }
197
+ /** Tasks whose judged runs fall below the power threshold. */
198
+ export function lowPowerTasks(report) {
199
+ return report.tasks.filter((t) => t.k < MIN_POWER_K);
200
+ }
201
+ /** Markdown of a report: conditions, warnings, then Measured and Estimated as separate sections. */
202
+ export function renderReportMarkdown(report) {
203
+ const { conditions: c } = report;
204
+ const lines = [
205
+ `# Reliability report: ${report.suiteId} ${report.suiteVersion}`,
206
+ "",
207
+ `- Suite: \`${report.suiteId}\` version \`${report.suiteVersion}\``,
208
+ `- Mode: **${c.mode}**`,
209
+ `- Provider / model: \`${c.provider}\` / \`${c.model}\``,
210
+ `- Harness version: \`${c.harnessVersion}\``,
211
+ `- Configuration hash: \`${c.configHash}\``,
212
+ `- Repetitions per task (k): ${c.k}; seed base: ${c.seedBase}`,
213
+ "",
214
+ ];
215
+ lines.push(c.mode === "replay"
216
+ ? "> Replay measures the harness, not the model: the provider answers come from a recording, so this says nothing about model quality."
217
+ : "> Live is not deterministic: the model can answer differently on every run. Repetitions and Wilson intervals estimate the noise; they do not remove it.", "");
218
+ const weak = lowPowerTasks(report);
219
+ if (weak.length > 0)
220
+ lines.push(`> **Low power:** ${weak.map((t) => `${t.taskId} (k=${t.k})`).join(", ")} ${weak.length === 1 ? "has" : "have"} fewer than ${MIN_POWER_K} judged runs. The intervals are wide; do not read an improvement or a regression from these numbers.`, "");
221
+ const { summary: s } = report;
222
+ lines.push(`Runs: ${s.runs ?? report.runs.length} (passed ${s.passed}, failed ${s.failed ?? "-"}, budget exceeded ${s.budgetExceeded}, errored ${s.errored}). Errored runs are infrastructure failures: they are excluded from every rate and spread below.`, "", "## Measured", "", "Provider-reported usage and wall time. Success is passes over judged runs, with a 95% Wilson interval; sd is the square root of the sample variance. Token figures are main-loop tokens (excludes compaction calls).", "", table([
223
+ "Task",
224
+ "k",
225
+ "Success [Wilson 95%]",
226
+ "pass@k",
227
+ "pass^k",
228
+ "Errored",
229
+ "Median wall",
230
+ "Wall sd",
231
+ "Tokens sd",
232
+ "Tokens / success",
233
+ "Wall / success",
234
+ ], report.tasks.map((t) => [
235
+ cell(t.taskId),
236
+ String(t.k),
237
+ t.k === 0
238
+ ? "n/a"
239
+ : `${percent(t.successRate.value)} [${percent(t.successRate.wilsonLow)}, ${percent(t.successRate.wilsonHigh)}]`,
240
+ t.passAtK ? "yes" : "no",
241
+ t.passPowK ? "yes" : "no",
242
+ String(t.errored ?? 0),
243
+ optional(t.measured.medianWallMs, seconds),
244
+ sd(t.measured.varianceWallMs, seconds),
245
+ sd(t.measured.varianceTokens, count),
246
+ optional(t.measured.tokensPerSuccess, count),
247
+ optional(t.measured.wallMsPerSuccess, seconds),
248
+ ])), "", "## Estimated", "");
249
+ if (!report.rates)
250
+ lines.push("Not computed: no rates were supplied, and Alisio does not guess prices.");
251
+ else {
252
+ lines.push(`Derived from the measured main-loop tokens (excludes compaction calls) at ${report.rates.inputPerMTokUsd} USD per million input tokens and ${report.rates.outputPerMTokUsd} USD per million output tokens (rates supplied by the caller). Cached-input and reasoning-token pricing are ignored. A task shows "-" when a run did not report usage on every turn or nothing succeeded.`, "", table(["Task", "Cost per success (USD)"], report.tasks.map((t) => [
253
+ cell(t.taskId),
254
+ optional(t.estimated?.costUsdPerSuccess, (v) => `$${v.toFixed(4)}`),
255
+ ])));
256
+ }
257
+ return `${lines.join("\n")}\n`;
258
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Runs a suite k times (specs/010 D6): plans the (task, repetition) pairs, refuses the whole batch
3
+ * up front when a task is not allowed, then runs them one after another through `runTask` (which
4
+ * itself serializes: the process environment it scopes is global). The caller resolves the
5
+ * provider and the configuration; nothing here reads the network or the user's config.
6
+ */
7
+ import type { EvalRunRecord, EvalTask, ModelProvider, RunEvent } from "@alisio/sdk";
8
+ import type { BuiltinPlugin } from "../application.ts";
9
+ import type { Config } from "../config.ts";
10
+ import type { RunOptions } from "../core/runner.ts";
11
+ import type { Cassette, CassetteRecorder } from "./cassette.ts";
12
+ import { type EvalApplication, type EvalRunMode, type InvokerPermissions } from "./engine.ts";
13
+ import type { LoadedSuite } from "./suite.ts";
14
+ export interface RunEvaluationOptions {
15
+ suite: LoadedSuite;
16
+ /** Required except in `replay` mode, where each task's cassette supplies the provider. */
17
+ provider?: ModelProvider;
18
+ /** `live` (default), `record` (a live run that also fills a recorder) or `replay` (from cassettes). */
19
+ mode?: EvalRunMode;
20
+ /** The recorder (record) or the recorded cassette (replay) of one task repetition. */
21
+ cassetteFor?: (task: EvalTask, repetition: number) => CassetteRecorder | Cassette | undefined;
22
+ /** Receives every RunEvent of every run (tests and tooling). */
23
+ onEvent?: (event: RunEvent) => void;
24
+ /** Variant overrides are already applied (they must be before the configuration is hashed). */
25
+ config: Config;
26
+ invoker: InvokerPermissions;
27
+ /** Repetitions per task (a positive integer). */
28
+ k: number;
29
+ /** Plans the run order; each run gets `seedBase + repetition`. */
30
+ seedBase: number;
31
+ /** Subset of task ids (default: every task of the suite). */
32
+ taskIds?: string[];
33
+ allowSuiteFull?: boolean;
34
+ keep?: boolean;
35
+ builtins?: BuiltinPlugin[];
36
+ resolveAgent?: (app: EvalApplication, agent: string) => RunOptions | Promise<RunOptions>;
37
+ tmpRoot?: string;
38
+ onRecord?: (record: EvalRunRecord, progress: {
39
+ done: number;
40
+ total: number;
41
+ }) => void;
42
+ }
43
+ export interface RunEvaluationResult {
44
+ records: EvalRunRecord[];
45
+ /** Run directories kept because of `keep`. */
46
+ keptDirs: string[];
47
+ }
48
+ export interface RunVariantsOptions extends Omit<RunEvaluationOptions, "config" | "onRecord"> {
49
+ /** Each variant's overrides are already applied to its `config`. */
50
+ variants: Array<{
51
+ id: string;
52
+ config: Config;
53
+ }>;
54
+ onRecord?: (variantId: string, record: EvalRunRecord, progress: {
55
+ done: number;
56
+ total: number;
57
+ }) => void;
58
+ }
59
+ export declare function runEvaluation(options: RunEvaluationOptions): Promise<RunEvaluationResult>;
60
+ /**
61
+ * Runs several variants over the SAME plan, interleaved at each plan step (A,B then B,A ...)
62
+ * instead of all of A and then all of B. The whole batch is refused up front like a single run.
63
+ * Remaining drift: the runs of one step still happen one after another, so provider load can
64
+ * change between A and B inside a step; interleaving spreads it, it does not control it.
65
+ */
66
+ export declare function runVariantEvaluations(options: RunVariantsOptions): Promise<Array<{
67
+ id: string;
68
+ } & RunEvaluationResult>>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Runs a suite k times (specs/010 D6): plans the (task, repetition) pairs, refuses the whole batch
3
+ * up front when a task is not allowed, then runs them one after another through `runTask` (which
4
+ * itself serializes: the process environment it scopes is global). The caller resolves the
5
+ * provider and the configuration; nothing here reads the network or the user's config.
6
+ */
7
+ import { checkTaskPolicy, EvalRunRefusedError, runTask, } from "./engine.js";
8
+ import { interleaveVariants, planRuns } from "./plan.js";
9
+ /** Validates the options and plans the runs shared by every variant. */
10
+ function prepareRuns(options) {
11
+ if (!Number.isInteger(options.k) || options.k < 1)
12
+ throw new EvalRunRefusedError(`k must be a positive integer, got ${options.k}.`);
13
+ const all = options.suite.suite.tasks;
14
+ const selected = options.taskIds
15
+ ? [...new Set(options.taskIds)].map((id) => {
16
+ const found = all.find((t) => t.id === id);
17
+ if (!found)
18
+ throw new EvalRunRefusedError(`Unknown task "${id}" in suite "${options.suite.suite.suiteId}". Tasks: ${all.map((t) => t.id).join(", ")}.`);
19
+ return found;
20
+ })
21
+ : all;
22
+ for (const task of selected)
23
+ checkTaskPolicy(task, {
24
+ invoker: options.invoker,
25
+ ...(options.allowSuiteFull ? { allowSuiteFull: true } : {}),
26
+ });
27
+ const plan = planRuns(selected.map((t) => t.id), options.k, options.seedBase);
28
+ return { selected, plan };
29
+ }
30
+ export async function runEvaluation(options) {
31
+ const { selected, plan } = prepareRuns(options);
32
+ const records = [];
33
+ const keptDirs = [];
34
+ for (const step of plan) {
35
+ const task = selected.find((t) => t.id === step.taskId);
36
+ if (!task)
37
+ continue;
38
+ const { record, keptDir } = await runOne(options, options.config, task, step.repetition);
39
+ records.push(record);
40
+ if (keptDir)
41
+ keptDirs.push(keptDir);
42
+ options.onRecord?.(record, { done: records.length, total: plan.length });
43
+ }
44
+ return { records, keptDirs };
45
+ }
46
+ /**
47
+ * Runs several variants over the SAME plan, interleaved at each plan step (A,B then B,A ...)
48
+ * instead of all of A and then all of B. The whole batch is refused up front like a single run.
49
+ * Remaining drift: the runs of one step still happen one after another, so provider load can
50
+ * change between A and B inside a step; interleaving spreads it, it does not control it.
51
+ */
52
+ export async function runVariantEvaluations(options) {
53
+ const { selected, plan } = prepareRuns(options);
54
+ const results = options.variants.map((variant) => ({
55
+ id: variant.id,
56
+ records: [],
57
+ keptDirs: [],
58
+ }));
59
+ const steps = interleaveVariants(plan, options.variants.map((v) => v.id));
60
+ let done = 0;
61
+ for (const step of steps) {
62
+ const task = selected.find((t) => t.id === step.taskId);
63
+ const variant = options.variants.find((v) => v.id === step.variantId);
64
+ const result = results.find((r) => r.id === step.variantId);
65
+ if (!task || !variant || !result)
66
+ continue;
67
+ const { record, keptDir } = await runOne(options, variant.config, task, step.repetition);
68
+ result.records.push(record);
69
+ if (keptDir)
70
+ result.keptDirs.push(keptDir);
71
+ done += 1;
72
+ options.onRecord?.(variant.id, record, { done, total: steps.length });
73
+ }
74
+ return results;
75
+ }
76
+ function runOne(options, config, task, repetition) {
77
+ return runTask(task, {
78
+ suite: options.suite,
79
+ ...(options.provider ? { provider: options.provider } : {}),
80
+ ...(options.mode ? { mode: options.mode } : {}),
81
+ ...(options.cassetteFor ? cassetteOption(options.cassetteFor(task, repetition)) : {}),
82
+ ...(options.onEvent ? { onEvent: options.onEvent } : {}),
83
+ config,
84
+ seed: options.seedBase + repetition,
85
+ repetition,
86
+ invoker: options.invoker,
87
+ ...(options.allowSuiteFull ? { allowSuiteFull: true } : {}),
88
+ ...(options.keep ? { keep: true } : {}),
89
+ ...(options.builtins ? { builtins: options.builtins } : {}),
90
+ ...(options.resolveAgent ? { resolveAgent: options.resolveAgent } : {}),
91
+ ...(options.tmpRoot ? { tmpRoot: options.tmpRoot } : {}),
92
+ });
93
+ }
94
+ const cassetteOption = (cassette) => cassette ? { cassette } : {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Secret scan applied to everything a cassette stores and every diff excerpt it prints (specs/010
3
+ * 5.3, 10). It looks for well-known credential shapes plus the exact values of the credentials the
4
+ * run could hold. A hit on write REFUSES the cassette (nothing is stripped: a stripped cassette
5
+ * would silently stop matching what the model saw); diagnostics are redacted instead.
6
+ *
7
+ * Findings carry the rule name, never the matched text.
8
+ */
9
+ /** Names of the rules `text` matches (each once). `exact` are credential values to look for verbatim. */
10
+ export declare function findSecrets(text: string, exact?: readonly string[]): string[];
11
+ /** `text` with every match replaced by `[redacted]`. */
12
+ export declare function redactSecrets(text: string, exact?: readonly string[]): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Secret scan applied to everything a cassette stores and every diff excerpt it prints (specs/010
3
+ * 5.3, 10). It looks for well-known credential shapes plus the exact values of the credentials the
4
+ * run could hold. A hit on write REFUSES the cassette (nothing is stripped: a stripped cassette
5
+ * would silently stop matching what the model saw); diagnostics are redacted instead.
6
+ *
7
+ * Findings carry the rule name, never the matched text.
8
+ */
9
+ const RULES = [
10
+ { name: "Anthropic API key (sk-ant-...)", pattern: /sk-ant-[A-Za-z0-9_-]{16,}/ },
11
+ { name: "API key (sk-...)", pattern: /\bsk-[A-Za-z0-9_-]{20,}/ },
12
+ { name: "GitHub token (ghp_/gho_)", pattern: /\bgh[po]_[A-Za-z0-9]{20,}/ },
13
+ { name: "GitHub fine-grained token (github_pat_)", pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
14
+ { name: "AWS access key id (AKIA...)", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
15
+ { name: "Slack token (xoxb-/xoxp-)", pattern: /\bxox[bp]-[A-Za-z0-9-]{10,}/ },
16
+ { name: "Bearer token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/ },
17
+ { name: "PEM private key header", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
18
+ {
19
+ name: "*_API_KEY assignment",
20
+ pattern: /\b[A-Z][A-Z0-9_]*_API_KEY\s*=\s*['"]?[^\s'"]{4,}/,
21
+ },
22
+ ];
23
+ /** Exact credential values shorter than this are ignored: they would match ordinary text. */
24
+ const MIN_EXACT_LENGTH = 8;
25
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ /** Names of the rules `text` matches (each once). `exact` are credential values to look for verbatim. */
27
+ export function findSecrets(text, exact = []) {
28
+ const hits = [];
29
+ for (const rule of RULES)
30
+ if (rule.pattern.test(text))
31
+ hits.push(rule.name);
32
+ if (exact.some((value) => value.length >= MIN_EXACT_LENGTH && text.includes(value)))
33
+ hits.push("a credential of the provider profile or environment");
34
+ return hits;
35
+ }
36
+ /** `text` with every match replaced by `[redacted]`. */
37
+ export function redactSecrets(text, exact = []) {
38
+ let out = text;
39
+ for (const rule of RULES)
40
+ out = out.replace(new RegExp(rule.pattern, "g"), "[redacted]");
41
+ for (const value of exact)
42
+ if (value.length >= MIN_EXACT_LENGTH)
43
+ out = out.replace(new RegExp(escape(value), "g"), "[redacted]");
44
+ return out;
45
+ }
@@ -0,0 +1,17 @@
1
+ /** Pure statistics of the reliability lab (specs/010 D6, D17). No I/O, no dependencies. */
2
+ /**
3
+ * Wilson score interval of a proportion (default 95 %). Unlike the normal approximation it stays
4
+ * inside [0, 1] and is informative at 0/n and n/n. With no trials it carries no information.
5
+ */
6
+ export declare function wilsonInterval(successes: number, trials: number, z?: number): {
7
+ low: number;
8
+ high: number;
9
+ };
10
+ /** Median; null for an empty set. Does not mutate the input. */
11
+ export declare function median(values: number[]): number | null;
12
+ /** Sample variance (n - 1), the same convention as `scripts/eval-baseline.ts`; null below two samples. */
13
+ export declare function sampleVariance(values: number[]): number | null;
14
+ /** pass@k: at least one of the k repetitions succeeded. No repetitions is not a pass. */
15
+ export declare function passAtK(results: boolean[]): boolean;
16
+ /** pass^k: every one of the k repetitions succeeded (consistency). No repetitions is not a pass. */
17
+ export declare function passPowK(results: boolean[]): boolean;