@prismatic-io/lux 0.0.2-preview.3 → 0.0.2-preview.5
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/answerers/terminal/index.js +1 -1
- package/lib/answerers/terminal/index.js.map +1 -1
- package/lib/assertions/authoring.d.ts +2 -2
- package/lib/assertions/authoring.d.ts.map +1 -1
- package/lib/assertions/authoring.js.map +1 -1
- package/lib/assertions/core/helpers.d.ts +2 -7
- package/lib/assertions/core/helpers.d.ts.map +1 -1
- package/lib/assertions/core/helpers.js +2 -6
- package/lib/assertions/core/helpers.js.map +1 -1
- package/lib/assertions/core/index.d.ts.map +1 -1
- package/lib/assertions/core/index.js +2 -1
- package/lib/assertions/core/index.js.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +19 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.js +32 -0
- package/lib/assertions/core/resource-checks.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +12 -67
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +16 -3
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/cli/run-options.d.ts +1 -0
- package/lib/cli/run-options.d.ts.map +1 -1
- package/lib/cli/run-options.js.map +1 -1
- package/lib/cli/view.d.ts +13 -0
- package/lib/cli/view.d.ts.map +1 -0
- package/lib/cli/view.js +126 -0
- package/lib/cli/view.js.map +1 -0
- package/lib/core/action-events.d.ts +29 -0
- package/lib/core/action-events.d.ts.map +1 -0
- package/lib/core/action-events.js +51 -0
- package/lib/core/action-events.js.map +1 -0
- package/lib/core/artifact-evidence.d.ts +8 -0
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +36 -4
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/assertion.d.ts +12 -0
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +13 -0
- package/lib/core/assertion.js.map +1 -1
- package/lib/core/driver-metrics.d.ts +98 -0
- package/lib/core/driver-metrics.d.ts.map +1 -0
- package/lib/core/driver-metrics.js +36 -0
- package/lib/core/driver-metrics.js.map +1 -0
- package/lib/core/events.d.ts +2 -0
- package/lib/core/events.d.ts.map +1 -1
- package/lib/core/events.js +3 -2
- package/lib/core/events.js.map +1 -1
- package/lib/core/experiment.d.ts +19 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +2 -0
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +2 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/run.d.ts +51 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/run.js +6 -0
- package/lib/core/run.js.map +1 -1
- package/lib/core/wire.d.ts +1 -0
- package/lib/core/wire.d.ts.map +1 -1
- package/lib/core/wire.js +5 -1
- package/lib/core/wire.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/compare.d.ts +9 -1
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +8 -0
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +0 -2
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +19 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +17 -0
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +19 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +38 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/grader-experiment.d.ts +12 -0
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +2 -1
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +16 -4
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/index.d.ts +1 -1
- package/lib/orchestrator/index.d.ts.map +1 -1
- package/lib/orchestrator/index.js +1 -1
- package/lib/orchestrator/index.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts +6 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +15 -1
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts +3 -0
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +3 -0
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +6 -0
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +13 -3
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts +34 -0
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +68 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +22 -7
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/artifacts.d.ts +25 -0
- package/lib/previewer/artifacts.d.ts.map +1 -0
- package/lib/previewer/artifacts.js +180 -0
- package/lib/previewer/artifacts.js.map +1 -0
- package/lib/previewer/server.d.ts +104 -0
- package/lib/previewer/server.d.ts.map +1 -0
- package/lib/previewer/server.js +541 -0
- package/lib/previewer/server.js.map +1 -0
- package/package.json +2 -2
- package/skills/lux/SKILL.md +7 -3
- package/skills/lux/references/cli.md +25 -2
- package/skills/lux/references/eval-authoring.md +158 -4
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/terminal/index.ts +1 -1
- package/src/assertions/authoring.ts +2 -0
- package/src/assertions/core/helpers.ts +3 -6
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/cli/program.ts +14 -93
- package/src/cli/render/reporter.ts +18 -3
- package/src/cli/run-options.ts +1 -0
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/artifact-evidence.ts +47 -4
- package/src/core/assertion.ts +15 -0
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/events.ts +3 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/run.ts +6 -0
- package/src/core/wire.ts +5 -1
- package/src/index.ts +25 -5
- package/src/orchestrator/compare.ts +12 -0
- package/src/orchestrator/config.ts +0 -2
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/grading.ts +21 -2
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +25 -0
- package/src/orchestrator/orchestrator.ts +6 -0
- package/src/orchestrator/run-execution.ts +14 -1
- package/src/orchestrator/suite-summary.ts +36 -8
- package/src/previewer/artifacts.ts +221 -0
- package/src/previewer/server.ts +763 -0
- package/viewer/app.css +2022 -0
- package/viewer/app.js +601 -0
- package/viewer/artifact-view.js +48 -0
- package/viewer/charts.js +86 -0
- package/viewer/detail.js +1108 -0
- package/viewer/format.js +129 -0
- package/viewer/index.html +151 -0
- package/lib/answerers/scripted/index.d.ts +0 -8
- package/lib/answerers/scripted/index.d.ts.map +0 -1
- package/lib/answerers/scripted/index.js +0 -163
- package/lib/answerers/scripted/index.js.map +0 -1
- package/src/answerers/scripted/index.ts +0 -186
|
@@ -101,6 +101,23 @@ const evaluationMetrics = (
|
|
|
101
101
|
),
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
const evaluationMetricDefinitions = (summary: SuiteSummary) =>
|
|
105
|
+
Object.fromEntries(
|
|
106
|
+
Object.entries(summary.metrics).flatMap(([name, metric]) => {
|
|
107
|
+
if (!metric.unit && !metric.label && !metric.better) return [];
|
|
108
|
+
return [
|
|
109
|
+
[
|
|
110
|
+
name,
|
|
111
|
+
{
|
|
112
|
+
unit: metric.unit ?? "number",
|
|
113
|
+
better: metric.better ?? "neutral",
|
|
114
|
+
...(metric.label ? { label: metric.label } : {}),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
];
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
104
121
|
const summaryUsage = (summary: SuiteSummary) =>
|
|
105
122
|
summary.samples.reduce(
|
|
106
123
|
(usage, sample) =>
|
|
@@ -277,6 +294,7 @@ const agentEvaluationFromSuite = (input: {
|
|
|
277
294
|
summary.cases.map((entry) => [entry.canonicalCaseId, entry.passRate]),
|
|
278
295
|
),
|
|
279
296
|
metrics: evaluationMetrics(summary, candidate),
|
|
297
|
+
metricDefinitions: evaluationMetricDefinitions(summary),
|
|
280
298
|
samples: evaluationSamples(summary),
|
|
281
299
|
metricCalls: summary.samples.length,
|
|
282
300
|
judgeCalls: results.reduce(
|
|
@@ -352,6 +370,10 @@ const aggregateAgentUnits = (
|
|
|
352
370
|
.map(([, values]) => average(values)),
|
|
353
371
|
]),
|
|
354
372
|
);
|
|
373
|
+
const metricDefinitions = Object.assign(
|
|
374
|
+
{},
|
|
375
|
+
...units.map(({ evaluation }) => evaluation.metricDefinitions ?? {}),
|
|
376
|
+
);
|
|
355
377
|
return {
|
|
356
378
|
candidateId: candidate.id,
|
|
357
379
|
profileId,
|
|
@@ -367,6 +389,7 @@ const aggregateAgentUnits = (
|
|
|
367
389
|
name === "subject.bytes" ? candidate.sourceBytes : average(values),
|
|
368
390
|
]),
|
|
369
391
|
),
|
|
392
|
+
metricDefinitions,
|
|
370
393
|
samples,
|
|
371
394
|
metricCalls: units.reduce((total, unit) => total + (unit.evaluation.metricCalls ?? 0), 0),
|
|
372
395
|
judgeCalls: units.reduce((total, unit) => total + (unit.evaluation.judgeCalls ?? 0), 0),
|
|
@@ -76,6 +76,7 @@ export const graderConfigHash = (input: {
|
|
|
76
76
|
assertions: readonly AssertionEvaluatorIdentity[];
|
|
77
77
|
validatedAssertionsHash: string;
|
|
78
78
|
graderContext?: unknown;
|
|
79
|
+
skipJudge?: boolean;
|
|
79
80
|
}): string =>
|
|
80
81
|
valueHash({
|
|
81
82
|
luxVersion: input.luxVersion,
|
|
@@ -83,6 +84,7 @@ export const graderConfigHash = (input: {
|
|
|
83
84
|
assertions: input.assertions,
|
|
84
85
|
validatedAssertionsHash: input.validatedAssertionsHash,
|
|
85
86
|
graderContext: input.graderContext ?? null,
|
|
87
|
+
skipJudge: input.skipJudge ?? false,
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
export const validateAssertionPlan = async (
|
|
@@ -133,6 +135,7 @@ export const gradeRun = async (
|
|
|
133
135
|
complete: (attemptIds: readonly string[]) => void | Promise<void>;
|
|
134
136
|
},
|
|
135
137
|
validatedConfigs?: readonly unknown[],
|
|
138
|
+
skipJudge = false,
|
|
136
139
|
): Promise<GradingReport> => {
|
|
137
140
|
const guardedObserver = guardRunObserver(observer);
|
|
138
141
|
const assertions = run.case.assertions ?? [];
|
|
@@ -141,12 +144,27 @@ export const gradeRun = async (
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
const entries: GradingAssertionEntry[] = [];
|
|
147
|
+
const skippedAssertions: Array<{
|
|
148
|
+
id: string;
|
|
149
|
+
type: string;
|
|
150
|
+
name: string;
|
|
151
|
+
reason: "judge-disabled";
|
|
152
|
+
}> = [];
|
|
144
153
|
let weightedScoreSum = 0;
|
|
145
154
|
let weightSum = 0;
|
|
146
155
|
let allPassed = true;
|
|
147
156
|
let gradingIncomplete = false;
|
|
148
157
|
|
|
149
158
|
for (const [index, a] of assertions.entries()) {
|
|
159
|
+
if (skipJudge && a.type === "rubric") {
|
|
160
|
+
skippedAssertions.push({
|
|
161
|
+
id: a.id ?? `${a.type}:${index + 1}`,
|
|
162
|
+
type: a.type,
|
|
163
|
+
name: describeAssertion(a),
|
|
164
|
+
reason: "judge-disabled",
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
150
168
|
// Grading is the last cancellation point — an LLM judge can run for
|
|
151
169
|
// minutes per assertion. An aborted grade can't claim everything passed.
|
|
152
170
|
if (abortSignal?.aborted) {
|
|
@@ -247,12 +265,13 @@ export const gradeRun = async (
|
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
let caseScore: number | null = null;
|
|
250
|
-
if (!gradingIncomplete
|
|
268
|
+
if (!gradingIncomplete && weightSum > 0) caseScore = weightedScoreSum / weightSum;
|
|
251
269
|
return {
|
|
252
270
|
schemaVersion: 1,
|
|
253
|
-
casePassed: gradingIncomplete ? null : allPassed,
|
|
271
|
+
casePassed: gradingIncomplete || weightSum === 0 ? null : allPassed,
|
|
254
272
|
caseScore,
|
|
255
273
|
assertions: entries,
|
|
274
|
+
...(skippedAssertions.length > 0 ? { skippedAssertions } : {}),
|
|
256
275
|
...(gradingIncomplete
|
|
257
276
|
? { incomplete: { exitReason: "error" as const, phase: "grading" as const } }
|
|
258
277
|
: {}),
|
|
@@ -2,6 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { constants, lstat, open, readdir } from "node:fs/promises";
|
|
3
3
|
import { basename, join, relative, sep } from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
ActionProgressPayloadSchema,
|
|
6
|
+
type Artifact,
|
|
7
|
+
ArtifactSchema,
|
|
5
8
|
type GradingReport,
|
|
6
9
|
GradingReportSchema,
|
|
7
10
|
harnessUsage,
|
|
@@ -31,6 +34,10 @@ export type ListedRun = {
|
|
|
31
34
|
startedAt: string | null;
|
|
32
35
|
endedAt: string | null;
|
|
33
36
|
durationMs: number | null;
|
|
37
|
+
timeToFirstActionMs: number | null;
|
|
38
|
+
timeToFirstMutationMs: number | null;
|
|
39
|
+
timeToFirstDurableMutationMs: number | null;
|
|
40
|
+
driverMetrics: NonNullable<RunMetadata["driverMetrics"]>;
|
|
34
41
|
casePassed: boolean | null;
|
|
35
42
|
caseScore: number | null;
|
|
36
43
|
assertionPassed: number;
|
|
@@ -63,6 +70,10 @@ const summarizeRun = async (
|
|
|
63
70
|
startedAt,
|
|
64
71
|
endedAt,
|
|
65
72
|
durationMs,
|
|
73
|
+
timeToFirstActionMs: meta?.actionTiming?.timeToFirstActionMs ?? null,
|
|
74
|
+
timeToFirstMutationMs: meta?.actionTiming?.timeToFirstMutationMs ?? null,
|
|
75
|
+
timeToFirstDurableMutationMs: meta?.actionTiming?.timeToFirstDurableMutationMs ?? null,
|
|
76
|
+
driverMetrics: meta?.driverMetrics ?? {},
|
|
66
77
|
casePassed: grading?.casePassed ?? null,
|
|
67
78
|
caseScore: grading?.caseScore ?? null,
|
|
68
79
|
assertionPassed: passing,
|
|
@@ -156,6 +167,9 @@ export type RunDetail = {
|
|
|
156
167
|
artifacts: string[];
|
|
157
168
|
};
|
|
158
169
|
|
|
170
|
+
export const loadRunArtifacts = async (runDir: string): Promise<Artifact[]> =>
|
|
171
|
+
(await safeReadJson(join(runDir, "artifacts.json"), ArtifactSchema.array())) ?? [];
|
|
172
|
+
|
|
159
173
|
const readEventsJsonl = async (path: string): Promise<RunEvent[]> => {
|
|
160
174
|
if (!existsSync(path)) return [];
|
|
161
175
|
if ((await lstat(path)).isSymbolicLink()) {
|
|
@@ -291,6 +305,13 @@ const appendMetadataDetail = (lines: string[], detail: RunDetail): void => {
|
|
|
291
305
|
`phases: agent=${secs(phases.agent)} collect=${secs(phases.collect)} grading=${secs(phases.grading)}`,
|
|
292
306
|
);
|
|
293
307
|
}
|
|
308
|
+
const actions = detail.metadata.actionTiming;
|
|
309
|
+
if (actions) {
|
|
310
|
+
const value = (ms: number | undefined): string => (ms === undefined ? "—" : `${ms}ms`);
|
|
311
|
+
lines.push(
|
|
312
|
+
`first action: any=${value(actions.timeToFirstActionMs)} mutation=${value(actions.timeToFirstMutationMs)} durable-mutation=${value(actions.timeToFirstDurableMutationMs)}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
294
315
|
}
|
|
295
316
|
};
|
|
296
317
|
|
|
@@ -362,6 +383,10 @@ const formatEventLine = (event: RunEvent): string => {
|
|
|
362
383
|
}
|
|
363
384
|
return `responded(custom): ${JSON.stringify(event.payload).slice(0, 80)}`;
|
|
364
385
|
case "progress":
|
|
386
|
+
if (event.kind === "action") {
|
|
387
|
+
const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
|
|
388
|
+
return payload ? `action: ${payload.name} (${payload.category})` : "action: malformed";
|
|
389
|
+
}
|
|
365
390
|
if (event.kind === "tool-call") {
|
|
366
391
|
const payload = ToolCallPayloadSchema.safeParse(event.payload).data;
|
|
367
392
|
return payload ? `tool-call: ${payload.name}` : "tool-call: malformed";
|
|
@@ -128,6 +128,8 @@ export type RunRequest = {
|
|
|
128
128
|
/** Root that relative `fixtures` dir paths resolve against. */
|
|
129
129
|
fixturesRoot?: string | undefined;
|
|
130
130
|
answererOverride?: string | undefined;
|
|
131
|
+
/** Skip built-in rubric judge assertions without changing subject execution. */
|
|
132
|
+
skipJudge?: boolean | undefined;
|
|
131
133
|
startedAt?: Date | undefined;
|
|
132
134
|
/** The `runSuite` invocation this case attempt belongs to. */
|
|
133
135
|
suiteId?: string | undefined;
|
|
@@ -568,6 +570,7 @@ export class Orchestrator {
|
|
|
568
570
|
assertions: assertionImplementations,
|
|
569
571
|
validatedAssertionsHash,
|
|
570
572
|
graderContext: this.harness,
|
|
573
|
+
skipJudge: request.skipJudge ?? false,
|
|
571
574
|
});
|
|
572
575
|
return await this.runWithPlugins(
|
|
573
576
|
{
|
|
@@ -593,6 +596,7 @@ export class Orchestrator {
|
|
|
593
596
|
assertions: assertionImplementations,
|
|
594
597
|
lifecycleFixtures,
|
|
595
598
|
validatedAssertionsHash,
|
|
599
|
+
skipJudge: request.skipJudge ?? false,
|
|
596
600
|
}),
|
|
597
601
|
fixtureValues: valuesForFixtures(lifecycleFixtures),
|
|
598
602
|
driver,
|
|
@@ -617,6 +621,7 @@ export class Orchestrator {
|
|
|
617
621
|
runsRoot: string;
|
|
618
622
|
fixturesRoot?: string | undefined;
|
|
619
623
|
answererOverride?: string | undefined;
|
|
624
|
+
skipJudge?: boolean | undefined;
|
|
620
625
|
observer?: SuiteObserver | undefined;
|
|
621
626
|
/** Aborts the cases in flight and stops the pool pulling new ones. */
|
|
622
627
|
abortSignal?: AbortSignal | undefined;
|
|
@@ -788,6 +793,7 @@ export class Orchestrator {
|
|
|
788
793
|
runsRoot: opts.runsRoot,
|
|
789
794
|
fixturesRoot: opts.fixturesRoot,
|
|
790
795
|
answererOverride: opts.answererOverride,
|
|
796
|
+
skipJudge: opts.skipJudge,
|
|
791
797
|
observer: handle ?? observer,
|
|
792
798
|
abortSignal: opts.abortSignal,
|
|
793
799
|
suiteId: suite.manifest.suiteId,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AnswererModelCallAccountingSchema,
|
|
11
11
|
type AnswerHistoryEntry,
|
|
12
12
|
type Artifact,
|
|
13
|
+
actionTiming,
|
|
13
14
|
type EvalCase,
|
|
14
15
|
emptyUsage,
|
|
15
16
|
type FixturesRef,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
guardRunObserver,
|
|
19
20
|
inspectRunArtifact,
|
|
20
21
|
type LifecycleStage,
|
|
22
|
+
normalizeDriverMetrics,
|
|
21
23
|
type OperationalFailure,
|
|
22
24
|
type PhaseDurations,
|
|
23
25
|
type ReadyState,
|
|
@@ -183,6 +185,11 @@ const modelAccountingFromSummary = (
|
|
|
183
185
|
): { modelCalls: number; unpricedModelCalls?: number | undefined } | null =>
|
|
184
186
|
SummaryModelAccountingSchema.safeParse(summary).data ?? null;
|
|
185
187
|
|
|
188
|
+
const metricsFromSummary = (summary: unknown) => {
|
|
189
|
+
if (!summary || typeof summary !== "object") return {};
|
|
190
|
+
return normalizeDriverMetrics((summary as { metrics?: unknown }).metrics);
|
|
191
|
+
};
|
|
192
|
+
|
|
186
193
|
const artifactMeasurements = async (
|
|
187
194
|
artifacts: readonly Artifact[],
|
|
188
195
|
runDir: string,
|
|
@@ -390,6 +397,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
390
397
|
metadata.exitReason = "error";
|
|
391
398
|
metadata.operationalFailure = { phase: operationalPhase, reason };
|
|
392
399
|
metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
|
|
400
|
+
metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
393
401
|
await this.runDir.writeJson("run.json", metadata);
|
|
394
402
|
await this.args.request.recordPreGradingAccounting?.(metadata);
|
|
395
403
|
await this.args.request.recordModelAttemptsComplete?.(this.subjectAttemptIds);
|
|
@@ -407,6 +415,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
407
415
|
this.lifecycle("collect-end");
|
|
408
416
|
await this.runDir.events.drain();
|
|
409
417
|
const completed = await this.completedAgentRun();
|
|
418
|
+
completed.metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
410
419
|
this.metadata = completed.metadata;
|
|
411
420
|
this.usage = completed.usage;
|
|
412
421
|
this.run = completed.run;
|
|
@@ -430,6 +439,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
430
439
|
grading.assertions.flatMap((assertion) => (assertion.usage ? [assertion.usage] : [])),
|
|
431
440
|
);
|
|
432
441
|
metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
|
|
442
|
+
metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
433
443
|
await this.runDir.events.drain();
|
|
434
444
|
await this.runDir.writeJson("run.json", metadata);
|
|
435
445
|
await this.runDir.writeJson("artifacts.json", this.collection.artifacts);
|
|
@@ -560,9 +570,10 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
560
570
|
const outcome = this.require(this.loopOutcome, "driver outcome");
|
|
561
571
|
const agentEndedAt = new Date();
|
|
562
572
|
const exitReason = agentExitReason(outcome, this.collection.error);
|
|
563
|
-
const summary = outcome.kind === "done" ? outcome.done.summary :
|
|
573
|
+
const summary = outcome.kind === "done" ? outcome.done.summary : outcome.error.summary;
|
|
564
574
|
const driverCost = costFromSummary(summary);
|
|
565
575
|
const reportedModelAccounting = modelAccountingFromSummary(summary);
|
|
576
|
+
const driverMetrics = metricsFromSummary(summary);
|
|
566
577
|
const answererAccounting = readAnswererModelAccounting(this.args.answerer);
|
|
567
578
|
const agentCalls = this.ready ? (reportedModelAccounting?.modelCalls ?? 1) : 0;
|
|
568
579
|
const operationalFailure = loopFailure(outcome, this.collection.error);
|
|
@@ -597,6 +608,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
597
608
|
...(operationalFailure ? { operationalFailure } : {}),
|
|
598
609
|
provenance: this.runProvenance(),
|
|
599
610
|
...(driverCost !== undefined ? { cost: { usd: driverCost } } : {}),
|
|
611
|
+
...(Object.keys(driverMetrics).length > 0 ? { driverMetrics } : {}),
|
|
600
612
|
};
|
|
601
613
|
return {
|
|
602
614
|
metadata,
|
|
@@ -657,6 +669,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
657
669
|
request.recordGradingAssertion,
|
|
658
670
|
modelAttempts,
|
|
659
671
|
this.args.validatedAssertionConfigs,
|
|
672
|
+
request.skipJudge ?? false,
|
|
660
673
|
);
|
|
661
674
|
}
|
|
662
675
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type DriverMetric,
|
|
6
|
+
DriverMetricUnitSchema,
|
|
7
|
+
harnessUsage,
|
|
8
|
+
isUsageEmpty,
|
|
9
|
+
type RunMetadata,
|
|
10
|
+
} from "../core/index.js";
|
|
5
11
|
import { runComparisonIdentity } from "./comparison-identity.js";
|
|
6
12
|
import { safeReadJson } from "./fs-read.js";
|
|
7
13
|
import type { SuiteCaseResult } from "./orchestrator.js";
|
|
@@ -16,6 +22,9 @@ export const MetricSummarySchema = z.object({
|
|
|
16
22
|
mean: z.number(),
|
|
17
23
|
median: z.number(),
|
|
18
24
|
p90: z.number(),
|
|
25
|
+
unit: z.exactOptional(DriverMetricUnitSchema),
|
|
26
|
+
label: z.exactOptional(z.string()),
|
|
27
|
+
better: z.exactOptional(z.enum(["lower", "higher", "neutral"])),
|
|
19
28
|
});
|
|
20
29
|
export type MetricSummary = z.infer<typeof MetricSummarySchema>;
|
|
21
30
|
|
|
@@ -59,13 +68,22 @@ export const SuiteSummarySchema = z.object({
|
|
|
59
68
|
});
|
|
60
69
|
export type SuiteSummary = z.infer<typeof SuiteSummarySchema>;
|
|
61
70
|
|
|
62
|
-
type MetricSamples = Map<
|
|
71
|
+
type MetricSamples = Map<
|
|
72
|
+
string,
|
|
73
|
+
{ values: number[]; descriptor?: Omit<DriverMetric, "value"> | undefined }
|
|
74
|
+
>;
|
|
63
75
|
|
|
64
|
-
const add = (
|
|
76
|
+
const add = (
|
|
77
|
+
samples: MetricSamples,
|
|
78
|
+
name: string,
|
|
79
|
+
value: number | undefined,
|
|
80
|
+
descriptor?: Omit<DriverMetric, "value">,
|
|
81
|
+
): void => {
|
|
65
82
|
if (value === undefined || !Number.isFinite(value)) return;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
const sample = samples.get(name) ?? { values: [], descriptor };
|
|
84
|
+
if (!sample.descriptor && descriptor) sample.descriptor = descriptor;
|
|
85
|
+
sample.values.push(value);
|
|
86
|
+
samples.set(name, sample);
|
|
69
87
|
};
|
|
70
88
|
|
|
71
89
|
const cacheHitRate = (usage: {
|
|
@@ -137,6 +155,13 @@ const collectMetadata = (samples: MetricSamples, metadata: RunMetadata): void =>
|
|
|
137
155
|
add(samples, "phase.agentMs", metadata.phaseDurationMs?.agent);
|
|
138
156
|
add(samples, "phase.collectMs", metadata.phaseDurationMs?.collect);
|
|
139
157
|
add(samples, "phase.gradingMs", metadata.phaseDurationMs?.grading);
|
|
158
|
+
add(samples, "timeToFirstActionMs", metadata.actionTiming?.timeToFirstActionMs);
|
|
159
|
+
add(samples, "timeToFirstMutationMs", metadata.actionTiming?.timeToFirstMutationMs);
|
|
160
|
+
add(samples, "timeToFirstDurableMutationMs", metadata.actionTiming?.timeToFirstDurableMutationMs);
|
|
161
|
+
for (const [name, metric] of Object.entries(metadata.driverMetrics ?? {})) {
|
|
162
|
+
const { value, ...descriptor } = metric;
|
|
163
|
+
add(samples, `driver.${name}`, value, descriptor);
|
|
164
|
+
}
|
|
140
165
|
};
|
|
141
166
|
|
|
142
167
|
const percentile = (sorted: readonly number[], fraction: number): number => {
|
|
@@ -165,7 +190,10 @@ const summarizeMetrics = (samples: MetricSamples): Record<string, MetricSummary>
|
|
|
165
190
|
Object.fromEntries(
|
|
166
191
|
[...samples.entries()]
|
|
167
192
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
168
|
-
.map(([name, values]) => [
|
|
193
|
+
.map(([name, { values, descriptor }]) => [
|
|
194
|
+
name,
|
|
195
|
+
MetricSummarySchema.parse({ ...summarizeNumbers(values), ...descriptor }),
|
|
196
|
+
]),
|
|
169
197
|
);
|
|
170
198
|
|
|
171
199
|
const summarizeOutcomes = (
|
|
@@ -258,7 +286,7 @@ export const buildSuiteSummary = (
|
|
|
258
286
|
? { score: result.grading.caseScore }
|
|
259
287
|
: {}),
|
|
260
288
|
metrics: Object.fromEntries(
|
|
261
|
-
[...samples.entries()].map(([name,
|
|
289
|
+
[...samples.entries()].map(([name, sample]) => [name, sample.values[0] ?? 0]),
|
|
262
290
|
),
|
|
263
291
|
};
|
|
264
292
|
}),
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Artifact } from "../core/index.js";
|
|
2
|
+
import { inspectRunArtifact, readRunArtifactBytes } from "../core/index.js";
|
|
3
|
+
|
|
4
|
+
const MAX_PREVIEW_BYTES = 512_000;
|
|
5
|
+
const MAX_DIFF_LINES = 2_000;
|
|
6
|
+
const MAX_DIFF_CELLS = 1_000_000;
|
|
7
|
+
|
|
8
|
+
export type ArtifactPreview = {
|
|
9
|
+
path: string;
|
|
10
|
+
exists: boolean;
|
|
11
|
+
viewable: boolean;
|
|
12
|
+
size: number | null;
|
|
13
|
+
contentType: string | null;
|
|
14
|
+
text: string | null;
|
|
15
|
+
reason: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ArtifactDiffOperation = {
|
|
19
|
+
kind: "same" | "added" | "removed";
|
|
20
|
+
lines: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ArtifactComparison = {
|
|
24
|
+
path: string;
|
|
25
|
+
before: ArtifactPreview;
|
|
26
|
+
after: ArtifactPreview;
|
|
27
|
+
operations: ArtifactDiffOperation[] | null;
|
|
28
|
+
reason: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const evidenceArtifacts = (
|
|
32
|
+
indexed: readonly Artifact[],
|
|
33
|
+
knownPaths: readonly string[],
|
|
34
|
+
): Artifact[] => {
|
|
35
|
+
const out = [...indexed];
|
|
36
|
+
const indexedPaths = new Set(indexed.map((artifact) => artifact.path));
|
|
37
|
+
for (const path of knownPaths) {
|
|
38
|
+
if (!indexedPaths.has(path)) out.push({ path });
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const loadArtifactPreview = async (
|
|
44
|
+
runDir: string,
|
|
45
|
+
indexed: readonly Artifact[],
|
|
46
|
+
knownPaths: readonly string[],
|
|
47
|
+
path: string,
|
|
48
|
+
): Promise<ArtifactPreview> => {
|
|
49
|
+
if (!knownPaths.includes(path) && !indexed.some((artifact) => artifact.path === path)) {
|
|
50
|
+
return {
|
|
51
|
+
path,
|
|
52
|
+
exists: false,
|
|
53
|
+
viewable: false,
|
|
54
|
+
size: null,
|
|
55
|
+
contentType: null,
|
|
56
|
+
text: null,
|
|
57
|
+
reason: "artifact was not captured by this run",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const artifacts = evidenceArtifacts(indexed, knownPaths);
|
|
61
|
+
const run = { runDir, artifacts };
|
|
62
|
+
const inspected = await inspectRunArtifact(run, path);
|
|
63
|
+
const contentType = inspected.artifact?.contentType ?? null;
|
|
64
|
+
if (!inspected.stats?.isFile()) {
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
exists: false,
|
|
68
|
+
viewable: false,
|
|
69
|
+
size: inspected.stats?.size ?? null,
|
|
70
|
+
contentType,
|
|
71
|
+
text: null,
|
|
72
|
+
reason: inspected.issue ?? "artifact is not a regular file",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (inspected.stats.size > MAX_PREVIEW_BYTES) {
|
|
76
|
+
return {
|
|
77
|
+
path,
|
|
78
|
+
exists: true,
|
|
79
|
+
viewable: false,
|
|
80
|
+
size: inspected.stats.size,
|
|
81
|
+
contentType,
|
|
82
|
+
text: null,
|
|
83
|
+
reason: `artifact exceeds the ${MAX_PREVIEW_BYTES}-byte preview limit`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const bytes = await readRunArtifactBytes(run, path, { maxBytes: MAX_PREVIEW_BYTES });
|
|
87
|
+
let text: string;
|
|
88
|
+
try {
|
|
89
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
90
|
+
} catch {
|
|
91
|
+
return {
|
|
92
|
+
path,
|
|
93
|
+
exists: true,
|
|
94
|
+
viewable: false,
|
|
95
|
+
size: bytes.length,
|
|
96
|
+
contentType,
|
|
97
|
+
text: null,
|
|
98
|
+
reason: "artifact is not valid UTF-8 text",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (text.includes("\0")) {
|
|
102
|
+
return {
|
|
103
|
+
path,
|
|
104
|
+
exists: true,
|
|
105
|
+
viewable: false,
|
|
106
|
+
size: bytes.length,
|
|
107
|
+
contentType,
|
|
108
|
+
text: null,
|
|
109
|
+
reason: "artifact appears to be binary",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
path,
|
|
114
|
+
exists: true,
|
|
115
|
+
viewable: true,
|
|
116
|
+
size: bytes.length,
|
|
117
|
+
contentType,
|
|
118
|
+
text,
|
|
119
|
+
reason: null,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const appendOperation = (
|
|
124
|
+
operations: ArtifactDiffOperation[],
|
|
125
|
+
kind: ArtifactDiffOperation["kind"],
|
|
126
|
+
line: string,
|
|
127
|
+
): void => {
|
|
128
|
+
const current = operations.at(-1);
|
|
129
|
+
if (current?.kind === kind) current.lines.push(line);
|
|
130
|
+
else operations.push({ kind, lines: [line] });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const diffLines = (beforeText: string, afterText: string): ArtifactDiffOperation[] | null => {
|
|
134
|
+
const before = beforeText.split("\n");
|
|
135
|
+
const after = afterText.split("\n");
|
|
136
|
+
if (
|
|
137
|
+
before.length > MAX_DIFF_LINES ||
|
|
138
|
+
after.length > MAX_DIFF_LINES ||
|
|
139
|
+
before.length * after.length > MAX_DIFF_CELLS
|
|
140
|
+
) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const width = after.length + 1;
|
|
144
|
+
const table = new Uint32Array((before.length + 1) * width);
|
|
145
|
+
for (let left = before.length - 1; left >= 0; left--) {
|
|
146
|
+
for (let right = after.length - 1; right >= 0; right--) {
|
|
147
|
+
const cell = left * width + right;
|
|
148
|
+
table[cell] =
|
|
149
|
+
before[left] === after[right]
|
|
150
|
+
? (table[(left + 1) * width + right + 1] ?? 0) + 1
|
|
151
|
+
: Math.max(table[(left + 1) * width + right] ?? 0, table[left * width + right + 1] ?? 0);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const operations: ArtifactDiffOperation[] = [];
|
|
155
|
+
let left = 0;
|
|
156
|
+
let right = 0;
|
|
157
|
+
while (left < before.length && right < after.length) {
|
|
158
|
+
if (before[left] === after[right]) {
|
|
159
|
+
appendOperation(operations, "same", before[left] ?? "");
|
|
160
|
+
left++;
|
|
161
|
+
right++;
|
|
162
|
+
} else if ((table[(left + 1) * width + right] ?? 0) >= (table[left * width + right + 1] ?? 0)) {
|
|
163
|
+
appendOperation(operations, "removed", before[left] ?? "");
|
|
164
|
+
left++;
|
|
165
|
+
} else {
|
|
166
|
+
appendOperation(operations, "added", after[right] ?? "");
|
|
167
|
+
right++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
while (left < before.length) {
|
|
171
|
+
appendOperation(operations, "removed", before[left] ?? "");
|
|
172
|
+
left++;
|
|
173
|
+
}
|
|
174
|
+
while (right < after.length) {
|
|
175
|
+
appendOperation(operations, "added", after[right] ?? "");
|
|
176
|
+
right++;
|
|
177
|
+
}
|
|
178
|
+
return operations;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const compareArtifactPreviews = (
|
|
182
|
+
path: string,
|
|
183
|
+
before: ArtifactPreview,
|
|
184
|
+
after: ArtifactPreview,
|
|
185
|
+
): ArtifactComparison => {
|
|
186
|
+
if (before.viewable && before.text !== null && !after.exists) {
|
|
187
|
+
return {
|
|
188
|
+
path,
|
|
189
|
+
before,
|
|
190
|
+
after,
|
|
191
|
+
operations: [{ kind: "removed", lines: before.text.split("\n") }],
|
|
192
|
+
reason: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (!before.exists && after.viewable && after.text !== null) {
|
|
196
|
+
return {
|
|
197
|
+
path,
|
|
198
|
+
before,
|
|
199
|
+
after,
|
|
200
|
+
operations: [{ kind: "added", lines: after.text.split("\n") }],
|
|
201
|
+
reason: null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!before.viewable || !after.viewable || before.text === null || after.text === null) {
|
|
205
|
+
return {
|
|
206
|
+
path,
|
|
207
|
+
before,
|
|
208
|
+
after,
|
|
209
|
+
operations: null,
|
|
210
|
+
reason: before.reason ?? after.reason ?? "artifact text is unavailable",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const operations = diffLines(before.text, after.text);
|
|
214
|
+
return {
|
|
215
|
+
path,
|
|
216
|
+
before,
|
|
217
|
+
after,
|
|
218
|
+
operations,
|
|
219
|
+
reason: operations ? null : "artifact is too line-dense for an inline diff",
|
|
220
|
+
};
|
|
221
|
+
};
|