@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.
Files changed (169) hide show
  1. package/lib/answerers/terminal/index.js +1 -1
  2. package/lib/answerers/terminal/index.js.map +1 -1
  3. package/lib/assertions/authoring.d.ts +2 -2
  4. package/lib/assertions/authoring.d.ts.map +1 -1
  5. package/lib/assertions/authoring.js.map +1 -1
  6. package/lib/assertions/core/helpers.d.ts +2 -7
  7. package/lib/assertions/core/helpers.d.ts.map +1 -1
  8. package/lib/assertions/core/helpers.js +2 -6
  9. package/lib/assertions/core/helpers.js.map +1 -1
  10. package/lib/assertions/core/index.d.ts.map +1 -1
  11. package/lib/assertions/core/index.js +2 -1
  12. package/lib/assertions/core/index.js.map +1 -1
  13. package/lib/assertions/core/resource-checks.d.ts +19 -0
  14. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  15. package/lib/assertions/core/resource-checks.js +32 -0
  16. package/lib/assertions/core/resource-checks.js.map +1 -1
  17. package/lib/cli/program.d.ts.map +1 -1
  18. package/lib/cli/program.js +12 -67
  19. package/lib/cli/program.js.map +1 -1
  20. package/lib/cli/render/reporter.d.ts.map +1 -1
  21. package/lib/cli/render/reporter.js +16 -3
  22. package/lib/cli/render/reporter.js.map +1 -1
  23. package/lib/cli/run-options.d.ts +1 -0
  24. package/lib/cli/run-options.d.ts.map +1 -1
  25. package/lib/cli/run-options.js.map +1 -1
  26. package/lib/cli/view.d.ts +13 -0
  27. package/lib/cli/view.d.ts.map +1 -0
  28. package/lib/cli/view.js +126 -0
  29. package/lib/cli/view.js.map +1 -0
  30. package/lib/core/action-events.d.ts +29 -0
  31. package/lib/core/action-events.d.ts.map +1 -0
  32. package/lib/core/action-events.js +51 -0
  33. package/lib/core/action-events.js.map +1 -0
  34. package/lib/core/artifact-evidence.d.ts +8 -0
  35. package/lib/core/artifact-evidence.d.ts.map +1 -1
  36. package/lib/core/artifact-evidence.js +36 -4
  37. package/lib/core/artifact-evidence.js.map +1 -1
  38. package/lib/core/assertion.d.ts +12 -0
  39. package/lib/core/assertion.d.ts.map +1 -1
  40. package/lib/core/assertion.js +13 -0
  41. package/lib/core/assertion.js.map +1 -1
  42. package/lib/core/driver-metrics.d.ts +98 -0
  43. package/lib/core/driver-metrics.d.ts.map +1 -0
  44. package/lib/core/driver-metrics.js +36 -0
  45. package/lib/core/driver-metrics.js.map +1 -0
  46. package/lib/core/events.d.ts +2 -0
  47. package/lib/core/events.d.ts.map +1 -1
  48. package/lib/core/events.js +3 -2
  49. package/lib/core/events.js.map +1 -1
  50. package/lib/core/experiment.d.ts +19 -0
  51. package/lib/core/experiment.d.ts.map +1 -1
  52. package/lib/core/experiment.js +2 -0
  53. package/lib/core/experiment.js.map +1 -1
  54. package/lib/core/index.d.ts +2 -0
  55. package/lib/core/index.d.ts.map +1 -1
  56. package/lib/core/index.js +2 -0
  57. package/lib/core/index.js.map +1 -1
  58. package/lib/core/run.d.ts +51 -0
  59. package/lib/core/run.d.ts.map +1 -1
  60. package/lib/core/run.js +6 -0
  61. package/lib/core/run.js.map +1 -1
  62. package/lib/core/wire.d.ts +1 -0
  63. package/lib/core/wire.d.ts.map +1 -1
  64. package/lib/core/wire.js +5 -1
  65. package/lib/core/wire.js.map +1 -1
  66. package/lib/index.d.ts +3 -2
  67. package/lib/index.d.ts.map +1 -1
  68. package/lib/index.js +3 -2
  69. package/lib/index.js.map +1 -1
  70. package/lib/orchestrator/compare.d.ts +9 -1
  71. package/lib/orchestrator/compare.d.ts.map +1 -1
  72. package/lib/orchestrator/compare.js +8 -0
  73. package/lib/orchestrator/compare.js.map +1 -1
  74. package/lib/orchestrator/config.d.ts.map +1 -1
  75. package/lib/orchestrator/config.js +0 -2
  76. package/lib/orchestrator/config.js.map +1 -1
  77. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  78. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  79. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  80. package/lib/orchestrator/experiment-evaluation.js +17 -0
  81. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  82. package/lib/orchestrator/experiment-report.d.ts +19 -0
  83. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  84. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  85. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  86. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  87. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  88. package/lib/orchestrator/grading.d.ts +2 -1
  89. package/lib/orchestrator/grading.d.ts.map +1 -1
  90. package/lib/orchestrator/grading.js +16 -4
  91. package/lib/orchestrator/grading.js.map +1 -1
  92. package/lib/orchestrator/index.d.ts +1 -1
  93. package/lib/orchestrator/index.d.ts.map +1 -1
  94. package/lib/orchestrator/index.js +1 -1
  95. package/lib/orchestrator/index.js.map +1 -1
  96. package/lib/orchestrator/list-runs.d.ts +6 -1
  97. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  98. package/lib/orchestrator/list-runs.js +15 -1
  99. package/lib/orchestrator/list-runs.js.map +1 -1
  100. package/lib/orchestrator/orchestrator.d.ts +3 -0
  101. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  102. package/lib/orchestrator/orchestrator.js +3 -0
  103. package/lib/orchestrator/orchestrator.js.map +1 -1
  104. package/lib/orchestrator/regrade.d.ts +6 -0
  105. package/lib/orchestrator/regrade.d.ts.map +1 -1
  106. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  107. package/lib/orchestrator/run-execution.js +13 -3
  108. package/lib/orchestrator/run-execution.js.map +1 -1
  109. package/lib/orchestrator/suite-compare.d.ts +34 -0
  110. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  111. package/lib/orchestrator/suite-summary.d.ts +68 -0
  112. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  113. package/lib/orchestrator/suite-summary.js +22 -7
  114. package/lib/orchestrator/suite-summary.js.map +1 -1
  115. package/lib/previewer/artifacts.d.ts +25 -0
  116. package/lib/previewer/artifacts.d.ts.map +1 -0
  117. package/lib/previewer/artifacts.js +180 -0
  118. package/lib/previewer/artifacts.js.map +1 -0
  119. package/lib/previewer/server.d.ts +104 -0
  120. package/lib/previewer/server.d.ts.map +1 -0
  121. package/lib/previewer/server.js +541 -0
  122. package/lib/previewer/server.js.map +1 -0
  123. package/package.json +2 -2
  124. package/skills/lux/SKILL.md +7 -3
  125. package/skills/lux/references/cli.md +25 -2
  126. package/skills/lux/references/eval-authoring.md +158 -4
  127. package/skills/lux-answerer/SKILL.md +1 -1
  128. package/src/answerers/terminal/index.ts +1 -1
  129. package/src/assertions/authoring.ts +2 -0
  130. package/src/assertions/core/helpers.ts +3 -6
  131. package/src/assertions/core/index.ts +2 -0
  132. package/src/assertions/core/resource-checks.ts +35 -0
  133. package/src/cli/program.ts +14 -93
  134. package/src/cli/render/reporter.ts +18 -3
  135. package/src/cli/run-options.ts +1 -0
  136. package/src/cli/view.ts +168 -0
  137. package/src/core/action-events.ts +59 -0
  138. package/src/core/artifact-evidence.ts +47 -4
  139. package/src/core/assertion.ts +15 -0
  140. package/src/core/driver-metrics.ts +51 -0
  141. package/src/core/events.ts +3 -2
  142. package/src/core/experiment.ts +2 -0
  143. package/src/core/index.ts +2 -0
  144. package/src/core/run.ts +6 -0
  145. package/src/core/wire.ts +5 -1
  146. package/src/index.ts +25 -5
  147. package/src/orchestrator/compare.ts +12 -0
  148. package/src/orchestrator/config.ts +0 -2
  149. package/src/orchestrator/experiment-evaluation.ts +23 -0
  150. package/src/orchestrator/grading.ts +21 -2
  151. package/src/orchestrator/index.ts +1 -0
  152. package/src/orchestrator/list-runs.ts +25 -0
  153. package/src/orchestrator/orchestrator.ts +6 -0
  154. package/src/orchestrator/run-execution.ts +14 -1
  155. package/src/orchestrator/suite-summary.ts +36 -8
  156. package/src/previewer/artifacts.ts +221 -0
  157. package/src/previewer/server.ts +763 -0
  158. package/viewer/app.css +2022 -0
  159. package/viewer/app.js +601 -0
  160. package/viewer/artifact-view.js +48 -0
  161. package/viewer/charts.js +86 -0
  162. package/viewer/detail.js +1108 -0
  163. package/viewer/format.js +129 -0
  164. package/viewer/index.html +151 -0
  165. package/lib/answerers/scripted/index.d.ts +0 -8
  166. package/lib/answerers/scripted/index.d.ts.map +0 -1
  167. package/lib/answerers/scripted/index.js +0 -163
  168. package/lib/answerers/scripted/index.js.map +0 -1
  169. 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) caseScore = weightSum > 0 ? weightedScoreSum / weightSum : 0;
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
  : {}),
@@ -122,6 +122,7 @@ export {
122
122
  formatRunsTable,
123
123
  type ListedRun,
124
124
  listRuns,
125
+ loadRunArtifacts,
125
126
  loadRunDetail,
126
127
  type RunDetail,
127
128
  } from "./list-runs.js";
@@ -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 : undefined;
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 { harnessUsage, isUsageEmpty, type RunMetadata } from "../core/index.js";
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<string, number[]>;
71
+ type MetricSamples = Map<
72
+ string,
73
+ { values: number[]; descriptor?: Omit<DriverMetric, "value"> | undefined }
74
+ >;
63
75
 
64
- const add = (samples: MetricSamples, name: string, value: number | undefined): void => {
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 values = samples.get(name) ?? [];
67
- values.push(value);
68
- samples.set(name, values);
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]) => [name, summarizeNumbers(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, values]) => [name, values[0] ?? 0]),
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
+ };