@tangle-network/agent-eval 0.126.6 → 0.127.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -2
  3. package/dist/analyst/index.d.ts +52 -40
  4. package/dist/analyst/index.js +4 -4
  5. package/dist/belief-state/index.d.ts +34 -36
  6. package/dist/belief-state/index.js +2 -2
  7. package/dist/benchmarks/index.d.ts +30 -12
  8. package/dist/benchmarks/index.js +12 -10
  9. package/dist/builder-eval/index.js +1 -1
  10. package/dist/campaign/index.d.ts +432 -146
  11. package/dist/campaign/index.js +11 -9
  12. package/dist/{chunk-ARU2PZFM.js → chunk-424XHVKV.js} +43 -23
  13. package/dist/chunk-424XHVKV.js.map +1 -0
  14. package/dist/{chunk-NUKSVU3W.js → chunk-5DTSBUL2.js} +2 -2
  15. package/dist/{chunk-BTACVO2W.js → chunk-65QETEKG.js} +542 -258
  16. package/dist/chunk-65QETEKG.js.map +1 -0
  17. package/dist/{chunk-CGG5SLH3.js → chunk-ARCFIY7U.js} +2 -2
  18. package/dist/{chunk-P22LJ3Y2.js → chunk-CLELFRQG.js} +6 -4
  19. package/dist/chunk-CLELFRQG.js.map +1 -0
  20. package/dist/{chunk-LUNF2SEL.js → chunk-DJKY2TSY.js} +3 -3
  21. package/dist/chunk-DPUHNQLN.js +232 -0
  22. package/dist/chunk-DPUHNQLN.js.map +1 -0
  23. package/dist/{chunk-IPYXE555.js → chunk-EJGRPCO3.js} +36 -13
  24. package/dist/chunk-EJGRPCO3.js.map +1 -0
  25. package/dist/{chunk-K6IAZZ6L.js → chunk-FA3MFYNA.js} +132 -110
  26. package/dist/chunk-FA3MFYNA.js.map +1 -0
  27. package/dist/chunk-IG35YDN3.js +614 -0
  28. package/dist/chunk-IG35YDN3.js.map +1 -0
  29. package/dist/{chunk-DPZAEKA6.js → chunk-IMGJZBFG.js} +84 -56
  30. package/dist/chunk-IMGJZBFG.js.map +1 -0
  31. package/dist/{chunk-H5UD2323.js → chunk-IN455XKS.js} +53 -1
  32. package/dist/chunk-IN455XKS.js.map +1 -0
  33. package/dist/{chunk-PJQFMIOX.js → chunk-MHELPNRP.js} +31 -1
  34. package/dist/chunk-MHELPNRP.js.map +1 -0
  35. package/dist/{chunk-IILEIWGW.js → chunk-NL33OXEP.js} +41 -36
  36. package/dist/chunk-NL33OXEP.js.map +1 -0
  37. package/dist/{chunk-474LBSOX.js → chunk-OD5NQ2CS.js} +41 -16
  38. package/dist/chunk-OD5NQ2CS.js.map +1 -0
  39. package/dist/{chunk-QOTFXW5L.js → chunk-P5W7RQKK.js} +2 -2
  40. package/dist/{chunk-IR3KBHOY.js → chunk-P6FYH6K4.js} +39 -3
  41. package/dist/chunk-P6FYH6K4.js.map +1 -0
  42. package/dist/{chunk-J5SQWP6Y.js → chunk-PBE2LOSS.js} +18 -2
  43. package/dist/chunk-PBE2LOSS.js.map +1 -0
  44. package/dist/{chunk-E3IADPLG.js → chunk-PRB53E3J.js} +50 -224
  45. package/dist/chunk-PRB53E3J.js.map +1 -0
  46. package/dist/{chunk-X4UCIOTZ.js → chunk-S5YLIBFX.js} +2 -2
  47. package/dist/{chunk-7VYEGHWF.js → chunk-SZLVEKMJ.js} +41 -20
  48. package/dist/chunk-SZLVEKMJ.js.map +1 -0
  49. package/dist/{chunk-LKKT3IVV.js → chunk-TSN7JT6D.js} +6 -4
  50. package/dist/{chunk-LKKT3IVV.js.map → chunk-TSN7JT6D.js.map} +1 -1
  51. package/dist/{chunk-M7AH34KV.js → chunk-UTA2TGC4.js} +32 -16
  52. package/dist/chunk-UTA2TGC4.js.map +1 -0
  53. package/dist/{chunk-MAX3TN3C.js → chunk-UWZZKKU7.js} +7 -19
  54. package/dist/chunk-UWZZKKU7.js.map +1 -0
  55. package/dist/{chunk-ZVCHKKOP.js → chunk-WED5KLZQ.js} +39 -15
  56. package/dist/chunk-WED5KLZQ.js.map +1 -0
  57. package/dist/{chunk-OCFJACJU.js → chunk-X6VOPMLD.js} +74 -14
  58. package/dist/chunk-X6VOPMLD.js.map +1 -0
  59. package/dist/{chunk-MOXWMGPC.js → chunk-Y5CYOL42.js} +159 -70
  60. package/dist/chunk-Y5CYOL42.js.map +1 -0
  61. package/dist/{chunk-FO7HEH76.js → chunk-YJBNWCAA.js} +2 -2
  62. package/dist/chunk-ZPUEEM5U.js +179 -0
  63. package/dist/chunk-ZPUEEM5U.js.map +1 -0
  64. package/dist/{chunk-KE2VWPZX.js → chunk-ZUUWPZCV.js} +3 -3
  65. package/dist/cli.js +4 -4
  66. package/dist/contract/index.d.ts +464 -126
  67. package/dist/contract/index.js +425 -211
  68. package/dist/contract/index.js.map +1 -1
  69. package/dist/control.d.ts +34 -32
  70. package/dist/control.js +2 -2
  71. package/dist/hosted/index.d.ts +142 -41
  72. package/dist/hosted/index.js +23 -1
  73. package/dist/index.d.ts +365 -256
  74. package/dist/index.js +315 -226
  75. package/dist/index.js.map +1 -1
  76. package/dist/meta-eval/index.d.ts +32 -32
  77. package/dist/meta-eval/index.js +2 -2
  78. package/dist/multishot/index.d.ts +1 -0
  79. package/dist/openapi.json +1 -1
  80. package/dist/pipelines/index.js +2 -2
  81. package/dist/reporting.d.ts +97 -74
  82. package/dist/reporting.js +5 -4
  83. package/dist/rl.d.ts +177 -131
  84. package/dist/rl.js +232 -126
  85. package/dist/rl.js.map +1 -1
  86. package/dist/rollout/index.d.ts +53 -63
  87. package/dist/rollout/index.js +7 -5
  88. package/dist/{run-campaign-FFRM3RH5.js → run-campaign-3A2EBYBE.js} +4 -3
  89. package/dist/supervisor-run/index.d.ts +7 -15
  90. package/dist/supervisor-run/index.js +2 -2
  91. package/dist/traces.d.ts +66 -41
  92. package/dist/traces.js +9 -5
  93. package/dist/wire/index.d.ts +1 -0
  94. package/dist/wire/index.js +2 -2
  95. package/docs/concepts.md +18 -1
  96. package/docs/hosted-ingest-spec.md +37 -40
  97. package/docs/insight-report.md +57 -7
  98. package/docs/research-report-methodology.md +20 -21
  99. package/docs/rollout.md +13 -8
  100. package/package.json +21 -14
  101. package/dist/chunk-474LBSOX.js.map +0 -1
  102. package/dist/chunk-7VYEGHWF.js.map +0 -1
  103. package/dist/chunk-ARU2PZFM.js.map +0 -1
  104. package/dist/chunk-BTACVO2W.js.map +0 -1
  105. package/dist/chunk-DPZAEKA6.js.map +0 -1
  106. package/dist/chunk-E3IADPLG.js.map +0 -1
  107. package/dist/chunk-H5UD2323.js.map +0 -1
  108. package/dist/chunk-IILEIWGW.js.map +0 -1
  109. package/dist/chunk-IPYXE555.js.map +0 -1
  110. package/dist/chunk-IR3KBHOY.js.map +0 -1
  111. package/dist/chunk-J5SQWP6Y.js.map +0 -1
  112. package/dist/chunk-K6IAZZ6L.js.map +0 -1
  113. package/dist/chunk-M7AH34KV.js.map +0 -1
  114. package/dist/chunk-MAX3TN3C.js.map +0 -1
  115. package/dist/chunk-MOXWMGPC.js.map +0 -1
  116. package/dist/chunk-OCFJACJU.js.map +0 -1
  117. package/dist/chunk-P22LJ3Y2.js.map +0 -1
  118. package/dist/chunk-PJQFMIOX.js.map +0 -1
  119. package/dist/chunk-ZVCHKKOP.js.map +0 -1
  120. package/dist/chunk-ZZUXHH3R.js +0 -99
  121. package/dist/chunk-ZZUXHH3R.js.map +0 -1
  122. /package/dist/{chunk-NUKSVU3W.js.map → chunk-5DTSBUL2.js.map} +0 -0
  123. /package/dist/{chunk-CGG5SLH3.js.map → chunk-ARCFIY7U.js.map} +0 -0
  124. /package/dist/{chunk-LUNF2SEL.js.map → chunk-DJKY2TSY.js.map} +0 -0
  125. /package/dist/{chunk-QOTFXW5L.js.map → chunk-P5W7RQKK.js.map} +0 -0
  126. /package/dist/{chunk-X4UCIOTZ.js.map → chunk-S5YLIBFX.js.map} +0 -0
  127. /package/dist/{chunk-FO7HEH76.js.map → chunk-YJBNWCAA.js.map} +0 -0
  128. /package/dist/{chunk-KE2VWPZX.js.map → chunk-ZUUWPZCV.js.map} +0 -0
  129. /package/dist/{run-campaign-FFRM3RH5.js.map → run-campaign-3A2EBYBE.js.map} +0 -0
@@ -7,14 +7,14 @@ import {
7
7
  fromPigraphSession,
8
8
  observeCodeAgentSession,
9
9
  parseCodeAgentJsonl
10
- } from "../chunk-7VYEGHWF.js";
10
+ } from "../chunk-SZLVEKMJ.js";
11
11
  import {
12
12
  createHostedClient
13
- } from "../chunk-ZZUXHH3R.js";
13
+ } from "../chunk-IG35YDN3.js";
14
14
  import {
15
15
  analyzeRuns,
16
16
  summarizeExecution
17
- } from "../chunk-K6IAZZ6L.js";
17
+ } from "../chunk-FA3MFYNA.js";
18
18
  import {
19
19
  REFERENCE_EQUIVALENCE_INPUT_LIMITS,
20
20
  REFERENCE_EQUIVALENCE_JUDGE_VERSION,
@@ -40,7 +40,7 @@ import {
40
40
  skillOptOptimizationMethod,
41
41
  surfaceContentHash,
42
42
  surfaceHash
43
- } from "../chunk-BTACVO2W.js";
43
+ } from "../chunk-65QETEKG.js";
44
44
  import {
45
45
  campaignSplitDigest,
46
46
  createRunCostLedger,
@@ -48,47 +48,60 @@ import {
48
48
  inMemoryCampaignStorage,
49
49
  resolveRunDir,
50
50
  runCampaign
51
- } from "../chunk-ZVCHKKOP.js";
51
+ } from "../chunk-WED5KLZQ.js";
52
52
  import {
53
53
  buildDefaultAnalystRegistry,
54
54
  createChatClient
55
- } from "../chunk-LUNF2SEL.js";
55
+ } from "../chunk-DJKY2TSY.js";
56
56
  import "../chunk-HHWE3POT.js";
57
57
  import "../chunk-WGXIEX7P.js";
58
58
  import {
59
59
  FileSystemOutcomeStore,
60
60
  InMemoryOutcomeStore
61
61
  } from "../chunk-3RF76KTD.js";
62
- import "../chunk-ARU2PZFM.js";
63
- import "../chunk-J5SQWP6Y.js";
64
- import "../chunk-DPZAEKA6.js";
62
+ import "../chunk-424XHVKV.js";
63
+ import {
64
+ campaignCellExecutionEvidence,
65
+ campaignCellJudgeDimensions,
66
+ campaignCellTaskScore,
67
+ campaignCellToRunRecord
68
+ } from "../chunk-ZPUEEM5U.js";
69
+ import "../chunk-PBE2LOSS.js";
70
+ import "../chunk-IMGJZBFG.js";
71
+ import "../chunk-DPUHNQLN.js";
65
72
  import {
66
73
  pairedBootstrap
67
- } from "../chunk-PJQFMIOX.js";
74
+ } from "../chunk-MHELPNRP.js";
68
75
  import "../chunk-WS3NZZQQ.js";
69
76
  import "../chunk-VI2UW6B6.js";
70
77
  import {
71
78
  recordAggregateMeasurements,
72
- summarizeExecutionMeasurements
73
- } from "../chunk-H5UD2323.js";
79
+ summarizeExecutionMeasurements,
80
+ summarizeTraceErrors
81
+ } from "../chunk-IN455XKS.js";
74
82
  import "../chunk-PXE2VKMX.js";
75
83
  import "../chunk-ZET2UAYW.js";
76
84
  import "../chunk-GGE4NNQT.js";
77
- import "../chunk-IR3KBHOY.js";
85
+ import {
86
+ classifyOtlpSpanRole,
87
+ isOtlpModelCall
88
+ } from "../chunk-P6FYH6K4.js";
78
89
  import "../chunk-PC4UYEBM.js";
79
90
  import {
80
91
  parseRunRecordSafe
81
- } from "../chunk-IILEIWGW.js";
92
+ } from "../chunk-NL33OXEP.js";
82
93
  import "../chunk-MA6HLL3S.js";
83
94
  import "../chunk-VSMTAMNK.js";
84
- import "../chunk-ONWEPEDO.js";
85
95
  import {
86
- LLM_MODEL_ATTR_KEYS
96
+ ValidationError
97
+ } from "../chunk-ONWEPEDO.js";
98
+ import {
99
+ LLM_MODEL_ATTR_KEYS,
100
+ SPAN_KIND_ATTR_KEYS
87
101
  } from "../chunk-K4DBDHLK.js";
88
102
  import "../chunk-PZ5AY32C.js";
89
103
 
90
104
  // src/contract/self-improve.ts
91
- import { createHash } from "crypto";
92
105
  var SelfImproveRunError = class extends Error {
93
106
  cost;
94
107
  receipts;
@@ -343,11 +356,11 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
343
356
  findings: opts.findings,
344
357
  selectionRankKey: opts.selectionRankKey
345
358
  });
346
- const winnerSearch = holdoutDeferred ? winnerSearchCampaign(result) : void 0;
347
- const baseline = meanComposite(
348
- (holdoutDeferred ? result.baselineCampaign : result.baselineOnHoldout).aggregates.byScenario
349
- );
350
- const winnerStats = meanComposite((winnerSearch ?? result.winnerOnHoldout).aggregates.byScenario);
359
+ const reportSplit = holdoutDeferred ? "search" : "holdout";
360
+ const reportBaselineCampaign = holdoutDeferred ? result.baselineCampaign : result.baselineOnHoldout;
361
+ const reportWinnerCampaign = holdoutDeferred ? winnerSearchCampaign(result) : result.winnerOnHoldout;
362
+ const baseline = meanComposite(reportBaselineCampaign.aggregates.byScenario);
363
+ const winnerStats = meanComposite(reportWinnerCampaign.aggregates.byScenario);
351
364
  let power;
352
365
  const baselineHoldoutComposites = result.baselineOnHoldout.cells.filter((cell) => !cell.error).map((cell) => {
353
366
  const scores = Object.values(cell.judgeScores);
@@ -390,12 +403,19 @@ async function runSelfImprove(opts, costLedger, startedAt, runDir, storage) {
390
403
  const totalCost = cost.totalCostUsd;
391
404
  const insight = await analyzeRuns({
392
405
  runs: [
393
- ...cellsToRunRecords(result.baselineCampaign.cells, "baseline", runDir, opts.baselineSurface),
394
406
  ...cellsToRunRecords(
395
- (winnerSearch ?? result.winnerOnHoldout).cells,
407
+ reportBaselineCampaign.cells,
408
+ "baseline",
409
+ runDir,
410
+ opts.baselineSurface,
411
+ reportSplit
412
+ ),
413
+ ...cellsToRunRecords(
414
+ reportWinnerCampaign.cells,
396
415
  "winner",
397
416
  runDir,
398
- result.winnerSurface
417
+ result.winnerSurface,
418
+ reportSplit
399
419
  )
400
420
  ],
401
421
  baselineCandidateId: "baseline",
@@ -468,19 +488,21 @@ async function shipEvalRunToHosted(tenant, opts, summary, raw, runDir) {
468
488
  const client = createHostedClient(tenant);
469
489
  function snapshotFromCampaign(index, surface, campaign, durationMs) {
470
490
  const cells = campaign.cells.map((cell) => {
471
- const judgeScores = Object.values(cell.judgeScores);
472
- const composite = judgeScores.length === 0 ? 0 : judgeScores.reduce((s, j) => s + j.composite, 0) / judgeScores.length;
491
+ const execution = campaignCellExecutionEvidence(cell);
473
492
  return {
474
493
  scenarioId: cell.scenarioId,
475
494
  rep: cell.rep,
476
- compositeMean: composite,
477
- dimensions: Object.fromEntries(
478
- Object.entries(cell.judgeScores).map(([name, score]) => [name, score.dimensions])
479
- ),
495
+ compositeMean: campaignCellTaskScore(cell) ?? null,
496
+ dimensions: campaignCellJudgeDimensions(cell),
497
+ terminalOutcome: execution.terminalOutcome,
498
+ executionErrorCount: execution.executionErrorCount ?? null,
480
499
  errorMessage: cell.error ?? void 0
481
500
  };
482
501
  });
483
- const compositeMean = cells.length === 0 ? 0 : cells.reduce((s, c) => s + c.compositeMean, 0) / cells.length;
502
+ const scoredCells = cells.flatMap(
503
+ (cell) => cell.compositeMean === null ? [] : [cell.compositeMean]
504
+ );
505
+ const compositeMean = scoredCells.length === 0 ? null : scoredCells.reduce((sum, score) => sum + score, 0) / scoredCells.length;
484
506
  return {
485
507
  index,
486
508
  surfaceHash: surfaceHash(surface),
@@ -531,67 +553,23 @@ function hashString(s) {
531
553
  }
532
554
  return h.toString(16).padStart(8, "0");
533
555
  }
534
- function cellsToRunRecords(cells, candidateId, runId, surface) {
556
+ function cellsToRunRecords(cells, candidateId, runId, surface, splitTag) {
535
557
  const promptHash = surfaceContentHash(surface);
536
- const configHash = `sha256:${createHash("sha256").update(candidateId).digest("hex")}`;
537
- return cells.map((cell) => {
538
- const perJudge = {};
539
- const perDimMeanAccum = {};
540
- let compositeSum = 0;
541
- let compositeCount = 0;
542
- for (const [judgeId, score] of Object.entries(cell.judgeScores)) {
543
- perJudge[judgeId] = { ...score.dimensions };
544
- for (const [dim, value] of Object.entries(score.dimensions)) {
545
- if (!Number.isFinite(value)) continue;
546
- const accum = perDimMeanAccum[dim] ?? { sum: 0, n: 0 };
547
- accum.sum += value;
548
- accum.n += 1;
549
- perDimMeanAccum[dim] = accum;
550
- }
551
- if (Number.isFinite(score.composite)) {
552
- compositeSum += score.composite;
553
- compositeCount += 1;
554
- }
555
- }
556
- const perDimMean = {};
557
- for (const [dim, { sum, n }] of Object.entries(perDimMeanAccum)) {
558
- perDimMean[dim] = n === 0 ? 0 : sum / n;
559
- }
560
- const composite = compositeCount === 0 ? 0 : compositeSum / compositeCount;
561
- const judgeScores = {
562
- perJudge,
563
- perDimMean,
564
- composite
565
- };
566
- return {
558
+ const configHash = surfaceContentHash(candidateId);
559
+ return cells.map(
560
+ (cell) => campaignCellToRunRecord(cell, {
567
561
  runId: `${runId}::${candidateId}::${cell.cellId}`,
568
562
  experimentId: runId,
569
563
  candidateId,
570
- // Pair on (scenarioId, rep) analyzeRuns pairs on (experimentId, seed).
571
- // Synthesize a stable seed for that pairing.
564
+ // scenarioId is explicit; seed keeps repeated runs distinct.
572
565
  seed: cell.rep * 1e6 + hashString(cell.scenarioId).slice(0, 6).split("").reduce((a, c) => a * 31 + c.charCodeAt(0) >>> 0, 0),
573
566
  model: "campaign-cell",
574
567
  promptHash,
575
568
  configHash,
576
569
  commitSha: "cell",
577
- wallMs: cell.durationMs,
578
- costUsd: cell.costUsd,
579
- tokenUsage: {
580
- input: cell.tokenUsage.input,
581
- output: cell.tokenUsage.output,
582
- ...cell.tokenUsage.reasoning === void 0 ? {} : { reasoning: cell.tokenUsage.reasoning },
583
- ...cell.tokenUsage.cached === void 0 ? {} : { cached: cell.tokenUsage.cached },
584
- ...cell.tokenUsage.cacheWrite === void 0 ? {} : { cacheWrite: cell.tokenUsage.cacheWrite }
585
- },
586
- outcome: {
587
- holdoutScore: composite,
588
- raw: {},
589
- judgeScores
590
- },
591
- splitTag: "holdout",
592
- ...cell.error ? { failureMode: cell.error } : {}
593
- };
594
- });
570
+ splitTag
571
+ })
572
+ );
595
573
  }
596
574
 
597
575
  // src/contract/define-agent-eval.ts
@@ -698,6 +676,7 @@ import {
698
676
  agentCandidateBenchmarkSuiteSchema,
699
677
  agentCandidateBenchmarkTaskSchema,
700
678
  agentCandidateBundleSchema,
679
+ agentCandidateEvaluationPolicySchema,
701
680
  agentCandidateExperimentSchema,
702
681
  agentImprovementMeasuredComparisonSchema,
703
682
  candidateExecutionEvidenceSchema,
@@ -800,19 +779,28 @@ async function runCandidateExperiment(options) {
800
779
  await Promise.all(lanes);
801
780
  return measurements;
802
781
  }
803
- function measuredComparisonFromCandidateExperiment(options) {
804
- const experiment = verifyCandidateExperiment(options.experiment);
782
+ function evaluatePairedMeasurements(options) {
783
+ if (options.measurements.length === 0) {
784
+ throw new Error("paired measurement evaluation requires at least one paired cell");
785
+ }
786
+ const additionalCostUsd = options.additionalCostUsd ?? 0;
787
+ if (!Number.isFinite(additionalCostUsd) || additionalCostUsd < 0) {
788
+ throw new Error("paired measurement evaluation additionalCostUsd must be a non-negative number");
789
+ }
790
+ if (typeof options.sharedScorerChannel !== "boolean") {
791
+ throw new Error("paired measurement evaluation sharedScorerChannel must be a boolean");
792
+ }
793
+ const policy = agentCandidateEvaluationPolicySchema.parse(options.policy);
805
794
  const measurements = options.measurements.map(
806
- (measurement, index) => verifyMeasurement(experiment, measurement, index)
795
+ (measurement, index) => projectPairedMeasurement(measurement, index, options.adapter)
807
796
  );
808
- const expectedN = experiment.benchmark.suite.taskDigests.length * experiment.benchmark.suite.reps;
809
- if (measurements.length !== expectedN) {
810
- throw new Error(
811
- `candidate experiment is incomplete (${measurements.length}/${expectedN} paired cells)`
812
- );
797
+ const cellIds2 = measurements.map((measurement) => measurement.cellId);
798
+ if (new Set(cellIds2).size !== cellIds2.length) {
799
+ throw new Error("paired measurement evaluation cell ids must be unique");
813
800
  }
814
- verifyStableProfileMaterialization(measurements);
815
- if (!options.runId.trim()) throw new Error("candidate experiment runId is required");
801
+ const dimensions = sharedProjectedDimensions(measurements);
802
+ const baselineScores = measurements.map((measurement) => measurement.baseline.score);
803
+ const candidateScores = measurements.map((measurement) => measurement.candidate.score);
816
804
  const {
817
805
  confidenceLevel: confidence,
818
806
  resamples,
@@ -822,15 +810,23 @@ function measuredComparisonFromCandidateExperiment(options) {
822
810
  budgetUsd,
823
811
  criticalDimensions,
824
812
  regressionTolerance
825
- } = experiment.policy;
826
- const baselineScores = measurements.map(scoreOf("baseline"));
827
- const candidateScores = measurements.map(scoreOf("candidate"));
813
+ } = policy;
814
+ const significance = heldoutSignificance(
815
+ { before: baselineScores, after: candidateScores, cellIds: cellIds2 },
816
+ {
817
+ confidence,
818
+ resamples,
819
+ seed: bootstrapSeed,
820
+ statistic: "mean",
821
+ deltaThreshold,
822
+ minProductiveRuns
823
+ }
824
+ );
828
825
  const overall = measuredEstimate(baselineScores, candidateScores, {
829
826
  confidence,
830
827
  resamples,
831
828
  seed: bootstrapSeed
832
829
  });
833
- const dimensions = sharedDimensions(measurements);
834
830
  const objectives = [
835
831
  {
836
832
  kind: "objective",
@@ -848,21 +844,33 @@ function measuredComparisonFromCandidateExperiment(options) {
848
844
  unit: "score",
849
845
  availability: "measured",
850
846
  ...measuredEstimate(
851
- measurements.map(dimensionOf("baseline", name)),
852
- measurements.map(dimensionOf("candidate", name)),
853
- { confidence, resamples, seed: bootstrapSeed + index + 1 }
847
+ measurements.map((measurement) => dimensionScore(measurement.baseline, name)),
848
+ measurements.map((measurement) => dimensionScore(measurement.candidate, name)),
849
+ {
850
+ confidence,
851
+ resamples,
852
+ seed: bootstrapSeed + index + 1
853
+ }
854
854
  )
855
855
  }))
856
856
  ];
857
857
  const cost = measuredEstimate(
858
- measurements.map(costOf("baseline")),
859
- measurements.map(costOf("candidate")),
860
- { confidence, resamples, seed: bootstrapSeed + dimensions.length + 1 }
858
+ measurements.map((measurement) => measurement.baseline.costUsd),
859
+ measurements.map((measurement) => measurement.candidate.costUsd),
860
+ {
861
+ confidence,
862
+ resamples,
863
+ seed: bootstrapSeed + dimensions.length + 1
864
+ }
861
865
  );
862
866
  const latency = measuredEstimate(
863
- measurements.map(latencyOf("baseline")),
864
- measurements.map(latencyOf("candidate")),
865
- { confidence, resamples, seed: bootstrapSeed + dimensions.length + 2 }
867
+ measurements.map((measurement) => measurement.baseline.latencyMs),
868
+ measurements.map((measurement) => measurement.candidate.latencyMs),
869
+ {
870
+ confidence,
871
+ resamples,
872
+ seed: bootstrapSeed + dimensions.length + 2
873
+ }
866
874
  );
867
875
  objectives.push(
868
876
  {
@@ -882,23 +890,12 @@ function measuredComparisonFromCandidateExperiment(options) {
882
890
  ...latency
883
891
  }
884
892
  );
885
- const significance = heldoutSignificance(
886
- { before: baselineScores, after: candidateScores, cellIds: cellIds(experiment) },
887
- {
888
- confidence,
889
- resamples,
890
- seed: bootstrapSeed,
891
- statistic: "mean",
892
- deltaThreshold,
893
- minProductiveRuns
894
- }
895
- );
896
893
  const power = baselineScores.length >= 3 ? powerPreflight({
897
894
  baselineComposites: baselineScores,
898
895
  pairedN: baselineScores.length,
899
896
  deltaThreshold,
900
897
  confidence,
901
- sharedScorerChannel: true
898
+ sharedScorerChannel: options.sharedScorerChannel
902
899
  }) : void 0;
903
900
  const powerSufficient = baselineScores.length >= minProductiveRuns && power !== void 0 && !power.underpowered;
904
901
  const guardedDimensions = new Set(criticalDimensions);
@@ -909,20 +906,21 @@ function measuredComparisonFromCandidateExperiment(options) {
909
906
  (objective) => objective.kind === "dimension" && guardedDimensions.has(objective.name) && objective.availability === "measured" && objective.confidenceInterval.lower < -regressionTolerance
910
907
  );
911
908
  const executionCostUsd = measurements.reduce(
912
- (sum, measurement) => sum + costFromEvidence(measurement.baseline) + costFromEvidence(measurement.candidate),
909
+ (sum, measurement) => sum + measurement.baseline.costUsd + measurement.candidate.costUsd,
910
+ 0
911
+ );
912
+ const executionDurationMs = measurements.reduce(
913
+ (sum, measurement) => sum + measurement.baseline.latencyMs + measurement.candidate.latencyMs,
913
914
  0
914
915
  );
915
- const searchCostUsd = options.searchCostUsd ?? 0;
916
- const totalCostUsd = executionCostUsd + searchCostUsd;
917
- const budgetPassed = budgetUsd === void 0 || totalCostUsd <= budgetUsd;
918
916
  const completedRuns = measurements.flatMap((measurement) => [
919
917
  measurement.baseline,
920
918
  measurement.candidate
921
919
  ]);
922
- const incompleteRuns = completedRuns.filter((evidence) => !completedSuccessfully(evidence));
923
- const failedCandidateResults = measurements.filter(
924
- (measurement) => !measurement.candidate.receipt.benchmarkResult.material.passed
925
- );
920
+ const incompleteRuns = completedRuns.filter((run) => !run.completed);
921
+ const failedCandidateResults = measurements.filter((measurement) => !measurement.candidate.passed);
922
+ const totalCostUsd = executionCostUsd + additionalCostUsd;
923
+ const budgetPassed = budgetUsd === void 0 || totalCostUsd <= budgetUsd;
926
924
  const checks = [
927
925
  { name: "paired-significance", passed: significance.significant },
928
926
  { name: "statistical-power", passed: powerSufficient },
@@ -946,17 +944,7 @@ function measuredComparisonFromCandidateExperiment(options) {
946
944
  ...failedCandidateResults.length === 0 ? [] : [`candidate failed ${failedCandidateResults.length} benchmark tasks`],
947
945
  ...budgetPassed ? [] : [`total cost ${totalCostUsd} exceeded budget ${budgetUsd}`]
948
946
  ];
949
- const diff = deriveCandidateBundleDiff(experiment);
950
- const executionDurationMs = measurements.reduce(
951
- (sum, measurement) => sum + latencyFromEvidence(measurement.baseline) + latencyFromEvidence(measurement.candidate),
952
- 0
953
- );
954
- const searchDurationMs = options.searchDurationMs ?? 0;
955
- const durationMs = executionDurationMs + searchDurationMs;
956
- const provisional = agentImprovementMeasuredComparisonSchema.parse({
957
- kind: "agent-improvement-measured-comparison",
958
- experiment,
959
- measurements,
947
+ return {
960
948
  overall: {
961
949
  name: "composite",
962
950
  direction: "higher-is-better",
@@ -964,7 +952,6 @@ function measuredComparisonFromCandidateExperiment(options) {
964
952
  ...overall
965
953
  },
966
954
  objectives,
967
- ...options.candidate ? { candidate: options.candidate } : {},
968
955
  decision: {
969
956
  outcome: shipped ? "ship" : significance.fewRuns || !powerSufficient ? "need_more_work" : "hold",
970
957
  reasons: reasons.length > 0 ? reasons : ["all measured checks passed"],
@@ -976,9 +963,51 @@ function measuredComparisonFromCandidateExperiment(options) {
976
963
  minimumDetectableDelta: power?.mde ?? 1,
977
964
  confidenceLevel: confidence,
978
965
  scaleAssumed: power?.scaleAssumed ?? true,
979
- sharedScorerChannel: true,
966
+ sharedScorerChannel: options.sharedScorerChannel,
980
967
  reason: power?.recommendation ?? `need at least ${Math.max(3, minProductiveRuns)} paired runs`
981
968
  },
969
+ executionCostUsd,
970
+ totalCostUsd,
971
+ executionDurationMs
972
+ };
973
+ }
974
+ function measuredComparisonFromCandidateExperiment(options) {
975
+ const experiment = verifyCandidateExperiment(options.experiment);
976
+ const measurements = options.measurements.map(
977
+ (measurement, index) => verifyMeasurement(experiment, measurement, index)
978
+ );
979
+ const expectedN = experiment.benchmark.suite.taskDigests.length * experiment.benchmark.suite.reps;
980
+ if (measurements.length !== expectedN) {
981
+ throw new Error(
982
+ `candidate experiment is incomplete (${measurements.length}/${expectedN} paired cells)`
983
+ );
984
+ }
985
+ verifyStableProfileMaterialization(measurements);
986
+ if (!options.runId.trim()) throw new Error("candidate experiment runId is required");
987
+ const searchCostUsd = options.searchCostUsd ?? 0;
988
+ const evaluation = evaluatePairedMeasurements({
989
+ measurements: measurements.map((measurement, index) => ({
990
+ cellId: cellIds(experiment)[index],
991
+ ...measurement
992
+ })),
993
+ policy: experiment.policy,
994
+ adapter: candidateExecutionEvidenceAdapter,
995
+ sharedScorerChannel: true,
996
+ additionalCostUsd: searchCostUsd
997
+ });
998
+ const diff = deriveCandidateBundleDiff(experiment);
999
+ const searchDurationMs = options.searchDurationMs ?? 0;
1000
+ const totalCostUsd = evaluation.totalCostUsd;
1001
+ const durationMs = evaluation.executionDurationMs + searchDurationMs;
1002
+ const provisional = agentImprovementMeasuredComparisonSchema.parse({
1003
+ kind: "agent-improvement-measured-comparison",
1004
+ experiment,
1005
+ measurements,
1006
+ overall: evaluation.overall,
1007
+ objectives: evaluation.objectives,
1008
+ ...options.candidate ? { candidate: options.candidate } : {},
1009
+ decision: evaluation.decision,
1010
+ power: evaluation.power,
982
1011
  provenance: {
983
1012
  kind: "agent-eval-loop",
984
1013
  schema: "agent-candidate-experiment",
@@ -991,10 +1020,10 @@ function measuredComparisonFromCandidateExperiment(options) {
991
1020
  evaluation: {
992
1021
  generationsExplored: options.generationsExplored ?? 0,
993
1022
  searchDurationMs,
994
- executionDurationMs,
1023
+ executionDurationMs: evaluation.executionDurationMs,
995
1024
  durationMs,
996
1025
  searchCostUsd,
997
- executionCostUsd,
1026
+ executionCostUsd: evaluation.executionCostUsd,
998
1027
  totalCostUsd
999
1028
  },
1000
1029
  ...options.metadata ? { metadata: options.metadata } : {}
@@ -1197,6 +1226,65 @@ function verifyMaterialAddressed(evidence, label) {
1197
1226
  throw new Error(`${label} digest is invalid`);
1198
1227
  }
1199
1228
  }
1229
+ function projectPairedMeasurement(measurement, index, adapter) {
1230
+ if (typeof measurement.cellId !== "string" || !measurement.cellId.trim()) {
1231
+ throw new Error(`paired measurement ${index} requires a cell id`);
1232
+ }
1233
+ return {
1234
+ cellId: measurement.cellId,
1235
+ baseline: projectRun(measurement.baseline, adapter, `paired measurement ${index} baseline`),
1236
+ candidate: projectRun(measurement.candidate, adapter, `paired measurement ${index} candidate`)
1237
+ };
1238
+ }
1239
+ function projectRun(run, adapter, label) {
1240
+ const suppliedDimensions = adapter.dimensions(run);
1241
+ if (!Array.isArray(suppliedDimensions)) {
1242
+ throw new Error(`${label} dimensions must be an array`);
1243
+ }
1244
+ const dimensions = /* @__PURE__ */ new Map();
1245
+ for (const dimension of suppliedDimensions) {
1246
+ if (typeof dimension.name !== "string" || !dimension.name.trim()) {
1247
+ throw new Error(`${label} contains an unnamed dimension`);
1248
+ }
1249
+ if (dimensions.has(dimension.name)) {
1250
+ throw new Error(`${label} repeats dimension '${dimension.name}'`);
1251
+ }
1252
+ dimensions.set(dimension.name, finiteMeasurement(dimension.score, `${label} ${dimension.name}`));
1253
+ }
1254
+ const completed = adapter.completed(run);
1255
+ const passed = adapter.passed(run);
1256
+ if (typeof completed !== "boolean" || typeof passed !== "boolean") {
1257
+ throw new Error(`${label} completion and pass values must be booleans`);
1258
+ }
1259
+ return {
1260
+ score: finiteMeasurement(adapter.score(run), `${label} score`),
1261
+ dimensions,
1262
+ costUsd: nonNegativeMeasurement(adapter.costUsd(run), `${label} cost`),
1263
+ latencyMs: nonNegativeMeasurement(adapter.latencyMs(run), `${label} latency`),
1264
+ completed,
1265
+ passed
1266
+ };
1267
+ }
1268
+ function sharedProjectedDimensions(measurements) {
1269
+ const expected = [...measurements[0].baseline.dimensions.keys()];
1270
+ for (const [index, measurement] of measurements.entries()) {
1271
+ for (const [arm, run] of [
1272
+ ["baseline", measurement.baseline],
1273
+ ["candidate", measurement.candidate]
1274
+ ]) {
1275
+ const actual = [...run.dimensions.keys()];
1276
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
1277
+ throw new Error(`paired measurement ${index} ${arm} dimensions do not match the suite`);
1278
+ }
1279
+ }
1280
+ }
1281
+ return expected;
1282
+ }
1283
+ function dimensionScore(run, name) {
1284
+ const value = run.dimensions.get(name);
1285
+ if (value === void 0) throw new Error(`paired measurement is missing dimension '${name}'`);
1286
+ return value;
1287
+ }
1200
1288
  function measuredEstimate(baseline, candidate, options) {
1201
1289
  const bootstrap = pairedBootstrap(baseline, candidate, {
1202
1290
  confidence: options.confidence,
@@ -1222,46 +1310,22 @@ function measuredEstimate(baseline, candidate, options) {
1222
1310
  n: bootstrap.n
1223
1311
  };
1224
1312
  }
1225
- function sharedDimensions(measurements) {
1226
- const expected = dimensionNames(measurements[0]?.baseline);
1227
- for (const [index, measurement] of measurements.entries()) {
1228
- for (const [arm, evidence] of [
1229
- ["baseline", measurement.baseline],
1230
- ["candidate", measurement.candidate]
1231
- ]) {
1232
- const actual = dimensionNames(evidence);
1233
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
1234
- throw new Error(
1235
- `candidate experiment measurement ${index} ${arm} dimensions do not match the suite`
1236
- );
1237
- }
1238
- }
1239
- }
1240
- return expected;
1241
- }
1242
- function dimensionNames(evidence) {
1243
- return (evidence?.receipt.benchmarkResult.material.dimensions ?? []).map(
1244
- (dimension) => dimension.name
1245
- );
1246
- }
1247
- function scoreOf(arm) {
1248
- return (measurement) => measurement[arm].receipt.benchmarkResult.material.score;
1249
- }
1250
- function dimensionOf(arm, name) {
1251
- return (measurement) => {
1252
- const dimension = measurement[arm].receipt.benchmarkResult.material.dimensions.find(
1253
- (entry) => entry.name === name
1254
- );
1255
- if (!dimension) throw new Error(`candidate experiment is missing dimension '${name}'`);
1256
- return dimension.score;
1257
- };
1258
- }
1259
- function costOf(arm) {
1260
- return (measurement) => costFromEvidence(measurement[arm]);
1313
+ function finiteMeasurement(value, label) {
1314
+ if (!Number.isFinite(value)) throw new Error(`${label} must be finite`);
1315
+ return value;
1261
1316
  }
1262
- function latencyOf(arm) {
1263
- return (measurement) => latencyFromEvidence(measurement[arm]);
1317
+ function nonNegativeMeasurement(value, label) {
1318
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be non-negative`);
1319
+ return value;
1264
1320
  }
1321
+ var candidateExecutionEvidenceAdapter = {
1322
+ score: (evidence) => evidence.receipt.benchmarkResult.material.score,
1323
+ dimensions: (evidence) => evidence.receipt.benchmarkResult.material.dimensions,
1324
+ costUsd: costFromEvidence,
1325
+ latencyMs: latencyFromEvidence,
1326
+ completed: completedSuccessfully,
1327
+ passed: (evidence) => evidence.receipt.benchmarkResult.material.passed
1328
+ };
1265
1329
  function costFromEvidence(evidence) {
1266
1330
  return combinedUsage(evidence).costUsdNanos / 1e9;
1267
1331
  }
@@ -1479,7 +1543,7 @@ function diffGenerations(before, after) {
1479
1543
  rep: beforeCell.rep,
1480
1544
  compositeBefore: beforeCell.compositeMean,
1481
1545
  compositeAfter: afterCell.compositeMean,
1482
- compositeDelta: afterCell.compositeMean - beforeCell.compositeMean,
1546
+ compositeDelta: beforeCell.compositeMean === null || afterCell.compositeMean === null ? null : afterCell.compositeMean - beforeCell.compositeMean,
1483
1547
  dimensions: diffDimensions(beforeCell.dimensions, afterCell.dimensions)
1484
1548
  });
1485
1549
  }
@@ -1497,7 +1561,7 @@ function diffGenerations(before, after) {
1497
1561
  added,
1498
1562
  compositeBefore: before.compositeMean,
1499
1563
  compositeAfter: after.compositeMean,
1500
- compositeDelta: after.compositeMean - before.compositeMean,
1564
+ compositeDelta: before.compositeMean === null || after.compositeMean === null ? null : after.compositeMean - before.compositeMean,
1501
1565
  costUsdBefore: before.costUsd,
1502
1566
  costUsdAfter: after.costUsd,
1503
1567
  costUsdDelta: after.costUsd - before.costUsd,
@@ -1650,14 +1714,13 @@ function fromFeedbackTable(opts) {
1650
1714
  perDimMean: { rating: meanScore },
1651
1715
  composite: meanScore
1652
1716
  };
1717
+ const splitTag = runMeta.splitTag ?? "holdout";
1653
1718
  const outcome = {
1654
- // Feedback corpora ARE the holdout signal score lands on
1655
- // `holdoutScore` so downstream substrate primitives (`paretoChart`,
1656
- // promotion gates) read it correctly by default.
1657
- holdoutScore: meanScore,
1719
+ ...splitTag === "holdout" ? { holdoutScore: meanScore } : { searchScore: meanScore },
1658
1720
  raw: Object.fromEntries(normalised.map((r) => [`rater:${r.rater}`, r.score])),
1659
1721
  judgeScores
1660
1722
  };
1723
+ const costUsd = runMeta.costUsd ?? null;
1661
1724
  runs.push({
1662
1725
  runId,
1663
1726
  experimentId: runMeta.experimentId ?? "feedback-corpus",
@@ -1668,10 +1731,13 @@ function fromFeedbackTable(opts) {
1668
1731
  configHash: runMeta.configHash ?? "sha256:unknown",
1669
1732
  commitSha: runMeta.commitSha ?? "unknown",
1670
1733
  wallMs: runMeta.wallMs ?? 0,
1671
- costUsd: runMeta.costUsd ?? 0,
1734
+ costUsd,
1735
+ costProvenance: costUsd === null ? { kind: "uncaptured", usd: null } : { kind: "observed", usd: costUsd },
1672
1736
  tokenUsage: { input: 0, output: 0 },
1737
+ terminalOutcome: "unknown",
1673
1738
  outcome,
1674
- splitTag: runMeta.splitTag ?? "holdout"
1739
+ splitTag,
1740
+ scenarioId: runMeta.scenarioId ?? runId
1675
1741
  });
1676
1742
  if (emitRaterScores) {
1677
1743
  for (const r of normalised) raterScores.push({ runId, rater: r.rater, score: r.score });
@@ -1681,7 +1747,12 @@ function fromFeedbackTable(opts) {
1681
1747
  }
1682
1748
 
1683
1749
  // src/contract/intake/otel-spans.ts
1684
- var SCORE_KEYS = ["tangle.score", "eval.score", "score"];
1750
+ var TASK_SCORE_ATTR_KEYS = [
1751
+ "gen_ai.evaluation.score.value",
1752
+ "tangle.task.score",
1753
+ "eval.score",
1754
+ "tangle.score"
1755
+ ];
1685
1756
  var MODEL_KEYS = ["tangle.model", ...LLM_MODEL_ATTR_KEYS, "model"];
1686
1757
  var PROMPT_HASH_KEYS = ["tangle.prompt_hash", "prompt.hash"];
1687
1758
  var CONFIG_HASH_KEYS = ["tangle.config_hash", "config.hash"];
@@ -1703,15 +1774,31 @@ function fromOtelSpans(opts) {
1703
1774
  );
1704
1775
  const callSpanIds = new Set(measurements.callSpanIds);
1705
1776
  const callSpans = groupSpans2.filter((span) => callSpanIds.has(span.spanId));
1706
- const wallMs = Math.max(0, (root.endTimeUnixNano - root.startTimeUnixNano) / 1e6);
1777
+ const wallMs = unixNanoDurationMs(root.startTimeUnixNano, root.endTimeUnixNano);
1707
1778
  const model = readAttrString(callSpans, MODEL_KEYS) ?? readAttrString(groupSpans2, MODEL_KEYS) ?? "unknown@unknown";
1708
1779
  const capturedCost = (measurements.cost.complete ? measurements.cost.value : void 0) ?? measurements.aggregate?.costUsd;
1709
- const costUsd = capturedCost ?? 0;
1780
+ const costUsd = capturedCost ?? null;
1781
+ const scenarioId = readConsistentScenarioId(groupKey, groupSpans2) ?? groupKey;
1710
1782
  const promptHash = readAttrString(groupSpans2, PROMPT_HASH_KEYS) ?? "sha256:unknown";
1711
1783
  const configHash = readAttrString(groupSpans2, CONFIG_HASH_KEYS) ?? "sha256:unknown";
1712
- const score = readAttrNumber(groupSpans2, SCORE_KEYS);
1784
+ const score = resolveTaskScore(groupKey, groupSpans2, opts.scoreForRun);
1713
1785
  const rawNumeric = collectNumericAttrs(groupSpans2);
1714
- rawNumeric.error_span_count = groupSpans2.filter((span) => span.status?.code === "ERROR").length;
1786
+ const errorSummary = summarizeTraceErrors(
1787
+ groupSpans2.map((span) => ({
1788
+ id: spanIdentity(span),
1789
+ ...span.parentSpanId ? { parentId: parentIdentity(span) } : {},
1790
+ role: errorRoleForSpan(span),
1791
+ error: span.status?.code === "ERROR",
1792
+ processRoot: !span.parentSpanId && isTerminalRootCandidate(span)
1793
+ }))
1794
+ );
1795
+ rawNumeric.error_span_count = errorSummary.total;
1796
+ rawNumeric.execution_error_count = errorSummary.execution;
1797
+ rawNumeric.process_error_count = errorSummary.process;
1798
+ rawNumeric.guardrail_error_count = errorSummary.guardrail;
1799
+ rawNumeric.judge_error_count = errorSummary.evaluation;
1800
+ rawNumeric.propagated_error_count = errorSummary.propagated;
1801
+ rawNumeric.unclassified_error_count = errorSummary.unclassified;
1715
1802
  rawNumeric.llm_span_count = measurements.modelCallCount;
1716
1803
  if (measurements.cost.value !== void 0 && !measurements.cost.complete) {
1717
1804
  rawNumeric.partial_observed_cost_usd = measurements.cost.value;
@@ -1722,12 +1809,18 @@ function fromOtelSpans(opts) {
1722
1809
  perDimMean: { score },
1723
1810
  composite: score
1724
1811
  } : void 0;
1725
- const errorSpan = groupSpans2.find((s) => s.status?.code === "ERROR");
1812
+ const terminalOutcome = terminalOutcomeFromRoots(groupSpans2);
1813
+ const failedRoot = terminalOutcome === "failed" ? groupSpans2.find(
1814
+ (span) => !span.parentSpanId && isTerminalRootCandidate(span) && span.status?.code === "ERROR"
1815
+ ) : void 0;
1726
1816
  const outcome = {
1727
- ...opts.defaultSplit === "search" ? { searchScore: score } : { holdoutScore: score },
1728
1817
  raw: rawNumeric,
1729
1818
  ...judgeScores ? { judgeScores } : {}
1730
1819
  };
1820
+ if (score !== void 0) {
1821
+ if (defaultSplit === "holdout") outcome.holdoutScore = score;
1822
+ else outcome.searchScore = score;
1823
+ }
1731
1824
  runs.push({
1732
1825
  runId: groupKey,
1733
1826
  experimentId,
@@ -1741,22 +1834,55 @@ function fromOtelSpans(opts) {
1741
1834
  costUsd,
1742
1835
  costProvenance: capturedCost === void 0 ? { kind: "uncaptured", usd: null } : { kind: "observed", usd: capturedCost },
1743
1836
  tokenUsage: measurements.tokenUsage,
1837
+ terminalOutcome,
1838
+ ...failedRoot ? { terminalFailureReason: failedRoot.status?.message ?? failedRoot.name } : {},
1744
1839
  outcome,
1745
1840
  splitTag: defaultSplit,
1746
- ...errorSpan ? { failureMode: errorSpan.name } : {}
1841
+ scenarioId
1747
1842
  });
1748
1843
  }
1749
1844
  return runs;
1750
1845
  }
1846
+ function terminalOutcomeFromRoots(spans) {
1847
+ const roots = spans.filter((span) => !span.parentSpanId && isTerminalRootCandidate(span));
1848
+ if (roots.length !== 1) return "unknown";
1849
+ if (roots[0].status?.code === "OK") return "succeeded";
1850
+ if (roots[0].status?.code === "ERROR") return "failed";
1851
+ return "unknown";
1852
+ }
1853
+ function isTerminalRootCandidate(span) {
1854
+ const role = errorRoleForSpan(span);
1855
+ if (role === "LLM" || role === "TOOL" || role === "GUARDRAIL" || role === "EVALUATOR") {
1856
+ return false;
1857
+ }
1858
+ return true;
1859
+ }
1860
+ function readSpanKind(span) {
1861
+ return readAttrString([span], [...SPAN_KIND_ATTR_KEYS, "span.kind"])?.toUpperCase();
1862
+ }
1863
+ function errorRoleForSpan(span) {
1864
+ return classifyOtlpSpanRole({
1865
+ kind: readSpanKind(span),
1866
+ name: span.name,
1867
+ attributes: span.attributes
1868
+ });
1869
+ }
1870
+ function spanIdentity(span) {
1871
+ return `${span.traceId}:${span.spanId}`;
1872
+ }
1873
+ function parentIdentity(span) {
1874
+ return `${span.traceId}:${span.parentSpanId}`;
1875
+ }
1751
1876
  function isExplicitModelCall(span) {
1752
- const kind = span.attributes["openinference.span.kind"];
1753
- if (typeof kind === "string") return kind.toUpperCase() === "LLM";
1754
- const spanType = span.attributes["span.type"];
1755
- return typeof spanType === "string" && spanType.toLowerCase() === "llm_request" || span.name.toLowerCase().includes("llm") || readAttrString([span], MODEL_KEYS) !== void 0 || typeof span.attributes["gen_ai.operation.name"] === "string";
1877
+ return isOtlpModelCall({
1878
+ kind: readSpanKind(span),
1879
+ name: span.name,
1880
+ attributes: span.attributes
1881
+ });
1756
1882
  }
1757
1883
  function isExplicitAggregate(span) {
1758
- const kind = span.attributes["openinference.span.kind"];
1759
- return typeof kind === "string" && kind.toUpperCase() !== "LLM";
1884
+ const kind = readSpanKind(span);
1885
+ return kind !== void 0 && kind !== "LLM";
1760
1886
  }
1761
1887
  function groupSpans(spans) {
1762
1888
  const m = /* @__PURE__ */ new Map();
@@ -1769,7 +1895,10 @@ function groupSpans(spans) {
1769
1895
  return m;
1770
1896
  }
1771
1897
  function findRoot(group) {
1772
- return group.find((s) => !s.parentSpanId) ?? group[0];
1898
+ const structuralRoots = group.filter((span) => !span.parentSpanId);
1899
+ const terminalRoots = structuralRoots.filter(isTerminalRootCandidate);
1900
+ const pool = terminalRoots.length > 0 ? terminalRoots : structuralRoots.length > 0 ? structuralRoots : group;
1901
+ return orderSpans(pool)[0];
1773
1902
  }
1774
1903
  function readAttrString(spans, keys) {
1775
1904
  for (const span of spans) {
@@ -1780,18 +1909,102 @@ function readAttrString(spans, keys) {
1780
1909
  }
1781
1910
  return void 0;
1782
1911
  }
1783
- function readAttrNumber(spans, keys) {
1912
+ function readConsistentScenarioId(runId, spans) {
1913
+ const values = /* @__PURE__ */ new Set();
1784
1914
  for (const span of spans) {
1785
- for (const key of keys) {
1786
- const v = span.attributes[key];
1787
- if (typeof v === "number" && Number.isFinite(v)) return v;
1788
- if (typeof v === "string") {
1789
- const parsed = Number(v);
1790
- if (Number.isFinite(parsed)) return parsed;
1915
+ const topLevel = span["tangle.scenarioId"];
1916
+ if (typeof topLevel === "string" && topLevel.length > 0) values.add(topLevel);
1917
+ const attribute = span.attributes["tangle.scenarioId"];
1918
+ if (typeof attribute === "string" && attribute.length > 0) values.add(attribute);
1919
+ }
1920
+ if (values.size > 1) {
1921
+ throw new ValidationError(
1922
+ `fromOtelSpans: conflicting scenario ids for run '${runId}': ${[...values].sort().join(", ")}`
1923
+ );
1924
+ }
1925
+ return values.values().next().value;
1926
+ }
1927
+ function resolveTaskScore(runId, spans, scoreForRun) {
1928
+ const orderedSpans = orderSpans(spans);
1929
+ const sources = [];
1930
+ if (scoreForRun) {
1931
+ const supplied = scoreForRun(runId, orderedSpans);
1932
+ if (supplied !== void 0) {
1933
+ if (typeof supplied !== "number" || !Number.isFinite(supplied)) {
1934
+ throw new ValidationError(
1935
+ `fromOtelSpans: scoreForRun returned a non-finite number for run '${runId}'`
1936
+ );
1791
1937
  }
1938
+ sources.push({ label: "scoreForRun", value: supplied });
1792
1939
  }
1793
1940
  }
1794
- return void 0;
1941
+ for (const span of orderedSpans) {
1942
+ const role = errorRoleForSpan(span);
1943
+ if (span.parentSpanId && role !== "EVALUATOR") continue;
1944
+ if (role === "EVALUATOR" && span.status?.code === "ERROR") continue;
1945
+ for (const key of TASK_SCORE_ATTR_KEYS) {
1946
+ if (!Object.hasOwn(span.attributes, key)) continue;
1947
+ sources.push({
1948
+ label: `span '${span.spanId}' attribute '${key}'`,
1949
+ value: parseTaskScoreAttribute(runId, span.spanId, key, span.attributes[key])
1950
+ });
1951
+ }
1952
+ }
1953
+ if (sources.length === 0) return void 0;
1954
+ sources.sort((left, right) => left.label.localeCompare(right.label));
1955
+ const score = sources[0].value;
1956
+ if (sources.some((source) => source.value !== score)) {
1957
+ const details = sources.map((source) => `${source.label}=${source.value}`).join(", ");
1958
+ throw new ValidationError(
1959
+ `fromOtelSpans: conflicting task-quality scores for run '${runId}': ${details}`
1960
+ );
1961
+ }
1962
+ return score;
1963
+ }
1964
+ function parseTaskScoreAttribute(runId, spanId, key, value) {
1965
+ const source = `span '${spanId}' attribute '${key}'`;
1966
+ if (typeof value === "string") {
1967
+ if (value.trim().length === 0) {
1968
+ throw new ValidationError(
1969
+ `fromOtelSpans: ${source} is blank for run '${runId}'; task quality must be finite`
1970
+ );
1971
+ }
1972
+ const parsed = Number(value);
1973
+ if (Number.isFinite(parsed)) return parsed;
1974
+ } else if (typeof value === "number" && Number.isFinite(value)) {
1975
+ return value;
1976
+ }
1977
+ throw new ValidationError(
1978
+ `fromOtelSpans: ${source} is not a finite task-quality score for run '${runId}'`
1979
+ );
1980
+ }
1981
+ function orderSpans(spans) {
1982
+ return [...spans].sort(
1983
+ (left, right) => compareUnixNano(left.startTimeUnixNano, right.startTimeUnixNano) || left.spanId.localeCompare(right.spanId)
1984
+ );
1985
+ }
1986
+ function parseUnixNano(value) {
1987
+ try {
1988
+ return BigInt(value);
1989
+ } catch {
1990
+ throw new ValidationError(`fromOtelSpans: invalid Unix nanosecond timestamp '${value}'`);
1991
+ }
1992
+ }
1993
+ function compareUnixNano(left, right) {
1994
+ const leftValue = parseUnixNano(left);
1995
+ const rightValue = parseUnixNano(right);
1996
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
1997
+ }
1998
+ function unixNanoDurationMs(start, end) {
1999
+ const delta = parseUnixNano(end) - parseUnixNano(start);
2000
+ if (delta <= 0n) return 0;
2001
+ const wholeMs = delta / 1000000n;
2002
+ const fractionalMs = delta % 1000000n;
2003
+ const value = Number(wholeMs) + Number(fractionalMs) / 1e6;
2004
+ if (!Number.isSafeInteger(Number(wholeMs))) {
2005
+ throw new ValidationError("fromOtelSpans: span duration exceeds the safe millisecond range");
2006
+ }
2007
+ return value;
1795
2008
  }
1796
2009
  function collectNumericAttrs(spans) {
1797
2010
  const raw = {};
@@ -1822,6 +2035,7 @@ export {
1822
2035
  diffRunBaselineToWinner,
1823
2036
  diffRuns,
1824
2037
  evalReportingSuite,
2038
+ evaluatePairedMeasurements,
1825
2039
  externalTextOptimizationMethod,
1826
2040
  fromClaudeCodeSession,
1827
2041
  fromCodexSession,