@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 CHANGED
@@ -18,6 +18,10 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-
18
18
  - Pass track identity, operation, vision, ancestry, and proposer choice to candidate generation so independent tracks can pursue distinct strategies.
19
19
  - Route named tracks to caller-supplied proposers, with the default proposer as fallback, and make heuristic branches inherit their parent track's proposer.
20
20
 
21
+ ### Fixed
22
+
23
+ - Compute contextual-bandit doubly robust estimates with separate logged-action and target-policy value terms, expose how many rows use DR versus IPS or the deprecated scalar path, and carry both values through belief-state records.
24
+
21
25
  ## [0.122.2] — 2026-07-17 — premeasured optimization continuation
22
26
 
23
27
  ### Added
@@ -281,9 +281,10 @@ interface CalibrationReport {
281
281
  * - For LLM agents, propensity scores must be supplied by the caller
282
282
  * (logged in the trace, recovered from token log-probs, or estimated
283
283
  * via a learned propensity model). We do NOT estimate propensity here.
284
- * - Doubly-robust requires a Q-function (model-based reward predictor).
285
- * We accept any callable; consumers pass either a tabular average,
286
- * a regression fit, or a learned reward model.
284
+ * - Doubly-robust requires two outputs from a Q-function: its prediction
285
+ * for the logged action and its expectation under the target policy.
286
+ * Consumers compute these with a tabular estimate, regression fit, or
287
+ * learned reward model before constructing the trajectories.
287
288
  *
288
289
  * Bias / variance tradeoffs:
289
290
  * - IPS: unbiased; high variance for small overlap, infinite variance
@@ -316,11 +317,33 @@ interface OffPolicyTrajectory {
316
317
  */
317
318
  targetProb: number;
318
319
  /**
319
- * Optional model-based reward prediction at the same context. Used by
320
- * `doublyRobust`. Set to `null` for IPS-only evaluation.
320
+ * Model-based reward prediction for the action selected by the behavior
321
+ * policy: `Q_hat(context, loggedAction)`. Supply this together with
322
+ * `vHatTarget` for contextual-bandit doubly-robust estimation.
323
+ */
324
+ qHatChosen?: number | null;
325
+ /**
326
+ * Expected model-based reward under the target policy:
327
+ * `sum_action targetPolicy(action | context) * Q_hat(context, action)`.
328
+ * Supply this together with `qHatChosen`. For an honest evaluation, both
329
+ * values must come from a model cross-fitted or trained outside this row.
330
+ */
331
+ vHatTarget?: number | null;
332
+ /**
333
+ * @deprecated Use `qHatChosen` and `vHatTarget` together. When the new pair
334
+ * is absent, this scalar is used as both terms to preserve existing results.
335
+ * When the new pair is present, this field is ignored.
321
336
  */
322
337
  qHat?: number | null;
323
338
  }
339
+ interface OffPolicyContributionCounts {
340
+ /** Contributions using the contextual-bandit doubly-robust formula. */
341
+ dr: number;
342
+ /** Contributions using exact IPS because no reward-model estimate was supplied. */
343
+ ipsFallback: number;
344
+ /** Contributions using the deprecated single-scalar formula. */
345
+ legacyScalar: number;
346
+ }
324
347
  interface OffPolicyEstimate {
325
348
  /** Estimated value of the target policy. */
326
349
  value: number;
@@ -335,6 +358,8 @@ interface OffPolicyEstimate {
335
358
  * mean) are a red flag — variance is dominated by a few outliers.
336
359
  */
337
360
  maxImportanceWeight: number;
361
+ /** Populated by `doublyRobust` to expose which formula each row used. */
362
+ contributionCounts?: OffPolicyContributionCounts;
338
363
  }
339
364
  interface OffPolicyOptions {
340
365
  /**
@@ -446,6 +471,9 @@ interface BeliefDecisionPoint {
446
471
  confidence?: number;
447
472
  behaviorProb?: number;
448
473
  targetProb?: number;
474
+ qHatChosen?: number | null;
475
+ vHatTarget?: number | null;
476
+ /** @deprecated Use `qHatChosen` and `vHatTarget` together. */
449
477
  qHat?: number | null;
450
478
  costUsd?: number;
451
479
  evidence: BeliefEvidenceRef[];
@@ -468,6 +496,9 @@ interface BeliefPolicyDecision {
468
496
  action: BeliefPolicyAction;
469
497
  confidence?: number;
470
498
  targetProb?: number;
499
+ qHatChosen?: number | null;
500
+ vHatTarget?: number | null;
501
+ /** @deprecated Use `qHatChosen` and `vHatTarget` together. */
471
502
  qHat?: number | null;
472
503
  reason?: string;
473
504
  reasons?: BeliefDecisionReason[];
@@ -479,6 +510,9 @@ interface BeliefSelectivePolicy {
479
510
  interface BeliefOpeTargetPolicy {
480
511
  id: string;
481
512
  targetProbOf(point: BeliefDecisionPoint): number | null | undefined;
513
+ qHatChosenOf?(point: BeliefDecisionPoint): number | null | undefined;
514
+ vHatTargetOf?(point: BeliefDecisionPoint): number | null | undefined;
515
+ /** @deprecated Use `qHatChosenOf` and `vHatTargetOf` together. */
482
516
  qHatOf?(point: BeliefDecisionPoint): number | null | undefined;
483
517
  }
484
518
  interface BeliefUtilityOptions {
@@ -1189,6 +1223,9 @@ interface RuntimeBeliefDecisionPointOptions {
1189
1223
  confidence?: number;
1190
1224
  behaviorProb?: number;
1191
1225
  targetProb?: number;
1226
+ qHatChosen?: number | null;
1227
+ vHatTarget?: number | null;
1228
+ /** @deprecated Use `qHatChosen` and `vHatTarget` together. */
1192
1229
  qHat?: number | null;
1193
1230
  costUsd?: number;
1194
1231
  outcome?: BeliefDecisionOutcome;
@@ -1230,6 +1267,9 @@ interface RuntimeBeliefDecisionLabel {
1230
1267
  confidence?: number;
1231
1268
  behaviorProb?: number;
1232
1269
  targetProb?: number;
1270
+ qHatChosen?: number | null;
1271
+ vHatTarget?: number | null;
1272
+ /** @deprecated Use `qHatChosen` and `vHatTarget` together. */
1233
1273
  qHat?: number | null;
1234
1274
  costUsd?: number;
1235
1275
  splitTag?: RunSplitTag;
@@ -14,7 +14,7 @@ import {
14
14
  } from "../chunk-T4SQEITX.js";
15
15
  import {
16
16
  offPolicyEstimateAll
17
- } from "../chunk-DTJ6QUQB.js";
17
+ } from "../chunk-VGRCHJON.js";
18
18
  import {
19
19
  confidenceInterval
20
20
  } from "../chunk-PJQFMIOX.js";
@@ -68,6 +68,12 @@ function embeddedBeliefOpeTargetPolicy(id = "embedded-target-prob") {
68
68
  targetProbOf(point) {
69
69
  return point.targetProb;
70
70
  },
71
+ qHatChosenOf(point) {
72
+ return point.qHatChosen;
73
+ },
74
+ vHatTargetOf(point) {
75
+ return point.vHatTarget;
76
+ },
71
77
  qHatOf(point) {
72
78
  return point.qHat;
73
79
  }
@@ -86,9 +92,13 @@ function beliefDecisionsToOffPolicyTrajectories(points, targetPolicy, options =
86
92
  continue;
87
93
  }
88
94
  let targetProb;
95
+ let qHatChosen;
96
+ let vHatTarget;
89
97
  let qHat;
90
98
  try {
91
99
  targetProb = targetPolicy.targetProbOf(point);
100
+ qHatChosen = targetPolicy.qHatChosenOf?.(point);
101
+ vHatTarget = targetPolicy.vHatTargetOf?.(point);
92
102
  qHat = targetPolicy.qHatOf?.(point);
93
103
  } catch (error) {
94
104
  diagnostics.push(
@@ -100,7 +110,19 @@ function beliefDecisionsToOffPolicyTrajectories(points, targetPolicy, options =
100
110
  diagnostics.push(`${point.id}: invalid targetProb ${formatProbability(targetProb)}`);
101
111
  continue;
102
112
  }
103
- if (qHat !== null && qHat !== void 0 && !isTargetProbability(qHat)) {
113
+ const hasQHatChosen = qHatChosen !== null && qHatChosen !== void 0;
114
+ const hasVHatTarget = vHatTarget !== null && vHatTarget !== void 0;
115
+ if (hasQHatChosen !== hasVHatTarget) {
116
+ diagnostics.push(`${point.id}: qHatChosen and vHatTarget must be supplied together`);
117
+ continue;
118
+ }
119
+ if (hasQHatChosen && hasVHatTarget && (!isTargetProbability(qHatChosen) || !isTargetProbability(vHatTarget))) {
120
+ diagnostics.push(
121
+ `${point.id}: invalid contextual Q pair qHatChosen=${formatProbability(qHatChosen)} vHatTarget=${formatProbability(vHatTarget)}`
122
+ );
123
+ continue;
124
+ }
125
+ if (!hasQHatChosen && !hasVHatTarget && qHat !== null && qHat !== void 0 && !isTargetProbability(qHat)) {
104
126
  diagnostics.push(`${point.id}: invalid qHat ${formatProbability(qHat)}; ignoring qHat`);
105
127
  qHat = null;
106
128
  }
@@ -109,6 +131,8 @@ function beliefDecisionsToOffPolicyTrajectories(points, targetPolicy, options =
109
131
  reward: rewardOf(point),
110
132
  behaviorProb: point.behaviorProb,
111
133
  targetProb,
134
+ ...qHatChosen !== void 0 ? { qHatChosen } : {},
135
+ ...vHatTarget !== void 0 ? { vHatTarget } : {},
112
136
  qHat
113
137
  });
114
138
  }
@@ -127,7 +151,8 @@ function evaluateBeliefOffPolicy(points, targetPolicy, options = {}) {
127
151
  minEffectiveSampleSize: options.minEffectiveSampleSize ?? 30,
128
152
  minEffectiveSampleRatio: options.minEffectiveSampleRatio ?? 0.25,
129
153
  dropped: trajectoryReport.dropped,
130
- diagnostics: trajectoryReport.diagnostics
154
+ diagnostics: trajectoryReport.diagnostics,
155
+ legacyScalarContributions: estimates.dr.contributionCounts?.legacyScalar ?? 0
131
156
  });
132
157
  return { targetPolicyId: targetPolicy.id, ...estimates, support };
133
158
  }
@@ -140,6 +165,11 @@ function supportDiagnostics(estimate, options) {
140
165
  if (options.dropped > 0) {
141
166
  reasons.push(`dropped ${options.dropped} unsupported decision(s)`);
142
167
  }
168
+ if (options.legacyScalarContributions > 0) {
169
+ reasons.push(
170
+ `${options.legacyScalarContributions} decision(s) used deprecated scalar qHat; supply qHatChosen and vHatTarget for contextual doubly robust estimation`
171
+ );
172
+ }
143
173
  if (estimate.effectiveSampleSize < options.minEffectiveSampleSize) {
144
174
  reasons.push(
145
175
  `effective sample size ${estimate.effectiveSampleSize.toFixed(2)} below ${options.minEffectiveSampleSize}`
@@ -216,6 +246,8 @@ function thresholdSelectivePolicy(options) {
216
246
  action: confidence >= threshold ? "accept" : belowThresholdAction,
217
247
  confidence,
218
248
  targetProb: point.targetProb,
249
+ qHatChosen: point.qHatChosen,
250
+ vHatTarget: point.vHatTarget,
219
251
  qHat: point.qHat,
220
252
  reason: confidence >= threshold ? "confidence threshold passed" : "confidence threshold failed"
221
253
  };
@@ -1041,6 +1073,8 @@ function parseDecisionEvent(event, context) {
1041
1073
  confidence: finiteUnitField(payload, "confidence"),
1042
1074
  behaviorProb: numberField(payload, "behaviorProb"),
1043
1075
  targetProb: numberField(payload, "targetProb"),
1076
+ qHatChosen: finiteUnitField(payload, "qHatChosen"),
1077
+ vHatTarget: finiteUnitField(payload, "vHatTarget"),
1044
1078
  qHat: finiteUnitField(payload, "qHat"),
1045
1079
  costUsd: nonNegativeNumberField(payload, "costUsd"),
1046
1080
  evidence,
@@ -1155,6 +1189,8 @@ function runtimeDecisionPointToBeliefDecisionPoint(point, options) {
1155
1189
  confidence: unitProbabilityOrUndefined(options.confidence),
1156
1190
  behaviorProb: finiteNumberOrUndefined(options.behaviorProb),
1157
1191
  targetProb: finiteNumberOrUndefined(options.targetProb),
1192
+ qHatChosen: options.qHatChosen === null ? null : unitProbabilityOrUndefined(options.qHatChosen),
1193
+ vHatTarget: options.vHatTarget === null ? null : unitProbabilityOrUndefined(options.vHatTarget),
1158
1194
  qHat: options.qHat === null ? null : unitProbabilityOrUndefined(options.qHat),
1159
1195
  costUsd: nonNegativeNumberOrUndefined(options.costUsd),
1160
1196
  evidence: evidence.map((ref) => runtimeEvidenceToBeliefEvidence(ref, point)),
@@ -1384,6 +1420,8 @@ function buildRuntimeBeliefPhase0Measurement(options) {
1384
1420
  confidence: label.confidence,
1385
1421
  behaviorProb: label.behaviorProb,
1386
1422
  targetProb: label.targetProb,
1423
+ qHatChosen: label.qHatChosen,
1424
+ vHatTarget: label.vHatTarget,
1387
1425
  qHat: label.qHat,
1388
1426
  costUsd: label.costUsd,
1389
1427
  outcome: label.outcome,