@valerypopoff/rivet2-evaluations 2.3.0 → 2.4.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/dist/cjs/bundle.cjs +572 -309
- package/dist/cjs/bundle.cjs.map +4 -4
- package/dist/esm/canonical.js +37 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/library.js +84 -0
- package/dist/esm/normalization.js +101 -1
- package/dist/esm/runner.js +201 -129
- package/dist/esm/serialization.js +3 -0
- package/dist/esm/store.js +25 -5
- package/dist/types/canonical.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/library.d.ts +8 -0
- package/dist/types/normalization.d.ts +15 -0
- package/dist/types/runner.d.ts +58 -2
- package/dist/types/store.d.ts +10 -3
- package/dist/types/types.d.ts +74 -2
- package/package.json +2 -2
package/dist/esm/runner.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function runEvaluationWorkPool(options) {
|
|
|
41
41
|
const work = options.work[index];
|
|
42
42
|
const result = await options.execute(work, index);
|
|
43
43
|
results[index] = result;
|
|
44
|
-
options.onSettled?.(result, index, results);
|
|
44
|
+
await options.onSettled?.(result, index, results);
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
await Promise.all(Array.from({ length: Math.min(options.concurrency, options.work.length) }, worker));
|
|
@@ -744,8 +744,8 @@ async function runWithTimeout(run, timeoutMs, outer) {
|
|
|
744
744
|
outer?.removeEventListener('abort', abort);
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
|
-
async function
|
|
748
|
-
const { project, suite, dataset, purpose, testCase, caseIndex, trialIndex, runId, runGraph, signal } = input;
|
|
747
|
+
export async function runEvaluationTrial(input) {
|
|
748
|
+
const { project, suite, dataset, purpose, testCase, caseIndex, trialIndex, runId, runGraph, signal, trialId } = input;
|
|
749
749
|
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
750
750
|
const { inputs, expected } = resolveCaseInputs(suite, dataset, testCase, purpose);
|
|
751
751
|
const seed = deriveSeed(suite.configuration?.seed, testCase.id, trialIndex);
|
|
@@ -792,7 +792,7 @@ async function runTrial(input) {
|
|
|
792
792
|
const executionStatus = 'canceled';
|
|
793
793
|
const qualityStatus = 'not-evaluated';
|
|
794
794
|
return {
|
|
795
|
-
id: makeId('trial'),
|
|
795
|
+
id: trialId ?? makeId('trial'),
|
|
796
796
|
caseId: testCase.id,
|
|
797
797
|
caseName: testCase.name,
|
|
798
798
|
caseIndex,
|
|
@@ -919,7 +919,7 @@ async function runTrial(input) {
|
|
|
919
919
|
const executionStatus = 'canceled';
|
|
920
920
|
const qualityStatus = 'not-evaluated';
|
|
921
921
|
return {
|
|
922
|
-
id: makeId('trial'),
|
|
922
|
+
id: trialId ?? makeId('trial'),
|
|
923
923
|
caseId: testCase.id,
|
|
924
924
|
caseName: testCase.name,
|
|
925
925
|
caseIndex,
|
|
@@ -1015,7 +1015,7 @@ async function runTrial(input) {
|
|
|
1015
1015
|
}
|
|
1016
1016
|
const totalMetrics = mergeMetrics(targetMetrics, evaluatorMetrics);
|
|
1017
1017
|
return {
|
|
1018
|
-
id: makeId('trial'),
|
|
1018
|
+
id: trialId ?? makeId('trial'),
|
|
1019
1019
|
caseId: testCase.id,
|
|
1020
1020
|
caseName: testCase.name,
|
|
1021
1021
|
caseIndex,
|
|
@@ -1042,6 +1042,15 @@ function percentile(values, percentage) {
|
|
|
1042
1042
|
const ordered = values.slice().sort((left, right) => left - right);
|
|
1043
1043
|
return ordered[Math.min(ordered.length - 1, Math.ceil((percentage / 100) * ordered.length) - 1)];
|
|
1044
1044
|
}
|
|
1045
|
+
function median(values) {
|
|
1046
|
+
if (values.length === 0)
|
|
1047
|
+
return 0;
|
|
1048
|
+
const ordered = values.slice().sort((left, right) => left - right);
|
|
1049
|
+
const upperIndex = Math.floor(ordered.length / 2);
|
|
1050
|
+
if (ordered.length % 2 !== 0)
|
|
1051
|
+
return ordered[upperIndex];
|
|
1052
|
+
return (ordered[upperIndex - 1] + ordered[upperIndex]) / 2;
|
|
1053
|
+
}
|
|
1045
1054
|
function meanObservationScore(observations) {
|
|
1046
1055
|
let weightedScore = 0;
|
|
1047
1056
|
let totalWeight = 0;
|
|
@@ -1141,6 +1150,7 @@ function aggregate(trials, evaluationMode = 'pass-fail') {
|
|
|
1141
1150
|
const scoringMeanScore = scoringCaseMeans.length === 0
|
|
1142
1151
|
? undefined
|
|
1143
1152
|
: scoringCaseMeans.reduce((sum, score) => sum + score, 0) / scoringCaseMeans.length;
|
|
1153
|
+
const incurredLatencies = incurred.map((trial) => trial.totalMetrics.durationMs);
|
|
1144
1154
|
const aggregateValue = {
|
|
1145
1155
|
trialCount: trials.length,
|
|
1146
1156
|
evaluatedTrialCount: evaluated.length,
|
|
@@ -1156,14 +1166,19 @@ function aggregate(trials, evaluationMode = 'pass-fail') {
|
|
|
1156
1166
|
scoredTrialCount,
|
|
1157
1167
|
missingScoreTrialCount: trials.length - scoredTrialCount,
|
|
1158
1168
|
...(scoringMeanScore === undefined ? {} : { meanScore: scoringMeanScore }),
|
|
1169
|
+
...(scoringCaseMeans.length === 0
|
|
1170
|
+
? {}
|
|
1171
|
+
: {
|
|
1172
|
+
medianScore: median(scoringCaseMeans),
|
|
1173
|
+
p95Score: percentile(scoringCaseMeans, 95),
|
|
1174
|
+
}),
|
|
1159
1175
|
}
|
|
1160
1176
|
: meanScore === undefined
|
|
1161
1177
|
? {}
|
|
1162
1178
|
: { meanScore }),
|
|
1163
|
-
averageLatencyMs: incurred.length === 0
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
p95LatencyMs: percentile(incurred.map((trial) => trial.totalMetrics.durationMs), 95),
|
|
1179
|
+
averageLatencyMs: incurred.length === 0 ? 0 : incurredLatencies.reduce((sum, durationMs) => sum + durationMs, 0) / incurred.length,
|
|
1180
|
+
medianLatencyMs: median(incurredLatencies),
|
|
1181
|
+
p95LatencyMs: percentile(incurredLatencies, 95),
|
|
1167
1182
|
...(incurred.length === 0 || totalCostUsd === undefined
|
|
1168
1183
|
? {}
|
|
1169
1184
|
: { totalCostUsd, averageCostUsd: totalCostUsd / incurred.length }),
|
|
@@ -1515,7 +1530,12 @@ function createProvenance(project, suite, dataset, options) {
|
|
|
1515
1530
|
accountingComplete: true,
|
|
1516
1531
|
};
|
|
1517
1532
|
}
|
|
1518
|
-
|
|
1533
|
+
/**
|
|
1534
|
+
* Validates an immutable Evaluation submission and expands its enabled cases
|
|
1535
|
+
* into deterministic work. Hosts can persist this plan before any graph side
|
|
1536
|
+
* effect, while local callers continue to use runEvaluationSuite.
|
|
1537
|
+
*/
|
|
1538
|
+
export function createEvaluationRunPlan(options) {
|
|
1519
1539
|
const suite = resolveSuite(options.evaluationData, options.suiteId);
|
|
1520
1540
|
const purpose = options.purpose ?? 'evaluation';
|
|
1521
1541
|
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
@@ -1525,6 +1545,119 @@ export async function runEvaluationSuite(options) {
|
|
|
1525
1545
|
: `Evaluation suite "${suite.name}" has no required quality check or threshold. Add a quality criterion or run it as an execution benchmark.`);
|
|
1526
1546
|
}
|
|
1527
1547
|
validateSuite(options.project, suite, options.dataset, purpose);
|
|
1548
|
+
const trialCount = Math.max(1, Math.floor(suite.configuration?.trialCount ?? 1));
|
|
1549
|
+
const work = options.dataset.cases
|
|
1550
|
+
.filter((testCase) => testCase.enabled !== false)
|
|
1551
|
+
.flatMap((testCase, caseIndex) => Array.from({ length: trialCount }, (_, trialIndex) => ({ testCase, caseIndex, trialIndex })));
|
|
1552
|
+
return { suite, purpose, evaluationMode, work };
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Applies the same aggregate, quality, accounting, and baseline semantics to
|
|
1556
|
+
* locally scheduled and durably hosted trials. Callers must provide every
|
|
1557
|
+
* settled trial in deterministic case/trial order; never synthesize retries.
|
|
1558
|
+
*/
|
|
1559
|
+
export function finalizeEvaluationRun(options) {
|
|
1560
|
+
const run = options.run;
|
|
1561
|
+
const evaluationMode = getEvaluationSuiteMode(options.suite);
|
|
1562
|
+
run.trials = options.trials.slice();
|
|
1563
|
+
run.completedAt = options.completedAt ?? new Date().toISOString();
|
|
1564
|
+
const outcome = aggregate(run.trials, evaluationMode);
|
|
1565
|
+
run.aggregate = outcome.aggregate;
|
|
1566
|
+
run.provenance.accountingComplete = !run.trials.some((trial) => trial.totalMetrics.hasUnknownCost);
|
|
1567
|
+
run.accountingStatus = run.provenance.accountingComplete ? 'complete' : 'partial';
|
|
1568
|
+
if (run.accountingStatus === 'partial' && !run.warnings.includes('Some provider pricing was unavailable. Cost totals are unavailable, and cost requirements cannot be evaluated.')) {
|
|
1569
|
+
run.warnings.push('Some provider pricing was unavailable. Cost totals are unavailable, and cost requirements cannot be evaluated.');
|
|
1570
|
+
}
|
|
1571
|
+
if (options.canceled) {
|
|
1572
|
+
run.executionStatus = 'canceled';
|
|
1573
|
+
run.qualityStatus = 'not-evaluated';
|
|
1574
|
+
run.qualityReason = { code: 'canceled', message: 'The evaluation run was canceled.' };
|
|
1575
|
+
return run;
|
|
1576
|
+
}
|
|
1577
|
+
if (run.purpose === 'execution-benchmark') {
|
|
1578
|
+
run.executionStatus = 'completed';
|
|
1579
|
+
run.qualityStatus = 'not-evaluated';
|
|
1580
|
+
run.qualityReason = { code: 'benchmark', message: 'This run measured execution without evaluating output quality.' };
|
|
1581
|
+
return run;
|
|
1582
|
+
}
|
|
1583
|
+
if (evaluationMode === 'scoring') {
|
|
1584
|
+
run.executionStatus = 'completed';
|
|
1585
|
+
if ((outcome.aggregate.missingScoreTrialCount ?? run.trials.length) > 0) {
|
|
1586
|
+
run.qualityStatus = 'unable-to-evaluate';
|
|
1587
|
+
run.qualityReason = { code: 'scores-incomplete', message: 'One or more requested trials did not produce a usable score. Available averages are shown with coverage.' };
|
|
1588
|
+
run.warnings.push(`Score coverage is ${outcome.aggregate.scoredTrialCount ?? 0} of ${outcome.aggregate.trialCount} requested trials.`);
|
|
1589
|
+
}
|
|
1590
|
+
else if ((outcome.aggregate.scoredTrialCount ?? 0) > 0) {
|
|
1591
|
+
run.qualityStatus = 'scored';
|
|
1592
|
+
run.qualityReason = { code: 'scores-complete', message: 'Every requested trial produced a score. Overall score is the equal-weight average of case averages.' };
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
run.qualityStatus = 'unable-to-evaluate';
|
|
1596
|
+
run.qualityReason = { code: 'scores-incomplete', message: 'No requested trial produced a usable score.' };
|
|
1597
|
+
}
|
|
1598
|
+
return run;
|
|
1599
|
+
}
|
|
1600
|
+
const baseline = options.baseline ?? options.evaluationData.baselines.find((candidate) => candidate.suiteId === options.suite.id);
|
|
1601
|
+
const usableBaseline = baseline && baselineCompatible(run.provenance, baseline) ? baseline : undefined;
|
|
1602
|
+
if (baseline && !usableBaseline && !run.warnings.includes('The suite baseline is stale because its target, dataset, bindings, or evaluator definition changed.')) {
|
|
1603
|
+
run.warnings.push('The suite baseline is stale because its target, dataset, bindings, or evaluator definition changed.');
|
|
1604
|
+
}
|
|
1605
|
+
run.thresholdResults = evaluateThresholdResults(outcome.aggregate, options.suite.thresholds, usableBaseline);
|
|
1606
|
+
const hasPassRateThreshold = options.suite.thresholds?.some((threshold) => threshold.metric === 'pass-rate') ?? false;
|
|
1607
|
+
const hasTargetErrorRateThreshold = options.suite.thresholds?.some((threshold) => threshold.metric === 'target-error-rate') ?? false;
|
|
1608
|
+
const hasUngovernedCheckFailure = !hasPassRateThreshold && run.trials.some((trial) => trial.executionStatus === 'completed' && trial.qualityStatus === 'failed');
|
|
1609
|
+
const hasUngovernedTargetError = !hasTargetErrorRateThreshold && run.trials.some((trial) => trial.executionStatus === 'error');
|
|
1610
|
+
const hasThresholdFailure = run.thresholdResults.some((result) => result.status === 'failed');
|
|
1611
|
+
const hasUnavailableEvidence = run.trials.some((trial) => trial.qualityStatus === 'unable-to-evaluate') || run.thresholdResults.some((result) => result.status === 'unavailable');
|
|
1612
|
+
const hasAuthoritativePass = run.trials.some((trial) => trial.executionStatus === 'completed' && trial.qualityStatus === 'passed') || run.thresholdResults.some((result) => result.status === 'passed');
|
|
1613
|
+
run.executionStatus = 'completed';
|
|
1614
|
+
if (hasUngovernedCheckFailure || hasUngovernedTargetError || hasThresholdFailure) {
|
|
1615
|
+
run.qualityStatus = 'failed';
|
|
1616
|
+
run.qualityReason = { code: hasThresholdFailure ? 'thresholds-failed' : 'checks-failed', message: hasThresholdFailure ? 'One or more required aggregate thresholds failed.' : 'One or more target executions or required quality checks failed.' };
|
|
1617
|
+
}
|
|
1618
|
+
else if (hasUnavailableEvidence) {
|
|
1619
|
+
run.qualityStatus = 'unable-to-evaluate';
|
|
1620
|
+
run.qualityReason = { code: run.thresholdResults.some((result) => result.status === 'unavailable') ? 'required-metric-unavailable' : 'required-check-error', message: run.thresholdResults.some((result) => result.status === 'unavailable') ? 'A required aggregate metric could not be evaluated.' : 'A required quality check could not be evaluated.' };
|
|
1621
|
+
}
|
|
1622
|
+
else if (hasAuthoritativePass) {
|
|
1623
|
+
run.qualityStatus = 'passed';
|
|
1624
|
+
run.qualityReason = { code: run.thresholdResults.length > 0 ? 'thresholds-passed' : 'checks-passed', message: run.thresholdResults.length > 0 ? 'All required quality checks and aggregate thresholds passed.' : 'All required quality checks passed.' };
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
run.qualityStatus = 'not-evaluated';
|
|
1628
|
+
run.qualityReason = { code: 'no-completed-trials', message: 'No completed trial produced an authoritative quality result.' };
|
|
1629
|
+
}
|
|
1630
|
+
return run;
|
|
1631
|
+
}
|
|
1632
|
+
/** Creates a durable running shell only after the complete submission validates. */
|
|
1633
|
+
export function createEvaluationRunShell(options) {
|
|
1634
|
+
const plan = createEvaluationRunPlan(options);
|
|
1635
|
+
const runId = options.runId ?? makeId('evaluation');
|
|
1636
|
+
const run = {
|
|
1637
|
+
version: 2,
|
|
1638
|
+
id: runId,
|
|
1639
|
+
projectId: options.project.metadata.id,
|
|
1640
|
+
suiteId: plan.suite.id,
|
|
1641
|
+
suiteName: plan.suite.name,
|
|
1642
|
+
revision: 0,
|
|
1643
|
+
startedAt: new Date().toISOString(),
|
|
1644
|
+
purpose: plan.purpose,
|
|
1645
|
+
evaluationMode: plan.evaluationMode,
|
|
1646
|
+
executionStatus: 'running',
|
|
1647
|
+
qualityStatus: 'not-evaluated',
|
|
1648
|
+
qualityReason: { code: 'in-progress', message: 'The run is still in progress.' },
|
|
1649
|
+
accountingStatus: 'complete',
|
|
1650
|
+
provenance: createProvenance(options.project, plan.suite, options.dataset, options),
|
|
1651
|
+
trials: [],
|
|
1652
|
+
thresholdResults: [],
|
|
1653
|
+
warnings: [],
|
|
1654
|
+
requestedTrialCount: plan.work.length,
|
|
1655
|
+
};
|
|
1656
|
+
return { run, plan };
|
|
1657
|
+
}
|
|
1658
|
+
export async function runEvaluationSuite(options) {
|
|
1659
|
+
const plan = createEvaluationRunPlan(options);
|
|
1660
|
+
const { suite, purpose, evaluationMode } = plan;
|
|
1528
1661
|
const runId = options.runId ?? makeId('evaluation');
|
|
1529
1662
|
const run = {
|
|
1530
1663
|
version: 2,
|
|
@@ -1545,30 +1678,62 @@ export async function runEvaluationSuite(options) {
|
|
|
1545
1678
|
thresholdResults: [],
|
|
1546
1679
|
warnings: [],
|
|
1547
1680
|
};
|
|
1548
|
-
const trialCount = Math.max(1, Math.floor(suite.configuration?.trialCount ?? 1));
|
|
1549
1681
|
const concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(suite.configuration?.concurrency ?? DEFAULT_CONCURRENCY)));
|
|
1550
|
-
const work =
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1682
|
+
const work = plan.work;
|
|
1683
|
+
run.requestedTrialCount = work.length;
|
|
1684
|
+
const settledTrials = Array.from({ length: work.length });
|
|
1685
|
+
let settledTrialCount = 0;
|
|
1686
|
+
const nextRevision = () => {
|
|
1554
1687
|
run.revision = (run.revision ?? 0) + 1;
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
//
|
|
1559
|
-
|
|
1688
|
+
return run.revision;
|
|
1689
|
+
};
|
|
1690
|
+
const publishLegacySnapshot = () => {
|
|
1691
|
+
// Legacy consumers may retain earlier revisions. Keep their snapshots
|
|
1692
|
+
// detached, while incremental consumers avoid cloning all prior trials.
|
|
1693
|
+
if (!options.onUpdate)
|
|
1694
|
+
return;
|
|
1695
|
+
run.trials = settledTrials.filter((trial) => trial !== undefined);
|
|
1696
|
+
options.onUpdate(structuredClone(run));
|
|
1697
|
+
};
|
|
1698
|
+
const publishStarted = async () => {
|
|
1699
|
+
const revision = nextRevision();
|
|
1700
|
+
if (options.onEvent) {
|
|
1701
|
+
const shell = structuredClone(run);
|
|
1702
|
+
shell.trials = [];
|
|
1703
|
+
await options.onEvent({ type: 'run-started', revision, run: shell });
|
|
1704
|
+
}
|
|
1705
|
+
publishLegacySnapshot();
|
|
1706
|
+
};
|
|
1707
|
+
const publishSettledTrial = async (trial) => {
|
|
1708
|
+
const revision = nextRevision();
|
|
1709
|
+
await options.onEvent?.({
|
|
1710
|
+
type: 'trial-settled',
|
|
1711
|
+
revision,
|
|
1712
|
+
runId: run.id,
|
|
1713
|
+
projectId: run.projectId,
|
|
1714
|
+
suiteId: run.suiteId,
|
|
1715
|
+
requestedTrialCount: run.requestedTrialCount ?? work.length,
|
|
1716
|
+
settledTrialCount,
|
|
1717
|
+
trial: structuredClone(trial),
|
|
1718
|
+
});
|
|
1719
|
+
publishLegacySnapshot();
|
|
1720
|
+
};
|
|
1721
|
+
const publishFinalized = async () => {
|
|
1722
|
+
const revision = nextRevision();
|
|
1723
|
+
await options.onEvent?.({ type: 'run-finalized', revision, run: structuredClone(run) });
|
|
1724
|
+
publishLegacySnapshot();
|
|
1560
1725
|
};
|
|
1561
1726
|
// Publish the running shell before constructing the worker-pool promise:
|
|
1562
1727
|
// async workers start immediately, so publishing afterwards can otherwise
|
|
1563
1728
|
// let the first graph call race ahead of the run's initial state.
|
|
1564
|
-
|
|
1729
|
+
await publishStarted();
|
|
1565
1730
|
const workPool = runEvaluationWorkPool({
|
|
1566
1731
|
work,
|
|
1567
1732
|
concurrency,
|
|
1568
1733
|
signal: options.signal,
|
|
1569
1734
|
execute: async (task) => {
|
|
1570
1735
|
try {
|
|
1571
|
-
return await
|
|
1736
|
+
return await runEvaluationTrial({
|
|
1572
1737
|
...task,
|
|
1573
1738
|
project: options.project,
|
|
1574
1739
|
suite,
|
|
@@ -1612,115 +1777,22 @@ export async function runEvaluationSuite(options) {
|
|
|
1612
1777
|
};
|
|
1613
1778
|
}
|
|
1614
1779
|
},
|
|
1615
|
-
onSettled: (
|
|
1616
|
-
|
|
1617
|
-
|
|
1780
|
+
onSettled: async (result, index) => {
|
|
1781
|
+
settledTrials[index] = result;
|
|
1782
|
+
settledTrialCount += 1;
|
|
1783
|
+
await publishSettledTrial(result);
|
|
1618
1784
|
},
|
|
1619
1785
|
});
|
|
1620
1786
|
const results = await workPool;
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if (options.signal?.aborted) {
|
|
1631
|
-
run.executionStatus = 'canceled';
|
|
1632
|
-
run.qualityStatus = 'not-evaluated';
|
|
1633
|
-
run.qualityReason = { code: 'canceled', message: 'The evaluation run was canceled.' };
|
|
1634
|
-
}
|
|
1635
|
-
else if (purpose === 'execution-benchmark') {
|
|
1636
|
-
run.executionStatus = 'completed';
|
|
1637
|
-
run.qualityStatus = 'not-evaluated';
|
|
1638
|
-
run.qualityReason = {
|
|
1639
|
-
code: 'benchmark',
|
|
1640
|
-
message: 'This run measured execution without evaluating output quality.',
|
|
1641
|
-
};
|
|
1642
|
-
}
|
|
1643
|
-
else if (evaluationMode === 'scoring') {
|
|
1644
|
-
run.executionStatus = 'completed';
|
|
1645
|
-
if ((outcome.aggregate.missingScoreTrialCount ?? run.trials.length) > 0) {
|
|
1646
|
-
run.qualityStatus = 'unable-to-evaluate';
|
|
1647
|
-
run.qualityReason = {
|
|
1648
|
-
code: 'scores-incomplete',
|
|
1649
|
-
message: 'One or more requested trials did not produce a usable score. Available averages are shown with coverage.',
|
|
1650
|
-
};
|
|
1651
|
-
run.warnings.push(`Score coverage is ${outcome.aggregate.scoredTrialCount ?? 0} of ${outcome.aggregate.trialCount} requested trials.`);
|
|
1652
|
-
}
|
|
1653
|
-
else if ((outcome.aggregate.scoredTrialCount ?? 0) > 0) {
|
|
1654
|
-
run.qualityStatus = 'scored';
|
|
1655
|
-
run.qualityReason = {
|
|
1656
|
-
code: 'scores-complete',
|
|
1657
|
-
message: 'Every requested trial produced a score. Overall score is the equal-weight average of case averages.',
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
else {
|
|
1661
|
-
run.qualityStatus = 'unable-to-evaluate';
|
|
1662
|
-
run.qualityReason = { code: 'scores-incomplete', message: 'No requested trial produced a usable score.' };
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
else {
|
|
1666
|
-
const baseline = options.baseline ?? options.evaluationData.baselines.find((candidate) => candidate.suiteId === suite.id);
|
|
1667
|
-
const usableBaseline = baseline && baselineCompatible(run.provenance, baseline) ? baseline : undefined;
|
|
1668
|
-
if (baseline && !usableBaseline)
|
|
1669
|
-
run.warnings.push('The suite baseline is stale because its target, dataset, bindings, or evaluator definition changed.');
|
|
1670
|
-
run.thresholdResults = evaluateThresholdResults(outcome.aggregate, suite.thresholds, usableBaseline);
|
|
1671
|
-
const hasPassRateThreshold = suite.thresholds?.some((threshold) => threshold.metric === 'pass-rate') ?? false;
|
|
1672
|
-
const hasTargetErrorRateThreshold = suite.thresholds?.some((threshold) => threshold.metric === 'target-error-rate') ?? false;
|
|
1673
|
-
// Per-trial checks and target execution errors are strict by default. An
|
|
1674
|
-
// author can deliberately replace either default with its matching
|
|
1675
|
-
// aggregate tolerance, but one kind of threshold must never hide the
|
|
1676
|
-
// other kind of failure.
|
|
1677
|
-
const hasUngovernedCheckFailure = !hasPassRateThreshold &&
|
|
1678
|
-
run.trials.some((trial) => trial.executionStatus === 'completed' && trial.qualityStatus === 'failed');
|
|
1679
|
-
const hasUngovernedTargetError = !hasTargetErrorRateThreshold && run.trials.some((trial) => trial.executionStatus === 'error');
|
|
1680
|
-
const hasThresholdFailure = run.thresholdResults.some((result) => result.status === 'failed');
|
|
1681
|
-
const hasUnavailableEvidence = run.trials.some((trial) => trial.qualityStatus === 'unable-to-evaluate') ||
|
|
1682
|
-
run.thresholdResults.some((result) => result.status === 'unavailable');
|
|
1683
|
-
const hasAuthoritativePass = run.trials.some((trial) => trial.executionStatus === 'completed' && trial.qualityStatus === 'passed') ||
|
|
1684
|
-
run.thresholdResults.some((result) => result.status === 'passed');
|
|
1685
|
-
if (hasUngovernedCheckFailure || hasUngovernedTargetError || hasThresholdFailure) {
|
|
1686
|
-
run.qualityStatus = 'failed';
|
|
1687
|
-
run.qualityReason = {
|
|
1688
|
-
code: hasThresholdFailure ? 'thresholds-failed' : 'checks-failed',
|
|
1689
|
-
message: hasThresholdFailure
|
|
1690
|
-
? 'One or more required aggregate thresholds failed.'
|
|
1691
|
-
: 'One or more target executions or required quality checks failed.',
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
else if (hasUnavailableEvidence) {
|
|
1695
|
-
run.qualityStatus = 'unable-to-evaluate';
|
|
1696
|
-
run.qualityReason = {
|
|
1697
|
-
code: run.thresholdResults.some((result) => result.status === 'unavailable')
|
|
1698
|
-
? 'required-metric-unavailable'
|
|
1699
|
-
: 'required-check-error',
|
|
1700
|
-
message: run.thresholdResults.some((result) => result.status === 'unavailable')
|
|
1701
|
-
? 'A required aggregate metric could not be evaluated.'
|
|
1702
|
-
: 'A required quality check could not be evaluated.',
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
else if (hasAuthoritativePass) {
|
|
1706
|
-
run.qualityStatus = 'passed';
|
|
1707
|
-
run.qualityReason = {
|
|
1708
|
-
code: run.thresholdResults.length > 0 ? 'thresholds-passed' : 'checks-passed',
|
|
1709
|
-
message: run.thresholdResults.length > 0
|
|
1710
|
-
? 'All required quality checks and aggregate thresholds passed.'
|
|
1711
|
-
: 'All required quality checks passed.',
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
else {
|
|
1715
|
-
run.qualityStatus = 'not-evaluated';
|
|
1716
|
-
run.qualityReason = {
|
|
1717
|
-
code: 'no-completed-trials',
|
|
1718
|
-
message: 'No completed trial produced an authoritative quality result.',
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
run.executionStatus = 'completed';
|
|
1722
|
-
}
|
|
1723
|
-
publish();
|
|
1787
|
+
finalizeEvaluationRun({
|
|
1788
|
+
run,
|
|
1789
|
+
trials: results.filter((result) => result !== undefined),
|
|
1790
|
+
suite,
|
|
1791
|
+
evaluationData: options.evaluationData,
|
|
1792
|
+
baseline: options.baseline,
|
|
1793
|
+
canceled: options.signal?.aborted === true,
|
|
1794
|
+
});
|
|
1795
|
+
await publishFinalized();
|
|
1724
1796
|
return run;
|
|
1725
1797
|
}
|
|
1726
1798
|
export async function runEvaluationCases(options) {
|
|
@@ -209,6 +209,9 @@ function validateBaselineShape(value, index) {
|
|
|
209
209
|
'notEvaluatedTrialCount',
|
|
210
210
|
'unableToEvaluateTrialCount',
|
|
211
211
|
'meanScore',
|
|
212
|
+
'medianScore',
|
|
213
|
+
'p95Score',
|
|
214
|
+
'medianLatencyMs',
|
|
212
215
|
'totalCostUsd',
|
|
213
216
|
'averageCostUsd',
|
|
214
217
|
'scoredTrialCount',
|
package/dist/esm/store.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import { assertEvaluationDatasetSnapshot } from './canonical.js';
|
|
2
|
-
import { normalizeEvaluationRun,
|
|
1
|
+
import { assertEvaluationDatasetSnapshot, assertEvaluationRecordingArtifact } from './canonical.js';
|
|
2
|
+
import { normalizeEvaluationRun, reconcileEvaluationRunSnapshots } from './normalization.js';
|
|
3
|
+
import { createEmptyEvaluationLibrary, normalizeEvaluationLibrary } from './library.js';
|
|
3
4
|
export class InMemoryEvaluationRunStore {
|
|
4
5
|
#runs = new Map();
|
|
5
6
|
#datasetSnapshots = new Map();
|
|
6
7
|
#recordings = new Map();
|
|
8
|
+
#library = createEmptyEvaluationLibrary();
|
|
9
|
+
async getLibrary() {
|
|
10
|
+
return structuredClone(this.#library);
|
|
11
|
+
}
|
|
12
|
+
async putLibrary(library) {
|
|
13
|
+
this.#library = structuredClone(normalizeEvaluationLibrary(library));
|
|
14
|
+
}
|
|
7
15
|
async put(run) {
|
|
8
16
|
const normalized = normalizeEvaluationRun(run);
|
|
9
17
|
const key = `${normalized.projectId}/${normalized.id}`;
|
|
10
18
|
const existing = this.#runs.get(key);
|
|
11
|
-
|
|
19
|
+
const next = reconcileEvaluationRunSnapshots(existing, normalized);
|
|
20
|
+
if (next === existing)
|
|
12
21
|
return;
|
|
13
|
-
this.#runs.set(key, structuredClone(
|
|
22
|
+
this.#runs.set(key, structuredClone(next));
|
|
23
|
+
}
|
|
24
|
+
async updateRunName(input) {
|
|
25
|
+
const key = `${input.projectId}/${input.runId}`;
|
|
26
|
+
const existing = this.#runs.get(key);
|
|
27
|
+
if (!existing)
|
|
28
|
+
return undefined;
|
|
29
|
+
const renamed = normalizeEvaluationRun({ ...existing, name: input.name });
|
|
30
|
+
this.#runs.set(key, structuredClone(renamed));
|
|
31
|
+
return structuredClone(renamed);
|
|
14
32
|
}
|
|
15
33
|
async get(input) {
|
|
16
34
|
const run = this.#runs.get(`${input.projectId}/${input.runId}`);
|
|
@@ -43,6 +61,7 @@ export class InMemoryEvaluationRunStore {
|
|
|
43
61
|
return snapshot ? structuredClone(snapshot) : undefined;
|
|
44
62
|
}
|
|
45
63
|
async putRecording(artifact) {
|
|
64
|
+
assertEvaluationRecordingArtifact(artifact);
|
|
46
65
|
const key = `${artifact.projectId}/${artifact.reference.id}`;
|
|
47
66
|
const existing = this.#recordings.get(key);
|
|
48
67
|
if (existing && (existing.runId !== artifact.runId || existing.trialId !== artifact.trialId)) {
|
|
@@ -71,12 +90,13 @@ export class InMemoryEvaluationRunStore {
|
|
|
71
90
|
const key = `${input.projectId}/${input.recordingId}`;
|
|
72
91
|
const artifact = this.#recordings.get(key);
|
|
73
92
|
if (!artifact)
|
|
74
|
-
return;
|
|
93
|
+
return false;
|
|
75
94
|
artifact.reference = {
|
|
76
95
|
id: artifact.reference.id,
|
|
77
96
|
retention: input.retention,
|
|
78
97
|
...(input.expiresAt === undefined ? {} : { expiresAt: input.expiresAt }),
|
|
79
98
|
};
|
|
99
|
+
return true;
|
|
80
100
|
}
|
|
81
101
|
async promoteBaseline(input) {
|
|
82
102
|
for (const artifact of this.#recordings.values()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EvaluationDatasetSnapshot, PortableJson } from './types.js';
|
|
1
|
+
import type { EvaluationDatasetSnapshot, EvaluationRecordingArtifact, PortableJson } from './types.js';
|
|
2
2
|
export declare function assertPortableJson(value: unknown, path?: string, stack?: Set<object>): asserts value is PortableJson;
|
|
3
3
|
export declare function canonicalizePortableJson(value: unknown): PortableJson;
|
|
4
4
|
/**
|
|
@@ -21,3 +21,5 @@ export declare function fingerprintEvaluationDataset(value: {
|
|
|
21
21
|
* accepting a snapshot that can no longer prove which cases were run.
|
|
22
22
|
*/
|
|
23
23
|
export declare function assertEvaluationDatasetSnapshot(snapshot: EvaluationDatasetSnapshot): void;
|
|
24
|
+
/** Rejects malformed replay evidence at both write and durable-read boundaries. */
|
|
25
|
+
export declare function assertEvaluationRecordingArtifact(value: unknown): asserts value is EvaluationRecordingArtifact;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvaluationLibrary } from './types.js';
|
|
2
|
+
export declare function createEmptyEvaluationLibrary(): EvaluationLibrary;
|
|
3
|
+
/**
|
|
4
|
+
* Repairs a library resource-by-resource so one malformed suite, baseline, or
|
|
5
|
+
* dataset cannot hide unrelated valid resources. The top-level persistence
|
|
6
|
+
* envelope is validated by the owning store before this function is called.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeEvaluationLibrary(value: unknown): EvaluationLibrary;
|
|
@@ -5,6 +5,21 @@ import type { EvaluationAggregate, EvaluationBaselineSnapshot, EvaluationRun, Ev
|
|
|
5
5
|
* rule so persistence cannot disagree with what the workspace presents.
|
|
6
6
|
*/
|
|
7
7
|
export declare function shouldReplaceEvaluationRun(existing: Pick<EvaluationRun, 'executionStatus' | 'revision'> | undefined, incoming: Pick<EvaluationRun, 'executionStatus' | 'revision'>): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Execution snapshots never own a user-assigned run name. Preserve it when a
|
|
10
|
+
* runner publishes a newer progress or terminal revision after a rename.
|
|
11
|
+
*/
|
|
12
|
+
export declare function preserveEvaluationRunName(existing: Pick<EvaluationRun, 'name'> | undefined, incoming: EvaluationRun): EvaluationRun;
|
|
13
|
+
/**
|
|
14
|
+
* Reconciles two snapshots of the same run without discarding evidence.
|
|
15
|
+
*
|
|
16
|
+
* Revisions remain authoritative. Within one revision, however, persistence
|
|
17
|
+
* and recording-retention updates can race with a terminal UI snapshot. Keep
|
|
18
|
+
* the snapshot with the fuller trial set, while still allowing a terminal
|
|
19
|
+
* snapshot to complete a progress snapshot and allowing recording references
|
|
20
|
+
* and warnings to enrich already-settled evidence.
|
|
21
|
+
*/
|
|
22
|
+
export declare function reconcileEvaluationRunSnapshots(existing: EvaluationRun | undefined, incoming: EvaluationRun): EvaluationRun;
|
|
8
23
|
export declare function normalizeEvaluationTrial(value: EvaluationTrial | unknown, purpose?: EvaluationRunPurpose, evaluationMode?: EvaluationSuiteMode): EvaluationTrial;
|
|
9
24
|
export declare function normalizeEvaluationAggregate(value: EvaluationAggregate | unknown, trials?: readonly EvaluationTrial[]): EvaluationAggregate;
|
|
10
25
|
export declare function normalizeEvaluationBaselineSnapshot(value: EvaluationBaselineSnapshot | unknown): EvaluationBaselineSnapshot;
|
package/dist/types/runner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EvaluationAggregate, EvaluationBaselineSnapshot, EvaluationCaseAggregate, EvaluationDataset, EvaluationExecutionMetrics, EvaluationGraphRunner, EvaluationGraphEvaluator, EvaluationProjectData, EvaluationRun, EvaluationRunPurpose, EvaluationSuite, EvaluationSuiteMode, EvaluationThreshold, EvaluationThresholdResult } from './types.js';
|
|
1
|
+
import type { EvaluationAggregate, EvaluationBaselineSnapshot, EvaluationCaseAggregate, EvaluationDataset, EvaluationExecutionMetrics, EvaluationGraphRunner, EvaluationGraphEvaluator, EvaluationProjectData, EvaluationRun, EvaluationRunEvent, EvaluationRunPurpose, EvaluationSuite, EvaluationSuiteMode, EvaluationThreshold, EvaluationThresholdResult, EvaluationTrial } from './types.js';
|
|
2
2
|
import type { Project } from '@valerypopoff/rivet2-core';
|
|
3
3
|
export declare const LEGACY_EVALUATOR_INPUT_IDS: readonly ["case", "inputs", "expected", "outputs", "run"];
|
|
4
4
|
export type RunEvaluationSuiteOptions = {
|
|
@@ -15,6 +15,9 @@ export type RunEvaluationSuiteOptions = {
|
|
|
15
15
|
purpose?: EvaluationRunPurpose;
|
|
16
16
|
/** Explicit caller selection; otherwise the suite's saved baseline is used. */
|
|
17
17
|
baseline?: EvaluationBaselineSnapshot;
|
|
18
|
+
/** Incremental live-run transport. Prefer this over onUpdate in new integrations. */
|
|
19
|
+
onEvent?: (event: EvaluationRunEvent) => void | Promise<void>;
|
|
20
|
+
/** @deprecated Prefer onEvent; this clones the complete accumulated run after every trial. */
|
|
18
21
|
onUpdate?: (run: EvaluationRun) => void;
|
|
19
22
|
};
|
|
20
23
|
export declare function hasAuthoritativeEvaluationCriteria(suite: EvaluationSuite): boolean;
|
|
@@ -35,7 +38,7 @@ export type RunEvaluationWorkPoolOptions<TWork, TResult> = {
|
|
|
35
38
|
concurrency: number;
|
|
36
39
|
signal?: AbortSignal;
|
|
37
40
|
execute: (work: TWork, index: number) => Promise<TResult>;
|
|
38
|
-
onSettled?: (result: TResult, index: number, results: readonly (TResult | undefined)[]) => void
|
|
41
|
+
onSettled?: (result: TResult, index: number, results: readonly (TResult | undefined)[]) => void | Promise<void>;
|
|
39
42
|
};
|
|
40
43
|
export declare function runEvaluationWorkPool<TWork, TResult>(options: RunEvaluationWorkPoolOptions<TWork, TResult>): Promise<readonly (TResult | undefined)[]>;
|
|
41
44
|
export declare function createEmptyMetrics(): EvaluationExecutionMetrics;
|
|
@@ -50,7 +53,60 @@ export declare function createEmptyMetrics(): EvaluationExecutionMetrics;
|
|
|
50
53
|
* otherwise-successful recording in the suite.
|
|
51
54
|
*/
|
|
52
55
|
export declare function finalizeEvaluationRecordingRetention(run: EvaluationRun, policy?: 'failures-and-baselines' | 'all', now?: number): EvaluationRun;
|
|
56
|
+
export type RunEvaluationTrialOptions = {
|
|
57
|
+
project: Project;
|
|
58
|
+
suite: EvaluationSuite;
|
|
59
|
+
dataset: EvaluationDataset;
|
|
60
|
+
purpose: EvaluationRunPurpose;
|
|
61
|
+
testCase: EvaluationDataset['cases'][number];
|
|
62
|
+
caseIndex: number;
|
|
63
|
+
trialIndex: number;
|
|
64
|
+
runId: string;
|
|
65
|
+
runGraph: EvaluationGraphRunner;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
/** Stable server-owned job id. Local callers may omit it. */
|
|
68
|
+
trialId?: string;
|
|
69
|
+
};
|
|
70
|
+
export declare function runEvaluationTrial(input: RunEvaluationTrialOptions): Promise<EvaluationTrial>;
|
|
53
71
|
export declare function evaluateThresholdResults(aggregateValue: EvaluationAggregate, thresholds?: EvaluationThreshold[], baseline?: EvaluationBaselineSnapshot): EvaluationThresholdResult[];
|
|
72
|
+
export type EvaluationRunWorkItem = {
|
|
73
|
+
testCase: EvaluationDataset['cases'][number];
|
|
74
|
+
caseIndex: number;
|
|
75
|
+
trialIndex: number;
|
|
76
|
+
};
|
|
77
|
+
export type EvaluationRunPlan = {
|
|
78
|
+
suite: EvaluationSuite;
|
|
79
|
+
purpose: EvaluationRunPurpose;
|
|
80
|
+
evaluationMode: EvaluationSuiteMode;
|
|
81
|
+
work: readonly EvaluationRunWorkItem[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Validates an immutable Evaluation submission and expands its enabled cases
|
|
85
|
+
* into deterministic work. Hosts can persist this plan before any graph side
|
|
86
|
+
* effect, while local callers continue to use runEvaluationSuite.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createEvaluationRunPlan(options: Pick<RunEvaluationSuiteOptions, 'project' | 'evaluationData' | 'dataset' | 'suiteId' | 'purpose'>): EvaluationRunPlan;
|
|
89
|
+
export type FinalizeEvaluationRunOptions = {
|
|
90
|
+
run: EvaluationRun;
|
|
91
|
+
trials: readonly EvaluationTrial[];
|
|
92
|
+
suite: EvaluationSuite;
|
|
93
|
+
evaluationData: EvaluationProjectData;
|
|
94
|
+
baseline?: EvaluationBaselineSnapshot;
|
|
95
|
+
canceled?: boolean;
|
|
96
|
+
completedAt?: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Applies the same aggregate, quality, accounting, and baseline semantics to
|
|
100
|
+
* locally scheduled and durably hosted trials. Callers must provide every
|
|
101
|
+
* settled trial in deterministic case/trial order; never synthesize retries.
|
|
102
|
+
*/
|
|
103
|
+
export declare function finalizeEvaluationRun(options: FinalizeEvaluationRunOptions): EvaluationRun;
|
|
104
|
+
export type CreateEvaluationRunShellOptions = Pick<RunEvaluationSuiteOptions, 'project' | 'evaluationData' | 'dataset' | 'suiteId' | 'purpose' | 'runId' | 'projectFingerprint' | 'executionMode'>;
|
|
105
|
+
/** Creates a durable running shell only after the complete submission validates. */
|
|
106
|
+
export declare function createEvaluationRunShell(options: CreateEvaluationRunShellOptions): {
|
|
107
|
+
run: EvaluationRun;
|
|
108
|
+
plan: EvaluationRunPlan;
|
|
109
|
+
};
|
|
54
110
|
export declare function runEvaluationSuite(options: RunEvaluationSuiteOptions): Promise<EvaluationRun>;
|
|
55
111
|
export declare function runEvaluationCases(options: RunEvaluationSuiteOptions & {
|
|
56
112
|
caseIds: readonly string[];
|