@prismatic-io/lux 0.0.2-preview.30 → 0.0.2-preview.33
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/lib/assertions/core/command-exits-zero.d.ts +1 -0
- package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
- package/lib/assertions/core/file-checks.d.ts +6 -0
- package/lib/assertions/core/file-checks.d.ts.map +1 -1
- package/lib/assertions/core/json-pointer-equals.d.ts +1 -0
- package/lib/assertions/core/json-pointer-equals.d.ts.map +1 -1
- package/lib/assertions/core/output-checks.d.ts +3 -0
- package/lib/assertions/core/output-checks.d.ts.map +1 -1
- package/lib/assertions/core/predicate.d.ts +1 -0
- package/lib/assertions/core/predicate.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +6 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/rubric/index.d.ts +2 -0
- package/lib/assertions/rubric/index.d.ts.map +1 -1
- package/lib/cli/output-schemas.d.ts +11 -5
- package/lib/cli/output-schemas.d.ts.map +1 -1
- package/lib/cli/output-schemas.js +5 -2
- package/lib/cli/output-schemas.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +2 -1
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/format.d.ts.map +1 -1
- package/lib/cli/render/format.js +1 -1
- package/lib/cli/render/format.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +11 -4
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/core/assertion.d.ts +5 -2
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +3 -1
- package/lib/core/assertion.js.map +1 -1
- package/lib/core/case.d.ts +5 -1
- package/lib/core/case.d.ts.map +1 -1
- package/lib/core/case.js +1 -0
- package/lib/core/case.js.map +1 -1
- package/lib/core/experiment.d.ts +1 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +5 -1
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/rubric.d.ts +1 -0
- package/lib/core/rubric.d.ts.map +1 -1
- package/lib/core/run.d.ts +1 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/orchestrator/compare.d.ts +6 -5
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +20 -11
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/config.d.ts +2 -0
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +1 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-effects.d.ts.map +1 -1
- package/lib/orchestrator/experiment-effects.js +4 -2
- package/lib/orchestrator/experiment-effects.js.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +15 -2
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +1 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +2 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/experiment-selection.d.ts.map +1 -1
- package/lib/orchestrator/experiment-selection.js +99 -43
- package/lib/orchestrator/experiment-selection.js.map +1 -1
- package/lib/orchestrator/grader-experiment.d.ts +4 -2
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +1 -0
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +34 -31
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +7 -7
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/paired-effects.d.ts.map +1 -1
- package/lib/orchestrator/paired-effects.js +7 -1
- package/lib/orchestrator/paired-effects.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +2 -1
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-compare.js +18 -6
- package/lib/orchestrator/suite-compare.js.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +2 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +6 -2
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/server.d.ts.map +1 -1
- package/lib/previewer/server.js +2 -1
- package/lib/previewer/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/cli/output-schemas.ts +5 -2
- package/src/cli/program.ts +3 -1
- package/src/cli/render/format.ts +1 -1
- package/src/cli/render/reporter.ts +10 -4
- package/src/core/assertion.ts +3 -1
- package/src/core/case.ts +5 -1
- package/src/core/experiment.ts +7 -1
- package/src/orchestrator/compare.ts +28 -12
- package/src/orchestrator/experiment-effects.ts +4 -2
- package/src/orchestrator/experiment-evaluation.ts +18 -2
- package/src/orchestrator/experiment-selection.ts +101 -18
- package/src/orchestrator/grading.ts +50 -28
- package/src/orchestrator/list-runs.ts +8 -8
- package/src/orchestrator/paired-effects.ts +6 -1
- package/src/orchestrator/suite-compare.ts +19 -5
- package/src/orchestrator/suite-summary.ts +7 -2
- package/src/previewer/server.ts +2 -0
- package/viewer/app.css +36 -0
- package/viewer/charts.js +16 -4
- package/viewer/detail.js +40 -16
- package/viewer/format.js +2 -2
|
@@ -60,7 +60,9 @@ export const ViewOutputSchema = z.union([
|
|
|
60
60
|
z.object({ url: z.string(), runsRoot: z.string() }),
|
|
61
61
|
]);
|
|
62
62
|
const interrupts = z.object({ ask: z.number(), approve: z.number(), custom: z.number() });
|
|
63
|
-
const verdict = z
|
|
63
|
+
const verdict = z
|
|
64
|
+
.object({ passed: z.boolean().nullable(), score: z.number().nullable() })
|
|
65
|
+
.nullable();
|
|
64
66
|
const runComparison = z.object({
|
|
65
67
|
pathA: z.string(),
|
|
66
68
|
pathB: z.string(),
|
|
@@ -83,6 +85,7 @@ const runComparison = z.object({
|
|
|
83
85
|
casePassedB: z.boolean().nullable(),
|
|
84
86
|
caseScoreA: nullableNumber,
|
|
85
87
|
caseScoreB: nullableNumber,
|
|
88
|
+
caseScoreComparable: z.boolean(),
|
|
86
89
|
deltas: z.array(
|
|
87
90
|
z.object({
|
|
88
91
|
id: z.string(),
|
|
@@ -90,7 +93,7 @@ const runComparison = z.object({
|
|
|
90
93
|
name: z.string(),
|
|
91
94
|
before: verdict,
|
|
92
95
|
after: verdict,
|
|
93
|
-
status: z.enum(["added", "removed", "improved", "regressed", "unchanged"]),
|
|
96
|
+
status: z.enum(["added", "removed", "improved", "regressed", "unchanged", "unavailable"]),
|
|
94
97
|
}),
|
|
95
98
|
),
|
|
96
99
|
improvements: z.number(),
|
package/src/cli/program.ts
CHANGED
|
@@ -948,7 +948,9 @@ const runCommand = async (filters: string[], options: RunOptions, human = false)
|
|
|
948
948
|
caseScore: result.result?.grading?.caseScore ?? null,
|
|
949
949
|
assertionsPassed:
|
|
950
950
|
result.result?.grading?.assertions.filter((assertion) => assertion.passed).length ?? 0,
|
|
951
|
-
assertionsTotal:
|
|
951
|
+
assertionsTotal:
|
|
952
|
+
result.result?.grading?.assertions.filter((assertion) => assertion.passed !== null)
|
|
953
|
+
.length ?? 0,
|
|
952
954
|
})),
|
|
953
955
|
};
|
|
954
956
|
} finally {
|
package/src/cli/render/format.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const formatAlignedUsage = (usage: TokenUsage): string =>
|
|
|
59
59
|
].join(" "),
|
|
60
60
|
].join(" ");
|
|
61
61
|
|
|
62
|
-
export const formatScore = (score: number): string => `${
|
|
62
|
+
export const formatScore = (score: number): string => `${Number((score * 100).toPrecision(15))}%`;
|
|
63
63
|
|
|
64
64
|
/** A one-line count summary: "2 passed · 1 failed · 1 running (4)". */
|
|
65
65
|
export const formatTally = (parts: [count: number, label: string][], total: number): string => {
|
|
@@ -121,7 +121,10 @@ const statusMark = (status: CaseStatus, theme: Theme): string => {
|
|
|
121
121
|
|
|
122
122
|
const assertionTally = (view: CaseView): string => {
|
|
123
123
|
const passed = view.assertions.filter((a) => a.passed).length;
|
|
124
|
-
const evaluated =
|
|
124
|
+
const evaluated =
|
|
125
|
+
view.assertions.length === 0
|
|
126
|
+
? "—"
|
|
127
|
+
: `${passed}/${view.assertions.filter((a) => a.passed !== null).length}`;
|
|
125
128
|
return view.skippedAssertions.length > 0
|
|
126
129
|
? `${evaluated} · ${view.skippedAssertions.length} skipped`
|
|
127
130
|
: evaluated;
|
|
@@ -162,7 +165,8 @@ const cell = (text: string, width: number): string => {
|
|
|
162
165
|
const scoreNote = (entry: GradingAssertionEntry): string => {
|
|
163
166
|
if (entry.errorMessage) return entry.errorMessage;
|
|
164
167
|
if (entry.abstention) return "insufficient evidence";
|
|
165
|
-
if (entry.passed) return
|
|
168
|
+
if (entry.passed === null) return `informational; score ${formatScore(entry.score)}`;
|
|
169
|
+
if (entry.passed) return entry.score < 1 ? `score ${formatScore(entry.score)}` : "";
|
|
166
170
|
const score = formatScore(entry.score);
|
|
167
171
|
if (entry.threshold > 0) return `score ${score} < ${formatScore(entry.threshold)}`;
|
|
168
172
|
return entry.score > 0 ? `score ${score}` : "";
|
|
@@ -464,11 +468,12 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
464
468
|
);
|
|
465
469
|
for (const entry of view.assertions) {
|
|
466
470
|
let mark = entry.passed ? this.theme.green("✓") : this.theme.red("✗");
|
|
471
|
+
if (entry.passed === null) mark = this.theme.dim("ℹ");
|
|
467
472
|
if (entry.abstention) mark = this.theme.dim("?");
|
|
468
473
|
const duration = formatDuration(entry.durationMs).padStart(DURATION_COL);
|
|
469
474
|
const note = scoreNote(entry);
|
|
470
475
|
const suffix = note
|
|
471
|
-
? ` ${entry.abstention ? this.theme.dim(note) : this.theme.red(note)}`
|
|
476
|
+
? ` ${entry.abstention || entry.passed !== false ? this.theme.dim(note) : this.theme.red(note)}`
|
|
472
477
|
: "";
|
|
473
478
|
nodes.push({
|
|
474
479
|
head: `${mark} ${cell(entry.name, width)} ${this.theme.dim(duration)}${suffix}`,
|
|
@@ -581,11 +586,12 @@ export class ConsoleReporter implements SuiteObserver {
|
|
|
581
586
|
const skippedAssertions = this.cases.flatMap((c) => c.skippedAssertions);
|
|
582
587
|
if (assertions.length > 0 || skippedAssertions.length > 0) {
|
|
583
588
|
const ok = assertions.filter((a) => a.passed).length;
|
|
584
|
-
const bad = assertions.
|
|
589
|
+
const bad = assertions.filter((a) => a.passed === false).length;
|
|
585
590
|
const tally = formatTally(
|
|
586
591
|
[
|
|
587
592
|
[ok, this.theme.green("passed")],
|
|
588
593
|
[bad, this.theme.red("failed")],
|
|
594
|
+
[assertions.filter((a) => a.passed === null).length, this.theme.dim("informational")],
|
|
589
595
|
[skippedAssertions.length, this.theme.dim("skipped")],
|
|
590
596
|
],
|
|
591
597
|
assertions.length + skippedAssertions.length,
|
package/src/core/assertion.ts
CHANGED
|
@@ -107,6 +107,7 @@ export const baseAssertionFields = {
|
|
|
107
107
|
weight: z.number().positive().optional(),
|
|
108
108
|
threshold: z.number().min(0).max(1).optional(),
|
|
109
109
|
name: z.string().optional(),
|
|
110
|
+
informational: z.boolean().optional(),
|
|
110
111
|
} as const;
|
|
111
112
|
|
|
112
113
|
export const defineAssertion = <S extends z.ZodObject>(
|
|
@@ -137,7 +138,8 @@ export const GradingAssertionEntrySchema = z.object({
|
|
|
137
138
|
id: z.exactOptional(z.string()),
|
|
138
139
|
type: z.string(),
|
|
139
140
|
name: z.string(),
|
|
140
|
-
passed: z.boolean(),
|
|
141
|
+
passed: z.boolean().nullable(),
|
|
142
|
+
informational: z.exactOptional(z.boolean()),
|
|
141
143
|
score: z.number(),
|
|
142
144
|
weight: z.number(),
|
|
143
145
|
threshold: z.number(),
|
package/src/core/case.ts
CHANGED
|
@@ -45,6 +45,7 @@ export const AssertionSchema = z
|
|
|
45
45
|
name: z.string().optional(),
|
|
46
46
|
weight: z.number().positive().optional(),
|
|
47
47
|
threshold: z.number().min(0).max(1).optional(),
|
|
48
|
+
informational: z.boolean().optional(),
|
|
48
49
|
})
|
|
49
50
|
.loose();
|
|
50
51
|
export type Assertion = z.infer<typeof AssertionSchema>;
|
|
@@ -83,7 +84,10 @@ export const EvalCaseSchema = EvalCaseObjectSchema
|
|
|
83
84
|
});
|
|
84
85
|
export type EvalCase = z.infer<typeof EvalCaseSchema>;
|
|
85
86
|
|
|
86
|
-
type AssertionPlanFields = Pick<
|
|
87
|
+
type AssertionPlanFields = Pick<
|
|
88
|
+
Assertion,
|
|
89
|
+
"id" | "name" | "threshold" | "weight" | "informational"
|
|
90
|
+
>;
|
|
87
91
|
type AssertionInputOf<Plugin> =
|
|
88
92
|
Plugin extends AssertionPlugin<Assertion, infer Input> ? Input & AssertionPlanFields : never;
|
|
89
93
|
|
package/src/core/experiment.ts
CHANGED
|
@@ -173,6 +173,11 @@ const validateConstraintConfidence = (
|
|
|
173
173
|
) {
|
|
174
174
|
issue("relative constraints require a metric with per-case observations");
|
|
175
175
|
}
|
|
176
|
+
if (constraint.metric === "score" && constraint.confidence?.method === "wilson") {
|
|
177
|
+
issue(
|
|
178
|
+
"continuous scores are not Bernoulli observations; use Hoeffding or percentile-bootstrap confidence",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
176
181
|
const bounded = boundedMetrics.has(constraint.metric);
|
|
177
182
|
if (
|
|
178
183
|
(constraint.confidence?.method === "hoeffding" || constraint.confidence?.method === "wilson") &&
|
|
@@ -263,7 +268,7 @@ export const ExperimentCampaignSchema = z
|
|
|
263
268
|
"grader.operationalErrorRate",
|
|
264
269
|
"grader.scoreMae",
|
|
265
270
|
]);
|
|
266
|
-
const unitIntervalMetrics = graderConfidenceMetrics;
|
|
271
|
+
const unitIntervalMetrics = new Set([...graderConfidenceMetrics, "score"]);
|
|
267
272
|
if (campaign.optimizer && !campaign.splits.train) {
|
|
268
273
|
ctx.addIssue({
|
|
269
274
|
code: "custom",
|
|
@@ -454,6 +459,7 @@ export const CandidateEvaluationSchema = z.object({
|
|
|
454
459
|
z.string(),
|
|
455
460
|
z.object({
|
|
456
461
|
identityHash: z.string().nullable(),
|
|
462
|
+
scoreBasis: z.exactOptional(z.string().nullable()),
|
|
457
463
|
clusterId: z.exactOptional(z.string().min(1)),
|
|
458
464
|
metrics: z.record(
|
|
459
465
|
z.string(),
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { hasAgentUsage } from "../core/usage.js";
|
|
21
21
|
import { stableDriverIdentity } from "./comparison-identity.js";
|
|
22
22
|
import { safeReadJson, walkFiles } from "./fs-read.js";
|
|
23
|
+
import { gradingScoreBasis } from "./grading.js";
|
|
23
24
|
|
|
24
25
|
export type ComparisonInput = {
|
|
25
26
|
pathA: string;
|
|
@@ -39,9 +40,9 @@ export type GradingDelta = {
|
|
|
39
40
|
id: string;
|
|
40
41
|
type: string;
|
|
41
42
|
name: string;
|
|
42
|
-
before: { passed: boolean; score: number } | null;
|
|
43
|
-
after: { passed: boolean; score: number } | null;
|
|
44
|
-
status: "added" | "removed" | "improved" | "regressed" | "unchanged";
|
|
43
|
+
before: { passed: boolean | null; score: number | null } | null;
|
|
44
|
+
after: { passed: boolean | null; score: number | null } | null;
|
|
45
|
+
status: "added" | "removed" | "improved" | "regressed" | "unchanged" | "unavailable";
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export type RunComparison = {
|
|
@@ -67,6 +68,7 @@ export type RunComparison = {
|
|
|
67
68
|
casePassedB: boolean | null;
|
|
68
69
|
caseScoreA: number | null;
|
|
69
70
|
caseScoreB: number | null;
|
|
71
|
+
caseScoreComparable: boolean;
|
|
70
72
|
deltas: GradingDelta[];
|
|
71
73
|
improvements: number;
|
|
72
74
|
regressions: number;
|
|
@@ -156,6 +158,9 @@ const assertionsByIdentity = (
|
|
|
156
158
|
return out;
|
|
157
159
|
};
|
|
158
160
|
|
|
161
|
+
const measuredScore = (entry: GradingReport["assertions"][number]): number | null =>
|
|
162
|
+
entry.errorMessage !== undefined || entry.abstention !== undefined ? null : entry.score;
|
|
163
|
+
|
|
159
164
|
const computeGradingDeltas = (
|
|
160
165
|
a: GradingReport | null,
|
|
161
166
|
b: GradingReport | null,
|
|
@@ -174,21 +179,24 @@ const computeGradingDeltas = (
|
|
|
174
179
|
if (!ax && bx) status = "added";
|
|
175
180
|
else if (ax && !bx) status = "removed";
|
|
176
181
|
else if (ax && bx) {
|
|
177
|
-
if (ax
|
|
182
|
+
if (measuredScore(ax) === null || measuredScore(bx) === null) status = "unavailable";
|
|
183
|
+
else if (ax.passed === bx.passed && ax.score === bx.score) status = "unchanged";
|
|
178
184
|
// A pass/fail flip dominates any score movement.
|
|
179
185
|
else if (ax.passed !== bx.passed) status = bx.passed ? "improved" : "regressed";
|
|
180
186
|
else status = ax.score < bx.score ? "improved" : "regressed";
|
|
181
187
|
} else {
|
|
182
188
|
status = "unchanged";
|
|
183
189
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
190
|
+
if (ax?.passed !== null && bx?.passed !== null) {
|
|
191
|
+
if (status === "improved") improvements++;
|
|
192
|
+
if (status === "regressed") regressions++;
|
|
193
|
+
}
|
|
186
194
|
deltas.push({
|
|
187
195
|
id: name,
|
|
188
196
|
type: ax?.type ?? bx?.type ?? "unknown",
|
|
189
197
|
name: ax?.id || bx?.id ? (ax?.name ?? bx?.name ?? name) : name,
|
|
190
|
-
before: ax ? { passed: ax.passed, score: ax
|
|
191
|
-
after: bx ? { passed: bx.passed, score: bx
|
|
198
|
+
before: ax ? { passed: ax.passed, score: measuredScore(ax) } : null,
|
|
199
|
+
after: bx ? { passed: bx.passed, score: measuredScore(bx) } : null,
|
|
192
200
|
status,
|
|
193
201
|
});
|
|
194
202
|
}
|
|
@@ -312,6 +320,9 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
|
|
|
312
320
|
casePassedB: b.grading?.casePassed ?? null,
|
|
313
321
|
caseScoreA: a.grading?.caseScore ?? null,
|
|
314
322
|
caseScoreB: b.grading?.caseScore ?? null,
|
|
323
|
+
caseScoreComparable:
|
|
324
|
+
gradingScoreBasis(a.grading) !== null &&
|
|
325
|
+
gradingScoreBasis(a.grading) === gradingScoreBasis(b.grading),
|
|
315
326
|
...grading,
|
|
316
327
|
},
|
|
317
328
|
artifactsDelta: computeFileDelta(a.fileTree, b.fileTree),
|
|
@@ -325,7 +336,8 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
|
|
|
325
336
|
export const hasRegression = (cmp: RunComparison): boolean =>
|
|
326
337
|
cmp.caseMismatch.length > 0 ||
|
|
327
338
|
cmp.grading.regressions > 0 ||
|
|
328
|
-
cmp.grading.deltas.some((delta) => delta.
|
|
339
|
+
cmp.grading.deltas.some((delta) => delta.after?.score === null && delta.after.passed !== null) ||
|
|
340
|
+
cmp.grading.deltas.some((delta) => delta.status === "removed" && delta.before?.passed !== null) ||
|
|
329
341
|
(cmp.grading.casePassedA === true && cmp.grading.casePassedB !== true);
|
|
330
342
|
|
|
331
343
|
const formatMismatchValue = (value: unknown): string => {
|
|
@@ -374,13 +386,17 @@ export const formatComparisonText = (cmp: RunComparison): string => {
|
|
|
374
386
|
lines.push("Grading:");
|
|
375
387
|
lines.push(
|
|
376
388
|
` passed: ${String(cmp.grading.casePassedA)} → ${String(cmp.grading.casePassedB)}; ` +
|
|
377
|
-
`score: ${cmp.grading.caseScoreA ?? "?"} → ${cmp.grading.caseScoreB ?? "?"}`,
|
|
389
|
+
`score: ${cmp.grading.caseScoreA ?? "?"} → ${cmp.grading.caseScoreB ?? "?"}${cmp.grading.caseScoreComparable ? "" : " (change unavailable: scoring basis differs or is unknown)"}`,
|
|
378
390
|
);
|
|
379
391
|
lines.push(` ${cmp.grading.improvements} improved, ${cmp.grading.regressions} regressed`);
|
|
380
392
|
for (const d of cmp.grading.deltas) {
|
|
381
393
|
if (d.status === "unchanged") continue;
|
|
382
|
-
const arrow = { added: "+", removed: "-", improved: "↑", regressed: "↓" }[
|
|
383
|
-
|
|
394
|
+
const arrow = { added: "+", removed: "-", improved: "↑", regressed: "↓", unavailable: "?" }[
|
|
395
|
+
d.status
|
|
396
|
+
];
|
|
397
|
+
lines.push(
|
|
398
|
+
` ${arrow} ${d.name}: ${d.before?.score ?? "unavailable"} → ${d.after?.score ?? "unavailable"}`,
|
|
399
|
+
);
|
|
384
400
|
}
|
|
385
401
|
lines.push("");
|
|
386
402
|
lines.push("Artifacts:");
|
|
@@ -23,7 +23,7 @@ export const experimentEffects = (
|
|
|
23
23
|
const metrics = new Set([
|
|
24
24
|
"quality.passRate",
|
|
25
25
|
experiment.evaluationKind === "grader" ? "harness.total" : "agent.total",
|
|
26
|
-
...(experiment.evaluationKind === "agent" ? ["interrupts.ask", "toolCalls"] : []),
|
|
26
|
+
...(experiment.evaluationKind === "agent" ? ["score", "interrupts.ask", "toolCalls"] : []),
|
|
27
27
|
...experiment.campaignSnapshot.objectives.constraints
|
|
28
28
|
.filter((constraint) => constraint.relativeTo)
|
|
29
29
|
.map((constraint) => constraint.metric),
|
|
@@ -46,7 +46,9 @@ export const experimentEffects = (
|
|
|
46
46
|
const confidence =
|
|
47
47
|
constraint?.confidence ??
|
|
48
48
|
ConfidenceConfigSchema.parse({
|
|
49
|
-
method:
|
|
49
|
+
method: ["quality.passRate", "score"].includes(metric)
|
|
50
|
+
? "hoeffding"
|
|
51
|
+
: "percentile-bootstrap",
|
|
50
52
|
});
|
|
51
53
|
return {
|
|
52
54
|
candidateId,
|
|
@@ -44,6 +44,7 @@ const gradingFailureMessage = (results: readonly SuiteCaseResult[]): string | un
|
|
|
44
44
|
}
|
|
45
45
|
const report = result.result?.grading;
|
|
46
46
|
const failedAssertion = report.assertions.find((assertion) => {
|
|
47
|
+
if (assertion.passed === null) return false;
|
|
47
48
|
if (assertion.errorMessage) return true;
|
|
48
49
|
const detail =
|
|
49
50
|
assertion.detail && typeof assertion.detail === "object"
|
|
@@ -299,10 +300,12 @@ const agentEvaluationFromSuite = (input: {
|
|
|
299
300
|
return {
|
|
300
301
|
[sample.canonicalCaseId]: {
|
|
301
302
|
identityHash,
|
|
303
|
+
scoreBasis: sample.scoreBasis ?? null,
|
|
302
304
|
...(input.clusterId ? { clusterId: input.clusterId } : {}),
|
|
303
305
|
metrics: Object.fromEntries(
|
|
304
306
|
Object.entries({
|
|
305
307
|
...sample.metrics,
|
|
308
|
+
...(sample.score !== undefined ? { score: sample.score } : {}),
|
|
306
309
|
"quality.passRate": sample.passed === true ? 1 : 0,
|
|
307
310
|
}).map(([name, value]) => [name, { mean: value, n: 1 }]),
|
|
308
311
|
),
|
|
@@ -322,7 +325,10 @@ const agentEvaluationFromSuite = (input: {
|
|
|
322
325
|
summary.cases.map((entry) => [entry.canonicalCaseId, entry.passRate]),
|
|
323
326
|
),
|
|
324
327
|
metrics: evaluationMetrics(summary, candidate),
|
|
325
|
-
metricDefinitions:
|
|
328
|
+
metricDefinitions: {
|
|
329
|
+
score: { unit: "score", better: "higher", label: "Quality score" },
|
|
330
|
+
...evaluationMetricDefinitions(summary),
|
|
331
|
+
},
|
|
326
332
|
samples: evaluationSamples(summary),
|
|
327
333
|
...(caseSamples ? { caseSamples } : {}),
|
|
328
334
|
metricCalls: summary.samples.length,
|
|
@@ -406,6 +412,12 @@ const aggregateAgentUnits = (
|
|
|
406
412
|
const caseSamples = clusterCaseSamples(units.map((unit) => unit.evaluation.caseSamples));
|
|
407
413
|
const grouped = groupedCaseMetricSamples(caseSamples, repetitions);
|
|
408
414
|
const samples = grouped ?? ungroupedSamples;
|
|
415
|
+
const completeScore =
|
|
416
|
+
caseSamples &&
|
|
417
|
+
Object.values(caseSamples).every(
|
|
418
|
+
(sample) => sample.metrics.score?.n === repetitions && Boolean(sample.scoreBasis),
|
|
419
|
+
);
|
|
420
|
+
if (!completeScore) delete samples.score;
|
|
409
421
|
return {
|
|
410
422
|
candidateId: candidate.id,
|
|
411
423
|
profileId,
|
|
@@ -417,7 +429,11 @@ const aggregateAgentUnits = (
|
|
|
417
429
|
),
|
|
418
430
|
metrics: Object.fromEntries(
|
|
419
431
|
[...metricValues]
|
|
420
|
-
.filter(([name]) =>
|
|
432
|
+
.filter(([name]) =>
|
|
433
|
+
name === "score"
|
|
434
|
+
? completeScore
|
|
435
|
+
: !grouped || name === "subject.bytes" || Object.hasOwn(grouped, name),
|
|
436
|
+
)
|
|
421
437
|
.map(([name, values]) => [
|
|
422
438
|
name,
|
|
423
439
|
name === "subject.bytes" ? candidate.sourceBytes : average(grouped?.[name] ?? values),
|
|
@@ -252,13 +252,65 @@ const normalizedConstraintViolation = (
|
|
|
252
252
|
return worst;
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
const hasScoreEvidence = (
|
|
256
|
+
experiment: OpenedExperimentRun["manifest"],
|
|
257
|
+
split: ExperimentSplit,
|
|
258
|
+
): boolean =>
|
|
259
|
+
[...new Set(experiment.evaluations.map((entry) => entry.candidateId))]
|
|
260
|
+
.flatMap((id) => candidateEvaluations(experiment, id))
|
|
261
|
+
.some(
|
|
262
|
+
(entry) =>
|
|
263
|
+
entry.split === split &&
|
|
264
|
+
(entry.metricDefinitions?.score !== undefined ||
|
|
265
|
+
entry.metrics.score !== undefined ||
|
|
266
|
+
Object.values(entry.caseSamples ?? {}).some(
|
|
267
|
+
(sample) => sample.metrics.score !== undefined,
|
|
268
|
+
)),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const completeScoreEvidence = (entry: ExperimentEvaluation): boolean =>
|
|
272
|
+
entry.metrics.score !== undefined &&
|
|
273
|
+
(!entry.caseSamples ||
|
|
274
|
+
Object.keys(entry.casePassRates).every((id) => {
|
|
275
|
+
const sample = entry.caseSamples?.[id];
|
|
276
|
+
return sample?.metrics.score?.n === entry.repetitions && Boolean(sample.scoreBasis);
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const comparableScoreBasis = (
|
|
280
|
+
entry: ExperimentEvaluation,
|
|
281
|
+
reference: ExperimentEvaluation | undefined,
|
|
282
|
+
): boolean => {
|
|
283
|
+
if (!reference || !completeScoreEvidence(reference)) return false;
|
|
284
|
+
if (!entry.caseSamples || !reference.caseSamples)
|
|
285
|
+
return !entry.caseSamples && !reference.caseSamples;
|
|
286
|
+
const ids = new Set([
|
|
287
|
+
...Object.keys(entry.casePassRates),
|
|
288
|
+
...Object.keys(reference.casePassRates),
|
|
289
|
+
]);
|
|
290
|
+
return [...ids].every((id) => {
|
|
291
|
+
const basis = entry.caseSamples?.[id]?.scoreBasis;
|
|
292
|
+
return Boolean(basis) && basis === reference.caseSamples?.[id]?.scoreBasis;
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
255
296
|
export const rankCandidates = <T extends ExperimentDecisionCandidate>(
|
|
256
297
|
campaign: ExperimentCampaign,
|
|
257
298
|
candidates: readonly T[],
|
|
258
299
|
experiment: OpenedExperimentRun["manifest"],
|
|
259
300
|
constraintSplits: readonly ExperimentSplit[] = ["train", "validation", "test"],
|
|
260
|
-
): T[] =>
|
|
261
|
-
|
|
301
|
+
): T[] => {
|
|
302
|
+
const validationEvaluations = (id: string): ExperimentEvaluation[] =>
|
|
303
|
+
candidateEvaluations(experiment, id).filter((entry) => entry.split === "validation");
|
|
304
|
+
const referenceId = ["baseline", ...candidates.map((candidate) => candidate.id).sort()].find(
|
|
305
|
+
(id) => {
|
|
306
|
+
const entries = validationEvaluations(id);
|
|
307
|
+
return campaign.profiles.every((profile) =>
|
|
308
|
+
entries.some((entry) => entry.profileId === profile && completeScoreEvidence(entry)),
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
const reference = referenceId ? validationEvaluations(referenceId) : [];
|
|
313
|
+
return [...candidates].sort((a, b) => {
|
|
262
314
|
const activeConstraints = campaign.objectives.constraints.filter((constraint) =>
|
|
263
315
|
constraintSplits.includes(constraint.split),
|
|
264
316
|
);
|
|
@@ -310,20 +362,40 @@ export const rankCandidates = <T extends ExperimentDecisionCandidate>(
|
|
|
310
362
|
}
|
|
311
363
|
}
|
|
312
364
|
const quality = (candidate: ExperimentDecisionCandidate): number => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
365
|
+
if (!hasScoreEvidence(experiment, "validation")) {
|
|
366
|
+
const values = metricValues(
|
|
367
|
+
candidate,
|
|
368
|
+
"quality.passRate",
|
|
369
|
+
undefined,
|
|
370
|
+
"validation",
|
|
371
|
+
experiment,
|
|
372
|
+
);
|
|
373
|
+
return values.length > 0 ? Math.min(...values) : -1;
|
|
374
|
+
}
|
|
375
|
+
const evaluations = candidateEvaluations(experiment, candidate.id);
|
|
376
|
+
const scores = campaign.profiles.map((profile) => {
|
|
377
|
+
const entry = evaluations.find(
|
|
378
|
+
(value) => value.profileId === profile && value.split === "validation",
|
|
379
|
+
);
|
|
380
|
+
if (
|
|
381
|
+
!entry ||
|
|
382
|
+
!completeScoreEvidence(entry) ||
|
|
383
|
+
!comparableScoreBasis(
|
|
384
|
+
entry,
|
|
385
|
+
reference.find((value) => value.profileId === profile),
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
return -1;
|
|
389
|
+
return entry.metrics.score ?? -1;
|
|
390
|
+
});
|
|
391
|
+
return scores.length > 0 ? Math.min(...scores) : -1;
|
|
321
392
|
};
|
|
322
393
|
const qualityDelta = quality(b) - quality(a);
|
|
323
394
|
if (qualityDelta !== 0) return qualityDelta;
|
|
324
395
|
if (a.sourceBytes !== b.sourceBytes) return a.sourceBytes - b.sourceBytes;
|
|
325
396
|
return a.id.localeCompare(b.id);
|
|
326
397
|
});
|
|
398
|
+
};
|
|
327
399
|
|
|
328
400
|
const candidateCaseVector = (
|
|
329
401
|
candidate: Candidate,
|
|
@@ -338,10 +410,15 @@ const candidateCaseVector = (
|
|
|
338
410
|
evaluations
|
|
339
411
|
.filter((entry) => entry.split === split)
|
|
340
412
|
.flatMap((entry) =>
|
|
341
|
-
Object.entries(entry.casePassRates).
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
413
|
+
Object.entries(entry.casePassRates).flatMap(([caseId, passRate]) => {
|
|
414
|
+
if (!hasScoreEvidence(experiment, split))
|
|
415
|
+
return [[`${entry.profileId}:${caseId}`, passRate]];
|
|
416
|
+
const sample = entry.caseSamples?.[caseId];
|
|
417
|
+
const score = sample?.metrics.score;
|
|
418
|
+
return score?.n === entry.repetitions && sample?.scoreBasis
|
|
419
|
+
? [[JSON.stringify([entry.profileId, caseId, sample.scoreBasis]), score.mean]]
|
|
420
|
+
: [];
|
|
421
|
+
}),
|
|
345
422
|
),
|
|
346
423
|
);
|
|
347
424
|
};
|
|
@@ -351,8 +428,9 @@ const dominates = (a: Record<string, number>, b: Record<string, number>): boolea
|
|
|
351
428
|
if (keys.size === 0) return false;
|
|
352
429
|
let strictlyBetter = false;
|
|
353
430
|
for (const key of keys) {
|
|
354
|
-
const av = a[key]
|
|
355
|
-
const bv = b[key]
|
|
431
|
+
const av = a[key];
|
|
432
|
+
const bv = b[key];
|
|
433
|
+
if (av === undefined || bv === undefined) return false;
|
|
356
434
|
if (av < bv) return false;
|
|
357
435
|
if (av > bv) strictlyBetter = true;
|
|
358
436
|
}
|
|
@@ -404,8 +482,13 @@ const frontierCoverage = (
|
|
|
404
482
|
const candidateVector = candidateCaseVector(candidate, experiment, "train");
|
|
405
483
|
const keys = new Set(vectors.flatMap((vector) => Object.keys(vector)));
|
|
406
484
|
return [...keys].filter((key) => {
|
|
407
|
-
const
|
|
408
|
-
|
|
485
|
+
const values = vectors.flatMap((vector) => (vector[key] === undefined ? [] : [vector[key]]));
|
|
486
|
+
const best = Math.max(...values);
|
|
487
|
+
return (
|
|
488
|
+
(frontier.length === 1 || values.length > 1) &&
|
|
489
|
+
candidateVector[key] !== undefined &&
|
|
490
|
+
candidateVector[key] === best
|
|
491
|
+
);
|
|
409
492
|
}).length;
|
|
410
493
|
};
|
|
411
494
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "../core/index.js";
|
|
16
16
|
import { validatePluginConfigAsync } from "./validate-plugin-config.js";
|
|
17
17
|
|
|
18
|
-
const FRAMEWORK_KEYS = new Set(["id", "type", "name", "weight", "threshold"]);
|
|
18
|
+
const FRAMEWORK_KEYS = new Set(["id", "type", "name", "weight", "threshold", "informational"]);
|
|
19
19
|
|
|
20
20
|
const shapeOf = (schema: z.ZodTypeAny): Record<string, unknown> | null => {
|
|
21
21
|
const shape = (schema as { shape?: unknown }).shape;
|
|
@@ -131,6 +131,50 @@ const assertionPassed = (result: AssertionResult, threshold: number): boolean =>
|
|
|
131
131
|
result.passed &&
|
|
132
132
|
result.score >= threshold;
|
|
133
133
|
|
|
134
|
+
export const gradingScoreBasis = (report: GradingReport | null): string | null =>
|
|
135
|
+
report?.caseScore === null || !report
|
|
136
|
+
? null
|
|
137
|
+
: valueHash(
|
|
138
|
+
report.assertions
|
|
139
|
+
.map((entry, index) => ({ entry, id: entry.id ?? `${entry.type}:${index + 1}` }))
|
|
140
|
+
.filter(({ entry }) => entry.errorMessage === undefined && entry.abstention === undefined)
|
|
141
|
+
.map(({ entry, id }) => ({ id, weight: entry.weight }))
|
|
142
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const aggregateGrading = (
|
|
146
|
+
entries: GradingAssertionEntry[],
|
|
147
|
+
skippedAssertions: NonNullable<GradingReport["skippedAssertions"]>,
|
|
148
|
+
gradingIncomplete: boolean,
|
|
149
|
+
): GradingReport => {
|
|
150
|
+
const requiredEntries = entries.filter((entry) => entry.passed !== null);
|
|
151
|
+
const incomplete =
|
|
152
|
+
gradingIncomplete || requiredEntries.some((entry) => entry.errorMessage !== undefined);
|
|
153
|
+
const gradingAbstained = requiredEntries.some((entry) => entry.abstention !== undefined);
|
|
154
|
+
const scoredEntries = entries.filter(
|
|
155
|
+
(entry) => entry.errorMessage === undefined && entry.abstention === undefined,
|
|
156
|
+
);
|
|
157
|
+
const weightSum = scoredEntries.reduce((sum, entry) => sum + entry.weight, 0);
|
|
158
|
+
const weightedScoreSum = scoredEntries.reduce(
|
|
159
|
+
(sum, entry) => sum + entry.weight * entry.score,
|
|
160
|
+
0,
|
|
161
|
+
);
|
|
162
|
+
const hasVerdict = !incomplete && !gradingAbstained && weightSum > 0;
|
|
163
|
+
return {
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
casePassed:
|
|
166
|
+
hasVerdict && requiredEntries.length > 0
|
|
167
|
+
? requiredEntries.every((entry) => entry.passed)
|
|
168
|
+
: null,
|
|
169
|
+
caseScore: hasVerdict ? weightedScoreSum / weightSum : null,
|
|
170
|
+
assertions: entries,
|
|
171
|
+
...(skippedAssertions.length > 0 ? { skippedAssertions } : {}),
|
|
172
|
+
...(incomplete
|
|
173
|
+
? { incomplete: { exitReason: "error" as const, phase: "grading" as const } }
|
|
174
|
+
: {}),
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
134
178
|
export const gradeRun = async (
|
|
135
179
|
run: Run,
|
|
136
180
|
registry: Registry,
|
|
@@ -157,11 +201,7 @@ export const gradeRun = async (
|
|
|
157
201
|
name: string;
|
|
158
202
|
reason: "judge-disabled";
|
|
159
203
|
}> = [];
|
|
160
|
-
let weightedScoreSum = 0;
|
|
161
|
-
let weightSum = 0;
|
|
162
|
-
let allPassed = true;
|
|
163
204
|
let gradingIncomplete = false;
|
|
164
|
-
let gradingAbstained = false;
|
|
165
205
|
|
|
166
206
|
for (const [index, a] of assertions.entries()) {
|
|
167
207
|
if (skipJudge && a.type === "rubric") {
|
|
@@ -232,7 +272,8 @@ export const gradeRun = async (
|
|
|
232
272
|
id,
|
|
233
273
|
type: a.type,
|
|
234
274
|
name,
|
|
235
|
-
passed,
|
|
275
|
+
passed: a.informational ? null : passed,
|
|
276
|
+
...(a.informational ? { informational: true } : {}),
|
|
236
277
|
score: result.score,
|
|
237
278
|
weight,
|
|
238
279
|
threshold,
|
|
@@ -243,29 +284,20 @@ export const gradeRun = async (
|
|
|
243
284
|
...(result.errorMessage !== undefined ? { errorMessage: result.errorMessage } : {}),
|
|
244
285
|
...(result.usage ? { usage: result.usage } : {}),
|
|
245
286
|
};
|
|
246
|
-
if (result.errorMessage !== undefined) {
|
|
247
|
-
gradingIncomplete = true;
|
|
248
|
-
} else if (result.abstention !== undefined) {
|
|
249
|
-
gradingAbstained = true;
|
|
250
|
-
} else {
|
|
251
|
-
if (!passed) allPassed = false;
|
|
252
|
-
weightedScoreSum += weight * result.score;
|
|
253
|
-
weightSum += weight;
|
|
254
|
-
}
|
|
255
287
|
} catch (err) {
|
|
256
288
|
const message = err instanceof Error ? err.message : String(err);
|
|
257
289
|
entry = {
|
|
258
290
|
id,
|
|
259
291
|
type: a.type,
|
|
260
292
|
name,
|
|
261
|
-
passed: false,
|
|
293
|
+
passed: a.informational ? null : false,
|
|
294
|
+
...(a.informational ? { informational: true } : {}),
|
|
262
295
|
score: 0,
|
|
263
296
|
weight,
|
|
264
297
|
threshold,
|
|
265
298
|
durationMs: Date.now() - startedAt,
|
|
266
299
|
errorMessage: message,
|
|
267
300
|
};
|
|
268
|
-
gradingIncomplete = true;
|
|
269
301
|
}
|
|
270
302
|
|
|
271
303
|
entries.push(entry);
|
|
@@ -274,15 +306,5 @@ export const gradeRun = async (
|
|
|
274
306
|
await modelAttempts?.complete(assertionAttemptIds);
|
|
275
307
|
}
|
|
276
308
|
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
schemaVersion: 1,
|
|
280
|
-
casePassed: hasVerdict ? allPassed : null,
|
|
281
|
-
caseScore: hasVerdict ? weightedScoreSum / weightSum : null,
|
|
282
|
-
assertions: entries,
|
|
283
|
-
...(skippedAssertions.length > 0 ? { skippedAssertions } : {}),
|
|
284
|
-
...(gradingIncomplete
|
|
285
|
-
? { incomplete: { exitReason: "error" as const, phase: "grading" as const } }
|
|
286
|
-
: {}),
|
|
287
|
-
};
|
|
309
|
+
return aggregateGrading(entries, skippedAssertions, gradingIncomplete);
|
|
288
310
|
};
|