@tangle-network/agent-eval 0.122.9 → 0.123.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/CHANGELOG.md +4 -0
- package/dist/belief-state/index.d.ts +45 -5
- package/dist/belief-state/index.js +41 -3
- package/dist/belief-state/index.js.map +1 -1
- package/dist/benchmarks/index.js +2 -2
- package/dist/campaign/index.d.ts +29 -6
- package/dist/campaign/index.js +2 -2
- package/dist/{chunk-CWF6SCBK.js → chunk-GS3FJGUF.js} +2 -2
- package/dist/{chunk-4EDDIK77.js → chunk-HZJF4IUO.js} +71 -32
- package/dist/{chunk-4EDDIK77.js.map → chunk-HZJF4IUO.js.map} +1 -1
- package/dist/{chunk-DTJ6QUQB.js → chunk-VGRCHJON.js} +39 -7
- package/dist/chunk-VGRCHJON.js.map +1 -0
- package/dist/contract/index.d.ts +62 -11
- package/dist/contract/index.js +33 -8
- package/dist/contract/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/openapi.json +1 -1
- package/dist/rl.d.ts +39 -11
- package/dist/rl.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-DTJ6QUQB.js.map +0 -1
- /package/dist/{chunk-CWF6SCBK.js.map → chunk-GS3FJGUF.js.map} +0 -0
|
@@ -74,8 +74,18 @@ function selfNormalizedImportanceWeighting(trajectories, opts = {}) {
|
|
|
74
74
|
function doublyRobust(trajectories, opts = {}) {
|
|
75
75
|
const cap = opts.weightCap ?? Infinity;
|
|
76
76
|
const clip = opts.rewardClip ?? { low: 0, high: 1 };
|
|
77
|
-
if (trajectories.length === 0)
|
|
77
|
+
if (trajectories.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
...zeroEstimate(),
|
|
80
|
+
contributionCounts: { dr: 0, ipsFallback: 0, legacyScalar: 0 }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
78
83
|
const contributions = [];
|
|
84
|
+
const contributionCounts = {
|
|
85
|
+
dr: 0,
|
|
86
|
+
ipsFallback: 0,
|
|
87
|
+
legacyScalar: 0
|
|
88
|
+
};
|
|
79
89
|
let maxW = 0;
|
|
80
90
|
let sumW = 0;
|
|
81
91
|
let sumW2 = 0;
|
|
@@ -85,11 +95,32 @@ function doublyRobust(trajectories, opts = {}) {
|
|
|
85
95
|
}
|
|
86
96
|
const w = Math.min(cap, t.targetProb / t.behaviorProb);
|
|
87
97
|
const r = clamp(t.reward, clip.low, clip.high);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
const rawQHatChosen = t.qHatChosen;
|
|
99
|
+
const rawVHatTarget = t.vHatTarget;
|
|
100
|
+
const hasQHatChosen = rawQHatChosen !== null && rawQHatChosen !== void 0;
|
|
101
|
+
const hasVHatTarget = rawVHatTarget !== null && rawVHatTarget !== void 0;
|
|
102
|
+
if (hasQHatChosen !== hasVHatTarget) {
|
|
103
|
+
throw new ValidationError(
|
|
104
|
+
`doublyRobust: qHatChosen and vHatTarget must be supplied together (runId=${t.runId})`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (hasQHatChosen && hasVHatTarget) {
|
|
108
|
+
if (!Number.isFinite(rawQHatChosen) || !Number.isFinite(rawVHatTarget)) {
|
|
109
|
+
throw new ValidationError(
|
|
110
|
+
`doublyRobust: qHatChosen and vHatTarget must be finite (runId=${t.runId})`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const qHatChosen = clamp(rawQHatChosen, clip.low, clip.high);
|
|
114
|
+
const vHatTarget = clamp(rawVHatTarget, clip.low, clip.high);
|
|
115
|
+
contributions.push(vHatTarget + w * (r - qHatChosen));
|
|
116
|
+
contributionCounts.dr += 1;
|
|
117
|
+
} else if (typeof t.qHat === "number" && Number.isFinite(t.qHat)) {
|
|
118
|
+
const qHat = clamp(t.qHat, clip.low, clip.high);
|
|
119
|
+
contributions.push(qHat + w * (r - qHat));
|
|
120
|
+
contributionCounts.legacyScalar += 1;
|
|
91
121
|
} else {
|
|
92
|
-
contributions.push(
|
|
122
|
+
contributions.push(w * r);
|
|
123
|
+
contributionCounts.ipsFallback += 1;
|
|
93
124
|
}
|
|
94
125
|
if (w > maxW) maxW = w;
|
|
95
126
|
sumW += w;
|
|
@@ -104,7 +135,8 @@ function doublyRobust(trajectories, opts = {}) {
|
|
|
104
135
|
standardError: Math.sqrt(variance / n),
|
|
105
136
|
effectiveSampleSize: effN,
|
|
106
137
|
n,
|
|
107
|
-
maxImportanceWeight: maxW
|
|
138
|
+
maxImportanceWeight: maxW,
|
|
139
|
+
contributionCounts
|
|
108
140
|
};
|
|
109
141
|
}
|
|
110
142
|
function offPolicyEstimateAll(trajectories, opts = {}) {
|
|
@@ -128,4 +160,4 @@ export {
|
|
|
128
160
|
doublyRobust,
|
|
129
161
|
offPolicyEstimateAll
|
|
130
162
|
};
|
|
131
|
-
//# sourceMappingURL=chunk-
|
|
163
|
+
//# sourceMappingURL=chunk-VGRCHJON.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rl/off-policy.ts"],"sourcesContent":["/**\n * Off-policy evaluation primitives.\n *\n * Standard inverse-probability-weighted (IPS), self-normalized\n * importance-weighted (SNIPS), and doubly-robust (DR) estimators for the\n * value of a *target* policy given trajectories collected under a\n * *behavior* policy. This is the canonical RL eval task: \"we have last\n * week's runs, we changed the policy — how would the new one do without\n * re-running?\"\n *\n * The math here is textbook (Dudík, Langford, Li 2011 for DR; Swaminathan\n * & Joachims 2015 for SNIPS) but the *application* to LLM-agent\n * evaluation needs care:\n *\n * - The \"policy\" is the (prompt, tool config, model snapshot) triple.\n * Two policies have the same probability over an action *iff* their\n * LLM call would emit the same token with the same probability —\n * which is generally unknowable without the model log-probs.\n * - For LLM agents, propensity scores must be supplied by the caller\n * (logged in the trace, recovered from token log-probs, or estimated\n * via a learned propensity model). We do NOT estimate propensity here.\n * - Doubly-robust requires two outputs from a Q-function: its prediction\n * for the logged action and its expectation under the target policy.\n * Consumers compute these with a tabular estimate, regression fit, or\n * learned reward model before constructing the trajectories.\n *\n * Bias / variance tradeoffs:\n * - IPS: unbiased; high variance for small overlap, infinite variance\n * when target has support outside behavior.\n * - SNIPS: lower variance, slight bias; usually preferred in practice.\n * - DR: doubly-robust — unbiased if either propensity OR Q-function is\n * correct. Lowest practical variance when Q is decent. Use this.\n *\n * Caveat the panel will land: on the LLM-agent setting, propensity scores\n * recovered from token log-probs are noisy, the action space is enormous,\n * and overlap is often poor. These estimators are useful but not magic;\n * complement with `replayCampaign` (exact replay where the request hashes\n * match) for high-confidence answers and OPE for the gap.\n */\n\nimport { ValidationError } from '../errors'\n\nexport interface OffPolicyTrajectory {\n /** Stable id, for traceability through the dataset. */\n runId: string\n /** Reward observed under the behavior policy (the realized outcome). */\n reward: number\n /**\n * Behavior-policy probability of the action that was taken. For LLM\n * agents this is typically `exp(sum(token_log_probs))` over the chosen\n * trajectory. Must be in (0, 1].\n */\n behaviorProb: number\n /**\n * Target-policy probability of the same action. For replay-style\n * counterfactual evaluation this is what the *new* policy would have\n * assigned to the *old* trajectory. Must be in [0, 1].\n */\n targetProb: number\n /**\n * Model-based reward prediction for the action selected by the behavior\n * policy: `Q_hat(context, loggedAction)`. Supply this together with\n * `vHatTarget` for contextual-bandit doubly-robust estimation.\n */\n qHatChosen?: number | null\n /**\n * Expected model-based reward under the target policy:\n * `sum_action targetPolicy(action | context) * Q_hat(context, action)`.\n * Supply this together with `qHatChosen`. For an honest evaluation, both\n * values must come from a model cross-fitted or trained outside this row.\n */\n vHatTarget?: number | null\n /**\n * @deprecated Use `qHatChosen` and `vHatTarget` together. When the new pair\n * is absent, this scalar is used as both terms to preserve existing results.\n * When the new pair is present, this field is ignored.\n */\n qHat?: number | null\n}\n\nexport interface OffPolicyContributionCounts {\n /** Contributions using the contextual-bandit doubly-robust formula. */\n dr: number\n /** Contributions using exact IPS because no reward-model estimate was supplied. */\n ipsFallback: number\n /** Contributions using the deprecated single-scalar formula. */\n legacyScalar: number\n}\n\nexport interface OffPolicyEstimate {\n /** Estimated value of the target policy. */\n value: number\n /** Standard error of the estimate. */\n standardError: number\n /** Effective sample size (Kong 1992). Lower = more reliance on a few high-weight samples. */\n effectiveSampleSize: number\n /** Number of trajectories used. */\n n: number\n /**\n * Diagnostic: maximum importance weight observed. Large values (>>10x\n * mean) are a red flag — variance is dominated by a few outliers.\n */\n maxImportanceWeight: number\n /** Populated by `doublyRobust` to expose which formula each row used. */\n contributionCounts?: OffPolicyContributionCounts\n}\n\nexport interface OffPolicyOptions {\n /**\n * Cap importance weights at this value (Ionides 2008 truncated IS) to\n * trade unbiasedness for variance reduction. Default `Infinity` (no cap).\n * Set e.g. `10` for stable estimates when the policies are close.\n */\n weightCap?: number\n /** Reward clipping range. Default `[0, 1]`. */\n rewardClip?: { low: number; high: number }\n}\n\n/**\n * Inverse Probability Weighting (Horvitz-Thompson). Unbiased estimator\n * of E[reward under target policy]. Variance scales with the spread of\n * target/behavior ratios.\n */\nexport function inverseProbabilityWeighting(\n trajectories: OffPolicyTrajectory[],\n opts: OffPolicyOptions = {},\n): OffPolicyEstimate {\n const cap = opts.weightCap ?? Infinity\n const clip = opts.rewardClip ?? { low: 0, high: 1 }\n\n if (trajectories.length === 0) {\n return zeroEstimate()\n }\n\n const weights: number[] = []\n const weightedRewards: number[] = []\n let maxW = 0\n for (const t of trajectories) {\n if (t.behaviorProb <= 0) {\n throw new ValidationError(\n `inverseProbabilityWeighting: behaviorProb must be > 0 (runId=${t.runId})`,\n )\n }\n const w = Math.min(cap, t.targetProb / t.behaviorProb)\n const r = clamp(t.reward, clip.low, clip.high)\n weights.push(w)\n weightedRewards.push(w * r)\n if (w > maxW) maxW = w\n }\n const n = weights.length\n const value = weightedRewards.reduce((s, x) => s + x, 0) / n\n const variance = weightedRewards.reduce((s, x) => s + (x - value) ** 2, 0) / Math.max(1, n - 1)\n const sumW = weights.reduce((s, w) => s + w, 0)\n const sumW2 = weights.reduce((s, w) => s + w * w, 0)\n const effN = sumW === 0 ? 0 : (sumW * sumW) / sumW2\n\n return {\n value,\n standardError: Math.sqrt(variance / n),\n effectiveSampleSize: effN,\n n,\n maxImportanceWeight: maxW,\n }\n}\n\n/**\n * Self-Normalized Importance Sampling. Lower variance than vanilla IPS at\n * the cost of small bias (vanishing as N grows). The right default for\n * LLM-agent evaluation where overlap is often poor.\n */\nexport function selfNormalizedImportanceWeighting(\n trajectories: OffPolicyTrajectory[],\n opts: OffPolicyOptions = {},\n): OffPolicyEstimate {\n const cap = opts.weightCap ?? Infinity\n const clip = opts.rewardClip ?? { low: 0, high: 1 }\n if (trajectories.length === 0) return zeroEstimate()\n\n const weights: number[] = []\n const rewards: number[] = []\n let maxW = 0\n for (const t of trajectories) {\n if (t.behaviorProb <= 0) {\n throw new ValidationError(\n `selfNormalizedImportanceWeighting: behaviorProb must be > 0 (runId=${t.runId})`,\n )\n }\n const w = Math.min(cap, t.targetProb / t.behaviorProb)\n weights.push(w)\n rewards.push(clamp(t.reward, clip.low, clip.high))\n if (w > maxW) maxW = w\n }\n const sumW = weights.reduce((s, w) => s + w, 0)\n const sumWR = weights.reduce((s, w, i) => s + w * rewards[i]!, 0)\n const value = sumW === 0 ? 0 : sumWR / sumW\n const sumW2 = weights.reduce((s, w) => s + w * w, 0)\n const effN = sumW === 0 ? 0 : (sumW * sumW) / sumW2\n // Influence-function-based SE for SNIPS (Owen 2013, Ch. 9).\n const phi = weights.map((w, i) => w * (rewards[i]! - value))\n const variance = phi.reduce((s, x) => s + x * x, 0) / Math.max(1, sumW * sumW)\n return {\n value,\n standardError: Math.sqrt(variance),\n effectiveSampleSize: effN,\n n: trajectories.length,\n maxImportanceWeight: maxW,\n }\n}\n\n/**\n * Doubly-robust off-policy estimator (Dudík, Langford, Li 2011).\n *\n * V_DR = (1/N) * sum_i [ v_hat_target_i\n * + (target_prob_i / behavior_prob_i) * (r_i - q_hat_chosen_i) ]\n *\n * Unbiased if EITHER:\n * - the importance ratios are correct (IPS-style validity), OR\n * - the Q-hat function is correct (model-based validity).\n *\n * In practice both are imperfect, but the residual bias is the *product*\n * of both errors — much smaller than either alone. This is why DR is the\n * default in production OPE pipelines.\n *\n * `qHatChosen` and `vHatTarget` must be supplied together. Rows with neither\n * use the exact IPS contribution. Deprecated `qHat` rows preserve the scalar\n * formula, and a complete new pair takes precedence when both forms exist.\n * `contributionCounts` makes the mix explicit in the result.\n * Callers must cross-fit the Q-function or train it on independent rows;\n * fitting and evaluating Q on the same outcomes leaks the answer.\n */\nexport function doublyRobust(\n trajectories: OffPolicyTrajectory[],\n opts: OffPolicyOptions = {},\n): OffPolicyEstimate {\n const cap = opts.weightCap ?? Infinity\n const clip = opts.rewardClip ?? { low: 0, high: 1 }\n if (trajectories.length === 0) {\n return {\n ...zeroEstimate(),\n contributionCounts: { dr: 0, ipsFallback: 0, legacyScalar: 0 },\n }\n }\n\n const contributions: number[] = []\n const contributionCounts: OffPolicyContributionCounts = {\n dr: 0,\n ipsFallback: 0,\n legacyScalar: 0,\n }\n let maxW = 0\n let sumW = 0\n let sumW2 = 0\n for (const t of trajectories) {\n if (t.behaviorProb <= 0) {\n throw new ValidationError(`doublyRobust: behaviorProb must be > 0 (runId=${t.runId})`)\n }\n const w = Math.min(cap, t.targetProb / t.behaviorProb)\n const r = clamp(t.reward, clip.low, clip.high)\n const rawQHatChosen = t.qHatChosen\n const rawVHatTarget = t.vHatTarget\n const hasQHatChosen = rawQHatChosen !== null && rawQHatChosen !== undefined\n const hasVHatTarget = rawVHatTarget !== null && rawVHatTarget !== undefined\n if (hasQHatChosen !== hasVHatTarget) {\n throw new ValidationError(\n `doublyRobust: qHatChosen and vHatTarget must be supplied together (runId=${t.runId})`,\n )\n }\n\n if (hasQHatChosen && hasVHatTarget) {\n if (!Number.isFinite(rawQHatChosen) || !Number.isFinite(rawVHatTarget)) {\n throw new ValidationError(\n `doublyRobust: qHatChosen and vHatTarget must be finite (runId=${t.runId})`,\n )\n }\n const qHatChosen = clamp(rawQHatChosen, clip.low, clip.high)\n const vHatTarget = clamp(rawVHatTarget, clip.low, clip.high)\n contributions.push(vHatTarget + w * (r - qHatChosen))\n contributionCounts.dr += 1\n } else if (typeof t.qHat === 'number' && Number.isFinite(t.qHat)) {\n const qHat = clamp(t.qHat, clip.low, clip.high)\n contributions.push(qHat + w * (r - qHat))\n contributionCounts.legacyScalar += 1\n } else {\n contributions.push(w * r)\n contributionCounts.ipsFallback += 1\n }\n if (w > maxW) maxW = w\n sumW += w\n sumW2 += w * w\n }\n const n = contributions.length\n const value = contributions.reduce((s, x) => s + x, 0) / n\n const variance = contributions.reduce((s, x) => s + (x - value) ** 2, 0) / Math.max(1, n - 1)\n const effN = sumW === 0 ? 0 : (sumW * sumW) / sumW2\n return {\n value,\n standardError: Math.sqrt(variance / n),\n effectiveSampleSize: effN,\n n,\n maxImportanceWeight: maxW,\n contributionCounts,\n }\n}\n\n/**\n * Convenience: run all three estimators and return them side-by-side.\n * The recommended diagnostic — agreement across estimators is a much\n * stronger signal than any single one.\n */\nexport function offPolicyEstimateAll(\n trajectories: OffPolicyTrajectory[],\n opts: OffPolicyOptions = {},\n): { ips: OffPolicyEstimate; snips: OffPolicyEstimate; dr: OffPolicyEstimate } {\n return {\n ips: inverseProbabilityWeighting(trajectories, opts),\n snips: selfNormalizedImportanceWeighting(trajectories, opts),\n dr: doublyRobust(trajectories, opts),\n }\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\nfunction zeroEstimate(): OffPolicyEstimate {\n return { value: 0, standardError: 0, effectiveSampleSize: 0, n: 0, maxImportanceWeight: 0 }\n}\n\nfunction clamp(x: number, lo: number, hi: number): number {\n if (!Number.isFinite(x)) return lo\n return Math.max(lo, Math.min(hi, x))\n}\n"],"mappings":"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","names":[]}
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -2386,6 +2386,15 @@ type RunImprovementLoopOptions<TScenario extends Scenario, TArtifact> = RunOptim
|
|
|
2386
2386
|
/** Holdout scenarios kept OUT of the training optimization pool — used
|
|
2387
2387
|
* ONLY to score baseline vs winner for the gate. */
|
|
2388
2388
|
holdoutScenarios: TScenario[];
|
|
2389
|
+
/** Holdout policy. Default `'measured'`: baseline + winner are re-scored on
|
|
2390
|
+
* `holdoutScenarios` and the gate decides on that held-out comparison.
|
|
2391
|
+
* `'deferred'`: the improvement-set (search) campaigns run exactly as usual,
|
|
2392
|
+
* but ZERO holdout cells are dispatched, the gate is forced to `'hold'`, and
|
|
2393
|
+
* the result + provenance record carry `holdout: 'deferred'` with NO
|
|
2394
|
+
* held-out lift — for callers that measure the held-out comparison in a
|
|
2395
|
+
* separate later run instead of faking a static holdout scenario and
|
|
2396
|
+
* recording a meaningless lift. */
|
|
2397
|
+
holdout?: 'measured' | 'deferred';
|
|
2389
2398
|
/** Promotion gate. Substrate strongly recommends `defaultProductionGate`
|
|
2390
2399
|
* for production wiring (composes red-team / reward-hacking / canary /
|
|
2391
2400
|
* heldout). */
|
|
@@ -2413,6 +2422,10 @@ interface RunImprovementLoopResult<TArtifact, TScenario extends Scenario> extend
|
|
|
2413
2422
|
neutralizedOnHoldout?: CampaignResult<TArtifact, TScenario>;
|
|
2414
2423
|
neutralizedSurface?: MutableSurface;
|
|
2415
2424
|
gateResult: Awaited<ReturnType<Gate<TArtifact, TScenario>['decide']>>;
|
|
2425
|
+
/** Present iff the loop ran with `holdout: 'deferred'`. When set,
|
|
2426
|
+
* `baselineOnHoldout`/`winnerOnHoldout` are the shared EMPTY campaign (zero
|
|
2427
|
+
* cells dispatched) and the gate verdict is the forced `'hold'`. */
|
|
2428
|
+
holdout?: 'deferred';
|
|
2416
2429
|
/** Unified baseline→winner surface diff. Computed UNCONDITIONALLY (not only
|
|
2417
2430
|
* when `autoOnPromote === 'pr'`) so the diff that the gate decided on is
|
|
2418
2431
|
* always present on the result + in the emitted provenance record. Empty
|
|
@@ -3715,12 +3728,18 @@ interface LoopProvenanceRecord {
|
|
|
3715
3728
|
detail: unknown;
|
|
3716
3729
|
}>;
|
|
3717
3730
|
};
|
|
3718
|
-
/**
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3731
|
+
/** Present iff the loop ran with `holdout: 'deferred'` — the held-out
|
|
3732
|
+
* comparison was intentionally not measured in this run, so the holdout
|
|
3733
|
+
* composites and `heldOutLift` are ABSENT rather than recorded as a
|
|
3734
|
+
* meaningless 0. */
|
|
3735
|
+
holdout?: 'deferred';
|
|
3736
|
+
/** baseline-on-holdout composite mean. Absent when `holdout === 'deferred'`. */
|
|
3737
|
+
baselineHoldoutComposite?: number;
|
|
3738
|
+
/** winner-on-holdout composite mean. Absent when `holdout === 'deferred'`. */
|
|
3739
|
+
winnerHoldoutComposite?: number;
|
|
3740
|
+
/** winnerHoldout - baselineHoldout — RECOMPUTABLE from this record. Absent
|
|
3741
|
+
* when `holdout === 'deferred'` (no held-out measurement ran). */
|
|
3742
|
+
heldOutLift?: number;
|
|
3724
3743
|
/** Backend provenance: stub-vs-real verdict + worker call count + models. */
|
|
3725
3744
|
backend: LoopProvenanceBackend;
|
|
3726
3745
|
totalCostUsd: number;
|
|
@@ -3767,8 +3786,22 @@ interface SelfImproveBudget {
|
|
|
3767
3786
|
holdoutFraction?: number;
|
|
3768
3787
|
/** Explicit held-out scenarios; overrides `holdoutFraction`. */
|
|
3769
3788
|
holdoutScenarios?: Scenario[];
|
|
3789
|
+
/** Holdout policy. Default `'measured'`: split, re-score baseline vs winner
|
|
3790
|
+
* on the held-out set, gate on that comparison. `'deferred'`: run the
|
|
3791
|
+
* improvement-set campaigns + search promotion, dispatch ZERO holdout cells,
|
|
3792
|
+
* force the gate to `'hold'`, return `lift: undefined`, and record
|
|
3793
|
+
* `holdout: 'deferred'` in the provenance record — for callers that measure
|
|
3794
|
+
* the held-out comparison in a separate later run instead of faking a
|
|
3795
|
+
* static holdout scenario and recording a meaningless lift. Unless
|
|
3796
|
+
* `holdoutScenarios` reserves an explicit set, ALL scenarios train. */
|
|
3797
|
+
holdout?: 'measured' | 'deferred';
|
|
3770
3798
|
/** Per-scenario replicates per cell — raises bootstrap-CI tightness. Default 1. */
|
|
3771
3799
|
reps?: number;
|
|
3800
|
+
/** DEPTH dial forwarded to the proposer's `propose()` as
|
|
3801
|
+
* `ctx.maxImprovementShots` — max iterations an agentic candidate generator
|
|
3802
|
+
* may take per candidate (verify-in-session retries). Unset ⇒ the
|
|
3803
|
+
* proposer's own default. */
|
|
3804
|
+
maxImprovementShots?: number;
|
|
3772
3805
|
/** @deprecated Must be 1 when supplied. The loop promotes only a candidate
|
|
3773
3806
|
* that replaces its single global incumbent. */
|
|
3774
3807
|
promoteTopK?: number;
|
|
@@ -3801,7 +3834,7 @@ type SelfImproveProgressEvent = {
|
|
|
3801
3834
|
} | {
|
|
3802
3835
|
kind: 'gate.decided';
|
|
3803
3836
|
decision: string;
|
|
3804
|
-
lift
|
|
3837
|
+
lift?: number;
|
|
3805
3838
|
} | {
|
|
3806
3839
|
kind: 'power.estimated';
|
|
3807
3840
|
n: number;
|
|
@@ -3835,6 +3868,18 @@ interface SelfImproveOptions<TScenario extends Scenario, TArtifact> {
|
|
|
3835
3868
|
baselineSurface: MutableSurface;
|
|
3836
3869
|
/** Budget + loop shape. All fields optional. */
|
|
3837
3870
|
budget?: SelfImproveBudget;
|
|
3871
|
+
/**
|
|
3872
|
+
* Complete prior measurement of `baselineSurface` over the TRAIN split.
|
|
3873
|
+
* Forwarded to the loop body, which validates its surface hash, scenario
|
|
3874
|
+
* split, seed (42), reps, and coverage, then skips the baseline search
|
|
3875
|
+
* campaign entirely — no baseline dispatch, no resumability lookup. The
|
|
3876
|
+
* train split is `scenarios` minus the holdout split, so premeasure with
|
|
3877
|
+
* exactly that scenario set (explicit `budget.holdoutScenarios`, or
|
|
3878
|
+
* `budget.holdout: 'deferred'` with no reserved set, makes the train split
|
|
3879
|
+
* deterministic). Prior spend stays in the imported campaign aggregates and
|
|
3880
|
+
* is not re-added to this run's cost ledger.
|
|
3881
|
+
*/
|
|
3882
|
+
premeasuredBaseline?: PremeasuredOptimizationBaseline<TArtifact, TScenario>;
|
|
3838
3883
|
/** Custom surface proposer. Default is `gepaProposer` configured from `llm` +
|
|
3839
3884
|
* `mutationPrimitives`. */
|
|
3840
3885
|
proposer?: SurfaceProposer;
|
|
@@ -3934,12 +3979,16 @@ interface SelfImproveOptions<TScenario extends Scenario, TArtifact> {
|
|
|
3934
3979
|
findings?: unknown[];
|
|
3935
3980
|
}
|
|
3936
3981
|
interface SelfImproveResult<TScenario extends Scenario, TArtifact> {
|
|
3937
|
-
/** Composite mean across all scenarios, baseline run.
|
|
3982
|
+
/** Composite mean across all scenarios, baseline run. When
|
|
3983
|
+
* `budget.holdout === 'deferred'` this is measured on the improvement
|
|
3984
|
+
* (search) split — no holdout campaign ran. */
|
|
3938
3985
|
baseline: {
|
|
3939
3986
|
compositeMean: number;
|
|
3940
3987
|
perScenario: Record<string, number>;
|
|
3941
3988
|
};
|
|
3942
|
-
/** Composite mean on the held-out set, winner run.
|
|
3989
|
+
/** Composite mean on the held-out set, winner run. When
|
|
3990
|
+
* `budget.holdout === 'deferred'` this is the winner's improvement-set
|
|
3991
|
+
* (search) measurement — no holdout campaign ran. */
|
|
3943
3992
|
winner: {
|
|
3944
3993
|
compositeMean: number;
|
|
3945
3994
|
perScenario: Record<string, number>;
|
|
@@ -3953,8 +4002,10 @@ interface SelfImproveResult<TScenario extends Scenario, TArtifact> {
|
|
|
3953
4002
|
rationale?: string;
|
|
3954
4003
|
};
|
|
3955
4004
|
/** `winner.compositeMean - baselineOnHoldout.compositeMean`. Positive
|
|
3956
|
-
* means the gate observed improvement.
|
|
3957
|
-
|
|
4005
|
+
* means the gate observed improvement. Absent iff
|
|
4006
|
+
* `budget.holdout === 'deferred'` — no held-out measurement ran, so there
|
|
4007
|
+
* is no lift to report (never a fabricated 0). */
|
|
4008
|
+
lift?: number;
|
|
3958
4009
|
/** The explicit baseline→winner unified diff. Always present (empty string
|
|
3959
4010
|
* when winner == baseline). */
|
|
3960
4011
|
diff: string;
|
package/dist/contract/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
runReferenceEquivalenceJudge,
|
|
37
37
|
surfaceContentHash,
|
|
38
38
|
surfaceHash
|
|
39
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-HZJF4IUO.js";
|
|
40
40
|
import {
|
|
41
41
|
campaignSplitDigest,
|
|
42
42
|
createRunCostLedger,
|
|
@@ -125,6 +125,15 @@ function meanComposite(byScenario) {
|
|
|
125
125
|
perScenario
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
function winnerSearchCampaign(result) {
|
|
129
|
+
for (let i = result.generations.length - 1; i >= 0; i--) {
|
|
130
|
+
const measured = result.generations[i]?.surfaces.find(
|
|
131
|
+
(s) => s.surfaceHash === result.winnerSurfaceHash
|
|
132
|
+
);
|
|
133
|
+
if (measured) return measured.campaign;
|
|
134
|
+
}
|
|
135
|
+
return result.baselineCampaign;
|
|
136
|
+
}
|
|
128
137
|
async function selfImprove(opts) {
|
|
129
138
|
const startedAt = Date.now();
|
|
130
139
|
const requestedRunDir = opts.runDir ?? `mem://selfImprove-${startedAt}`;
|
|
@@ -147,18 +156,20 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
147
156
|
const populationSize = budget.populationSize ?? 2;
|
|
148
157
|
const maxConcurrency = budget.maxConcurrency ?? 2;
|
|
149
158
|
const holdoutFraction = budget.holdoutFraction ?? 0.25;
|
|
159
|
+
const holdoutMode = budget.holdout ?? "measured";
|
|
160
|
+
const holdoutDeferred = holdoutMode === "deferred";
|
|
150
161
|
const expectUsage = opts.expectUsage ?? "assert";
|
|
151
162
|
const explicitHoldout = budget.holdoutScenarios;
|
|
152
163
|
const { train, holdout } = explicitHoldout ? {
|
|
153
164
|
train: opts.scenarios.filter((s) => !explicitHoldout.some((h) => h.id === s.id)),
|
|
154
165
|
holdout: explicitHoldout
|
|
155
|
-
} : splitTrainHoldout(opts.scenarios, holdoutFraction);
|
|
166
|
+
} : holdoutDeferred ? { train: opts.scenarios, holdout: [] } : splitTrainHoldout(opts.scenarios, holdoutFraction);
|
|
156
167
|
if (train.length === 0) {
|
|
157
168
|
throw new Error(
|
|
158
169
|
"selfImprove: train split is empty. Reduce holdoutFraction or pass more scenarios."
|
|
159
170
|
);
|
|
160
171
|
}
|
|
161
|
-
if (holdout.length === 0) {
|
|
172
|
+
if (holdout.length === 0 && !holdoutDeferred) {
|
|
162
173
|
throw new Error("selfImprove: holdout split is empty. Pass more scenarios.");
|
|
163
174
|
}
|
|
164
175
|
const proposer = opts.proposer ?? gepaProposer({
|
|
@@ -182,6 +193,7 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
182
193
|
const result = await runImprovementLoop({
|
|
183
194
|
scenarios: train,
|
|
184
195
|
baselineSurface: opts.baselineSurface,
|
|
196
|
+
premeasuredBaseline: opts.premeasuredBaseline,
|
|
185
197
|
dispatchWithSurface: opts.agent,
|
|
186
198
|
proposer,
|
|
187
199
|
judges: [opts.judge],
|
|
@@ -189,7 +201,9 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
189
201
|
maxGenerations: generations,
|
|
190
202
|
promoteTopK: budget.promoteTopK,
|
|
191
203
|
reps: budget.reps,
|
|
204
|
+
maxImprovementShots: budget.maxImprovementShots,
|
|
192
205
|
holdoutScenarios: holdout,
|
|
206
|
+
holdout: holdoutMode,
|
|
193
207
|
gate,
|
|
194
208
|
neutralize: opts.neutralize,
|
|
195
209
|
autoOnPromote: opts.autoOnPromote ?? "none",
|
|
@@ -207,8 +221,11 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
207
221
|
analyzeGeneration: opts.analyzeGeneration,
|
|
208
222
|
findings: opts.findings
|
|
209
223
|
});
|
|
210
|
-
const
|
|
211
|
-
const
|
|
224
|
+
const winnerSearch = holdoutDeferred ? winnerSearchCampaign(result) : void 0;
|
|
225
|
+
const baseline = meanComposite(
|
|
226
|
+
(holdoutDeferred ? result.baselineCampaign : result.baselineOnHoldout).aggregates.byScenario
|
|
227
|
+
);
|
|
228
|
+
const winnerStats = meanComposite((winnerSearch ?? result.winnerOnHoldout).aggregates.byScenario);
|
|
212
229
|
let power;
|
|
213
230
|
const baselineHoldoutComposites = result.baselineOnHoldout.cells.filter((cell) => !cell.error).map((cell) => {
|
|
214
231
|
const scores = Object.values(cell.judgeScores);
|
|
@@ -241,7 +258,10 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
241
258
|
opts.onProgress({
|
|
242
259
|
kind: "gate.decided",
|
|
243
260
|
decision: result.gateResult.decision,
|
|
244
|
-
|
|
261
|
+
// Deferred holdout has no held-out measurement: in that mode the summary
|
|
262
|
+
// stats are search-split numbers, and emitting their delta as `lift`
|
|
263
|
+
// would misreport a train-split delta as a held-out one. Omit instead.
|
|
264
|
+
...holdoutDeferred ? {} : { lift: winnerStats.compositeMean - baseline.compositeMean }
|
|
245
265
|
});
|
|
246
266
|
}
|
|
247
267
|
const cost = result.cost;
|
|
@@ -249,7 +269,12 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
249
269
|
const insight = await analyzeRuns({
|
|
250
270
|
runs: [
|
|
251
271
|
...cellsToRunRecords(result.baselineCampaign.cells, "baseline", runDir, opts.baselineSurface),
|
|
252
|
-
...cellsToRunRecords(
|
|
272
|
+
...cellsToRunRecords(
|
|
273
|
+
(winnerSearch ?? result.winnerOnHoldout).cells,
|
|
274
|
+
"winner",
|
|
275
|
+
runDir,
|
|
276
|
+
result.winnerSurface
|
|
277
|
+
)
|
|
253
278
|
],
|
|
254
279
|
baselineCandidateId: "baseline",
|
|
255
280
|
candidateCandidateId: "winner"
|
|
@@ -278,7 +303,7 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
|
|
|
278
303
|
...result.winnerLabel ? { label: result.winnerLabel } : {},
|
|
279
304
|
...result.winnerRationale ? { rationale: result.winnerRationale } : {}
|
|
280
305
|
},
|
|
281
|
-
lift: winnerStats.compositeMean - baseline.compositeMean,
|
|
306
|
+
...holdoutDeferred ? {} : { lift: winnerStats.compositeMean - baseline.compositeMean },
|
|
282
307
|
diff: result.promotedDiff,
|
|
283
308
|
provenance,
|
|
284
309
|
gateDecision: result.gateResult.decision,
|