@prismatic-io/lux 0.0.2-preview.4 → 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 (110) hide show
  1. package/lib/assertions/authoring.d.ts +2 -2
  2. package/lib/assertions/authoring.d.ts.map +1 -1
  3. package/lib/assertions/authoring.js.map +1 -1
  4. package/lib/assertions/core/index.d.ts.map +1 -1
  5. package/lib/assertions/core/index.js +2 -1
  6. package/lib/assertions/core/index.js.map +1 -1
  7. package/lib/assertions/core/resource-checks.d.ts +19 -0
  8. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  9. package/lib/assertions/core/resource-checks.js +32 -0
  10. package/lib/assertions/core/resource-checks.js.map +1 -1
  11. package/lib/cli/program.d.ts.map +1 -1
  12. package/lib/cli/program.js +6 -65
  13. package/lib/cli/program.js.map +1 -1
  14. package/lib/cli/view.d.ts +13 -0
  15. package/lib/cli/view.d.ts.map +1 -0
  16. package/lib/cli/view.js +126 -0
  17. package/lib/cli/view.js.map +1 -0
  18. package/lib/core/action-events.d.ts +29 -0
  19. package/lib/core/action-events.d.ts.map +1 -0
  20. package/lib/core/action-events.js +51 -0
  21. package/lib/core/action-events.js.map +1 -0
  22. package/lib/core/driver-metrics.d.ts +98 -0
  23. package/lib/core/driver-metrics.d.ts.map +1 -0
  24. package/lib/core/driver-metrics.js +36 -0
  25. package/lib/core/driver-metrics.js.map +1 -0
  26. package/lib/core/events.js +2 -2
  27. package/lib/core/experiment.d.ts +19 -0
  28. package/lib/core/experiment.d.ts.map +1 -1
  29. package/lib/core/experiment.js +2 -0
  30. package/lib/core/experiment.js.map +1 -1
  31. package/lib/core/index.d.ts +2 -0
  32. package/lib/core/index.d.ts.map +1 -1
  33. package/lib/core/index.js +2 -0
  34. package/lib/core/index.js.map +1 -1
  35. package/lib/core/run.d.ts +50 -0
  36. package/lib/core/run.d.ts.map +1 -1
  37. package/lib/core/run.js +6 -0
  38. package/lib/core/run.js.map +1 -1
  39. package/lib/index.d.ts +3 -1
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js +3 -1
  42. package/lib/index.js.map +1 -1
  43. package/lib/orchestrator/compare.d.ts +9 -1
  44. package/lib/orchestrator/compare.d.ts.map +1 -1
  45. package/lib/orchestrator/compare.js +8 -0
  46. package/lib/orchestrator/compare.js.map +1 -1
  47. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  48. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  49. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  50. package/lib/orchestrator/experiment-evaluation.js +17 -0
  51. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  52. package/lib/orchestrator/experiment-report.d.ts +19 -0
  53. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  54. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  55. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  56. package/lib/orchestrator/index.d.ts +1 -1
  57. package/lib/orchestrator/index.d.ts.map +1 -1
  58. package/lib/orchestrator/index.js +1 -1
  59. package/lib/orchestrator/index.js.map +1 -1
  60. package/lib/orchestrator/list-runs.d.ts +6 -1
  61. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  62. package/lib/orchestrator/list-runs.js +15 -1
  63. package/lib/orchestrator/list-runs.js.map +1 -1
  64. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  65. package/lib/orchestrator/run-execution.js +11 -1
  66. package/lib/orchestrator/run-execution.js.map +1 -1
  67. package/lib/orchestrator/suite-compare.d.ts +34 -0
  68. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  69. package/lib/orchestrator/suite-summary.d.ts +68 -0
  70. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  71. package/lib/orchestrator/suite-summary.js +22 -7
  72. package/lib/orchestrator/suite-summary.js.map +1 -1
  73. package/lib/previewer/artifacts.d.ts +25 -0
  74. package/lib/previewer/artifacts.d.ts.map +1 -0
  75. package/lib/previewer/artifacts.js +180 -0
  76. package/lib/previewer/artifacts.js.map +1 -0
  77. package/lib/previewer/server.d.ts +104 -0
  78. package/lib/previewer/server.d.ts.map +1 -0
  79. package/lib/previewer/server.js +541 -0
  80. package/lib/previewer/server.js.map +1 -0
  81. package/package.json +2 -1
  82. package/skills/lux/references/eval-authoring.md +4 -2
  83. package/skills/lux-answerer/SKILL.md +1 -1
  84. package/src/assertions/authoring.ts +2 -0
  85. package/src/assertions/core/index.ts +2 -0
  86. package/src/assertions/core/resource-checks.ts +35 -0
  87. package/src/cli/program.ts +5 -91
  88. package/src/cli/view.ts +168 -0
  89. package/src/core/action-events.ts +59 -0
  90. package/src/core/driver-metrics.ts +51 -0
  91. package/src/core/events.ts +2 -2
  92. package/src/core/experiment.ts +2 -0
  93. package/src/core/index.ts +2 -0
  94. package/src/core/run.ts +6 -0
  95. package/src/index.ts +25 -0
  96. package/src/orchestrator/compare.ts +12 -0
  97. package/src/orchestrator/experiment-evaluation.ts +23 -0
  98. package/src/orchestrator/index.ts +1 -0
  99. package/src/orchestrator/list-runs.ts +25 -0
  100. package/src/orchestrator/run-execution.ts +12 -0
  101. package/src/orchestrator/suite-summary.ts +36 -8
  102. package/src/previewer/artifacts.ts +221 -0
  103. package/src/previewer/server.ts +763 -0
  104. package/viewer/app.css +2022 -0
  105. package/viewer/app.js +601 -0
  106. package/viewer/artifact-view.js +48 -0
  107. package/viewer/charts.js +86 -0
  108. package/viewer/detail.js +1108 -0
  109. package/viewer/format.js +129 -0
  110. package/viewer/index.html +151 -0
@@ -44,6 +44,41 @@ export const latencyUnderAssertion: AssertionImpl<LatencyUnderConfig> = {
44
44
  },
45
45
  };
46
46
 
47
+ export const TimeToFirstActionUnderConfigSchema = z
48
+ .object({
49
+ type: z.literal("time-to-first-action-under"),
50
+ maxMs: z.number().nonnegative(),
51
+ category: z.enum(["any", "mutation", "durable-mutation"]).default("any"),
52
+ onUnavailable: UnavailablePolicySchema.default("fail"),
53
+ ...baseFields,
54
+ })
55
+ .strict();
56
+ export type TimeToFirstActionUnderConfig = z.infer<typeof TimeToFirstActionUnderConfigSchema>;
57
+
58
+ export const timeToFirstActionUnderAssertion: AssertionImpl<TimeToFirstActionUnderConfig> = {
59
+ type: "time-to-first-action-under",
60
+ configSchema: TimeToFirstActionUnderConfigSchema,
61
+ evaluate: async (config, run) => {
62
+ const fields = {
63
+ any: "timeToFirstActionMs",
64
+ mutation: "timeToFirstMutationMs",
65
+ "durable-mutation": "timeToFirstDurableMutationMs",
66
+ } as const;
67
+ const field = fields[config.category];
68
+ const value = run.metadata.actionTiming?.[field];
69
+ if (value === undefined) {
70
+ return unavailable(`time to first ${config.category} action`, config.onUnavailable);
71
+ }
72
+ const passed = value <= config.maxMs;
73
+ return {
74
+ passed,
75
+ score: passed ? 1 : 0,
76
+ comment: `${config.category} action at ${value}ms ${passed ? "<=" : ">"} ${config.maxMs}ms`,
77
+ detail: { category: config.category, timeToFirstActionMs: value },
78
+ };
79
+ },
80
+ };
81
+
47
82
  export const TokensUnderConfigSchema = z
48
83
  .object({
49
84
  type: z.literal("tokens-under"),
@@ -24,25 +24,15 @@ import {
24
24
  formatDoctorReport,
25
25
  formatExperimentDecisionMarkdown,
26
26
  formatExperimentRun,
27
- formatExperimentRunsTable,
28
- formatRunDetail,
29
- formatRunsTable,
30
27
  formatSuiteComparisonText,
31
- formatSuiteRunDetail,
32
- formatSuiteRunsTable,
33
28
  hasRegression,
34
29
  hasSuiteRegression,
35
- isExperimentRun,
36
- listExperimentRuns,
37
- listRuns,
38
- listSuiteRuns,
39
30
  loadEvalCase,
40
31
  loadExperimentCampaign,
41
32
  loadExperimentRun,
42
33
  loadFrozenRegradableRun,
43
34
  loadLuxConfig,
44
35
  loadRubricDefinition,
45
- loadRunDetail,
46
36
  loadSuiteRun,
47
37
  loadVerifiedExperimentDecisionSnapshot,
48
38
  Orchestrator,
@@ -84,6 +74,7 @@ import {
84
74
  installSkills,
85
75
  type SkillsPlatform,
86
76
  } from "./skills.js";
77
+ import { parsePreviewPort, viewCommand } from "./view.js";
87
78
 
88
79
  const requireFromHere = createRequire(import.meta.url);
89
80
  // From lib/cli/program.js (or src/cli/program.ts under test) to the package root.
@@ -496,88 +487,11 @@ const registerReportingCommands = (program: Command): void => {
496
487
  .option("--case <id>", "filter the run list to one case slug (list mode only)")
497
488
  .option("--suites", "list logical suite runs instead of individual runs")
498
489
  .option("--experiments", "list experiment runs instead of individual runs")
490
+ .option("--web", "open the local Lux Review web app")
491
+ .option("--port <n>", "listen on a specific local port in web mode", parsePreviewPort)
492
+ .option("--no-open", "start web mode without opening a browser")
499
493
  .option("--json", "emit machine-readable JSON")
500
- .action(
501
- async (
502
- runDirArg: string | undefined,
503
- options: {
504
- runsRoot?: string;
505
- case?: string;
506
- suites?: boolean;
507
- experiments?: boolean;
508
- json?: boolean;
509
- },
510
- ) => {
511
- if (runDirArg) {
512
- const path = resolve(runDirArg);
513
- if (!(await ensureDirectory("view", path))) return;
514
- if (isExperimentRun(path)) {
515
- const experiment = await loadExperimentRun(path);
516
- if (!experiment) {
517
- process.stderr.write(`lux view: invalid experiment at ${path}\n`);
518
- failWith(1);
519
- return;
520
- }
521
- process.stdout.write(
522
- options.json
523
- ? `${JSON.stringify({ experimentDir: path, ...experiment }, null, 2)}\n`
524
- : `${formatExperimentRun(path, experiment)}\n`,
525
- );
526
- return;
527
- }
528
- const suite = await loadSuiteRun(path);
529
- if (suite) {
530
- process.stdout.write(
531
- options.json
532
- ? `${JSON.stringify({ suiteDir: path, ...suite }, null, 2)}\n`
533
- : `${formatSuiteRunDetail(path, suite)}\n`,
534
- );
535
- return;
536
- }
537
- const detail = await loadRunDetail(path);
538
- if (options.json) {
539
- process.stdout.write(`${JSON.stringify(detail, null, 2)}\n`);
540
- } else {
541
- process.stdout.write(`${formatRunDetail(detail)}\n`);
542
- }
543
- return;
544
- }
545
-
546
- const cwd = process.cwd();
547
- const config = await loadLuxConfig(cwd);
548
- const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
549
- if (options.experiments) {
550
- const experiments = await listExperimentRuns(runsRoot);
551
- process.stdout.write(
552
- options.json
553
- ? `${JSON.stringify(experiments, null, 2)}\n`
554
- : `experiments at ${runsRoot}:\n${formatExperimentRunsTable(experiments)}\n`,
555
- );
556
- return;
557
- }
558
- if (options.suites) {
559
- const suites = await listSuiteRuns(runsRoot);
560
- process.stdout.write(
561
- options.json
562
- ? `${JSON.stringify(suites, null, 2)}\n`
563
- : `suite runs at ${runsRoot}:\n${formatSuiteRunsTable(suites)}\n`,
564
- );
565
- return;
566
- }
567
- const all = await listRuns(runsRoot);
568
- const runs = options.case ? all.filter((r) => r.caseSlug === options.case) : all;
569
- if (options.json) {
570
- process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
571
- } else {
572
- if (!options.case) {
573
- process.stdout.write(`suite runs at ${runsRoot}:\n`);
574
- process.stdout.write(`${formatSuiteRunsTable(await listSuiteRuns(runsRoot))}\n\n`);
575
- }
576
- process.stdout.write(`runs at ${runsRoot}:\n`);
577
- process.stdout.write(`${formatRunsTable(runs)}\n`);
578
- }
579
- },
580
- );
494
+ .action(viewCommand);
581
495
 
582
496
  program
583
497
  .command("compare <runA> <runB>")
@@ -0,0 +1,168 @@
1
+ import { resolve } from "node:path";
2
+ import { InvalidArgumentError } from "commander";
3
+ import {
4
+ formatExperimentRun,
5
+ formatExperimentRunsTable,
6
+ formatRunDetail,
7
+ formatRunsTable,
8
+ formatSuiteRunDetail,
9
+ formatSuiteRunsTable,
10
+ isExperimentRun,
11
+ listExperimentRuns,
12
+ listRuns,
13
+ listSuiteRuns,
14
+ loadExperimentRun,
15
+ loadLuxConfig,
16
+ loadRunDetail,
17
+ loadSuiteRun,
18
+ } from "../orchestrator/index.js";
19
+ import { openPreviewUrl, startPreviewServer } from "../previewer/server.js";
20
+ import { abortOnInterrupt, ensureDirectory, failWith } from "./command-runtime.js";
21
+
22
+ const SIGINT_EXIT_CODE = 130;
23
+
24
+ export type ViewOptions = {
25
+ runsRoot?: string;
26
+ case?: string;
27
+ suites?: boolean;
28
+ experiments?: boolean;
29
+ web?: boolean;
30
+ port?: number;
31
+ open?: boolean;
32
+ json?: boolean;
33
+ };
34
+
35
+ const previewKind = async (
36
+ initialPath: string | undefined,
37
+ options: ViewOptions,
38
+ ): Promise<"run" | "suite" | "experiment" | undefined> => {
39
+ if (initialPath) {
40
+ if (isExperimentRun(initialPath)) return "experiment";
41
+ if (await loadSuiteRun(initialPath)) return "suite";
42
+ return "run";
43
+ }
44
+ if (options.experiments) return "experiment";
45
+ if (options.suites) return "suite";
46
+ return undefined;
47
+ };
48
+
49
+ const webViewCommand = async (
50
+ runDirArg: string | undefined,
51
+ options: ViewOptions,
52
+ ): Promise<void> => {
53
+ if (options.json) {
54
+ throw new InvalidArgumentError("--web and --json are mutually exclusive");
55
+ }
56
+ const config = await loadLuxConfig(process.cwd());
57
+ const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
58
+ const initialPath = runDirArg ? resolve(runDirArg) : undefined;
59
+ if (initialPath && !(await ensureDirectory("view", initialPath))) return;
60
+ const initialKind = await previewKind(initialPath, options);
61
+ const controller = abortOnInterrupt();
62
+ const preview = await startPreviewServer({
63
+ runsRoot,
64
+ ...(options.port !== undefined ? { port: options.port } : {}),
65
+ ...(initialPath ? { initialPath } : {}),
66
+ ...(initialKind ? { initialKind } : {}),
67
+ ...(options.case ? { caseFilter: options.case } : {}),
68
+ signal: controller.signal,
69
+ });
70
+ process.stdout.write(`Lux Review: ${preview.url}\n`);
71
+ process.stdout.write(`Reading evidence from ${runsRoot}\n`);
72
+ if (options.open !== false) {
73
+ try {
74
+ await openPreviewUrl(preview.url);
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ process.stderr.write(`lux view: could not open a browser (${message})\n`);
78
+ }
79
+ }
80
+ await preview.closed;
81
+ if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
82
+ };
83
+
84
+ export const viewCommand = async (
85
+ runDirArg: string | undefined,
86
+ options: ViewOptions,
87
+ ): Promise<void> => {
88
+ if (options.web) {
89
+ await webViewCommand(runDirArg, options);
90
+ return;
91
+ }
92
+ if (options.port !== undefined || options.open === false) {
93
+ throw new InvalidArgumentError("--port and --no-open require --web");
94
+ }
95
+ if (runDirArg) {
96
+ const path = resolve(runDirArg);
97
+ if (!(await ensureDirectory("view", path))) return;
98
+ if (isExperimentRun(path)) {
99
+ const experiment = await loadExperimentRun(path);
100
+ if (!experiment) {
101
+ process.stderr.write(`lux view: invalid experiment at ${path}\n`);
102
+ failWith(1);
103
+ return;
104
+ }
105
+ process.stdout.write(
106
+ options.json
107
+ ? `${JSON.stringify({ experimentDir: path, ...experiment }, null, 2)}\n`
108
+ : `${formatExperimentRun(path, experiment)}\n`,
109
+ );
110
+ return;
111
+ }
112
+ const suite = await loadSuiteRun(path);
113
+ if (suite) {
114
+ process.stdout.write(
115
+ options.json
116
+ ? `${JSON.stringify({ suiteDir: path, ...suite }, null, 2)}\n`
117
+ : `${formatSuiteRunDetail(path, suite)}\n`,
118
+ );
119
+ return;
120
+ }
121
+ const detail = await loadRunDetail(path);
122
+ process.stdout.write(
123
+ options.json ? `${JSON.stringify(detail, null, 2)}\n` : `${formatRunDetail(detail)}\n`,
124
+ );
125
+ return;
126
+ }
127
+
128
+ const config = await loadLuxConfig(process.cwd());
129
+ const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
130
+ if (options.experiments) {
131
+ const experiments = await listExperimentRuns(runsRoot);
132
+ process.stdout.write(
133
+ options.json
134
+ ? `${JSON.stringify(experiments, null, 2)}\n`
135
+ : `experiments at ${runsRoot}:\n${formatExperimentRunsTable(experiments)}\n`,
136
+ );
137
+ return;
138
+ }
139
+ if (options.suites) {
140
+ const suites = await listSuiteRuns(runsRoot);
141
+ process.stdout.write(
142
+ options.json
143
+ ? `${JSON.stringify(suites, null, 2)}\n`
144
+ : `suite runs at ${runsRoot}:\n${formatSuiteRunsTable(suites)}\n`,
145
+ );
146
+ return;
147
+ }
148
+ const all = await listRuns(runsRoot);
149
+ const runs = options.case ? all.filter((run) => run.caseSlug === options.case) : all;
150
+ if (options.json) {
151
+ process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
152
+ return;
153
+ }
154
+ if (!options.case) {
155
+ process.stdout.write(`suite runs at ${runsRoot}:\n`);
156
+ process.stdout.write(`${formatSuiteRunsTable(await listSuiteRuns(runsRoot))}\n\n`);
157
+ }
158
+ process.stdout.write(`runs at ${runsRoot}:\n`);
159
+ process.stdout.write(`${formatRunsTable(runs)}\n`);
160
+ };
161
+
162
+ export const parsePreviewPort = (value: string): number => {
163
+ const port = Number(value);
164
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
165
+ throw new InvalidArgumentError("port must be an integer from 0 to 65535");
166
+ }
167
+ return port;
168
+ };
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import type { RunEvent } from "./events.js";
3
+
4
+ export const ActionCategorySchema = z.enum([
5
+ "query",
6
+ "mutation",
7
+ "durable-mutation",
8
+ "execution",
9
+ "interaction",
10
+ ]);
11
+ export type ActionCategory = z.infer<typeof ActionCategorySchema>;
12
+
13
+ export const ActionProgressPayloadSchema = z
14
+ .object({
15
+ name: z.string(),
16
+ category: ActionCategorySchema,
17
+ })
18
+ .strict();
19
+ export type ActionProgressPayload = z.infer<typeof ActionProgressPayloadSchema>;
20
+
21
+ export const ActionTimingSchema = z.object({
22
+ timeToFirstActionMs: z.exactOptional(z.number().nonnegative()),
23
+ timeToFirstMutationMs: z.exactOptional(z.number().nonnegative()),
24
+ timeToFirstDurableMutationMs: z.exactOptional(z.number().nonnegative()),
25
+ });
26
+ export type ActionTiming = z.infer<typeof ActionTimingSchema>;
27
+
28
+ export const actionTiming = (events: readonly RunEvent[]): ActionTiming => {
29
+ const startEvent = events.find(
30
+ (event) => event.type === "lifecycle" && event.stage === "start-driver",
31
+ );
32
+ const startedAt = startEvent?.type === "lifecycle" ? startEvent.ts : undefined;
33
+ if (startedAt === undefined) return {};
34
+
35
+ const actions = events.flatMap((event) => {
36
+ if (event.type !== "progress" || event.kind !== "action") return [];
37
+ const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
38
+ return payload ? [{ ...payload, ts: event.ts }] : [];
39
+ });
40
+ const elapsed = (category: "any" | "mutation" | "durable-mutation") => {
41
+ const timestamps = actions.flatMap(({ category: candidate, ts }) => {
42
+ if (category === "any") return [ts];
43
+ if (category === "mutation") {
44
+ return candidate === "mutation" || candidate === "durable-mutation" ? [ts] : [];
45
+ }
46
+ return candidate === category ? [ts] : [];
47
+ });
48
+ return timestamps.length > 0 ? Math.max(0, Math.min(...timestamps) - startedAt) : undefined;
49
+ };
50
+
51
+ const timeToFirstActionMs = elapsed("any");
52
+ const timeToFirstMutationMs = elapsed("mutation");
53
+ const timeToFirstDurableMutationMs = elapsed("durable-mutation");
54
+ return {
55
+ ...(timeToFirstActionMs === undefined ? {} : { timeToFirstActionMs }),
56
+ ...(timeToFirstMutationMs === undefined ? {} : { timeToFirstMutationMs }),
57
+ ...(timeToFirstDurableMutationMs === undefined ? {} : { timeToFirstDurableMutationMs }),
58
+ };
59
+ };
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const DriverMetricUnitSchema = z.enum([
4
+ "number",
5
+ "count",
6
+ "milliseconds",
7
+ "bytes",
8
+ "tokens",
9
+ "usd",
10
+ "ratio",
11
+ "percent",
12
+ "score",
13
+ ]);
14
+ export type DriverMetricUnit = z.infer<typeof DriverMetricUnitSchema>;
15
+
16
+ export const DriverMetricDescriptorSchema = z
17
+ .object({
18
+ unit: DriverMetricUnitSchema.default("number"),
19
+ label: z.string().min(1).max(120).optional(),
20
+ better: z.enum(["lower", "higher", "neutral"]).default("neutral"),
21
+ })
22
+ .strict();
23
+ export type DriverMetricDescriptor = z.infer<typeof DriverMetricDescriptorSchema>;
24
+
25
+ export const DriverMetricSchema = DriverMetricDescriptorSchema.extend({
26
+ value: z.number().finite(),
27
+ }).strict();
28
+ export type DriverMetric = z.infer<typeof DriverMetricSchema>;
29
+
30
+ export const DriverMetricsSchema = z.record(z.string().min(1).max(120), DriverMetricSchema);
31
+ export type DriverMetrics = z.infer<typeof DriverMetricsSchema>;
32
+
33
+ const ReportedDriverMetricSchema = z.union([z.number().finite(), DriverMetricSchema]);
34
+ export const ReportedDriverMetricsSchema = z.record(
35
+ z.string().min(1).max(120),
36
+ ReportedDriverMetricSchema,
37
+ );
38
+ export type ReportedDriverMetrics = z.input<typeof ReportedDriverMetricsSchema>;
39
+
40
+ export const normalizeDriverMetrics = (value: unknown): DriverMetrics => {
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
42
+ return Object.fromEntries(
43
+ Object.entries(value).flatMap(([name, metric]) => {
44
+ if (name.length === 0 || name.length > 120) return [];
45
+ const parsed = DriverMetricSchema.safeParse(
46
+ typeof metric === "number" ? { value: metric } : metric,
47
+ ).data;
48
+ return parsed ? [[name, parsed]] : [];
49
+ }),
50
+ );
51
+ };
@@ -69,8 +69,8 @@ export const DoneSchema = z.object({
69
69
  exitReason: z.literal("done"),
70
70
  /**
71
71
  * Driver-defined summary. The orchestrator recognizes `usage`, `cost`,
72
- * `modelCalls`, and `unpricedModelCalls` when present while preserving room
73
- * for domain-specific measurements.
72
+ * `modelCalls`, `unpricedModelCalls`, and scalar `metrics` when present while
73
+ * preserving room for other domain-specific evidence.
74
74
  */
75
75
  summary: z.unknown().optional(),
76
76
  });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { DriverMetricDescriptorSchema } from "./driver-metrics.js";
2
3
  import { ConfidenceConfigSchema } from "./statistics.js";
3
4
  import { TokenUsageSchema } from "./usage.js";
4
5
  import { formatZodIssues } from "./zod.js";
@@ -415,6 +416,7 @@ export const CandidateEvaluationSchema = z.object({
415
416
  passRate: z.number().min(0).max(1),
416
417
  casePassRates: z.record(z.string(), z.number().min(0).max(1)),
417
418
  metrics: z.record(z.string(), z.number()),
419
+ metricDefinitions: z.exactOptional(z.record(z.string(), DriverMetricDescriptorSchema)),
418
420
  metricCalls: z.exactOptional(z.number().int().nonnegative()),
419
421
  judgeCalls: z.exactOptional(z.number().int().nonnegative()),
420
422
  unpricedModelCalls: z.exactOptional(z.number().int().nonnegative()),
package/src/core/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./action-events.js";
1
2
  export * from "./annotation.js";
2
3
  export * from "./answerer.js";
3
4
  export * from "./artifact-evidence.js";
@@ -6,6 +7,7 @@ export * from "./assertion.js";
6
7
  export * from "./case.js";
7
8
  export * from "./conversation-turn.js";
8
9
  export * from "./driver.js";
10
+ export * from "./driver-metrics.js";
9
11
  export * from "./emitter.js";
10
12
  export * from "./events.js";
11
13
  export * from "./exec.js";
package/src/core/run.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { ActionTimingSchema } from "./action-events.js";
2
3
  import { EvalCaseSchema } from "./case.js";
3
4
  import { ArtifactSchema } from "./driver.js";
5
+ import { DriverMetricsSchema } from "./driver-metrics.js";
4
6
  import { ErrorEventReasonSchema, ReadyStateSchema, RunEventSchema } from "./events.js";
5
7
  import { ToolOutcomeSummarySchema } from "./tool-events.js";
6
8
  import { RunUsageSchema } from "./usage.js";
@@ -112,6 +114,8 @@ export const RunMetadataSchema = z.object({
112
114
  }),
113
115
  ),
114
116
  cost: z.exactOptional(z.object({ usd: z.exactOptional(z.number()) })),
117
+ /** Driver-defined scalar measurements with display and optimization semantics. */
118
+ driverMetrics: z.exactOptional(DriverMetricsSchema),
115
119
  interruptCounts: z.object({ ask: z.number(), approve: z.number(), custom: z.number() }),
116
120
  /** Legacy raw count of progress events whose kind is `tool-call`. */
117
121
  toolCallCount: z.exactOptional(z.number()),
@@ -124,6 +128,8 @@ export const RunMetadataSchema = z.object({
124
128
  ),
125
129
  /** Wall-clock per phase, derived from the lifecycle event stream. */
126
130
  phaseDurationMs: z.exactOptional(PhaseDurationsSchema),
131
+ /** Driver-reported semantic action latency, measured from `start-driver`. */
132
+ actionTiming: z.exactOptional(ActionTimingSchema),
127
133
  /**
128
134
  * Harness/infrastructure failure that invalidates this run as candidate
129
135
  * evidence. A driver-reported subject failure intentionally leaves this
package/src/index.ts CHANGED
@@ -87,6 +87,8 @@ export {
87
87
  registerCoreAssertions,
88
88
  runSucceededAssertion,
89
89
  subagentToolCalls,
90
+ type TimeToFirstActionUnderConfig,
91
+ TimeToFirstActionUnderConfigSchema,
90
92
  type TokensUnderConfig,
91
93
  TokensUnderConfigSchema,
92
94
  type ToolCall,
@@ -104,6 +106,7 @@ export {
104
106
  ToolResultIsErrorConfigSchema,
105
107
  type ToolResultJsonPointerEqualsConfig,
106
108
  ToolResultJsonPointerEqualsConfigSchema,
109
+ timeToFirstActionUnderAssertion,
107
110
  tokensUnderAssertion,
108
111
  toolCallCountAssertion,
109
112
  toolCalledAssertion,
@@ -141,6 +144,15 @@ export {
141
144
  rubricAssertion,
142
145
  } from "./assertions/rubric/index.js";
143
146
  // Authoring and extension contracts.
147
+ export {
148
+ type ActionCategory,
149
+ ActionCategorySchema,
150
+ type ActionProgressPayload,
151
+ ActionProgressPayloadSchema,
152
+ type ActionTiming,
153
+ ActionTimingSchema,
154
+ actionTiming,
155
+ } from "./core/action-events.js";
144
156
  export {
145
157
  defineGraderAnnotations,
146
158
  type GraderAnnotation,
@@ -227,6 +239,19 @@ export {
227
239
  ResolvedFixturesSchema,
228
240
  type StartContext,
229
241
  } from "./core/driver.js";
242
+ export {
243
+ type DriverMetric,
244
+ type DriverMetricDescriptor,
245
+ DriverMetricDescriptorSchema,
246
+ DriverMetricSchema,
247
+ type DriverMetrics,
248
+ DriverMetricsSchema,
249
+ type DriverMetricUnit,
250
+ DriverMetricUnitSchema,
251
+ normalizeDriverMetrics,
252
+ type ReportedDriverMetrics,
253
+ ReportedDriverMetricsSchema,
254
+ } from "./core/driver-metrics.js";
230
255
  export { type EventHandler, TypedEmitter } from "./core/emitter.js";
231
256
  export {
232
257
  type Answer,
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import {
5
+ type ActionTiming,
6
+ type DriverMetrics,
5
7
  emptyRunUsage,
6
8
  type GradingReport,
7
9
  GradingReportSchema,
@@ -48,6 +50,8 @@ export type RunComparison = {
48
50
  exitReason: { a: string; b: string };
49
51
  durationMsA: number | null;
50
52
  durationMsB: number | null;
53
+ actionTiming: { a: ActionTiming; b: ActionTiming };
54
+ driverMetrics: { a: DriverMetrics; b: DriverMetrics };
51
55
  interruptCounts: {
52
56
  a: { ask: number; approve: number; custom: number };
53
57
  b: { ask: number; approve: number; custom: number };
@@ -260,6 +264,14 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
260
264
  },
261
265
  durationMsA: durationFromMeta(a.runJson),
262
266
  durationMsB: durationFromMeta(b.runJson),
267
+ actionTiming: {
268
+ a: a.runJson?.actionTiming ?? {},
269
+ b: b.runJson?.actionTiming ?? {},
270
+ },
271
+ driverMetrics: {
272
+ a: a.runJson?.driverMetrics ?? {},
273
+ b: b.runJson?.driverMetrics ?? {},
274
+ },
263
275
  interruptCounts: {
264
276
  a: a.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
265
277
  b: b.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
@@ -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),
@@ -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";