@tangle-network/agent-eval 0.85.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -230,6 +230,9 @@ import {
230
230
  describeTraceInsightScope,
231
231
  domainEvidencePattern,
232
232
  exportRunAsOtlp,
233
+ extractUsage,
234
+ extractUsageFromResponse,
235
+ extractUsageFromSse,
233
236
  flattenOtlpExportToNdjson,
234
237
  inferDomainKeywords,
235
238
  iterateRawCalls,
@@ -240,7 +243,7 @@ import {
240
243
  scoreTraceInsightReadiness,
241
244
  tokenizeDomainWords,
242
245
  traceAnalystOnRunComplete
243
- } from "./chunk-XQL22JDG.js";
246
+ } from "./chunk-P2J6SOXT.js";
244
247
  import {
245
248
  DEFAULT_REDACTION_RULES,
246
249
  REDACTION_VERSION,
@@ -2391,16 +2394,16 @@ function aggregatePrReviewScore(dimensions, weights = {}) {
2391
2394
  return (merged.recall * clamp01(dimensions.recall) + merged.precision * clamp01(dimensions.precision) + merged.actionability * clamp01(dimensions.actionability) + merged.severityCalibration * clamp01(dimensions.severityCalibration) + merged.lowNoise * clamp01(dimensions.lowNoise)) / weightSum;
2392
2395
  }
2393
2396
  function matchReferenceFindings(references, comments) {
2394
- const matches = [];
2397
+ const matches2 = [];
2395
2398
  const usedCommentIds = /* @__PURE__ */ new Set();
2396
2399
  for (const reference of references) {
2397
2400
  const candidates = comments.filter((comment) => !usedCommentIds.has(comment.id)).map((comment) => ({ comment, score: matchScore(reference, comment) })).filter(({ score }) => score >= 0.55).sort((a, b) => b.score - a.score);
2398
2401
  const best = candidates[0];
2399
2402
  if (!best) continue;
2400
2403
  usedCommentIds.add(best.comment.id);
2401
- matches.push({ referenceId: reference.id, commentId: best.comment.id, score: best.score });
2404
+ matches2.push({ referenceId: reference.id, commentId: best.comment.id, score: best.score });
2402
2405
  }
2403
- return matches;
2406
+ return matches2;
2404
2407
  }
2405
2408
  function matchScore(reference, comment) {
2406
2409
  let score = 0;
@@ -2427,9 +2430,9 @@ function isActionableComment(comment) {
2427
2430
  body
2428
2431
  );
2429
2432
  }
2430
- function isSeverityAligned(comment, references, matches) {
2433
+ function isSeverityAligned(comment, references, matches2) {
2431
2434
  if (!comment.severity) return false;
2432
- const match = matches.find((candidate) => candidate.commentId === comment.id);
2435
+ const match = matches2.find((candidate) => candidate.commentId === comment.id);
2433
2436
  if (!match) return comment.severity === "nit" || comment.severity === "low";
2434
2437
  const reference = references.find((candidate) => candidate.id === match.referenceId);
2435
2438
  if (!reference) return false;
@@ -2725,28 +2728,28 @@ function analyzeAntiSlop(outputs, config) {
2725
2728
  }
2726
2729
  }
2727
2730
  for (const re of config.hedgingPatterns) {
2728
- const matches = output.match(
2731
+ const matches2 = output.match(
2729
2732
  new RegExp(re, re.flags.includes("g") ? re.flags : `${re.flags}g`)
2730
2733
  );
2731
- if (matches) {
2732
- counts.hedging += matches.length;
2734
+ if (matches2) {
2735
+ counts.hedging += matches2.length;
2733
2736
  issues.push({
2734
2737
  category: "hedging",
2735
- detail: `${matches.length}x ${re.source}`,
2736
- example: matches[0]
2738
+ detail: `${matches2.length}x ${re.source}`,
2739
+ example: matches2[0]
2737
2740
  });
2738
2741
  }
2739
2742
  }
2740
2743
  for (const re of config.apologyPatterns) {
2741
- const matches = output.match(
2744
+ const matches2 = output.match(
2742
2745
  new RegExp(re, re.flags.includes("g") ? re.flags : `${re.flags}g`)
2743
2746
  );
2744
- if (matches) {
2745
- counts.apology += matches.length;
2747
+ if (matches2) {
2748
+ counts.apology += matches2.length;
2746
2749
  issues.push({
2747
2750
  category: "apology",
2748
- detail: `${matches.length}x ${re.source}`,
2749
- example: matches[0]
2751
+ detail: `${matches2.length}x ${re.source}`,
2752
+ example: matches2[0]
2750
2753
  });
2751
2754
  }
2752
2755
  }
@@ -3016,14 +3019,14 @@ async function runHarnessExperiment(config) {
3016
3019
  const score = config.score ?? ((trace) => critic.scoreTrace(trace));
3017
3020
  const results = await mapLimit(jobs, config.parallelism ?? 1, async (request) => {
3018
3021
  const trace = await config.adapter.run(request);
3019
- const runScore3 = await score(trace, request);
3022
+ const runScore4 = await score(trace, request);
3020
3023
  const result = {
3021
3024
  variant: request.variant,
3022
3025
  scenario: request.scenario,
3023
3026
  trialIndex: request.trialIndex,
3024
3027
  trace,
3025
- score: runScore3,
3026
- aggregate: aggregateRunScore(runScore3, config.weights)
3028
+ score: runScore4,
3029
+ aggregate: aggregateRunScore(runScore4, config.weights)
3027
3030
  };
3028
3031
  await config.onResult?.(result);
3029
3032
  return result;
@@ -3669,13 +3672,118 @@ var BudgetGuard = class {
3669
3672
  }
3670
3673
  };
3671
3674
 
3675
+ // src/cost-ledger.ts
3676
+ function modelPriceKey(model) {
3677
+ return isModelPriced(model) ? model : null;
3678
+ }
3679
+ function costForUsage(model, usage) {
3680
+ assertNonNegative(usage.inputTokens, "inputTokens");
3681
+ assertNonNegative(usage.outputTokens, "outputTokens");
3682
+ if (usage.cachedTokens !== void 0) assertNonNegative(usage.cachedTokens, "cachedTokens");
3683
+ const pricing = resolveModelPricing(model);
3684
+ if (!pricing) return { costUsd: 0, costUnknown: true };
3685
+ const billedInput = usage.inputTokens + (usage.cachedTokens ?? 0);
3686
+ return { costUsd: estimateCost(billedInput, usage.outputTokens, model), costUnknown: false };
3687
+ }
3688
+ var CostLedger = class {
3689
+ entries = [];
3690
+ completedTasks = 0;
3691
+ /**
3692
+ * Record one LLM call. The cost is computed from pricing unless
3693
+ * `actualCostUsd` is supplied (a finite observed cost from the provider
3694
+ * response), in which case `costUnknown` is false regardless of pricing.
3695
+ */
3696
+ record(input) {
3697
+ const { costUsd, costUnknown } = costForUsage(input.model, input.usage);
3698
+ const hasActual = typeof input.actualCostUsd === "number" && Number.isFinite(input.actualCostUsd);
3699
+ if (hasActual) assertNonNegative(input.actualCostUsd, "actualCostUsd");
3700
+ const entry = {
3701
+ model: input.model,
3702
+ channel: input.channel,
3703
+ inputTokens: input.usage.inputTokens,
3704
+ outputTokens: input.usage.outputTokens,
3705
+ cachedTokens: input.usage.cachedTokens,
3706
+ costUsd: hasActual ? input.actualCostUsd : costUsd,
3707
+ costUnknown: hasActual ? false : costUnknown,
3708
+ actualCostUsd: hasActual ? input.actualCostUsd : void 0,
3709
+ tags: input.tags,
3710
+ timestamp: input.timestamp ?? Date.now()
3711
+ };
3712
+ this.entries.push(entry);
3713
+ return entry;
3714
+ }
3715
+ /** Increment the completed-task counter (used for cost-per-completed-task). */
3716
+ markCompleted(count = 1) {
3717
+ if (!Number.isInteger(count) || count < 0) {
3718
+ throw new ValidationError(
3719
+ `CostLedger.markCompleted: count must be a non-negative integer, got ${count}`
3720
+ );
3721
+ }
3722
+ this.completedTasks += count;
3723
+ }
3724
+ list() {
3725
+ return [...this.entries];
3726
+ }
3727
+ summary() {
3728
+ const byChannel = /* @__PURE__ */ new Map();
3729
+ const unpriced = /* @__PURE__ */ new Set();
3730
+ let totalCost = 0;
3731
+ let inputTokens = 0;
3732
+ let outputTokens = 0;
3733
+ let cachedTokens = 0;
3734
+ for (const e of this.entries) {
3735
+ totalCost += e.costUsd;
3736
+ inputTokens += e.inputTokens;
3737
+ outputTokens += e.outputTokens;
3738
+ cachedTokens += e.cachedTokens ?? 0;
3739
+ if (e.costUnknown) unpriced.add(e.model);
3740
+ const roll = byChannel.get(e.channel) ?? {
3741
+ channel: e.channel,
3742
+ calls: 0,
3743
+ inputTokens: 0,
3744
+ outputTokens: 0,
3745
+ cachedTokens: 0,
3746
+ costUsd: 0,
3747
+ unpricedCalls: 0
3748
+ };
3749
+ roll.calls += 1;
3750
+ roll.inputTokens += e.inputTokens;
3751
+ roll.outputTokens += e.outputTokens;
3752
+ roll.cachedTokens += e.cachedTokens ?? 0;
3753
+ roll.costUsd += e.costUsd;
3754
+ if (e.costUnknown) roll.unpricedCalls += 1;
3755
+ byChannel.set(e.channel, roll);
3756
+ }
3757
+ return {
3758
+ totalCalls: this.entries.length,
3759
+ inputTokens,
3760
+ outputTokens,
3761
+ cachedTokens,
3762
+ totalCostUsd: totalCost,
3763
+ byChannel: [...byChannel.values()].sort((a, b) => a.channel.localeCompare(b.channel)),
3764
+ unpricedModels: [...unpriced].sort(),
3765
+ fullyPriced: unpriced.size === 0
3766
+ };
3767
+ }
3768
+ /** Total spend divided by completed tasks; null when nothing completed. */
3769
+ costPerCompletedTask() {
3770
+ if (this.completedTasks === 0) return null;
3771
+ return this.summary().totalCostUsd / this.completedTasks;
3772
+ }
3773
+ };
3774
+ function assertNonNegative(n, name) {
3775
+ if (!Number.isFinite(n) || n < 0) {
3776
+ throw new ValidationError(`CostLedger: ${name} must be a non-negative finite number, got ${n}`);
3777
+ }
3778
+ }
3779
+
3672
3780
  // src/cost-tracker.ts
3673
3781
  var CostTracker = class {
3674
3782
  byScenario = /* @__PURE__ */ new Map();
3675
3783
  record(entry) {
3676
3784
  const full = { timestamp: entry.timestamp ?? Date.now(), ...entry };
3677
- assertNonNegative(full.inputTokens, "inputTokens");
3678
- assertNonNegative(full.outputTokens, "outputTokens");
3785
+ assertNonNegative2(full.inputTokens, "inputTokens");
3786
+ assertNonNegative2(full.outputTokens, "outputTokens");
3679
3787
  let bucket = this.byScenario.get(full.scenarioId);
3680
3788
  if (!bucket) {
3681
3789
  bucket = {
@@ -3754,12 +3862,433 @@ function costFor(entry) {
3754
3862
  }
3755
3863
  return estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
3756
3864
  }
3757
- function assertNonNegative(n, name) {
3865
+ function assertNonNegative2(n, name) {
3758
3866
  if (!Number.isFinite(n) || n < 0) {
3759
3867
  throw new Error(`CostTracker: ${name} must be a non-negative finite number, got ${n}`);
3760
3868
  }
3761
3869
  }
3762
3870
 
3871
+ // src/eval-trace-store.ts
3872
+ function runScore(record) {
3873
+ const { holdoutScore, searchScore } = record.outcome;
3874
+ if (typeof holdoutScore === "number") return holdoutScore;
3875
+ if (typeof searchScore === "number") return searchScore;
3876
+ throw new ValidationError(
3877
+ `EvalTraceStore: run ${record.runId} has neither holdoutScore nor searchScore`
3878
+ );
3879
+ }
3880
+ function matches(record, f) {
3881
+ if (f.experimentId && record.experimentId !== f.experimentId) return false;
3882
+ if (f.candidateId && record.candidateId !== f.candidateId) return false;
3883
+ if (f.scenarioId && record.scenarioId !== f.scenarioId) return false;
3884
+ if (f.model && record.model !== f.model) return false;
3885
+ if (f.splitTag && record.splitTag !== f.splitTag) return false;
3886
+ if (f.minScore !== void 0 && runScore(record) < f.minScore) return false;
3887
+ if (f.maxScore !== void 0 && runScore(record) > f.maxScore) return false;
3888
+ if (f.rawEquals && record.outcome.raw[f.rawEquals.key] !== f.rawEquals.value) return false;
3889
+ if (f.where && !f.where(record)) return false;
3890
+ return true;
3891
+ }
3892
+ function inMemoryRunRecordBackend(initial = []) {
3893
+ const rows = initial.map((r) => validateRunRecord(r));
3894
+ return {
3895
+ async append(record) {
3896
+ rows.push(record);
3897
+ },
3898
+ async load() {
3899
+ return [...rows];
3900
+ }
3901
+ };
3902
+ }
3903
+ function jsonlRunRecordBackend(path, opts = {}) {
3904
+ return {
3905
+ async append(record) {
3906
+ const fs2 = await import("fs/promises");
3907
+ const pathMod = await import("path");
3908
+ await fs2.mkdir(pathMod.dirname(path), { recursive: true });
3909
+ await fs2.appendFile(path, `${JSON.stringify(record)}
3910
+ `, "utf8");
3911
+ },
3912
+ async load() {
3913
+ const fs2 = await import("fs/promises");
3914
+ let raw;
3915
+ try {
3916
+ raw = await fs2.readFile(path, "utf8");
3917
+ } catch (err) {
3918
+ if (err.code === "ENOENT") return [];
3919
+ throw err;
3920
+ }
3921
+ const out = [];
3922
+ let lineNo = 0;
3923
+ for (const line of raw.split("\n")) {
3924
+ lineNo++;
3925
+ if (!line.trim()) continue;
3926
+ let parsed;
3927
+ try {
3928
+ parsed = JSON.parse(line);
3929
+ } catch (err) {
3930
+ if (opts.skipInvalid) continue;
3931
+ throw new ValidationError(`EvalTraceStore: ${path}:${lineNo} is not valid JSON`);
3932
+ }
3933
+ if (opts.skipInvalid) {
3934
+ if (isRunRecord(parsed)) out.push(parsed);
3935
+ continue;
3936
+ }
3937
+ try {
3938
+ out.push(validateRunRecord(parsed));
3939
+ } catch (err) {
3940
+ throw new ValidationError(
3941
+ `EvalTraceStore: ${path}:${lineNo} is not a valid RunRecord \u2014 ${err.message}`
3942
+ );
3943
+ }
3944
+ }
3945
+ return out;
3946
+ }
3947
+ };
3948
+ }
3949
+ var EvalTraceStore = class {
3950
+ backend;
3951
+ constructor(backend = inMemoryRunRecordBackend()) {
3952
+ this.backend = backend;
3953
+ }
3954
+ /** Validate and append one run. Throws on an invalid record — the corpus
3955
+ * stays paper-grade. */
3956
+ async append(record) {
3957
+ await this.backend.append(validateRunRecord(record));
3958
+ }
3959
+ async all() {
3960
+ return this.backend.load();
3961
+ }
3962
+ async query(filter = {}) {
3963
+ const rows = await this.backend.load();
3964
+ return rows.filter((r) => matches(r, filter));
3965
+ }
3966
+ /**
3967
+ * Highest-scoring run for a scenario (optionally restricted to a candidate).
3968
+ * Returns null when no run matches. Ties resolve to the earliest-appended run
3969
+ * so the result is stable.
3970
+ */
3971
+ async getBest(scenarioId, opts = {}) {
3972
+ const rows = await this.query({
3973
+ scenarioId,
3974
+ candidateId: opts.candidateId,
3975
+ splitTag: opts.splitTag
3976
+ });
3977
+ if (rows.length === 0) return null;
3978
+ let best = rows[0];
3979
+ let bestScore = runScore(best);
3980
+ for (let i = 1; i < rows.length; i++) {
3981
+ const s = runScore(rows[i]);
3982
+ if (s > bestScore) {
3983
+ best = rows[i];
3984
+ bestScore = s;
3985
+ }
3986
+ }
3987
+ return best;
3988
+ }
3989
+ /**
3990
+ * Compare two candidates on the scenarios they BOTH ran. When a candidate
3991
+ * ran a scenario more than once, its best `runScore` for that scenario is
3992
+ * used. Throws when there is no paired scenario — an unpaired "comparison" is
3993
+ * not one.
3994
+ */
3995
+ async compareRuns(candidateA, candidateB) {
3996
+ if (candidateA === candidateB) {
3997
+ throw new ValidationError(
3998
+ `EvalTraceStore.compareRuns: candidates must differ ("${candidateA}")`
3999
+ );
4000
+ }
4001
+ const rows = await this.backend.load();
4002
+ const bestByScenario = (candidate) => {
4003
+ const m = /* @__PURE__ */ new Map();
4004
+ for (const r of rows) {
4005
+ if (r.candidateId !== candidate) continue;
4006
+ const sid = r.scenarioId;
4007
+ if (!sid) continue;
4008
+ const s = runScore(r);
4009
+ const prev = m.get(sid);
4010
+ if (prev === void 0 || s > prev) m.set(sid, s);
4011
+ }
4012
+ return m;
4013
+ };
4014
+ const aScores = bestByScenario(candidateA);
4015
+ const bScores = bestByScenario(candidateB);
4016
+ const paired = [...aScores.keys()].filter((sid) => bScores.has(sid)).sort();
4017
+ if (paired.length === 0) {
4018
+ throw new ValidationError(
4019
+ `EvalTraceStore.compareRuns: "${candidateA}" and "${candidateB}" share no scenario (need scenarioId on records)`
4020
+ );
4021
+ }
4022
+ let sumA = 0;
4023
+ let sumB = 0;
4024
+ let bWins = 0;
4025
+ let aWins = 0;
4026
+ let ties = 0;
4027
+ for (const sid of paired) {
4028
+ const sa = aScores.get(sid);
4029
+ const sb = bScores.get(sid);
4030
+ sumA += sa;
4031
+ sumB += sb;
4032
+ if (sb > sa) bWins++;
4033
+ else if (sb < sa) aWins++;
4034
+ else ties++;
4035
+ }
4036
+ const meanA = sumA / paired.length;
4037
+ const meanB = sumB / paired.length;
4038
+ return {
4039
+ a: candidateA,
4040
+ b: candidateB,
4041
+ pairedScenarioIds: paired,
4042
+ meanA,
4043
+ meanB,
4044
+ meanDelta: meanB - meanA,
4045
+ bWins,
4046
+ ties,
4047
+ aWins
4048
+ };
4049
+ }
4050
+ };
4051
+
4052
+ // src/experiment-tracker.ts
4053
+ import { execSync } from "child_process";
4054
+ var DEFAULTS = {
4055
+ keepThreshold: 5,
4056
+ regressionThreshold: 5,
4057
+ iqrUnstableAbove: 10,
4058
+ stddevUnstableAbove: Number.POSITIVE_INFINITY,
4059
+ minRepsForVerdict: 3
4060
+ };
4061
+ function resolveThresholds(t) {
4062
+ const r = { ...DEFAULTS, ...t ?? {} };
4063
+ if (r.keepThreshold < 0) {
4064
+ throw new ValidationError(
4065
+ `experiment-tracker: keepThreshold must be >= 0, got ${r.keepThreshold}`
4066
+ );
4067
+ }
4068
+ if (r.regressionThreshold < 0) {
4069
+ throw new ValidationError(
4070
+ `experiment-tracker: regressionThreshold must be >= 0, got ${r.regressionThreshold}`
4071
+ );
4072
+ }
4073
+ if (r.minRepsForVerdict < 1) {
4074
+ throw new ValidationError(
4075
+ `experiment-tracker: minRepsForVerdict must be >= 1, got ${r.minRepsForVerdict}`
4076
+ );
4077
+ }
4078
+ return r;
4079
+ }
4080
+ function median(sorted) {
4081
+ const n = sorted.length;
4082
+ if (n === 0) return 0;
4083
+ const mid = Math.floor(n / 2);
4084
+ return n % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
4085
+ }
4086
+ function stddev(values, mean5) {
4087
+ if (values.length < 2) return 0;
4088
+ const variance = values.reduce((acc, v) => acc + (v - mean5) ** 2, 0) / values.length;
4089
+ return Math.sqrt(variance);
4090
+ }
4091
+ function computeExperimentStats(reps, thresholds) {
4092
+ const t = resolveThresholds(thresholds);
4093
+ const n = reps.length;
4094
+ if (n === 0) {
4095
+ return {
4096
+ median: 0,
4097
+ mean: 0,
4098
+ min: 0,
4099
+ max: 0,
4100
+ iqr: 0,
4101
+ stddev: 0,
4102
+ passRate: null,
4103
+ n: 0,
4104
+ stable: false
4105
+ };
4106
+ }
4107
+ const scores2 = reps.map((r) => {
4108
+ if (!Number.isFinite(r.score)) {
4109
+ throw new ValidationError(`experiment-tracker: rep ${r.rep} has non-finite score ${r.score}`);
4110
+ }
4111
+ return r.score;
4112
+ });
4113
+ const sorted = [...scores2].sort((a, b) => a - b);
4114
+ const mean5 = scores2.reduce((s, v) => s + v, 0) / n;
4115
+ const sd = stddev(scores2, mean5);
4116
+ const spread = iqr(scores2);
4117
+ const rated = reps.filter((r) => typeof r.passed === "boolean");
4118
+ const passRate = rated.length === 0 ? null : rated.filter((r) => r.passed).length / rated.length;
4119
+ const stable = spread < t.iqrUnstableAbove && sd < t.stddevUnstableAbove;
4120
+ return {
4121
+ median: median(sorted),
4122
+ mean: mean5,
4123
+ min: sorted[0],
4124
+ max: sorted[n - 1],
4125
+ iqr: spread,
4126
+ stddev: sd,
4127
+ passRate,
4128
+ n,
4129
+ stable
4130
+ };
4131
+ }
4132
+ function improvementVerdict(candidate, parent, thresholds) {
4133
+ const t = resolveThresholds(thresholds);
4134
+ if (!parent) {
4135
+ return {
4136
+ verdict: "ITERATE",
4137
+ medianDelta: null,
4138
+ reason: "no parent experiment to compare against"
4139
+ };
4140
+ }
4141
+ if (candidate.n < t.minRepsForVerdict || parent.n < t.minRepsForVerdict) {
4142
+ return {
4143
+ verdict: "ITERATE",
4144
+ medianDelta: null,
4145
+ reason: `need >= ${t.minRepsForVerdict} reps on both sides (candidate n=${candidate.n}, parent n=${parent.n})`
4146
+ };
4147
+ }
4148
+ if (!candidate.stable) {
4149
+ return {
4150
+ verdict: "NOISE",
4151
+ medianDelta: candidate.median - parent.median,
4152
+ reason: `candidate unstable (iqr=${candidate.iqr}, stddev=${candidate.stddev.toFixed(2)})`
4153
+ };
4154
+ }
4155
+ const medianDelta2 = candidate.median - parent.median;
4156
+ if (medianDelta2 > t.keepThreshold) {
4157
+ return { verdict: "KEEP", medianDelta: medianDelta2, reason: `median +${medianDelta2} > +${t.keepThreshold}` };
4158
+ }
4159
+ if (medianDelta2 < -t.regressionThreshold) {
4160
+ return {
4161
+ verdict: "REGRESSION",
4162
+ medianDelta: medianDelta2,
4163
+ reason: `median ${medianDelta2} < -${t.regressionThreshold}`
4164
+ };
4165
+ }
4166
+ return {
4167
+ verdict: "NOISE",
4168
+ medianDelta: medianDelta2,
4169
+ reason: `median delta ${medianDelta2} inside noise band [-${t.regressionThreshold}, +${t.keepThreshold}]`
4170
+ };
4171
+ }
4172
+ var gitProvenanceReader = () => {
4173
+ const run = (cmd) => execSync(cmd, { encoding: "utf8" }).trim();
4174
+ const commit = run("git rev-parse --short HEAD");
4175
+ const message = run("git log -1 --format=%s");
4176
+ const changedRaw = run("git diff --name-only HEAD~1");
4177
+ const changedFiles = changedRaw.length === 0 ? [] : changedRaw.split("\n").filter(Boolean);
4178
+ return { commit, message, changedFiles };
4179
+ };
4180
+ function inMemoryExperimentStore(initial = []) {
4181
+ let state = initial.map((e) => structuredClone(e));
4182
+ return {
4183
+ async load() {
4184
+ return state.map((e) => structuredClone(e));
4185
+ },
4186
+ async save(experiments) {
4187
+ state = experiments.map((e) => structuredClone(e));
4188
+ }
4189
+ };
4190
+ }
4191
+ function fileExperimentStore(path) {
4192
+ return {
4193
+ async load() {
4194
+ const fs2 = await import("fs/promises");
4195
+ try {
4196
+ const raw = await fs2.readFile(path, "utf8");
4197
+ const parsed = JSON.parse(raw);
4198
+ if (!Array.isArray(parsed)) {
4199
+ throw new ValidationError(`experiment-tracker: store at ${path} is not a JSON array`);
4200
+ }
4201
+ return parsed;
4202
+ } catch (err) {
4203
+ if (err.code === "ENOENT") return [];
4204
+ throw err;
4205
+ }
4206
+ },
4207
+ async save(experiments) {
4208
+ const fs2 = await import("fs/promises");
4209
+ const pathMod = await import("path");
4210
+ await fs2.mkdir(pathMod.dirname(path), { recursive: true });
4211
+ await fs2.writeFile(path, JSON.stringify(experiments, null, 2), "utf8");
4212
+ }
4213
+ };
4214
+ }
4215
+ var ExperimentTracker = class {
4216
+ store;
4217
+ provenanceReader;
4218
+ thresholds;
4219
+ now;
4220
+ constructor(options = {}) {
4221
+ this.store = options.store ?? inMemoryExperimentStore();
4222
+ this.provenanceReader = options.provenanceReader ?? gitProvenanceReader;
4223
+ this.thresholds = resolveThresholds(options.thresholds);
4224
+ this.now = options.now ?? Date.now;
4225
+ }
4226
+ async create(input) {
4227
+ const experiments = await this.store.load();
4228
+ if (experiments.some((e) => e.id === input.id)) {
4229
+ throw new ValidationError(`experiment-tracker: experiment id "${input.id}" already exists`);
4230
+ }
4231
+ if (input.parentId && !experiments.some((e) => e.id === input.parentId)) {
4232
+ throw new ValidationError(
4233
+ `experiment-tracker: parent experiment "${input.parentId}" not found`
4234
+ );
4235
+ }
4236
+ const provenance = input.provenance ?? await this.provenanceReader();
4237
+ const experiment = {
4238
+ id: input.id,
4239
+ label: input.label,
4240
+ provenance,
4241
+ parentId: input.parentId,
4242
+ changeSummary: input.changeSummary,
4243
+ reps: [],
4244
+ stats: computeExperimentStats([], this.thresholds),
4245
+ verdict: "ITERATE",
4246
+ createdAt: new Date(this.now()).toISOString()
4247
+ };
4248
+ experiments.push(experiment);
4249
+ await this.store.save(experiments);
4250
+ return structuredClone(experiment);
4251
+ }
4252
+ /** Append a rep (its `rep` index defaults to the current rep count) and
4253
+ * recompute stats + verdict. Returns the updated experiment. */
4254
+ async addRep(experimentId, rep) {
4255
+ const experiments = await this.store.load();
4256
+ const exp = experiments.find((e) => e.id === experimentId);
4257
+ if (!exp)
4258
+ throw new ValidationError(`experiment-tracker: experiment "${experimentId}" not found`);
4259
+ const fullRep = {
4260
+ rep: rep.rep ?? exp.reps.length,
4261
+ score: rep.score,
4262
+ passed: rep.passed,
4263
+ metrics: rep.metrics,
4264
+ timestamp: rep.timestamp ?? new Date(this.now()).toISOString()
4265
+ };
4266
+ exp.reps.push(fullRep);
4267
+ exp.stats = computeExperimentStats(exp.reps, this.thresholds);
4268
+ const parent = exp.parentId ? experiments.find((e) => e.id === exp.parentId) : void 0;
4269
+ exp.verdict = improvementVerdict(exp.stats, parent?.stats ?? null, this.thresholds).verdict;
4270
+ await this.store.save(experiments);
4271
+ return structuredClone(exp);
4272
+ }
4273
+ async get(experimentId) {
4274
+ const experiments = await this.store.load();
4275
+ const found = experiments.find((e) => e.id === experimentId);
4276
+ return found ? structuredClone(found) : void 0;
4277
+ }
4278
+ async list() {
4279
+ return this.store.load();
4280
+ }
4281
+ /** Full verdict (not just the enum) for an experiment vs its parent. */
4282
+ async verdictFor(experimentId) {
4283
+ const experiments = await this.store.load();
4284
+ const exp = experiments.find((e) => e.id === experimentId);
4285
+ if (!exp)
4286
+ throw new ValidationError(`experiment-tracker: experiment "${experimentId}" not found`);
4287
+ const parent = exp.parentId ? experiments.find((e) => e.id === exp.parentId) : void 0;
4288
+ return improvementVerdict(exp.stats, parent?.stats ?? null, this.thresholds);
4289
+ }
4290
+ };
4291
+
3763
4292
  // src/muffled-gate-scanner.ts
3764
4293
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
3765
4294
  import { join } from "path";
@@ -3872,7 +4401,7 @@ var DEFAULT_FINDERS = [
3872
4401
  ];
3873
4402
  var UNIVERSAL_FINDERS = [findConstructorCwdDropped];
3874
4403
  function autoDeriveImporters(repoRoot, roots, extensions, importsContain) {
3875
- const matches = [];
4404
+ const matches2 = [];
3876
4405
  const walk = (rel) => {
3877
4406
  const abs = join(repoRoot, rel);
3878
4407
  if (!existsSync(abs)) return;
@@ -3898,12 +4427,12 @@ function autoDeriveImporters(repoRoot, roots, extensions, importsContain) {
3898
4427
  } catch {
3899
4428
  continue;
3900
4429
  }
3901
- if (text.includes(importsContain)) matches.push(sub);
4430
+ if (text.includes(importsContain)) matches2.push(sub);
3902
4431
  }
3903
4432
  }
3904
4433
  };
3905
4434
  for (const r of roots) walk(r);
3906
- return matches;
4435
+ return matches2;
3907
4436
  }
3908
4437
  function scanForMuffledGates(opts) {
3909
4438
  const findings = [];
@@ -4075,16 +4604,80 @@ function isObject(v) {
4075
4604
  return typeof v === "object" && v !== null && !Array.isArray(v);
4076
4605
  }
4077
4606
 
4607
+ // src/partition-held-out.ts
4608
+ function fnv1a32(input) {
4609
+ let h = 2166136261;
4610
+ for (let i = 0; i < input.length; i++) {
4611
+ h ^= input.charCodeAt(i) & 255;
4612
+ h = Math.imul(h, 16777619);
4613
+ }
4614
+ return h >>> 0;
4615
+ }
4616
+ function hashToUnit(id, seed) {
4617
+ return fnv1a32(`${id}
4618
+ ${seed}`) / 4294967296;
4619
+ }
4620
+ function assignHeldOutTag(id, options = {}) {
4621
+ const seed = options.seed ?? "held-out-v1";
4622
+ const holdoutFraction = options.holdoutFraction ?? 0.5;
4623
+ assertFraction(holdoutFraction);
4624
+ return hashToUnit(id, seed) < holdoutFraction ? "holdout" : "search";
4625
+ }
4626
+ function partitionHeldOut(ids, options = {}) {
4627
+ const seed = options.seed ?? "held-out-v1";
4628
+ const holdoutFraction = options.holdoutFraction ?? 0.5;
4629
+ const minHoldout = options.minHoldout ?? 1;
4630
+ const minSearch = options.minSearch ?? 1;
4631
+ assertFraction(holdoutFraction);
4632
+ if (ids.length === 0) {
4633
+ throw new ValidationError("partitionHeldOut: no ids supplied");
4634
+ }
4635
+ const seen = /* @__PURE__ */ new Set();
4636
+ const search = [];
4637
+ const holdout = [];
4638
+ for (const id of ids) {
4639
+ if (typeof id !== "string" || id.length === 0) {
4640
+ throw new ValidationError(
4641
+ `partitionHeldOut: ids must be non-empty strings, got ${JSON.stringify(id)}`
4642
+ );
4643
+ }
4644
+ if (seen.has(id)) {
4645
+ throw new ValidationError(
4646
+ `partitionHeldOut: duplicate id "${id}" \u2014 a paired comparison must not double-count`
4647
+ );
4648
+ }
4649
+ seen.add(id);
4650
+ if (hashToUnit(id, seed) < holdoutFraction) holdout.push(id);
4651
+ else search.push(id);
4652
+ }
4653
+ if (holdout.length < minHoldout) {
4654
+ throw new ValidationError(
4655
+ `partitionHeldOut: holdout set has ${holdout.length} id(s), below the floor of ${minHoldout} (n=${ids.length}, holdoutFraction=${holdoutFraction}) \u2014 too few for a significant comparison`
4656
+ );
4657
+ }
4658
+ if (search.length < minSearch) {
4659
+ throw new ValidationError(
4660
+ `partitionHeldOut: search set has ${search.length} id(s), below the floor of ${minSearch} (n=${ids.length}, holdoutFraction=${holdoutFraction})`
4661
+ );
4662
+ }
4663
+ return { search, holdout, seed, holdoutFraction };
4664
+ }
4665
+ function assertFraction(f) {
4666
+ if (!Number.isFinite(f) || f <= 0 || f >= 1) {
4667
+ throw new ValidationError(`partitionHeldOut: holdoutFraction must be in (0, 1), got ${f}`);
4668
+ }
4669
+ }
4670
+
4078
4671
  // src/scorecard.ts
4079
4672
  import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "fs";
4080
4673
  import { dirname } from "path";
4081
- function median(xs) {
4674
+ function median2(xs) {
4082
4675
  if (xs.length === 0) return 0;
4083
4676
  const sorted = [...xs].sort((a, b) => a - b);
4084
4677
  const mid = Math.floor(sorted.length / 2);
4085
4678
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
4086
4679
  }
4087
- function runScore(run) {
4680
+ function runScore2(run) {
4088
4681
  return run.outcome.holdoutScore ?? run.outcome.searchScore;
4089
4682
  }
4090
4683
  function aggregatePerDimension(runs) {
@@ -4118,14 +4711,14 @@ function recordRuns(runs, opts) {
4118
4711
  }
4119
4712
  const lines = [];
4120
4713
  for (const [scenarioId, scenarioRuns] of byScenario) {
4121
- const scored = scenarioRuns.map((run) => ({ run, score: runScore(run) })).filter((s) => s.score !== void 0);
4714
+ const scored = scenarioRuns.map((run) => ({ run, score: runScore2(run) })).filter((s) => s.score !== void 0);
4122
4715
  if (scored.length === 0) continue;
4123
4716
  const scores2 = scored.map((s) => s.score);
4124
4717
  const entry = {
4125
4718
  commitSha: opts.commitSha,
4126
4719
  timestamp,
4127
4720
  scores: scores2,
4128
- composite: median(scores2),
4721
+ composite: median2(scores2),
4129
4722
  runIds: scored.map((s) => s.run.runId)
4130
4723
  };
4131
4724
  const perDimension = aggregatePerDimension(scenarioRuns);
@@ -5873,13 +6466,13 @@ function extractErrorCount(text, opts = {}) {
5873
6466
  (p) => !opts.only || opts.only.includes(p.name)
5874
6467
  );
5875
6468
  for (const p of patterns) {
5876
- const matches = Array.from(text.matchAll(p.regex));
5877
- if (matches.length === 0) continue;
5878
- const count = p.transform ? matches.reduce((sum3, m) => sum3 + p.transform(m), 0) : matches.length;
6469
+ const matches2 = Array.from(text.matchAll(p.regex));
6470
+ if (matches2.length === 0) continue;
6471
+ const count = p.transform ? matches2.reduce((sum3, m) => sum3 + p.transform(m), 0) : matches2.length;
5879
6472
  return {
5880
6473
  count,
5881
6474
  matched: p.name,
5882
- samples: matches.slice(0, 5).map((m) => m[0])
6475
+ samples: matches2.slice(0, 5).map((m) => m[0])
5883
6476
  };
5884
6477
  }
5885
6478
  return { count: null, matched: null, samples: [] };
@@ -6820,7 +7413,7 @@ function scoreScenario(scenario, matcher, threshold, matchStrategy) {
6820
7413
  }
6821
7414
  function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
6822
7415
  const candidatesLeft = scenario.candidates.map((candidate, index) => ({ candidate, index }));
6823
- const matches = [];
7416
+ const matches2 = [];
6824
7417
  for (const reference of scenario.references) {
6825
7418
  let best = null;
6826
7419
  for (const item of candidatesLeft) {
@@ -6833,7 +7426,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
6833
7426
  if (best && best.score >= threshold) {
6834
7427
  const matchIndex = candidatesLeft.findIndex((item) => item.index === best.index);
6835
7428
  if (matchIndex >= 0) candidatesLeft.splice(matchIndex, 1);
6836
- matches.push({
7429
+ matches2.push({
6837
7430
  scenarioId: scenario.id,
6838
7431
  referenceId: reference.id,
6839
7432
  candidateId: best.candidate.id,
@@ -6843,7 +7436,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
6843
7436
  reason: best.reason
6844
7437
  });
6845
7438
  } else {
6846
- matches.push({
7439
+ matches2.push({
6847
7440
  scenarioId: scenario.id,
6848
7441
  referenceId: reference.id,
6849
7442
  candidateId: best?.candidate.id ?? null,
@@ -6854,7 +7447,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
6854
7447
  });
6855
7448
  }
6856
7449
  }
6857
- return buildScenarioScore(scenario, matches, candidatesLeft.length);
7450
+ return buildScenarioScore(scenario, matches2, candidatesLeft.length);
6858
7451
  }
6859
7452
  function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
6860
7453
  const pairs = [];
@@ -6881,7 +7474,7 @@ function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
6881
7474
  selectedByReference.set(pair.referenceIndex, pair);
6882
7475
  selectedCandidates.add(pair.candidateIndex);
6883
7476
  }
6884
- const matches = scenario.references.map((reference, referenceIndex) => {
7477
+ const matches2 = scenario.references.map((reference, referenceIndex) => {
6885
7478
  const weight = reference.weight ?? 1;
6886
7479
  const selected = selectedByReference.get(referenceIndex);
6887
7480
  if (selected) {
@@ -6906,7 +7499,7 @@ function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
6906
7499
  reason: bestRejected?.reason ?? "no candidates"
6907
7500
  };
6908
7501
  });
6909
- return buildScenarioScore(scenario, matches, scenario.candidates.length - selectedCandidates.size);
7502
+ return buildScenarioScore(scenario, matches2, scenario.candidates.length - selectedCandidates.size);
6910
7503
  }
6911
7504
  function scorePair(scenario, matcher, reference, candidate) {
6912
7505
  const result = matcher(reference, candidate, scenario);
@@ -6917,11 +7510,11 @@ function scorePair(scenario, matcher, reference, candidate) {
6917
7510
  }
6918
7511
  return { score: clamp012(result.score), reason: result.reason ?? "" };
6919
7512
  }
6920
- function buildScenarioScore(scenario, matches, falsePositives) {
6921
- const matched = matches.filter((match) => match.matched).length;
7513
+ function buildScenarioScore(scenario, matches2, falsePositives) {
7514
+ const matched = matches2.filter((match) => match.matched).length;
6922
7515
  const total = scenario.references.length;
6923
- const matchedWeight = matches.filter((match) => match.matched).reduce((sum3, match) => sum3 + match.weight, 0);
6924
- const totalWeight = matches.reduce((sum3, match) => sum3 + match.weight, 0);
7516
+ const matchedWeight = matches2.filter((match) => match.matched).reduce((sum3, match) => sum3 + match.weight, 0);
7517
+ const totalWeight = matches2.reduce((sum3, match) => sum3 + match.weight, 0);
6925
7518
  const precision2 = ratio(matched, matched + falsePositives);
6926
7519
  const recall = ratio(matched, total);
6927
7520
  return {
@@ -6935,7 +7528,7 @@ function buildScenarioScore(scenario, matches, falsePositives) {
6935
7528
  precision: precision2,
6936
7529
  recall,
6937
7530
  f1: f1(precision2, recall),
6938
- matches
7531
+ matches: matches2
6939
7532
  };
6940
7533
  }
6941
7534
  function aggregateBySplit(scores2) {
@@ -7196,7 +7789,7 @@ function createDefaultReviewer(options) {
7196
7789
 
7197
7790
  // src/description-length-gate.ts
7198
7791
  import { gzipSync } from "zlib";
7199
- function runScore2(run) {
7792
+ function runScore3(run) {
7200
7793
  const o = run.outcome;
7201
7794
  const s = o.holdoutScore ?? o.searchScore ?? o.raw?.score;
7202
7795
  return typeof s === "number" && Number.isFinite(s) ? s : void 0;
@@ -7207,7 +7800,7 @@ function taskKey(run) {
7207
7800
  function perTaskMeanScore(runs) {
7208
7801
  const acc = /* @__PURE__ */ new Map();
7209
7802
  for (const run of runs) {
7210
- const s = runScore2(run);
7803
+ const s = runScore3(run);
7211
7804
  if (s === void 0) continue;
7212
7805
  const key = taskKey(run);
7213
7806
  const cur = acc.get(key) ?? { sum: 0, n: 0 };
@@ -7359,10 +7952,10 @@ async function discoverPersonas(dir, opts = {}) {
7359
7952
  function matchGoldens(goldens, candidates, options = {}) {
7360
7953
  const extract = options.text ?? defaultExtract2;
7361
7954
  const haystacks = candidates.map((c) => extract(c).toLowerCase());
7362
- const matches = goldens.map((golden) => goldenMatched(golden, haystacks));
7955
+ const matches2 = goldens.map((golden) => goldenMatched(golden, haystacks));
7363
7956
  return {
7364
- matches,
7365
- hits: matches.filter(Boolean).length,
7957
+ matches: matches2,
7958
+ hits: matches2.filter(Boolean).length,
7366
7959
  total: goldens.length
7367
7960
  };
7368
7961
  }
@@ -8517,6 +9110,7 @@ export {
8517
9110
  CaptureIntegrityError,
8518
9111
  ConfigError,
8519
9112
  ConvergenceTracker,
9113
+ CostLedger,
8520
9114
  CostTracker,
8521
9115
  CrossFamilyError,
8522
9116
  DEFAULT_AGENT_SLOS,
@@ -8538,6 +9132,8 @@ export {
8538
9132
  DockerSandboxDriver,
8539
9133
  DualAgentBench,
8540
9134
  ERROR_COUNT_PATTERNS,
9135
+ EvalTraceStore,
9136
+ ExperimentTracker,
8541
9137
  FAILURE_CLASSES,
8542
9138
  FAILURE_MODE_KIND_SPEC,
8543
9139
  FileSystemFeedbackTrajectoryStore,
@@ -8630,6 +9226,7 @@ export {
8630
9226
  assertRunCaptured,
8631
9227
  assertSingleBackend,
8632
9228
  assignFeedbackSplit,
9229
+ assignHeldOutTag,
8633
9230
  attributeCounterfactuals,
8634
9231
  backoffMs,
8635
9232
  deterministicSplit as benchmarkDeterministicSplit,
@@ -8677,6 +9274,7 @@ export {
8677
9274
  compilerJudge,
8678
9275
  composeParsers,
8679
9276
  composeValidators,
9277
+ computeExperimentStats,
8680
9278
  computeFindingId,
8681
9279
  computeToolUseMetrics,
8682
9280
  computeTraceMetrics,
@@ -8689,6 +9287,7 @@ export {
8689
9287
  convertTraceStoresToOtlp,
8690
9288
  corpusInterRaterAgreement,
8691
9289
  corpusInterRaterAgreementFromJudgeScores,
9290
+ costForUsage,
8692
9291
  createAnalystAi,
8693
9292
  createAntiSlopJudge,
8694
9293
  createChatClient,
@@ -8744,6 +9343,9 @@ export {
8744
9343
  extractErrorCount,
8745
9344
  extractOtlpAttributes,
8746
9345
  extractProducedState,
9346
+ extractUsage,
9347
+ extractUsageFromResponse,
9348
+ extractUsageFromSse,
8747
9349
  feedbackTrajectoriesToDatasetScenarios,
8748
9350
  feedbackTrajectoriesToOptimizerRows,
8749
9351
  feedbackTrajectoryToDatasetScenario,
@@ -8751,6 +9353,7 @@ export {
8751
9353
  fieldAgreement,
8752
9354
  fileContains,
8753
9355
  fileExists,
9356
+ fileExperimentStore,
8754
9357
  findAutoMatchNoExpectation,
8755
9358
  findConstructorCwdDropped,
8756
9359
  findFallbackToPass,
@@ -8760,12 +9363,14 @@ export {
8760
9363
  firstStringAttr,
8761
9364
  flattenOtlpExportToNdjson,
8762
9365
  flowLayer,
9366
+ fnv1a32,
8763
9367
  formatBenchmarkReport,
8764
9368
  formatDriverReport,
8765
9369
  formatFindings,
8766
9370
  formatScorecardDiff,
8767
9371
  gainHistogram,
8768
9372
  ghCliClient,
9373
+ gitProvenanceReader,
8769
9374
  precision as goldenPrecision,
8770
9375
  gradeSemanticStatus,
8771
9376
  groupBy,
@@ -8773,10 +9378,14 @@ export {
8773
9378
  hashContent,
8774
9379
  hashJson,
8775
9380
  hashScenarios,
9381
+ hashToUnit,
8776
9382
  htmlContainsElement,
8777
9383
  httpGithubClient,
9384
+ improvementVerdict,
9385
+ inMemoryExperimentStore,
8778
9386
  inMemoryReferenceReplayStore,
8779
9387
  inMemoryReviewStore,
9388
+ inMemoryRunRecordBackend,
8780
9389
  inferDomainKeywords,
8781
9390
  inferOtlpKind,
8782
9391
  interRaterReliability,
@@ -8797,6 +9406,7 @@ export {
8797
9406
  jsonShape,
8798
9407
  jsonlReferenceReplayStore,
8799
9408
  jsonlReviewStore,
9409
+ jsonlRunRecordBackend,
8800
9410
  judgeFamily,
8801
9411
  judgeReplayGate,
8802
9412
  judgeSpans,
@@ -8816,6 +9426,7 @@ export {
8816
9426
  mergeLayerResults,
8817
9427
  mergeSteeringBundle,
8818
9428
  modelDescriptionBits,
9429
+ modelPriceKey,
8819
9430
  multiToolchainLayer,
8820
9431
  nistAiRmfReport,
8821
9432
  normalizeScores,
@@ -8840,6 +9451,7 @@ export {
8840
9451
  parseRunRecordSafe,
8841
9452
  parseRuntimeTrajectoryHookEvent,
8842
9453
  partialCredit,
9454
+ partitionHeldOut,
8843
9455
  passOrthogonality,
8844
9456
  pixelDeltaRatio,
8845
9457
  planTraceInsightQuestions,
@@ -8905,6 +9517,7 @@ export {
8905
9517
  runProposeReview,
8906
9518
  runProposeReviewAsControlLoop,
8907
9519
  runReferenceReplay,
9520
+ runScore,
8908
9521
  runSelfPlay,
8909
9522
  runSemanticConceptJudge,
8910
9523
  runTestGradedScenario,