@cassiomc1/forgeloop 1.7.0 → 1.8.1
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/DOCS_INDEX.md +1 -1
- package/benchmarks/execution-profiles/README.md +18 -0
- package/benchmarks/execution-profiles/api-feature.json +1 -1
- package/benchmarks/execution-profiles/authentication-change.json +1 -1
- package/benchmarks/execution-profiles/documentation-correction.json +1 -1
- package/benchmarks/execution-profiles/infrastructure-release.json +1 -1
- package/benchmarks/execution-profiles/novatask-saas-landing-page.json +1 -1
- package/benchmarks/execution-profiles/small-bug-fix.json +1 -1
- package/benchmarks/execution-profiles/static-landing-page.json +1 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +1 -1
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +218 -3
- package/docs/MCP.md +1 -1
- package/docs/RELEASE_CHECKLIST.md +12 -0
- package/package.json +7 -2
- package/schemas/execution-profile-benchmark-aggregate.schema.json +51 -1
- package/schemas/execution-profile-benchmark-run.schema.json +40 -2
- package/scripts/report-execution-profile-outliers.mjs +136 -0
- package/scripts/report-tail-interpretation.mjs +156 -0
- package/scripts/run-execution-profile-benchmarks.mjs +112 -36
- package/src/core/execution-profile-benchmarks.js +509 -35
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
analyzeTokenOutliers,
|
|
8
|
+
BENCHMARK_MODES,
|
|
9
|
+
BENCHMARK_VERSION,
|
|
10
|
+
} from "../src/core/execution-profile-benchmarks.js";
|
|
11
|
+
import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
|
|
12
|
+
|
|
13
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const options = { results: defaultResultsDirectory, runSetId: null, json: false };
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const argument = argv[index];
|
|
20
|
+
if (argument === "--json") options.json = true;
|
|
21
|
+
else if (["--results", "--run-set"].includes(argument)) {
|
|
22
|
+
const value = argv[++index];
|
|
23
|
+
if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value`);
|
|
24
|
+
if (argument === "--results") options.results = value;
|
|
25
|
+
else options.runSetId = value;
|
|
26
|
+
} else if (argument === "--help" || argument === "-h") options.help = true;
|
|
27
|
+
else throw new Error(`unknown option: ${argument}`);
|
|
28
|
+
}
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function helpText() {
|
|
33
|
+
return "Usage: npm run benchmark:profiles:outliers -- [--results <directory>] [--run-set <id>] [--json]";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function diagnosticsValue(run, field) {
|
|
37
|
+
return run?.diagnostics?.[field] ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildRows(runSet, scenario) {
|
|
41
|
+
const runs = runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId);
|
|
42
|
+
if (runs.length === 0) return [];
|
|
43
|
+
const byMode = Object.fromEntries(BENCHMARK_MODES.map((mode) => [
|
|
44
|
+
mode,
|
|
45
|
+
runs.filter((run) => run.mode === mode).sort((left, right) => left.runIndex - right.runIndex),
|
|
46
|
+
]));
|
|
47
|
+
const runById = new Map(runs.map((run) => [run.runId, run]));
|
|
48
|
+
const analysis = analyzeTokenOutliers(byMode);
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const mode of BENCHMARK_MODES) {
|
|
51
|
+
const modeAnalysis = analysis.modes[mode];
|
|
52
|
+
for (const outlier of modeAnalysis.outliers) {
|
|
53
|
+
const run = runById.get(outlier.runId);
|
|
54
|
+
rows.push({
|
|
55
|
+
scenarioId: scenario.scenarioId,
|
|
56
|
+
mode,
|
|
57
|
+
runId: outlier.runId,
|
|
58
|
+
totalTokens: outlier.totalTokens,
|
|
59
|
+
scenarioMedianTokens: outlier.scenarioMedianTokens,
|
|
60
|
+
ratioToMedian: outlier.ratioToMedian,
|
|
61
|
+
reasons: outlier.reasons,
|
|
62
|
+
diagnosticSignals: outlier.diagnosticSignals,
|
|
63
|
+
verificationCycles: run?.verificationCycles ?? diagnosticsValue(run, "verificationCycles"),
|
|
64
|
+
modelTurns: diagnosticsValue(run, "modelTurns"),
|
|
65
|
+
toolCalls: diagnosticsValue(run, "toolCalls"),
|
|
66
|
+
retries: diagnosticsValue(run, "retries"),
|
|
67
|
+
correctionCycles: diagnosticsValue(run, "correctionCycles"),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatValue(value) {
|
|
75
|
+
return value === null || value === undefined ? "-" : String(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
const options = parseArgs(process.argv.slice(2));
|
|
80
|
+
if (options.help) {
|
|
81
|
+
console.log(helpText());
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const scenarios = await readBenchmarkScenarios(repositoryRoot);
|
|
85
|
+
const runSets = await readBenchmarkRunSets(path.resolve(options.results), options.runSetId);
|
|
86
|
+
const reports = [];
|
|
87
|
+
for (const runSet of runSets) {
|
|
88
|
+
for (const scenario of scenarios) {
|
|
89
|
+
const rows = buildRows(runSet, scenario);
|
|
90
|
+
if (rows.length > 0) reports.push({ runSetId: runSet.runSetId, scenarioId: scenario.scenarioId, outliers: rows });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const outlierCount = reports.reduce((sum, report) => sum + report.outliers.length, 0);
|
|
94
|
+
const output = {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
97
|
+
policy: "TOKEN_IQR_1_5",
|
|
98
|
+
runSetCount: runSets.length,
|
|
99
|
+
outlierCount,
|
|
100
|
+
reports,
|
|
101
|
+
note: "Outlier classification is a benchmark diagnostic. It never changes lifecycle truth or completion validity.",
|
|
102
|
+
};
|
|
103
|
+
if (options.json) {
|
|
104
|
+
console.log(JSON.stringify(output));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (outlierCount === 0) {
|
|
108
|
+
console.log("No token outliers detected under the TOKEN_IQR_1_5 policy.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const header = ["Scenario", "Mode", "Run ID", "Tokens", "Median", "Ratio", "VerCycles", "Turns", "ToolCalls", "Retries", "Corrections", "Signals"];
|
|
112
|
+
console.log(header.join("\t"));
|
|
113
|
+
for (const report of reports) {
|
|
114
|
+
for (const row of report.outliers) {
|
|
115
|
+
console.log([
|
|
116
|
+
row.scenarioId,
|
|
117
|
+
row.mode,
|
|
118
|
+
row.runId,
|
|
119
|
+
formatValue(row.totalTokens),
|
|
120
|
+
formatValue(row.scenarioMedianTokens),
|
|
121
|
+
formatValue(row.ratioToMedian),
|
|
122
|
+
formatValue(row.verificationCycles),
|
|
123
|
+
formatValue(row.modelTurns),
|
|
124
|
+
formatValue(row.toolCalls),
|
|
125
|
+
formatValue(row.retries),
|
|
126
|
+
formatValue(row.correctionCycles),
|
|
127
|
+
row.diagnosticSignals.length > 0 ? row.diagnosticSignals.join(",") : "-",
|
|
128
|
+
].join("\t"));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((error) => {
|
|
134
|
+
console.error(`ForgeLoop benchmark outlier report: ${error.message}`);
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
aggregateBenchmarkRuns,
|
|
8
|
+
BENCHMARK_VERSION,
|
|
9
|
+
} from "../src/core/execution-profile-benchmarks.js";
|
|
10
|
+
import { readBenchmarkRunSets, readBenchmarkScenarios } from "./lib/execution-profile-benchmark-io.mjs";
|
|
11
|
+
|
|
12
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const defaultResultsDirectory = path.join(repositoryRoot, "benchmarks", "execution-profiles", "results");
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const options = { results: defaultResultsDirectory, runSetId: null, json: false };
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const argument = argv[index];
|
|
19
|
+
if (argument === "--json") options.json = true;
|
|
20
|
+
else if (["--results", "--run-set"].includes(argument)) {
|
|
21
|
+
const value = argv[++index];
|
|
22
|
+
if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value`);
|
|
23
|
+
if (argument === "--results") options.results = value;
|
|
24
|
+
else options.runSetId = value;
|
|
25
|
+
} else if (argument === "--help" || argument === "-h") options.help = true;
|
|
26
|
+
else throw new Error(`unknown option: ${argument}`);
|
|
27
|
+
}
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function helpText() {
|
|
32
|
+
return "Usage: npm run benchmark:profiles:tail-analysis -- [--results <directory>] [--run-set <id>] [--json]";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildScenarioReport(aggregate) {
|
|
36
|
+
const directAgg = aggregate.modeAggregates?.direct;
|
|
37
|
+
const adaptiveComp = aggregate.comparisons?.forgeloopAdaptive;
|
|
38
|
+
const adaptiveAgg = aggregate.modeAggregates?.forgeloopAdaptive;
|
|
39
|
+
|
|
40
|
+
const directTokens = directAgg?.totalTokens ?? {};
|
|
41
|
+
const adaptiveTokens = adaptiveAgg?.totalTokens ?? {};
|
|
42
|
+
|
|
43
|
+
const paired = adaptiveComp?.pairedOverheadPercent ?? {};
|
|
44
|
+
const dist = adaptiveComp?.distributionDeltaPercent ?? {};
|
|
45
|
+
const tail = adaptiveComp?.tail ?? {};
|
|
46
|
+
const diag = adaptiveComp?.pairedRatioDiagnostics ?? {};
|
|
47
|
+
|
|
48
|
+
const hasComparableRuns = (adaptiveComp?.pairedRuns?.length ?? 0) > 0;
|
|
49
|
+
let compDirectP50 = diag.baselineP50 ?? directTokens.p50 ?? null;
|
|
50
|
+
let compDirectP95 = directTokens.p95 ?? null;
|
|
51
|
+
let compAdaptiveP50 = adaptiveTokens.p50 ?? null;
|
|
52
|
+
let compAdaptiveP95 = adaptiveTokens.p95 ?? null;
|
|
53
|
+
|
|
54
|
+
if (hasComparableRuns) {
|
|
55
|
+
const dTokens = adaptiveComp.pairedRuns.map((r) => r.directTokens).sort((a, b) => a - b);
|
|
56
|
+
const aTokens = adaptiveComp.pairedRuns.map((r) => r.candidateTokens).sort((a, b) => a - b);
|
|
57
|
+
const pos95 = (dTokens.length - 1) * 0.95;
|
|
58
|
+
const lower = Math.floor(pos95);
|
|
59
|
+
const upper = Math.ceil(pos95);
|
|
60
|
+
const calcP95 = (arr) => (lower === upper ? arr[lower] : arr[lower] + ((arr[upper] - arr[lower]) * (pos95 - lower)));
|
|
61
|
+
const pos50 = (dTokens.length - 1) * 0.5;
|
|
62
|
+
const lower50 = Math.floor(pos50);
|
|
63
|
+
const upper50 = Math.ceil(pos50);
|
|
64
|
+
const calcP50 = (arr) => (lower50 === upper50 ? arr[lower50] : arr[lower50] + ((arr[upper50] - arr[lower50]) * (pos50 - lower50)));
|
|
65
|
+
|
|
66
|
+
compDirectP50 = Number(calcP50(dTokens).toFixed(4));
|
|
67
|
+
compDirectP95 = Number(calcP95(dTokens).toFixed(4));
|
|
68
|
+
compAdaptiveP50 = Number(calcP50(aTokens).toFixed(4));
|
|
69
|
+
compAdaptiveP95 = Number(calcP95(aTokens).toFixed(4));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
scenarioId: aggregate.scenarioId,
|
|
74
|
+
expectedProfile: aggregate.expectedProfile,
|
|
75
|
+
totalRunsPerMode: directAgg?.runCount ?? 0,
|
|
76
|
+
verificationSuccessRate: adaptiveAgg?.verificationSuccessRate ?? 1,
|
|
77
|
+
directP50: compDirectP50,
|
|
78
|
+
adaptiveP50: compAdaptiveP50,
|
|
79
|
+
distributionP50DeltaPercent: dist.p50 ?? null,
|
|
80
|
+
directP95: compDirectP95,
|
|
81
|
+
adaptiveP95: compAdaptiveP95,
|
|
82
|
+
distributionP95DeltaPercent: dist.p95 ?? null,
|
|
83
|
+
pairedOverheadP50Percent: paired.p50 ?? null,
|
|
84
|
+
pairedOverheadP95Percent: paired.p95 ?? null,
|
|
85
|
+
lowBaselinePairCount: diag.lowBaselinePairCount ?? 0,
|
|
86
|
+
comparablePairCount: adaptiveComp?.tokenComparablePairs ?? 0,
|
|
87
|
+
pairedStatus: tail.pairedStatus ?? "NOT_ENOUGH_SAMPLES",
|
|
88
|
+
distributionStatus: tail.distributionStatus ?? "NOT_ENOUGH_SAMPLES",
|
|
89
|
+
combinedInterpretation: tail.combinedInterpretation ?? "TAIL_UNRESOLVED",
|
|
90
|
+
pairedRuns: adaptiveComp?.pairedRuns ?? [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const options = parseArgs(process.argv.slice(2));
|
|
96
|
+
if (options.help) {
|
|
97
|
+
console.log(helpText());
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const scenarios = await readBenchmarkScenarios(repositoryRoot);
|
|
101
|
+
const runSets = await readBenchmarkRunSets(path.resolve(options.results), options.runSetId);
|
|
102
|
+
const reports = [];
|
|
103
|
+
|
|
104
|
+
for (const runSet of runSets) {
|
|
105
|
+
const scenarioReports = [];
|
|
106
|
+
for (const scenario of scenarios) {
|
|
107
|
+
const runs = runSet.runs.filter((run) => run.scenarioId === scenario.scenarioId);
|
|
108
|
+
if (runs.length === 0) continue;
|
|
109
|
+
const aggregate = aggregateBenchmarkRuns({ scenario, runs });
|
|
110
|
+
scenarioReports.push(buildScenarioReport(aggregate));
|
|
111
|
+
}
|
|
112
|
+
reports.push({
|
|
113
|
+
runSetId: runSet.runSetId,
|
|
114
|
+
runCount: runSet.runs.length,
|
|
115
|
+
scenarios: scenarioReports,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const output = {
|
|
120
|
+
schemaVersion: 1,
|
|
121
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
122
|
+
status: reports.length === 0 ? "NOT_MEASURED" : "MEASURED",
|
|
123
|
+
runSets: reports,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (options.json) {
|
|
127
|
+
console.log(JSON.stringify(output, null, 2));
|
|
128
|
+
} else {
|
|
129
|
+
for (const runSet of reports) {
|
|
130
|
+
console.log(`\n======================================================`);
|
|
131
|
+
console.log(`Run Set: ${runSet.runSetId} (${runSet.runCount} runs)`);
|
|
132
|
+
console.log(`======================================================`);
|
|
133
|
+
for (const sc of runSet.scenarios) {
|
|
134
|
+
console.log(`\nScenario: ${sc.scenarioId} (expectedProfile: ${sc.expectedProfile})`);
|
|
135
|
+
console.log(` Direct P50: ${sc.directP50 ?? "N/A"}`);
|
|
136
|
+
console.log(` Adaptive P50: ${sc.adaptiveP50 ?? "N/A"}`);
|
|
137
|
+
console.log(` Distribution P50 Delta: ${sc.distributionP50DeltaPercent !== null ? `${sc.distributionP50DeltaPercent > 0 ? "+" : ""}${sc.distributionP50DeltaPercent}%` : "N/A"}`);
|
|
138
|
+
console.log(` Direct P95: ${sc.directP95 ?? "N/A"}`);
|
|
139
|
+
console.log(` Adaptive P95: ${sc.adaptiveP95 ?? "N/A"}`);
|
|
140
|
+
console.log(` Distribution P95 Delta: ${sc.distributionP95DeltaPercent !== null ? `${sc.distributionP95DeltaPercent > 0 ? "+" : ""}${sc.distributionP95DeltaPercent}%` : "N/A"}`);
|
|
141
|
+
console.log(` Paired Overhead P50: ${sc.pairedOverheadP50Percent !== null ? `${sc.pairedOverheadP50Percent > 0 ? "+" : ""}${sc.pairedOverheadP50Percent}%` : "N/A"}`);
|
|
142
|
+
console.log(` Paired Overhead P95: ${sc.pairedOverheadP95Percent !== null ? `${sc.pairedOverheadP95Percent > 0 ? "+" : ""}${sc.pairedOverheadP95Percent}%` : "N/A"}`);
|
|
143
|
+
console.log(` Low-Baseline Pairs: ${sc.lowBaselinePairCount}`);
|
|
144
|
+
console.log(` Comparable Pairs: ${sc.comparablePairCount}`);
|
|
145
|
+
console.log(` Paired Tail Status: ${sc.pairedStatus}`);
|
|
146
|
+
console.log(` Distribution Tail Status: ${sc.distributionStatus}`);
|
|
147
|
+
console.log(` Combined Interpretation: ${sc.combinedInterpretation}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch((error) => {
|
|
154
|
+
console.error(`ForgeLoop tail interpretation report: ${error.message}`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
});
|
|
@@ -12,6 +12,7 @@ import { assertSchema, readSchema } from "../src/core/schema-validation.js";
|
|
|
12
12
|
import {
|
|
13
13
|
aggregateBenchmarkRuns,
|
|
14
14
|
BENCHMARK_MODES,
|
|
15
|
+
BENCHMARK_TIERS,
|
|
15
16
|
BENCHMARK_VERSION,
|
|
16
17
|
assertBenchmarkScenario,
|
|
17
18
|
assertRequiredBenchmarkScenarios,
|
|
@@ -32,7 +33,8 @@ function parseArgs(argv) {
|
|
|
32
33
|
const options = {
|
|
33
34
|
target: process.cwd(),
|
|
34
35
|
adapter: null,
|
|
35
|
-
runs:
|
|
36
|
+
runs: null,
|
|
37
|
+
tier: null,
|
|
36
38
|
runSetId: null,
|
|
37
39
|
output: defaultOutputDirectory,
|
|
38
40
|
json: false,
|
|
@@ -47,6 +49,7 @@ function parseArgs(argv) {
|
|
|
47
49
|
if (argument === "--target") options.target = value();
|
|
48
50
|
else if (argument === "--adapter") options.adapter = value();
|
|
49
51
|
else if (argument === "--runs") options.runs = Number(value());
|
|
52
|
+
else if (argument === "--tier") options.tier = value();
|
|
50
53
|
else if (argument === "--run-set") options.runSetId = value();
|
|
51
54
|
else if (argument === "--output") options.output = value();
|
|
52
55
|
else if (argument === "--json") options.json = true;
|
|
@@ -56,9 +59,19 @@ function parseArgs(argv) {
|
|
|
56
59
|
}
|
|
57
60
|
if (options.help) return options;
|
|
58
61
|
if (!options.adapter) throw usageError("--adapter is required; ForgeLoop never invents provider or host measurements");
|
|
62
|
+
if (options.tier !== null && !Object.prototype.hasOwnProperty.call(BENCHMARK_TIERS, options.tier)) {
|
|
63
|
+
throw usageError(`--tier must be one of ${Object.keys(BENCHMARK_TIERS).join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
const tier = options.tier === null ? null : BENCHMARK_TIERS[options.tier];
|
|
66
|
+
if (options.runs === null) {
|
|
67
|
+
options.runs = tier === null ? 5 : tier.defaultRuns;
|
|
68
|
+
}
|
|
59
69
|
if (!Number.isInteger(options.runs) || options.runs < 1 || options.runs > 100) {
|
|
60
70
|
throw usageError("--runs must be an integer from 1 through 100");
|
|
61
71
|
}
|
|
72
|
+
if (tier !== null && (options.runs < tier.minimumRuns || options.runs > tier.maximumRuns)) {
|
|
73
|
+
throw usageError(`--runs must be between ${tier.minimumRuns} and ${tier.maximumRuns} for the ${options.tier} tier`);
|
|
74
|
+
}
|
|
62
75
|
if (options.runSetId !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(options.runSetId)) {
|
|
63
76
|
throw usageError("--run-set must contain only portable identifier characters");
|
|
64
77
|
}
|
|
@@ -70,7 +83,9 @@ function helpText() {
|
|
|
70
83
|
"Usage: npm run benchmark:profiles -- --adapter <module> [options]",
|
|
71
84
|
"",
|
|
72
85
|
"The adapter must execute each scenario and return actual provider/host usage, verification, comparable steps, and optional host-reported contextUsage.",
|
|
73
|
-
"
|
|
86
|
+
"An optional finalizeBenchmark hook may attach independently evaluated quality by runner-owned runId after all host timings finish.",
|
|
87
|
+
"Tiers are benchmark sample-size policy: smoke (1-3 runs), evidence (5-10 runs), tail (20-30 runs).",
|
|
88
|
+
"Options: --target <path> --runs <1..100> --tier <smoke|evidence|tail> --run-set <id> --output <directory> --json",
|
|
74
89
|
].join("\n");
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -93,11 +108,18 @@ async function loadScenarios() {
|
|
|
93
108
|
async function loadAdapter(adapterSpecifier) {
|
|
94
109
|
const adapterPath = path.resolve(process.cwd(), adapterSpecifier);
|
|
95
110
|
const adapterModule = await import(pathToFileURL(adapterPath).href);
|
|
96
|
-
const
|
|
97
|
-
|
|
111
|
+
const defaultExport = adapterModule.default;
|
|
112
|
+
const runBenchmark = adapterModule.runBenchmark
|
|
113
|
+
?? defaultExport?.runBenchmark
|
|
114
|
+
?? (typeof defaultExport === "function" ? defaultExport : null);
|
|
115
|
+
if (typeof runBenchmark !== "function") {
|
|
98
116
|
throw usageError("benchmark adapter must export runBenchmark(input) or be a function default export");
|
|
99
117
|
}
|
|
100
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
runBenchmark,
|
|
120
|
+
finalizeBenchmark: adapterModule.finalizeBenchmark ?? defaultExport?.finalizeBenchmark ?? null,
|
|
121
|
+
cleanup: adapterModule.cleanup ?? defaultExport?.cleanup ?? null,
|
|
122
|
+
};
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
function generatedRunSetId() {
|
|
@@ -139,14 +161,37 @@ function normalizedAdapterMetadata(response, target, repository, scenario, mode)
|
|
|
139
161
|
};
|
|
140
162
|
}
|
|
141
163
|
|
|
164
|
+
function normalizeFinalizationResult(value, records) {
|
|
165
|
+
if (value === undefined || value === null) {
|
|
166
|
+
return { qualityByRunId: {}, summary: { status: "NOT_CONFIGURED" } };
|
|
167
|
+
}
|
|
168
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
169
|
+
throw usageError("benchmark finalizer must return an object");
|
|
170
|
+
}
|
|
171
|
+
const qualityByRunId = value.qualityByRunId ?? {};
|
|
172
|
+
if (!qualityByRunId || typeof qualityByRunId !== "object" || Array.isArray(qualityByRunId)) {
|
|
173
|
+
throw usageError("benchmark finalizer qualityByRunId must be an object");
|
|
174
|
+
}
|
|
175
|
+
const runIds = new Set(records.map((record) => record.runId));
|
|
176
|
+
for (const runId of Object.keys(qualityByRunId)) {
|
|
177
|
+
if (!runIds.has(runId)) throw usageError(`benchmark finalizer returned an unknown runId: ${runId}`);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
qualityByRunId,
|
|
181
|
+
summary: value.summary && typeof value.summary === "object" && !Array.isArray(value.summary)
|
|
182
|
+
? value.summary
|
|
183
|
+
: { status: "MEASURED" },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
142
187
|
async function executeRuns({ adapter, scenarios, target, runSetId, runs }) {
|
|
143
188
|
const repository = await currentRepositoryFingerprint(target);
|
|
144
|
-
const
|
|
189
|
+
const records = [];
|
|
145
190
|
for (const scenario of scenarios) {
|
|
146
191
|
for (const mode of BENCHMARK_MODES) {
|
|
147
192
|
for (let runIndex = 1; runIndex <= runs; runIndex += 1) {
|
|
148
193
|
const started = performance.now();
|
|
149
|
-
const response = await adapter({
|
|
194
|
+
const response = await adapter.runBenchmark({
|
|
150
195
|
benchmarkVersion: BENCHMARK_VERSION,
|
|
151
196
|
scenario: structuredClone(scenario),
|
|
152
197
|
mode,
|
|
@@ -159,29 +204,50 @@ async function executeRuns({ adapter, scenarios, target, runSetId, runs }) {
|
|
|
159
204
|
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
160
205
|
throw usageError(`${scenario.scenarioId}/${mode}/${runIndex}: adapter must return an object`);
|
|
161
206
|
}
|
|
162
|
-
|
|
207
|
+
records.push({
|
|
163
208
|
runSetId,
|
|
164
209
|
runId: `run-${scenario.scenarioId}-${mode}-${String(runIndex).padStart(3, "0")}`,
|
|
165
210
|
runIndex,
|
|
166
211
|
scenario,
|
|
167
212
|
mode,
|
|
168
|
-
|
|
213
|
+
response,
|
|
169
214
|
wallClockMs,
|
|
170
|
-
verification: response.verification ?? "NOT_AVAILABLE",
|
|
171
|
-
verificationCycles: response.verificationCycles ?? null,
|
|
172
|
-
comparableSteps: response.comparableSteps ?? null,
|
|
173
|
-
contextUsage: response.contextUsage,
|
|
174
|
-
quality: response.quality,
|
|
175
|
-
metadata: normalizedAdapterMetadata(response, target, repository, scenario, mode),
|
|
176
215
|
});
|
|
177
|
-
allRuns.push(run);
|
|
178
216
|
}
|
|
179
217
|
}
|
|
180
218
|
}
|
|
181
|
-
|
|
219
|
+
const finalization = typeof adapter.finalizeBenchmark === "function"
|
|
220
|
+
? normalizeFinalizationResult(await adapter.finalizeBenchmark({
|
|
221
|
+
benchmarkVersion: BENCHMARK_VERSION,
|
|
222
|
+
records,
|
|
223
|
+
scenarios,
|
|
224
|
+
target,
|
|
225
|
+
runSetId,
|
|
226
|
+
}), records)
|
|
227
|
+
: normalizeFinalizationResult(null, records);
|
|
228
|
+
const allRuns = records.map((record) => {
|
|
229
|
+
const { scenario, mode, response } = record;
|
|
230
|
+
return createBenchmarkRun({
|
|
231
|
+
runSetId: record.runSetId,
|
|
232
|
+
runId: record.runId,
|
|
233
|
+
runIndex: record.runIndex,
|
|
234
|
+
scenario,
|
|
235
|
+
mode,
|
|
236
|
+
usage: response.usage ?? {},
|
|
237
|
+
wallClockMs: record.wallClockMs,
|
|
238
|
+
verification: response.verification ?? "NOT_AVAILABLE",
|
|
239
|
+
verificationCycles: response.verificationCycles ?? null,
|
|
240
|
+
comparableSteps: response.comparableSteps ?? null,
|
|
241
|
+
diagnostics: response.diagnostics,
|
|
242
|
+
contextUsage: response.contextUsage,
|
|
243
|
+
quality: finalization.qualityByRunId[record.runId] ?? response.quality,
|
|
244
|
+
metadata: normalizedAdapterMetadata(response, target, repository, scenario, mode),
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
return { runs: allRuns, finalization: finalization.summary };
|
|
182
248
|
}
|
|
183
249
|
|
|
184
|
-
async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
|
|
250
|
+
async function writeResults({ outputDirectory, runSetId, scenarios, runs, tier }) {
|
|
185
251
|
const rawDirectory = path.join(outputDirectory, "raw", runSetId);
|
|
186
252
|
const aggregateDirectory = path.join(outputDirectory, "aggregate", runSetId);
|
|
187
253
|
try {
|
|
@@ -214,6 +280,7 @@ async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
|
|
|
214
280
|
schemaVersion: 1,
|
|
215
281
|
benchmarkVersion: BENCHMARK_VERSION,
|
|
216
282
|
runSetId,
|
|
283
|
+
tier,
|
|
217
284
|
scenarioCount: scenarios.length,
|
|
218
285
|
runCount: runs.length,
|
|
219
286
|
claimsAllowed: aggregates.some((aggregate) => aggregate.claimsAllowed),
|
|
@@ -223,6 +290,7 @@ async function writeResults({ outputDirectory, runSetId, scenarios, runs }) {
|
|
|
223
290
|
claimsAllowed: aggregate.claimsAllowed,
|
|
224
291
|
lightObjectives: aggregate.lightObjectives,
|
|
225
292
|
contextInflation: aggregate.contextInflation ?? null,
|
|
293
|
+
outlierAnalysis: aggregate.outlierAnalysis ?? null,
|
|
226
294
|
})),
|
|
227
295
|
};
|
|
228
296
|
await writeFile(path.join(aggregateDirectory, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
@@ -239,24 +307,32 @@ async function main() {
|
|
|
239
307
|
const outputDirectory = path.resolve(options.output);
|
|
240
308
|
const runSetId = options.runSetId ?? generatedRunSetId();
|
|
241
309
|
const adapter = await loadAdapter(options.adapter);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
310
|
+
try {
|
|
311
|
+
const scenarios = await loadScenarios();
|
|
312
|
+
const execution = await executeRuns({ adapter, scenarios, target, runSetId, runs: options.runs });
|
|
313
|
+
const result = await writeResults({ outputDirectory, runSetId, scenarios, runs: execution.runs, tier: options.tier });
|
|
314
|
+
const output = {
|
|
315
|
+
status: "MEASURED",
|
|
316
|
+
runSetId,
|
|
317
|
+
tier: options.tier,
|
|
318
|
+
scenarioCount: scenarios.length,
|
|
319
|
+
runCount: execution.runs.length,
|
|
320
|
+
outputDirectory,
|
|
321
|
+
claimsAllowed: result.summary.claimsAllowed,
|
|
322
|
+
qualityFinalization: execution.finalization,
|
|
323
|
+
};
|
|
324
|
+
console.log(options.json ? JSON.stringify(output) : [
|
|
325
|
+
`Benchmark run set: ${runSetId}`,
|
|
326
|
+
`Tier: ${options.tier ?? "custom"}`,
|
|
327
|
+
`Scenarios: ${scenarios.length}`,
|
|
328
|
+
`Runs: ${execution.runs.length}`,
|
|
329
|
+
`Claims allowed: ${result.summary.claimsAllowed ? "yes (observational only)" : "no"}`,
|
|
330
|
+
`Quality finalization: ${execution.finalization.status ?? "MEASURED"}`,
|
|
331
|
+
`Results: ${outputDirectory}`,
|
|
332
|
+
].join("\n"));
|
|
333
|
+
} finally {
|
|
334
|
+
if (typeof adapter.cleanup === "function") await adapter.cleanup();
|
|
335
|
+
}
|
|
260
336
|
}
|
|
261
337
|
|
|
262
338
|
main().catch((error) => {
|