@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.
- package/lib/assertions/authoring.d.ts +2 -2
- package/lib/assertions/authoring.d.ts.map +1 -1
- package/lib/assertions/authoring.js.map +1 -1
- package/lib/assertions/core/index.d.ts.map +1 -1
- package/lib/assertions/core/index.js +2 -1
- package/lib/assertions/core/index.js.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +19 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.js +32 -0
- package/lib/assertions/core/resource-checks.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +6 -65
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/view.d.ts +13 -0
- package/lib/cli/view.d.ts.map +1 -0
- package/lib/cli/view.js +126 -0
- package/lib/cli/view.js.map +1 -0
- package/lib/core/action-events.d.ts +29 -0
- package/lib/core/action-events.d.ts.map +1 -0
- package/lib/core/action-events.js +51 -0
- package/lib/core/action-events.js.map +1 -0
- package/lib/core/driver-metrics.d.ts +98 -0
- package/lib/core/driver-metrics.d.ts.map +1 -0
- package/lib/core/driver-metrics.js +36 -0
- package/lib/core/driver-metrics.js.map +1 -0
- package/lib/core/events.js +2 -2
- package/lib/core/experiment.d.ts +19 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +2 -0
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +2 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/run.d.ts +50 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/run.js +6 -0
- package/lib/core/run.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/compare.d.ts +9 -1
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +8 -0
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +19 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +17 -0
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +19 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +38 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/index.d.ts +1 -1
- package/lib/orchestrator/index.d.ts.map +1 -1
- package/lib/orchestrator/index.js +1 -1
- package/lib/orchestrator/index.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts +6 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +15 -1
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +11 -1
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts +34 -0
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +68 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +22 -7
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/artifacts.d.ts +25 -0
- package/lib/previewer/artifacts.d.ts.map +1 -0
- package/lib/previewer/artifacts.js +180 -0
- package/lib/previewer/artifacts.js.map +1 -0
- package/lib/previewer/server.d.ts +104 -0
- package/lib/previewer/server.d.ts.map +1 -0
- package/lib/previewer/server.js +541 -0
- package/lib/previewer/server.js.map +1 -0
- package/package.json +2 -1
- package/skills/lux/references/eval-authoring.md +4 -2
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/assertions/authoring.ts +2 -0
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/cli/program.ts +5 -91
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/events.ts +2 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/run.ts +6 -0
- package/src/index.ts +25 -0
- package/src/orchestrator/compare.ts +12 -0
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +25 -0
- package/src/orchestrator/run-execution.ts +12 -0
- package/src/orchestrator/suite-summary.ts +36 -8
- package/src/previewer/artifacts.ts +221 -0
- package/src/previewer/server.ts +763 -0
- package/viewer/app.css +2022 -0
- package/viewer/app.js +601 -0
- package/viewer/artifact-view.js +48 -0
- package/viewer/charts.js +86 -0
- package/viewer/detail.js +1108 -0
- package/viewer/format.js +129 -0
- package/viewer/index.html +151 -0
|
@@ -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"),
|
package/src/cli/program.ts
CHANGED
|
@@ -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>")
|
package/src/cli/view.ts
ADDED
|
@@ -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
|
+
};
|
package/src/core/events.ts
CHANGED
|
@@ -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 `
|
|
73
|
-
* for domain-specific
|
|
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
|
});
|
package/src/core/experiment.ts
CHANGED
|
@@ -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),
|