Summary
scanReusableResults already reads all timestamp directories in chronological order and loads each summary.json — the data needed for cross-run trend analysis is fully present. There's no ExperimentTrendAnalyzer to compute pass-rate slope across sequential experiment runs.
The Gap
results/{experimentName}/{timestamp}/{evalName}/summary.json stores passRate, passedRuns, totalRuns, and fingerprint per eval per timestamp. scanReusableResults scans all timestamps newest-first for deduplication — the underlying iteration already touches every run.
What's missing: an analysis layer that reads those same summary.json files in chronological order and computes a slope over passRate across the last N runs.
If a model change causes passRate to drop from 92% → 85% → 78% → 71% across four experiment runs, each individual run "passes" and there's no signal.
Proposed API
// packages/agent-eval/src/lib/trend.ts
export interface EvalRunPoint {
timestamp: string;
passRate: number;
passedRuns: number;
totalRuns: number;
}
export interface EvalTrendResult {
evalName: string;
runCount: number;
slope: number; // % per run via OLS
direction: 'improving' | 'degrading' | 'stable';
recentPassRate: number;
firstPassRate: number;
}
export interface ExperimentTrendReport {
experimentName: string;
window: number;
evalTrends: EvalTrendResult[];
anyRegression: boolean;
}
export function analyzeExperimentTrend(
resultsDir: string,
experimentName: string,
window = 10,
): ExperimentTrendReport;Implementation Sketch
export function analyzeExperimentTrend(
resultsDir: string,
experimentName: string,
window = 10,
): ExperimentTrendReport {
const experimentDir = join(resultsDir, experimentName);
const timestamps = readdirSync(experimentDir)
.filter((t) => !t.startsWith('.') && statSync(join(experimentDir, t)).isDirectory())
.sort() // ISO timestamps sort chronologically
.slice(-window);
const evalHistory = new Map<string, EvalRunPoint[]>();
for (const timestamp of timestamps) {
const tsDir = join(experimentDir, timestamp);
for (const evalDir of readdirSync(tsDir).filter((d) => !d.startsWith('.'))) {
const summaryPath = join(tsDir, evalDir, 'summary.json');
if (!existsSync(summaryPath)) continue;
const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
if (!evalHistory.has(evalDir)) evalHistory.set(evalDir, []);
evalHistory.get(evalDir)!.push({
timestamp,
passRate: summary.passRate,
passedRuns: summary.passedRuns,
totalRuns: summary.totalRuns,
});
}
}
const evalTrends: EvalTrendResult[] = [];
for (const [evalName, points] of evalHistory) {
if (points.length < 2) continue;
const slope = olsSlope(points.map((p, i) => [i, p.passRate]));
evalTrends.push({
evalName,
runCount: points.length,
slope,
direction: slope < -0.5 ? 'degrading' : slope > 0.5 ? 'improving' : 'stable',
recentPassRate: points.at(-1)!.passRate,
firstPassRate: points[0].passRate,
});
}
return {
experimentName,
window,
evalTrends,
anyRegression: evalTrends.some((t) => t.direction === 'degrading'),
};
}OLS slope is ~8 lines of pure math (no new deps):
function olsSlope(pairs: [number, number][]): number {
const n = pairs.length;
const meanX = pairs.reduce((s, [x]) => s + x, 0) / n;
const meanY = pairs.reduce((s, [, y]) => s + y, 0) / n;
const num = pairs.reduce((s, [x, y]) => s + (x - meanX) * (y - meanY), 0);
const den = pairs.reduce((s, [x]) => s + (x - meanX) ** 2, 0);
return den === 0 ? 0 : num / den;
}CLI Integration
// In cli.ts — new `trend` subcommand
program
.command('trend [experimentName]')
.option('--window <n>', 'number of runs to analyze', '10')
.option('--exit-on-regression', 'exit 1 if any eval is degrading')
.action(async (experimentName, opts) => {
const report = analyzeExperimentTrend(resultsDir, experimentName, +opts.window);
console.table(report.evalTrends);
if (opts.exitOnRegression && report.anyRegression) process.exit(1);
});Context
This is a structural pattern observed across agent evaluation frameworks — the per-run data is present, the cross-run analysis layer is absent. Documented as an architectural blind spot in 115+ independent implementations (PDR v2.11, DOI: 10.5281/zenodo.19397914).
scanReusableResults already traverses the exact same directory structure this would use — the infrastructure is there.
Activity
0No activity yet.