@workos/quickstudy 0.7.0 → 0.8.0
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/package.json +1 -1
- package/src/build-info.generated.ts +1 -1
- package/src/evals/discovery.ts +4 -2
- package/src/evals/prompt.ts +39 -1
- package/src/evals/types.ts +14 -2
- package/src/report/aggregate.ts +82 -26
- package/src/report/report.ts +27 -10
- package/ui/dist/index.html +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/quickstudy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@ export const EMBEDDED_BUILD_INFO: {
|
|
|
9
9
|
// (the `x-release-please-version` marker anchors the replacement). It is
|
|
10
10
|
// restored as-is after a binary build (scripts/build-binary.ts), so the
|
|
11
11
|
// committed source `--version` stays in lockstep with package.json.
|
|
12
|
-
version: "0.
|
|
12
|
+
version: "0.8.0", // x-release-please-version
|
|
13
13
|
revision: null,
|
|
14
14
|
dirty: null,
|
|
15
15
|
sourceHash: null,
|
package/src/evals/discovery.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function discoverEvals(root: string): LoadedEval[] {
|
|
|
44
44
|
const promptPath = join(dir, "PROMPT.md");
|
|
45
45
|
if (!existsSync(promptPath)) continue;
|
|
46
46
|
|
|
47
|
-
const { metadata, promptBody, frameworks, local } = parsePrompt(readFileSync(promptPath, "utf8"), promptPath);
|
|
47
|
+
const { metadata, promptBody, frameworks, local, suites } = parsePrompt(readFileSync(promptPath, "utf8"), promptPath);
|
|
48
48
|
|
|
49
49
|
const scorerPath = join(dir, "EVAL.ts");
|
|
50
50
|
if (!existsSync(scorerPath)) {
|
|
@@ -132,7 +132,9 @@ export function discoverEvals(root: string): LoadedEval[] {
|
|
|
132
132
|
}
|
|
133
133
|
collected.push({
|
|
134
134
|
loaded: {
|
|
135
|
-
|
|
135
|
+
// A `suites:` entry retiers one variant (a saturated django port can be
|
|
136
|
+
// smoke while the nextjs port stays benchmark) without renaming its id.
|
|
137
|
+
metadata: { ...metadata, suite: suites?.[framework] ?? metadata.suite, id: `${metadata.id}-${framework}`, framework },
|
|
136
138
|
dir,
|
|
137
139
|
promptPath,
|
|
138
140
|
promptBody,
|
package/src/evals/prompt.ts
CHANGED
|
@@ -33,11 +33,13 @@ export interface ParsedPrompt {
|
|
|
33
33
|
promptBody: string;
|
|
34
34
|
/** `frameworks:` variant list — discovery expands one eval per entry. */
|
|
35
35
|
frameworks?: string[];
|
|
36
|
+
/** Per-framework suite overrides (`suites:`); variants not named keep `suite`. */
|
|
37
|
+
suites?: Record<string, EvalSuite>;
|
|
36
38
|
/** `local:` shared starting-state root, relative to the eval directory. */
|
|
37
39
|
local?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const KNOWN_KEYS = ["id", "suite", "product", "framework", "frameworks", "local", "topics"] as const;
|
|
42
|
+
const KNOWN_KEYS = ["id", "suite", "product", "framework", "frameworks", "suites", "local", "topics"] as const;
|
|
41
43
|
|
|
42
44
|
/** Framework tokens become id suffixes (`<dirname>-<fw>`), so they must be kebab-case. */
|
|
43
45
|
const FRAMEWORK_TOKEN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
@@ -137,6 +139,41 @@ export function parsePrompt(source: string, file: string): ParsedPrompt {
|
|
|
137
139
|
}
|
|
138
140
|
frameworks = tokens;
|
|
139
141
|
}
|
|
142
|
+
let suites: Record<string, EvalSuite> | undefined;
|
|
143
|
+
if (fm["suites"] !== undefined) {
|
|
144
|
+
const value = fm["suites"];
|
|
145
|
+
if (frameworks === undefined) {
|
|
146
|
+
throw new EvalLoadError(
|
|
147
|
+
file,
|
|
148
|
+
keyLine("suites"),
|
|
149
|
+
`"suites" requires "frameworks" — it overrides the suite per framework variant`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
153
|
+
throw new EvalLoadError(file, keyLine("suites"), `"suites" must be a mapping of framework token to suite`);
|
|
154
|
+
}
|
|
155
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
156
|
+
if (entries.length === 0) {
|
|
157
|
+
throw new EvalLoadError(file, keyLine("suites"), `"suites" must name at least one framework`);
|
|
158
|
+
}
|
|
159
|
+
for (const [token, suiteValue] of entries) {
|
|
160
|
+
if (!frameworks.includes(token)) {
|
|
161
|
+
throw new EvalLoadError(
|
|
162
|
+
file,
|
|
163
|
+
keyLine("suites"),
|
|
164
|
+
`"suites" names framework ${JSON.stringify(token)}, which is not in "frameworks"`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (typeof suiteValue !== "string" || !(EVAL_SUITES as readonly string[]).includes(suiteValue)) {
|
|
168
|
+
throw new EvalLoadError(
|
|
169
|
+
file,
|
|
170
|
+
keyLine("suites"),
|
|
171
|
+
`"suites" entry for ${JSON.stringify(token)} must be one of: ${EVAL_SUITES.join(", ")} (got ${JSON.stringify(suiteValue)})`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
suites = value as Record<string, EvalSuite>;
|
|
176
|
+
}
|
|
140
177
|
let local: string | undefined;
|
|
141
178
|
if (fm["local"] !== undefined) {
|
|
142
179
|
const value = fm["local"];
|
|
@@ -185,6 +222,7 @@ export function parsePrompt(source: string, file: string): ParsedPrompt {
|
|
|
185
222
|
metadata,
|
|
186
223
|
promptBody,
|
|
187
224
|
...(frameworks !== undefined ? { frameworks } : {}),
|
|
225
|
+
...(suites !== undefined ? { suites } : {}),
|
|
188
226
|
...(local !== undefined ? { local } : {}),
|
|
189
227
|
};
|
|
190
228
|
}
|
package/src/evals/types.ts
CHANGED
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
* plain TypeScript handed an {@link EvalContext} over the finished workspace.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Closed set of eval suites. `benchmark` and `regression` are the headline
|
|
10
|
+
* suites: the leaderboard aggregates count both. `smoke` holds evals every
|
|
11
|
+
* arm solves — run to catch breakage, never moving the headline. `other` is
|
|
12
|
+
* anything else outside the headline.
|
|
13
|
+
*/
|
|
14
|
+
export const EVAL_SUITES = ["benchmark", "regression", "smoke", "other"] as const;
|
|
10
15
|
export type EvalSuite = (typeof EVAL_SUITES)[number];
|
|
11
16
|
|
|
17
|
+
/** The suites whose attempts count toward the leaderboard aggregates. */
|
|
18
|
+
export const HEADLINE_SUITES: readonly EvalSuite[] = ["benchmark", "regression"];
|
|
19
|
+
|
|
20
|
+
export function isHeadlineSuite(suite: string): boolean {
|
|
21
|
+
return (HEADLINE_SUITES as readonly string[]).includes(suite);
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/** PROMPT.md frontmatter. New fields require a proven filtering/reporting use case. */
|
|
13
25
|
export interface EvalMetadata {
|
|
14
26
|
/** Must equal the eval's directory name (prevents silent aliasing). */
|
package/src/report/aggregate.ts
CHANGED
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
* Per-experiment leaderboard aggregate — one row per experiment, derived from
|
|
3
3
|
* the same attempts and (banded) pairs as the rest of the report.
|
|
4
4
|
*
|
|
5
|
-
* Every number is restricted to
|
|
6
|
-
*
|
|
7
|
-
* rule: sums and means cover
|
|
8
|
-
* null (never 0) when none did.
|
|
5
|
+
* Every number is restricted to headline-suite attempts (HEADLINE_SUITES:
|
|
6
|
+
* benchmark and regression), so smoke and other suites never move the
|
|
7
|
+
* headline row. Telemetry follows the report's rule: sums and means cover
|
|
8
|
+
* only attempts that reported a value, and are null (never 0) when none did.
|
|
9
9
|
*/
|
|
10
|
+
import { isHeadlineSuite } from "../evals/types.ts";
|
|
10
11
|
import type { AttemptRecord } from "../store/db.ts";
|
|
11
12
|
import type { RunCostEstimate } from "./costs.ts";
|
|
12
13
|
import type { ReportExperiment, ReportPair } from "./report.ts";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The pass-rate gate: an experiment whose headline pass rate is below this
|
|
17
|
+
* is listed after every experiment that clears it, whatever its cost or
|
|
18
|
+
* time. Pass rate is a gate, not a rank — the leaderboard orders the arms
|
|
19
|
+
* that clear it by time-to-pass, then cost-to-pass.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_PASS_GATE = 0.8;
|
|
22
|
+
|
|
14
23
|
export interface ExperimentAggregate {
|
|
15
|
-
/**
|
|
24
|
+
/** Headline-suite attempts of this experiment (every status). */
|
|
16
25
|
attempts: number;
|
|
17
26
|
completed: number;
|
|
18
27
|
passed: number;
|
|
@@ -21,11 +30,11 @@ export interface ExperimentAggregate {
|
|
|
21
30
|
errors: number;
|
|
22
31
|
/** Attempts whose agent phase hit the timeout limit (graded timeouts are also completed). */
|
|
23
32
|
timeouts: number;
|
|
24
|
-
/** passed / (passed + failed) over
|
|
33
|
+
/** passed / (passed + failed) over headline-suite pairs; null when nothing scored. */
|
|
25
34
|
pass_rate_all: number | null;
|
|
26
35
|
/** Same, restricted to evals whose band is "discriminating". */
|
|
27
36
|
pass_rate_discriminating: number | null;
|
|
28
|
-
/** Discriminating
|
|
37
|
+
/** Discriminating headline evals where this experiment scored at least one attempt. */
|
|
29
38
|
discriminating_evals: number;
|
|
30
39
|
/** Numerator and denominator of pass_rate_discriminating. */
|
|
31
40
|
discriminating_passed: number;
|
|
@@ -40,9 +49,25 @@ export interface ExperimentAggregate {
|
|
|
40
49
|
/** Provider-reported, summed over the attempts that reported it. */
|
|
41
50
|
cost_usd: number | null;
|
|
42
51
|
cost_attempts: number;
|
|
52
|
+
/**
|
|
53
|
+
* Cost-to-pass: reported cost summed over the attempts that reported it,
|
|
54
|
+
* divided by the passes among those same attempts — what one success cost,
|
|
55
|
+
* failures included. null when nothing reported cost or none of it passed.
|
|
56
|
+
*/
|
|
57
|
+
cost_per_pass_usd: number | null;
|
|
58
|
+
/**
|
|
59
|
+
* Time-to-pass: agent time summed over the scored attempts that recorded
|
|
60
|
+
* both stamps, divided by the passes among them. null when none did, or
|
|
61
|
+
* none of them passed.
|
|
62
|
+
*/
|
|
63
|
+
agent_ms_per_pass: number | null;
|
|
64
|
+
/** pass_rate_all >= the report's pass gate; null when nothing scored. */
|
|
65
|
+
clears_gate: boolean | null;
|
|
43
66
|
/** Present only when the report was built with --pricing. */
|
|
44
67
|
estimated_cost_usd?: number | null;
|
|
45
68
|
estimated_attempts?: number;
|
|
69
|
+
/** --pricing's estimate over the estimated attempts, divided by their passes. */
|
|
70
|
+
estimated_cost_per_pass_usd?: number | null;
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
const sum = (values: number[]): number => values.reduce((total, v) => total + v, 0);
|
|
@@ -66,33 +91,44 @@ export function collectExperimentAggregate(
|
|
|
66
91
|
pairs: ReportPair[],
|
|
67
92
|
experimentId: string,
|
|
68
93
|
costEstimate?: RunCostEstimate,
|
|
94
|
+
passGate: number = DEFAULT_PASS_GATE,
|
|
69
95
|
): ExperimentAggregate {
|
|
70
|
-
const mine = attempts.filter((a) => a.experimentId === experimentId && a.suite
|
|
96
|
+
const mine = attempts.filter((a) => a.experimentId === experimentId && isHeadlineSuite(a.suite));
|
|
71
97
|
const completed = mine.filter((a) => a.status === "completed");
|
|
72
98
|
const passed = completed.filter((a) => a.result?.passed).length;
|
|
73
99
|
|
|
74
|
-
const
|
|
75
|
-
const scoredAll = sum(
|
|
76
|
-
const passedAll = sum(
|
|
77
|
-
const discriminating =
|
|
100
|
+
const headlinePairs = pairs.filter((p) => p.experiment_id === experimentId && isHeadlineSuite(p.suite));
|
|
101
|
+
const scoredAll = sum(headlinePairs.map((p) => p.scored));
|
|
102
|
+
const passedAll = sum(headlinePairs.map((p) => p.passed));
|
|
103
|
+
const discriminating = headlinePairs.filter((p) => p.band === "discriminating" && p.scored > 0);
|
|
78
104
|
const discScored = sum(discriminating.map((p) => p.scored));
|
|
79
105
|
const discPassed = sum(discriminating.map((p) => p.passed));
|
|
80
106
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
);
|
|
107
|
+
const stamped = completed.filter((a) => typeof a.agentStartedAt === "number" && typeof a.agentFinishedAt === "number");
|
|
108
|
+
const durations = stamped.map((a) => (a.agentFinishedAt as number) - (a.agentStartedAt as number));
|
|
109
|
+
const stampedPasses = stamped.filter((a) => a.result?.passed).length;
|
|
84
110
|
const tokensIn = mine.map((a) => a.tokensIn).filter((v): v is number => v !== null && v !== undefined);
|
|
85
111
|
const tokensOut = mine.map((a) => a.tokensOut).filter((v): v is number => v !== null && v !== undefined);
|
|
86
|
-
const
|
|
112
|
+
const costed = mine.filter((a) => a.costUsd !== null && a.costUsd !== undefined);
|
|
113
|
+
const costs = costed.map((a) => a.costUsd as number);
|
|
114
|
+
const costedPasses = costed.filter((a) => a.status === "completed" && a.result?.passed).length;
|
|
87
115
|
|
|
88
|
-
let estimated: Pick<ExperimentAggregate, "estimated_cost_usd" | "estimated_attempts"> = {};
|
|
116
|
+
let estimated: Pick<ExperimentAggregate, "estimated_cost_usd" | "estimated_attempts" | "estimated_cost_per_pass_usd"> = {};
|
|
89
117
|
if (costEstimate !== undefined) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
const byId = new Map(mine.map((a) => [a.id, a]));
|
|
119
|
+
const priced = costEstimate.attempts.filter((e) => byId.has(e.attempt_id) && e.cost_usd !== null);
|
|
120
|
+
const values = priced.map((e) => e.cost_usd as number);
|
|
121
|
+
const pricedPasses = priced.filter((e) => {
|
|
122
|
+
const attempt = byId.get(e.attempt_id);
|
|
123
|
+
return attempt?.status === "completed" && attempt.result?.passed;
|
|
124
|
+
}).length;
|
|
125
|
+
estimated = {
|
|
126
|
+
estimated_cost_usd: values.length > 0 ? sum(values) : null,
|
|
127
|
+
estimated_attempts: values.length,
|
|
128
|
+
estimated_cost_per_pass_usd: values.length > 0 && pricedPasses > 0 ? sum(values) / pricedPasses : null,
|
|
129
|
+
};
|
|
95
130
|
}
|
|
131
|
+
const passRateAll = scoredAll > 0 ? passedAll / scoredAll : null;
|
|
96
132
|
|
|
97
133
|
return {
|
|
98
134
|
attempts: mine.length,
|
|
@@ -102,7 +138,7 @@ export function collectExperimentAggregate(
|
|
|
102
138
|
incomplete: mine.filter((a) => a.status === "incomplete").length,
|
|
103
139
|
errors: mine.filter((a) => a.status === "error").length,
|
|
104
140
|
timeouts: mine.filter((a) => a.status === "completed" && a.limitHit === "timeout").length,
|
|
105
|
-
pass_rate_all:
|
|
141
|
+
pass_rate_all: passRateAll,
|
|
106
142
|
pass_rate_discriminating: discScored > 0 ? discPassed / discScored : null,
|
|
107
143
|
discriminating_evals: new Set(discriminating.map((p) => p.eval_id)).size,
|
|
108
144
|
discriminating_passed: discPassed,
|
|
@@ -114,21 +150,41 @@ export function collectExperimentAggregate(
|
|
|
114
150
|
token_attempts: Math.max(tokensIn.length, tokensOut.length),
|
|
115
151
|
cost_usd: costs.length > 0 ? sum(costs) : null,
|
|
116
152
|
cost_attempts: costs.length,
|
|
153
|
+
cost_per_pass_usd: costs.length > 0 && costedPasses > 0 ? sum(costs) / costedPasses : null,
|
|
154
|
+
agent_ms_per_pass: durations.length > 0 && stampedPasses > 0 ? sum(durations) / stampedPasses : null,
|
|
155
|
+
clears_gate: passRateAll === null ? null : passRateAll >= passGate,
|
|
117
156
|
...estimated,
|
|
118
157
|
};
|
|
119
158
|
}
|
|
120
159
|
|
|
160
|
+
/** Ascending with nulls (and absent values) last; equal when both are absent. */
|
|
161
|
+
function ascending(a: number | null | undefined, b: number | null | undefined): number {
|
|
162
|
+
const x = a ?? null;
|
|
163
|
+
const y = b ?? null;
|
|
164
|
+
if (x === null && y === null) return 0;
|
|
165
|
+
if (x === null) return 1;
|
|
166
|
+
if (y === null) return -1;
|
|
167
|
+
return x - y;
|
|
168
|
+
}
|
|
169
|
+
|
|
121
170
|
/**
|
|
122
171
|
* Leaderboard order: agent arms before controls (the oracle would otherwise
|
|
123
|
-
* lead every board)
|
|
124
|
-
*
|
|
125
|
-
*
|
|
172
|
+
* lead every board); arms that clear the pass gate before those that do not;
|
|
173
|
+
* then time-to-pass ascending (every adapter reports agent time), then
|
|
174
|
+
* cost-to-pass ascending (null where the provider reports no dollars sorts
|
|
175
|
+
* last); then discriminating and all-eval pass rate desc as tie-breaks; then
|
|
176
|
+
* experiment id — a total order, so every consumer renders the same rows in
|
|
177
|
+
* the same sequence.
|
|
126
178
|
*/
|
|
127
179
|
export function compareLeaderboard(a: ReportExperiment, b: ReportExperiment): number {
|
|
128
180
|
const rate = (value: number | null | undefined): number => (value === null || value === undefined ? -1 : value);
|
|
129
181
|
const control = (experiment: ReportExperiment): number => ((experiment.role ?? "agent") === "agent" ? 0 : 1);
|
|
182
|
+
const gate = (experiment: ReportExperiment): number => (experiment.aggregate?.clears_gate === true ? 0 : 1);
|
|
130
183
|
return (
|
|
131
184
|
control(a) - control(b) ||
|
|
185
|
+
gate(a) - gate(b) ||
|
|
186
|
+
ascending(a.aggregate?.agent_ms_per_pass, b.aggregate?.agent_ms_per_pass) ||
|
|
187
|
+
ascending(a.aggregate?.cost_per_pass_usd, b.aggregate?.cost_per_pass_usd) ||
|
|
132
188
|
rate(b.aggregate?.pass_rate_discriminating) - rate(a.aggregate?.pass_rate_discriminating) ||
|
|
133
189
|
rate(b.aggregate?.pass_rate_all) - rate(a.aggregate?.pass_rate_all) ||
|
|
134
190
|
a.experiment_id.localeCompare(b.experiment_id)
|
package/src/report/report.ts
CHANGED
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
type ExperimentRole,
|
|
40
40
|
type ScorerHealth,
|
|
41
41
|
} from "./scorer-health.ts";
|
|
42
|
-
import { collectExperimentAggregate, compareLeaderboard, type ExperimentAggregate } from "./aggregate.ts";
|
|
42
|
+
import { collectExperimentAggregate, compareLeaderboard, DEFAULT_PASS_GATE, type ExperimentAggregate } from "./aggregate.ts";
|
|
43
|
+
import { HEADLINE_SUITES } from "../evals/types.ts";
|
|
43
44
|
|
|
44
45
|
export type { EvalBand, ExperimentRole, ScorerHealth, ScorerHealthSuspect } from "./scorer-health.ts";
|
|
45
46
|
export type { ExperimentAggregate } from "./aggregate.ts";
|
|
@@ -53,7 +54,7 @@ export class ReportError extends Error {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
export const REPORT_SCHEMA_VERSION =
|
|
57
|
+
export const REPORT_SCHEMA_VERSION = 13;
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* Substrings that must never appear in generated report prose: at n≤5 trials
|
|
@@ -69,7 +70,7 @@ export function findBannedLanguage(text: string): string[] {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Methodology is generated prose — it must itself pass the language guard. */
|
|
72
|
-
export function methodologyNote(trials: number): string {
|
|
73
|
+
export function methodologyNote(trials: number, passGate: number = DEFAULT_PASS_GATE): string {
|
|
73
74
|
return (
|
|
74
75
|
`Pass rates are per eval × experiment pair: passed / (passed + failed). Incomplete and errored ` +
|
|
75
76
|
`attempts are counted separately and never enter that denominator. Each pair has ${trials} planned ` +
|
|
@@ -88,7 +89,12 @@ export function methodologyNote(trials: number): string {
|
|
|
88
89
|
`about the instrument, not about any treatment. Control experiments — an oracle that applies each eval's ` +
|
|
89
90
|
`reference solution, a no-op that changes nothing — are reported alongside the agent experiments and ` +
|
|
90
91
|
`tagged as controls wherever they appear; bands and scorer health are decided over the agent experiments ` +
|
|
91
|
-
`only, and controls are ranked after every agent experiment
|
|
92
|
+
`only, and controls are ranked after every agent experiment. Pass rate is a gate, not a rank: an experiment ` +
|
|
93
|
+
`whose headline pass rate is below ${Math.round(passGate * 100)}% is listed after every ` +
|
|
94
|
+
`experiment that clears it, and the experiments that clear it are ordered by time-to-pass (agent time summed ` +
|
|
95
|
+
`over the scored attempts that recorded it, divided by the passes among them) and then cost-to-pass (reported ` +
|
|
96
|
+
`cost summed over the attempts that reported it, divided by the passes among them) — what one success took, ` +
|
|
97
|
+
`failures included. Both are null, never zero, where nothing was recorded.`
|
|
92
98
|
);
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -134,7 +140,7 @@ export interface ReportExperiment {
|
|
|
134
140
|
*/
|
|
135
141
|
sdk_recall?: ReportRecall;
|
|
136
142
|
/**
|
|
137
|
-
* Leaderboard row over
|
|
143
|
+
* Leaderboard row over headline-suite (benchmark + regression) attempts. Optional on the wire:
|
|
138
144
|
* absent in schema_version < 9 artifacts; readers must tolerate absence.
|
|
139
145
|
*/
|
|
140
146
|
aggregate?: ExperimentAggregate;
|
|
@@ -310,6 +316,8 @@ export interface ReportJson {
|
|
|
310
316
|
/** v6: empty denial logs are observed zero; absent logs are unavailable. */
|
|
311
317
|
egress_observation?: { available: string[]; unavailable: string[] };
|
|
312
318
|
methodology: string;
|
|
319
|
+
/** The pass-rate gate the leaderboard order applies (see methodology). */
|
|
320
|
+
pass_gate: number;
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
// ---------------------------------------------------------------------------
|
|
@@ -652,7 +660,7 @@ export function buildReport(options: BuildReportOptions): ReportJson {
|
|
|
652
660
|
const scorerHealth = collectScorerHealth(attempts, pairs, controls);
|
|
653
661
|
const costEstimate = options.pricing === undefined ? undefined : estimateRunCosts(run, attempts, options.resultsDir, options.pricing);
|
|
654
662
|
|
|
655
|
-
const methodology = methodologyNote(run.config.trials) + (costEstimate === undefined ? "" :
|
|
663
|
+
const methodology = methodologyNote(run.config.trials, DEFAULT_PASS_GATE) + (costEstimate === undefined ? "" :
|
|
656
664
|
" Optional token-cost estimates supplement reported cost using the supplied pricing snapshot. They cover only the selected attempts, not superseded retries, tool fees, diagnosis or infrastructure, and are not billing records.");
|
|
657
665
|
const banned = findBannedLanguage(methodology);
|
|
658
666
|
if (banned.length > 0) {
|
|
@@ -695,6 +703,7 @@ export function buildReport(options: BuildReportOptions): ReportJson {
|
|
|
695
703
|
unavailable: attempts.filter((a) => !existsSync(join(options.resultsDir, runId, a.id, "egress-denials.log"))).map((a) => a.id),
|
|
696
704
|
},
|
|
697
705
|
methodology,
|
|
706
|
+
pass_gate: DEFAULT_PASS_GATE,
|
|
698
707
|
};
|
|
699
708
|
}
|
|
700
709
|
|
|
@@ -707,12 +716,15 @@ function pct(rate: number | null): string {
|
|
|
707
716
|
}
|
|
708
717
|
|
|
709
718
|
function renderLeaderboard(experiments: ReportExperiment[]): string[] {
|
|
710
|
-
const header = ["experiment", "model", "pass (all/disc)", "agent median", "tokens in", "cost", "recall"];
|
|
719
|
+
const header = ["experiment", "model", "gate", "$/pass", "time/pass", "pass (all/disc)", "agent median", "tokens in", "cost", "recall"];
|
|
711
720
|
const rows = [...experiments].sort(compareLeaderboard).map((experiment) => {
|
|
712
721
|
const agg = experiment.aggregate;
|
|
713
722
|
const model = experiment.agent.model === "unspecified" ? "unpinned" : experiment.agent.model;
|
|
714
723
|
const reasoning = experiment.agent.reasoning === "unspecified" ? "" : ` · ${experiment.agent.reasoning}`;
|
|
715
|
-
if (!agg) return [experiment.experiment_id, `${model}${reasoning}`, "n/a", "n/a", "n/a", "n/a", "n/a"];
|
|
724
|
+
if (!agg) return [experiment.experiment_id, `${model}${reasoning}`, "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a"];
|
|
725
|
+
const gate = agg.clears_gate === null ? "n/a" : agg.clears_gate ? "clears" : "below";
|
|
726
|
+
const perPass = agg.cost_per_pass_usd === null ? "n/a" : `$${agg.cost_per_pass_usd.toFixed(2)}`;
|
|
727
|
+
const timePerPass = agg.agent_ms_per_pass === null ? "n/a" : `${(agg.agent_ms_per_pass / 1000).toFixed(1)}s`;
|
|
716
728
|
const all = agg.pass_rate_all === null ? "n/a" : `${agg.passed}/${agg.passed + agg.failed}`;
|
|
717
729
|
const disc = agg.pass_rate_discriminating === null ? "n/a" : `${agg.discriminating_passed}/${agg.discriminating_scored}`;
|
|
718
730
|
const time = agg.median_agent_ms === null ? "n/a" : `${(agg.median_agent_ms / 1000).toFixed(1)}s (${agg.agent_time_attempts})`;
|
|
@@ -723,11 +735,16 @@ function renderLeaderboard(experiments: ReportExperiment[]): string[] {
|
|
|
723
735
|
const recall = experiment.sdk_recall;
|
|
724
736
|
const recallText = recall && recall.rate !== null ? `${recall.recalled}/${recall.attempts}` : "n/a";
|
|
725
737
|
const id = (experiment.role ?? "agent") === "agent" ? experiment.experiment_id : `${experiment.experiment_id} (control)`;
|
|
726
|
-
return [id, `${model}${reasoning}`, `${all} · ${disc}`, time, tokens, cost, recallText];
|
|
738
|
+
return [id, `${model}${reasoning}`, gate, perPass, timePerPass, `${all} · ${disc}`, time, tokens, cost, recallText];
|
|
727
739
|
});
|
|
728
740
|
const widths = header.map((title, i) => Math.max(title.length, ...rows.map((row) => row[i]!.length)));
|
|
729
741
|
const format = (cells: string[]): string => ` ${cells.map((cell, i) => cell.padEnd(widths[i]!)).join(" ").trimEnd()}`;
|
|
730
|
-
return [
|
|
742
|
+
return [
|
|
743
|
+
`leaderboard (headline suites ${HEADLINE_SUITES.join(" + ")}; gate: pass rate ≥ ${Math.round(DEFAULT_PASS_GATE * 100)}%; ` +
|
|
744
|
+
`ordered by time-to-pass, then cost-to-pass; controls last):`,
|
|
745
|
+
format(header),
|
|
746
|
+
...rows.map(format),
|
|
747
|
+
];
|
|
731
748
|
}
|
|
732
749
|
|
|
733
750
|
/** Render the report as plain text. Renders FROM the JSON, never the store. */
|