@prismatic-io/lux 0.0.1 → 0.0.2-preview.10

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 (256) hide show
  1. package/lib/answerers/persona/index.d.ts +2 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/answerers/persona/index.js +19 -5
  4. package/lib/answerers/persona/index.js.map +1 -1
  5. package/lib/answerers/terminal/index.js +1 -1
  6. package/lib/answerers/terminal/index.js.map +1 -1
  7. package/lib/assertions/authoring.d.ts +14 -0
  8. package/lib/assertions/authoring.d.ts.map +1 -0
  9. package/lib/assertions/authoring.js +3 -0
  10. package/lib/assertions/authoring.js.map +1 -0
  11. package/lib/assertions/core/helpers.d.ts +2 -7
  12. package/lib/assertions/core/helpers.d.ts.map +1 -1
  13. package/lib/assertions/core/helpers.js +2 -6
  14. package/lib/assertions/core/helpers.js.map +1 -1
  15. package/lib/assertions/core/index.d.ts.map +1 -1
  16. package/lib/assertions/core/index.js +2 -1
  17. package/lib/assertions/core/index.js.map +1 -1
  18. package/lib/assertions/core/resource-checks.d.ts +19 -0
  19. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  20. package/lib/assertions/core/resource-checks.js +32 -0
  21. package/lib/assertions/core/resource-checks.js.map +1 -1
  22. package/lib/authoring.d.ts +1 -0
  23. package/lib/authoring.d.ts.map +1 -1
  24. package/lib/authoring.js +1 -0
  25. package/lib/authoring.js.map +1 -1
  26. package/lib/cli/program.d.ts.map +1 -1
  27. package/lib/cli/program.js +12 -67
  28. package/lib/cli/program.js.map +1 -1
  29. package/lib/cli/render/reporter.d.ts.map +1 -1
  30. package/lib/cli/render/reporter.js +16 -3
  31. package/lib/cli/render/reporter.js.map +1 -1
  32. package/lib/cli/run-options.d.ts +1 -0
  33. package/lib/cli/run-options.d.ts.map +1 -1
  34. package/lib/cli/run-options.js.map +1 -1
  35. package/lib/cli/view.d.ts +13 -0
  36. package/lib/cli/view.d.ts.map +1 -0
  37. package/lib/cli/view.js +126 -0
  38. package/lib/cli/view.js.map +1 -0
  39. package/lib/core/action-events.d.ts +29 -0
  40. package/lib/core/action-events.d.ts.map +1 -0
  41. package/lib/core/action-events.js +51 -0
  42. package/lib/core/action-events.js.map +1 -0
  43. package/lib/core/agent-activity.d.ts +49 -0
  44. package/lib/core/agent-activity.d.ts.map +1 -0
  45. package/lib/core/agent-activity.js +206 -0
  46. package/lib/core/agent-activity.js.map +1 -0
  47. package/lib/core/agent-events.d.ts +26 -0
  48. package/lib/core/agent-events.d.ts.map +1 -0
  49. package/lib/core/agent-events.js +13 -0
  50. package/lib/core/agent-events.js.map +1 -0
  51. package/lib/core/artifact-evidence.d.ts +8 -0
  52. package/lib/core/artifact-evidence.d.ts.map +1 -1
  53. package/lib/core/artifact-evidence.js +36 -4
  54. package/lib/core/artifact-evidence.js.map +1 -1
  55. package/lib/core/assertion.d.ts +19 -9
  56. package/lib/core/assertion.d.ts.map +1 -1
  57. package/lib/core/assertion.js +26 -0
  58. package/lib/core/assertion.js.map +1 -1
  59. package/lib/core/case-authoring-state.d.ts +12 -0
  60. package/lib/core/case-authoring-state.d.ts.map +1 -0
  61. package/lib/core/case-authoring-state.js +43 -0
  62. package/lib/core/case-authoring-state.js.map +1 -0
  63. package/lib/core/case.d.ts +29 -1
  64. package/lib/core/case.d.ts.map +1 -1
  65. package/lib/core/case.js +14 -6
  66. package/lib/core/case.js.map +1 -1
  67. package/lib/core/driver-metrics.d.ts +98 -0
  68. package/lib/core/driver-metrics.d.ts.map +1 -0
  69. package/lib/core/driver-metrics.js +36 -0
  70. package/lib/core/driver-metrics.js.map +1 -0
  71. package/lib/core/driver.d.ts +3 -0
  72. package/lib/core/driver.d.ts.map +1 -1
  73. package/lib/core/driver.js.map +1 -1
  74. package/lib/core/events.d.ts +36 -0
  75. package/lib/core/events.d.ts.map +1 -1
  76. package/lib/core/events.js +12 -2
  77. package/lib/core/events.js.map +1 -1
  78. package/lib/core/experiment.d.ts +19 -0
  79. package/lib/core/experiment.d.ts.map +1 -1
  80. package/lib/core/experiment.js +2 -0
  81. package/lib/core/experiment.js.map +1 -1
  82. package/lib/core/index.d.ts +4 -0
  83. package/lib/core/index.d.ts.map +1 -1
  84. package/lib/core/index.js +4 -0
  85. package/lib/core/index.js.map +1 -1
  86. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  87. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  88. package/lib/core/lifecycle-fixtures.js +4 -2
  89. package/lib/core/lifecycle-fixtures.js.map +1 -1
  90. package/lib/core/lifecycle-hooks.d.ts +9 -0
  91. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  92. package/lib/core/lifecycle-hooks.js +8 -2
  93. package/lib/core/lifecycle-hooks.js.map +1 -1
  94. package/lib/core/run.d.ts +150 -0
  95. package/lib/core/run.d.ts.map +1 -1
  96. package/lib/core/run.js +9 -0
  97. package/lib/core/run.js.map +1 -1
  98. package/lib/core/wire.d.ts +12 -0
  99. package/lib/core/wire.d.ts.map +1 -1
  100. package/lib/core/wire.js +5 -1
  101. package/lib/core/wire.js.map +1 -1
  102. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  103. package/lib/drivers/claude-code/parse-events.js +73 -8
  104. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  105. package/lib/drivers/codex/app-events.d.ts +9 -2
  106. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  107. package/lib/drivers/codex/app-events.js +93 -12
  108. package/lib/drivers/codex/app-events.js.map +1 -1
  109. package/lib/drivers/codex/index.d.ts.map +1 -1
  110. package/lib/drivers/codex/index.js +10 -14
  111. package/lib/drivers/codex/index.js.map +1 -1
  112. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  113. package/lib/drivers/subprocess/index.js +6 -0
  114. package/lib/drivers/subprocess/index.js.map +1 -1
  115. package/lib/index.d.ts +10 -6
  116. package/lib/index.d.ts.map +1 -1
  117. package/lib/index.js +10 -6
  118. package/lib/index.js.map +1 -1
  119. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  120. package/lib/orchestrator/annotation-store.js +6 -2
  121. package/lib/orchestrator/annotation-store.js.map +1 -1
  122. package/lib/orchestrator/case-extensions.d.ts +6 -0
  123. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  124. package/lib/orchestrator/case-extensions.js +28 -0
  125. package/lib/orchestrator/case-extensions.js.map +1 -0
  126. package/lib/orchestrator/compare.d.ts +13 -1
  127. package/lib/orchestrator/compare.d.ts.map +1 -1
  128. package/lib/orchestrator/compare.js +12 -0
  129. package/lib/orchestrator/compare.js.map +1 -1
  130. package/lib/orchestrator/config.d.ts.map +1 -1
  131. package/lib/orchestrator/config.js +0 -2
  132. package/lib/orchestrator/config.js.map +1 -1
  133. package/lib/orchestrator/doctor.d.ts.map +1 -1
  134. package/lib/orchestrator/doctor.js +5 -3
  135. package/lib/orchestrator/doctor.js.map +1 -1
  136. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  137. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  138. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  139. package/lib/orchestrator/experiment-evaluation.js +17 -0
  140. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  141. package/lib/orchestrator/experiment-report.d.ts +19 -0
  142. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  143. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  144. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  145. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  146. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  147. package/lib/orchestrator/grading.d.ts +2 -1
  148. package/lib/orchestrator/grading.d.ts.map +1 -1
  149. package/lib/orchestrator/grading.js +16 -4
  150. package/lib/orchestrator/grading.js.map +1 -1
  151. package/lib/orchestrator/index.d.ts +1 -1
  152. package/lib/orchestrator/index.d.ts.map +1 -1
  153. package/lib/orchestrator/index.js +1 -1
  154. package/lib/orchestrator/index.js.map +1 -1
  155. package/lib/orchestrator/list-runs.d.ts +7 -1
  156. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  157. package/lib/orchestrator/list-runs.js +50 -6
  158. package/lib/orchestrator/list-runs.js.map +1 -1
  159. package/lib/orchestrator/loader.d.ts.map +1 -1
  160. package/lib/orchestrator/loader.js +2 -0
  161. package/lib/orchestrator/loader.js.map +1 -1
  162. package/lib/orchestrator/orchestrator.d.ts +3 -0
  163. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  164. package/lib/orchestrator/orchestrator.js +42 -14
  165. package/lib/orchestrator/orchestrator.js.map +1 -1
  166. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  167. package/lib/orchestrator/owned-lock.js +52 -38
  168. package/lib/orchestrator/owned-lock.js.map +1 -1
  169. package/lib/orchestrator/regrade.d.ts +6 -0
  170. package/lib/orchestrator/regrade.d.ts.map +1 -1
  171. package/lib/orchestrator/regrade.js +8 -4
  172. package/lib/orchestrator/regrade.js.map +1 -1
  173. package/lib/orchestrator/run-execution.d.ts +2 -1
  174. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  175. package/lib/orchestrator/run-execution.js +20 -5
  176. package/lib/orchestrator/run-execution.js.map +1 -1
  177. package/lib/orchestrator/suite-compare.d.ts +34 -0
  178. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  179. package/lib/orchestrator/suite-summary.d.ts +68 -0
  180. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  181. package/lib/orchestrator/suite-summary.js +45 -7
  182. package/lib/orchestrator/suite-summary.js.map +1 -1
  183. package/lib/previewer/artifacts.d.ts +25 -0
  184. package/lib/previewer/artifacts.d.ts.map +1 -0
  185. package/lib/previewer/artifacts.js +180 -0
  186. package/lib/previewer/artifacts.js.map +1 -0
  187. package/lib/previewer/server.d.ts +105 -0
  188. package/lib/previewer/server.d.ts.map +1 -0
  189. package/lib/previewer/server.js +542 -0
  190. package/lib/previewer/server.js.map +1 -0
  191. package/package.json +2 -2
  192. package/skills/lux/SKILL.md +7 -3
  193. package/skills/lux/references/cli.md +25 -2
  194. package/skills/lux/references/eval-authoring.md +158 -4
  195. package/skills/lux-answerer/SKILL.md +1 -1
  196. package/src/answerers/persona/index.ts +20 -4
  197. package/src/answerers/terminal/index.ts +1 -1
  198. package/src/assertions/authoring.ts +81 -0
  199. package/src/assertions/core/helpers.ts +3 -6
  200. package/src/assertions/core/index.ts +2 -0
  201. package/src/assertions/core/resource-checks.ts +35 -0
  202. package/src/authoring.ts +1 -0
  203. package/src/cli/program.ts +14 -93
  204. package/src/cli/render/reporter.ts +18 -3
  205. package/src/cli/run-options.ts +1 -0
  206. package/src/cli/view.ts +168 -0
  207. package/src/core/action-events.ts +59 -0
  208. package/src/core/agent-activity.ts +236 -0
  209. package/src/core/agent-events.ts +17 -0
  210. package/src/core/artifact-evidence.ts +47 -4
  211. package/src/core/assertion.ts +41 -22
  212. package/src/core/case-authoring-state.ts +65 -0
  213. package/src/core/case.ts +57 -6
  214. package/src/core/driver-metrics.ts +51 -0
  215. package/src/core/driver.ts +3 -0
  216. package/src/core/events.ts +14 -2
  217. package/src/core/experiment.ts +2 -0
  218. package/src/core/index.ts +4 -0
  219. package/src/core/lifecycle-fixtures.ts +7 -2
  220. package/src/core/lifecycle-hooks.ts +16 -2
  221. package/src/core/run.ts +9 -0
  222. package/src/core/wire.ts +5 -1
  223. package/src/drivers/claude-code/parse-events.ts +98 -12
  224. package/src/drivers/codex/app-events.ts +116 -16
  225. package/src/drivers/codex/index.ts +16 -14
  226. package/src/drivers/subprocess/index.ts +6 -0
  227. package/src/index.ts +50 -6
  228. package/src/orchestrator/annotation-store.ts +5 -1
  229. package/src/orchestrator/case-extensions.ts +44 -0
  230. package/src/orchestrator/compare.ts +18 -0
  231. package/src/orchestrator/config.ts +0 -2
  232. package/src/orchestrator/doctor.ts +5 -3
  233. package/src/orchestrator/experiment-evaluation.ts +23 -0
  234. package/src/orchestrator/grading.ts +21 -2
  235. package/src/orchestrator/index.ts +1 -0
  236. package/src/orchestrator/list-runs.ts +66 -5
  237. package/src/orchestrator/loader.ts +2 -0
  238. package/src/orchestrator/orchestrator.ts +86 -40
  239. package/src/orchestrator/owned-lock.ts +54 -34
  240. package/src/orchestrator/regrade.ts +8 -7
  241. package/src/orchestrator/run-execution.ts +24 -3
  242. package/src/orchestrator/suite-summary.ts +101 -8
  243. package/src/previewer/artifacts.ts +221 -0
  244. package/src/previewer/server.ts +765 -0
  245. package/viewer/app.css +2075 -0
  246. package/viewer/app.js +606 -0
  247. package/viewer/artifact-view.js +48 -0
  248. package/viewer/charts.js +86 -0
  249. package/viewer/detail.js +1214 -0
  250. package/viewer/format.js +129 -0
  251. package/viewer/index.html +151 -0
  252. package/lib/answerers/scripted/index.d.ts +0 -8
  253. package/lib/answerers/scripted/index.d.ts.map +0 -1
  254. package/lib/answerers/scripted/index.js +0 -163
  255. package/lib/answerers/scripted/index.js.map +0 -1
  256. package/src/answerers/scripted/index.ts +0 -186
@@ -33,6 +33,7 @@ import {
33
33
  readRunArtifactBytes,
34
34
  valueHash,
35
35
  } from "../core/index.js";
36
+ import { registryForEvalCase } from "./case-extensions.js";
36
37
  import { evaluatorImplementationHash } from "./evaluator-identity.js";
37
38
  import { parsePersistedJson, readBoundedHandle, safeReadJson } from "./fs-read.js";
38
39
  import {
@@ -526,18 +527,18 @@ export const regradeRun = async (options: {
526
527
  now?: Date;
527
528
  }): Promise<{ path: string; snapshot: GradingSnapshot }> => {
528
529
  const run = await loadFrozenRegradableRun(options.runDir, options.case);
530
+ const registry = options.case
531
+ ? registryForEvalCase(options.registry, options.case)
532
+ : options.registry;
529
533
  try {
530
534
  const evidenceHash = await gradingEvidenceHash(run);
531
535
  const validatedAssertionConfigs = await validateAssertionPlan(
532
536
  run.case.assertions ?? [],
533
- options.registry,
537
+ registry,
534
538
  );
535
539
  const validatedAssertionsHash = valueHash(validatedAssertionConfigs);
536
- const assertionEvaluators = assertionEvaluatorIdentities(
537
- run.case.assertions ?? [],
538
- options.registry,
539
- );
540
- const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], options.registry);
540
+ const assertionEvaluators = assertionEvaluatorIdentities(run.case.assertions ?? [], registry);
541
+ const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], registry);
541
542
  const graderHash = graderConfigHash({
542
543
  luxVersion: LUX_VERSION,
543
544
  implementationHash: await currentEvaluatorImplementationHash(),
@@ -547,7 +548,7 @@ export const regradeRun = async (options: {
547
548
  });
548
549
  const report: GradingReport = await gradeRun(
549
550
  run,
550
- options.registry,
551
+ registry,
551
552
  undefined,
552
553
  options.abortSignal,
553
554
  undefined,
@@ -10,13 +10,16 @@ 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,
17
+ type FixtureValueBinding,
16
18
  type GradingReport,
17
19
  guardRunObserver,
18
20
  inspectRunArtifact,
19
21
  type LifecycleStage,
22
+ normalizeDriverMetrics,
20
23
  type OperationalFailure,
21
24
  type PhaseDurations,
22
25
  type ReadyState,
@@ -27,6 +30,7 @@ import {
27
30
  type RunMetadata,
28
31
  type RunProvenance,
29
32
  type RunUsage,
33
+ summarizeAgentActivity,
30
34
  summarizeToolOutcomes,
31
35
  sumUsage,
32
36
  type TokenUsage,
@@ -70,6 +74,7 @@ export type RunWithPluginsArgs = {
70
74
  validatedAssertionsHash: string;
71
75
  graderConfigHash: string;
72
76
  evaluatorConfigHash: string;
77
+ fixtureValues: readonly FixtureValueBinding[];
73
78
  luxVersion: string;
74
79
  registry: Registry;
75
80
  driver: AgentDriver;
@@ -181,6 +186,11 @@ const modelAccountingFromSummary = (
181
186
  ): { modelCalls: number; unpricedModelCalls?: number | undefined } | null =>
182
187
  SummaryModelAccountingSchema.safeParse(summary).data ?? null;
183
188
 
189
+ const metricsFromSummary = (summary: unknown) => {
190
+ if (!summary || typeof summary !== "object") return {};
191
+ return normalizeDriverMetrics((summary as { metrics?: unknown }).metrics);
192
+ };
193
+
184
194
  const artifactMeasurements = async (
185
195
  artifacts: readonly Artifact[],
186
196
  runDir: string,
@@ -360,6 +370,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
360
370
  caseId: this.args.caseId,
361
371
  prompt: evalCase.prompt,
362
372
  fixtures: staged?.fixtures,
373
+ ...(this.args.fixtureValues.length > 0 ? { fixtureValues: this.args.fixtureValues } : {}),
363
374
  runsRoot: resolve(request.runsRoot),
364
375
  runDir: this.runDir.path,
365
376
  artifactsDir: this.runDir.artifactsDir,
@@ -387,6 +398,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
387
398
  metadata.exitReason = "error";
388
399
  metadata.operationalFailure = { phase: operationalPhase, reason };
389
400
  metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
401
+ metadata.actionTiming = actionTiming(this.runDir.recorded);
390
402
  await this.runDir.writeJson("run.json", metadata);
391
403
  await this.args.request.recordPreGradingAccounting?.(metadata);
392
404
  await this.args.request.recordModelAttemptsComplete?.(this.subjectAttemptIds);
@@ -404,6 +416,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
404
416
  this.lifecycle("collect-end");
405
417
  await this.runDir.events.drain();
406
418
  const completed = await this.completedAgentRun();
419
+ completed.metadata.actionTiming = actionTiming(this.runDir.recorded);
407
420
  this.metadata = completed.metadata;
408
421
  this.usage = completed.usage;
409
422
  this.run = completed.run;
@@ -427,6 +440,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
427
440
  grading.assertions.flatMap((assertion) => (assertion.usage ? [assertion.usage] : [])),
428
441
  );
429
442
  metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
443
+ metadata.actionTiming = actionTiming(this.runDir.recorded);
430
444
  await this.runDir.events.drain();
431
445
  await this.runDir.writeJson("run.json", metadata);
432
446
  await this.runDir.writeJson("artifacts.json", this.collection.artifacts);
@@ -503,6 +517,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
503
517
  private fallbackFailureMetadata(): RunMetadata {
504
518
  const failedAt = new Date();
505
519
  const answererAccounting = readAnswererModelAccounting(this.args.answerer);
520
+ const ready = this.ready ?? { agentId: "start-failed" };
506
521
  return {
507
522
  schemaVersion: 1,
508
523
  luxVersion: this.args.luxVersion,
@@ -511,7 +526,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
511
526
  agentEndedAt: failedAt.toISOString(),
512
527
  endedAt: failedAt.toISOString(),
513
528
  exitReason: "error",
514
- driver: { name: this.args.driverName, ...(this.ready ?? { agentId: "start-failed" }) },
529
+ driver: { name: this.args.driverName, ...ready },
515
530
  answerer: answererMetadata(this.args.answererName, this.args.answererConfig),
516
531
  usage: {
517
532
  agent: emptyUsage(),
@@ -527,6 +542,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
527
542
  interruptCounts: this.interruptCounts,
528
543
  toolCallCount: this.toolCallCount,
529
544
  toolOutcomes: summarizeToolOutcomes(this.runDir.recorded),
545
+ agentActivity: summarizeAgentActivity(this.runDir.recorded, ready),
530
546
  artifacts: { count: this.collection.artifacts.length },
531
547
  provenance: this.runProvenance(),
532
548
  };
@@ -557,9 +573,10 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
557
573
  const outcome = this.require(this.loopOutcome, "driver outcome");
558
574
  const agentEndedAt = new Date();
559
575
  const exitReason = agentExitReason(outcome, this.collection.error);
560
- const summary = outcome.kind === "done" ? outcome.done.summary : undefined;
576
+ const summary = outcome.kind === "done" ? outcome.done.summary : outcome.error.summary;
561
577
  const driverCost = costFromSummary(summary);
562
578
  const reportedModelAccounting = modelAccountingFromSummary(summary);
579
+ const driverMetrics = metricsFromSummary(summary);
563
580
  const answererAccounting = readAnswererModelAccounting(this.args.answerer);
564
581
  const agentCalls = this.ready ? (reportedModelAccounting?.modelCalls ?? 1) : 0;
565
582
  const operationalFailure = loopFailure(outcome, this.collection.error);
@@ -568,6 +585,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
568
585
  answerer: readAnswererUsage(this.args.answerer),
569
586
  grading: emptyUsage(),
570
587
  };
588
+ const ready = this.ready ?? { agentId: "not-started" };
571
589
  const metadata: RunMetadata = {
572
590
  schemaVersion: 1,
573
591
  luxVersion: this.args.luxVersion,
@@ -576,7 +594,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
576
594
  agentEndedAt: agentEndedAt.toISOString(),
577
595
  endedAt: agentEndedAt.toISOString(),
578
596
  exitReason,
579
- driver: { name: this.args.driverName, ...(this.ready ?? { agentId: "not-started" }) },
597
+ driver: { name: this.args.driverName, ...ready },
580
598
  answerer: answererMetadata(this.args.answererName, this.args.answererConfig),
581
599
  usage,
582
600
  modelAccounting: {
@@ -590,10 +608,12 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
590
608
  interruptCounts: this.interruptCounts,
591
609
  toolCallCount: this.toolCallCount,
592
610
  toolOutcomes: summarizeToolOutcomes(this.runDir.recorded),
611
+ agentActivity: summarizeAgentActivity(this.runDir.recorded, ready),
593
612
  artifacts: await artifactMeasurements(this.collection.artifacts, this.runDir.path),
594
613
  ...(operationalFailure ? { operationalFailure } : {}),
595
614
  provenance: this.runProvenance(),
596
615
  ...(driverCost !== undefined ? { cost: { usd: driverCost } } : {}),
616
+ ...(Object.keys(driverMetrics).length > 0 ? { driverMetrics } : {}),
597
617
  };
598
618
  return {
599
619
  metadata,
@@ -654,6 +674,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
654
674
  request.recordGradingAssertion,
655
675
  modelAttempts,
656
676
  this.args.validatedAssertionConfigs,
677
+ request.skipJudge ?? false,
657
678
  );
658
679
  }
659
680
 
@@ -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,78 @@ 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
+ const subagents = (metadata.agentActivity ?? []).filter(
162
+ (activity) => activity.agent.role === "subagent",
163
+ );
164
+ add(samples, "subagents.count", subagents.length, {
165
+ unit: "count",
166
+ label: "Subagents spawned",
167
+ better: "neutral",
168
+ });
169
+ add(
170
+ samples,
171
+ "subagents.events",
172
+ subagents.reduce((sum, activity) => sum + activity.eventCount, 0),
173
+ { unit: "count", label: "Subagent events", better: "neutral" },
174
+ );
175
+ add(
176
+ samples,
177
+ "subagents.toolCalls",
178
+ subagents.reduce((sum, activity) => sum + activity.toolCalls.total, 0),
179
+ { unit: "count", label: "Subagent tool calls", better: "neutral" },
180
+ );
181
+ add(
182
+ samples,
183
+ "subagents.toolCalls.failed",
184
+ subagents.reduce((sum, activity) => sum + activity.toolCalls.failed, 0),
185
+ { unit: "count", label: "Failed subagent tool calls", better: "lower" },
186
+ );
187
+ add(
188
+ samples,
189
+ "subagents.tokens",
190
+ subagents.reduce((sum, activity) => sum + activity.usage.total, 0),
191
+ { unit: "tokens", label: "Subagent tokens", better: "lower" },
192
+ );
193
+ add(
194
+ samples,
195
+ "subagents.actions",
196
+ subagents.reduce((sum, activity) => sum + activity.actions.total, 0),
197
+ { unit: "count", label: "Subagent actions", better: "neutral" },
198
+ );
199
+ add(
200
+ samples,
201
+ "subagents.mutations",
202
+ subagents.reduce((sum, activity) => sum + activity.actions.mutations, 0),
203
+ { unit: "count", label: "Subagent mutations", better: "neutral" },
204
+ );
205
+ add(
206
+ samples,
207
+ "subagents.totalDurationMs",
208
+ subagents.reduce((sum, activity) => sum + activity.durationMs, 0),
209
+ { unit: "milliseconds", label: "Total subagent time", better: "neutral" },
210
+ );
211
+ add(
212
+ samples,
213
+ "subagents.maxDurationMs",
214
+ subagents.length > 0 ? Math.max(...subagents.map((activity) => activity.durationMs)) : 0,
215
+ { unit: "milliseconds", label: "Longest subagent", better: "lower" },
216
+ );
217
+ const firstSubagentAction = subagents
218
+ .map((activity) => activity.actions.timeToFirstActionMs)
219
+ .filter((value): value is number => value !== undefined)
220
+ .sort((a, b) => a - b)[0];
221
+ add(samples, "subagents.timeToFirstActionMs", firstSubagentAction, {
222
+ unit: "milliseconds",
223
+ label: "First subagent action",
224
+ better: "lower",
225
+ });
226
+ for (const [name, metric] of Object.entries(metadata.driverMetrics ?? {})) {
227
+ const { value, ...descriptor } = metric;
228
+ add(samples, `driver.${name}`, value, descriptor);
229
+ }
140
230
  };
141
231
 
142
232
  const percentile = (sorted: readonly number[], fraction: number): number => {
@@ -165,7 +255,10 @@ const summarizeMetrics = (samples: MetricSamples): Record<string, MetricSummary>
165
255
  Object.fromEntries(
166
256
  [...samples.entries()]
167
257
  .sort(([a], [b]) => a.localeCompare(b))
168
- .map(([name, values]) => [name, summarizeNumbers(values)]),
258
+ .map(([name, { values, descriptor }]) => [
259
+ name,
260
+ MetricSummarySchema.parse({ ...summarizeNumbers(values), ...descriptor }),
261
+ ]),
169
262
  );
170
263
 
171
264
  const summarizeOutcomes = (
@@ -258,7 +351,7 @@ export const buildSuiteSummary = (
258
351
  ? { score: result.grading.caseScore }
259
352
  : {}),
260
353
  metrics: Object.fromEntries(
261
- [...samples.entries()].map(([name, values]) => [name, values[0] ?? 0]),
354
+ [...samples.entries()].map(([name, sample]) => [name, sample.values[0] ?? 0]),
262
355
  ),
263
356
  };
264
357
  }),
@@ -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
+ };