textopt 0.0.0 → 0.2.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 (65) hide show
  1. package/README.md +65 -25
  2. package/dist/bootstrap-search/index.cjs +159 -73
  3. package/dist/bootstrap-search/index.d.cts +32 -10
  4. package/dist/bootstrap-search/index.d.mts +32 -10
  5. package/dist/bootstrap-search/index.mjs +150 -66
  6. package/dist/demos-9v5ts7F3.cjs +244 -0
  7. package/dist/{demos-B0pVQjYC.d.mts → demos-ASsSXYXA.d.mts} +10 -3
  8. package/dist/demos-Brobjfuc.mjs +215 -0
  9. package/dist/{demos-BTuzFNsp.d.cts → demos-ByaLZy-Z.d.cts} +10 -3
  10. package/dist/file-cache.cjs +27 -8
  11. package/dist/file-cache.d.cts +13 -0
  12. package/dist/file-cache.d.mts +13 -0
  13. package/dist/file-cache.mjs +27 -8
  14. package/dist/gepa/index.cjs +128 -80
  15. package/dist/gepa/index.d.cts +15 -7
  16. package/dist/gepa/index.d.mts +15 -7
  17. package/dist/gepa/index.mjs +101 -55
  18. package/dist/index.cjs +157 -30
  19. package/dist/index.d.cts +177 -7
  20. package/dist/index.d.mts +177 -7
  21. package/dist/index.mjs +139 -18
  22. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  23. package/dist/math-Dqme4rYz.mjs +123 -0
  24. package/dist/mipro/index.cjs +104 -70
  25. package/dist/mipro/index.d.cts +17 -14
  26. package/dist/mipro/index.d.mts +17 -14
  27. package/dist/mipro/index.mjs +90 -58
  28. package/dist/opro/index.cjs +136 -51
  29. package/dist/opro/index.d.cts +17 -9
  30. package/dist/opro/index.d.mts +17 -9
  31. package/dist/opro/index.mjs +121 -38
  32. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-4Zv-Zt2t.d.cts} +90 -5
  33. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ds5mzYjz.d.mts} +90 -5
  34. package/dist/random-search/index.cjs +99 -49
  35. package/dist/random-search/index.d.cts +15 -13
  36. package/dist/random-search/index.d.mts +15 -13
  37. package/dist/random-search/index.mjs +89 -41
  38. package/dist/{reflection-Cr_upzU0.d.mts → reflection-CMezGu6u.d.mts} +38 -14
  39. package/dist/{reflection-CQToe-5B.d.cts → reflection-D0A7eahD.d.cts} +38 -14
  40. package/dist/reporting-bq007_2z.d.cts +294 -0
  41. package/dist/reporting-bq007_2z.d.mts +294 -0
  42. package/dist/simba/index.cjs +216 -83
  43. package/dist/simba/index.d.cts +53 -13
  44. package/dist/simba/index.d.mts +53 -13
  45. package/dist/simba/index.mjs +206 -75
  46. package/dist/testing.cjs +1 -0
  47. package/dist/testing.d.cts +5 -3
  48. package/dist/testing.d.mts +5 -3
  49. package/dist/testing.mjs +1 -1
  50. package/dist/{evaluation-OZOp6TB7.cjs → warnings-CWRJF-jA.cjs} +228 -5
  51. package/dist/{evaluation-BV0nSZVx.mjs → warnings-OxvDi9kN.mjs} +175 -6
  52. package/docs/adapters.md +169 -0
  53. package/docs/benchmark.md +90 -0
  54. package/docs/data-prep.md +113 -0
  55. package/docs/distillation.md +128 -0
  56. package/docs/evaluation.md +87 -0
  57. package/docs/metric-preflight.md +132 -0
  58. package/docs/optimizers.md +293 -0
  59. package/docs/tuning.md +130 -0
  60. package/package.json +6 -4
  61. package/dist/demos-B9BJiNKz.cjs +0 -143
  62. package/dist/demos-Degx6UmP.mjs +0 -126
  63. package/dist/math-DhrDmpFS.mjs +0 -78
  64. package/dist/types-CWv4IQFF.d.cts +0 -129
  65. package/dist/types-CWv4IQFF.d.mts +0 -129
@@ -1,7 +1,7 @@
1
- import { c as candidateFingerprint, d as createMemoryCache, f as stableHash, i as measuredMean, l as runFingerprint, n as costExhausted, o as createDeadline, p as createBudget, r as createEvaluator, s as assertResumable, t as BudgetExhausted, u as componentNames } from "../evaluation-BV0nSZVx.mjs";
1
+ import { S as createBudget, _ as componentNames, a as instanceRow, b as defaultInstanceId, c as BudgetExhausted, d as measuredMean, g as runFingerprint, h as candidateFingerprint, i as flushReporters, l as costExhausted, m as assertResumable, n as seedScoreWarnings, o as isCandidateAccepted, p as createDeadline, r as createEmitter, s as isRunFinished, t as resolveValidationSet, u as createEvaluator, y as createMemoryCache } from "../warnings-OxvDi9kN.mjs";
2
2
  import { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
3
- import { i as sum, n as mean, r as signFlipPValue, t as argmax } from "../math-DhrDmpFS.mjs";
4
- import { n as formatDemos, r as parseDemos } from "../demos-Degx6UmP.mjs";
3
+ import { a as sum, i as signFlipPValue, r as mean, t as argmax } from "../math-Dqme4rYz.mjs";
4
+ import { r as parseDemos, t as formatDemos } from "../demos-Brobjfuc.mjs";
5
5
  import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
6
6
  import { a as createDefaultProposer, i as buildSimplifyPrompt, n as buildReflectionPrompt, o as diverseReflectionStrategies, r as buildRewritePrompt, t as buildGeneralizePrompt } from "../reflection-mwMhrjs_.mjs";
7
7
  import { t as createEpochShuffledSampler } from "../sampling-CfHt7Gue.mjs";
@@ -512,7 +512,9 @@ function hasSurvivor(args) {
512
512
  * `frontier` chooses what the fronts are taken over. "instance" is GEPA as
513
513
  * published. "objective" tracks candidates leading each named objective the
514
514
  * adapter reports, and "hybrid" pools both — a candidate then earns selection
515
- * weight for every instance it wins *and* every objective it leads.
515
+ * weight for every instance it wins *and* every objective it leads. The
516
+ * objectives are whatever the adapter put in `objectiveScores`, which for a
517
+ * judge is every criterion it graded, including any at `weight: 0`.
516
518
  */
517
519
  function paretoSelector(args = {}) {
518
520
  const { epsilon = 0, frontier = "instance" } = args;
@@ -659,7 +661,8 @@ function improvementAcceptance(args = {}) {
659
661
  */
660
662
  function pairedPermutationAcceptance(args = {}) {
661
663
  const { alpha = .2, maxExact = 16 } = args;
662
- return ({ parentScores, childScores }) => {
664
+ if (!Number.isFinite(alpha) || alpha <= 0 || alpha > 1) throw new Error(`alpha must be greater than 0 and at most 1, received ${alpha}`);
665
+ const policy = ({ parentScores, childScores }) => {
663
666
  const differences = [];
664
667
  for (let index = 0; index < parentScores.length; index += 1) differences.push(childScores[index] - parentScores[index]);
665
668
  const observed = sum(differences);
@@ -670,6 +673,33 @@ function pairedPermutationAcceptance(args = {}) {
670
673
  maxExact
671
674
  }) <= alpha;
672
675
  };
676
+ policy.minimumPairs = smallestAcceptableBatch({
677
+ alpha,
678
+ maxExact
679
+ });
680
+ return policy;
681
+ }
682
+ /**
683
+ * Smallest paired batch on which a sign-flip test at `alpha` could return a
684
+ * verdict of "accept" at all.
685
+ *
686
+ * Enumerating n non-zero differences gives 2^n equally likely sign
687
+ * assignments, so the smallest attainable p-value is 2^-n and no batch below
688
+ * `log2(1/alpha)` pairs can clear the bar. Past `maxExact` the p-value comes
689
+ * from a normal approximation, which has no such floor — so when the exact
690
+ * requirement is out of that regime's reach, the first batch that leaves the
691
+ * regime is the honest answer rather than an exact size that never applies.
692
+ */
693
+ function smallestAcceptableBatch(args) {
694
+ const { alpha, maxExact } = args;
695
+ let pairs = 1;
696
+ let smallestPValue = .5;
697
+ while (smallestPValue > alpha) {
698
+ pairs += 1;
699
+ smallestPValue /= 2;
700
+ if (pairs > maxExact) return maxExact + 1;
701
+ }
702
+ return pairs;
673
703
  }
674
704
  /**
675
705
  * Highest mean over the instances it was scored on, with wider coverage
@@ -738,16 +768,24 @@ var GepaOptimizer = class {
738
768
  this.#config = config;
739
769
  }
740
770
  async optimize(task) {
741
- return runGepa({
742
- config: this.#config,
743
- task
744
- });
771
+ try {
772
+ return await runGepa({
773
+ config: this.#config,
774
+ task
775
+ });
776
+ } finally {
777
+ await flushReporters(task.reporters ?? []);
778
+ }
745
779
  }
746
780
  };
747
781
  async function runGepa(args) {
748
782
  const { config, task } = args;
749
783
  const { minibatchSize = DEFAULT_MINIBATCH_SIZE, maxIterations = Number.POSITIVE_INFINITY, seed = 0, candidateSelector = paretoSelector(), acceptance = improvementAcceptance(), merge, skipPerfectScore = true, perfectScore = 1, rejectedProposalMemory = DEFAULT_REJECTED_PROPOSAL_MEMORY, proposals, reflection, checkpointCache = true, trackBestOutputs = false, raiseOnError = true } = config;
750
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, maxMetricCalls, componentSelector = roundRobinComponentSelector(), batchSampler = createEpochShuffledSampler({ minibatchSize }), valEvaluationPolicy = fullEvaluationPolicy(), cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
784
+ const { seedCandidate, trainingSet, validationSet: requestedValidationSet, testSet, adapter, reflect, maxMetricCalls, componentSelector = roundRobinComponentSelector(), batchSampler = createEpochShuffledSampler({ minibatchSize }), valEvaluationPolicy = fullEvaluationPolicy(), cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
785
+ const { validationSet, warnings } = resolveValidationSet({
786
+ validationSet: requestedValidationSet,
787
+ trainingSet
788
+ });
751
789
  const deadline = createDeadline({ maxWallClockMs });
752
790
  const seedComponents = componentNames(seedCandidate);
753
791
  const mergeConfig = {
@@ -827,8 +865,26 @@ async function runGepa(args) {
827
865
  let mergesDue = resumeFrom?.merge.due ?? 0;
828
866
  let totalMergesTested = resumeFrom?.merge.tested ?? 0;
829
867
  let lastIterationAccepted = resumeFrom?.merge.lastIterationAccepted ?? false;
830
- function emit(event) {
831
- onEvent?.(event);
868
+ const emit = createEmitter(reporters);
869
+ /**
870
+ * Everything an acceptance means, in one event: the text, the aggregate, and
871
+ * the row it put on the frontier. Emitted from one place because the merge
872
+ * path and the mutation path accept candidates separately, and a payload
873
+ * assembled twice is a payload that drifts.
874
+ */
875
+ function emitAccepted(record) {
876
+ const outputs = outputsByCandidate.get(record.id);
877
+ emit({
878
+ type: "candidateAccepted",
879
+ iteration,
880
+ candidateId: record.id,
881
+ parentIds: record.parentIds,
882
+ aggregateScore: record.aggregateScore,
883
+ source: record.source,
884
+ candidate: record.candidate,
885
+ instanceScores: record.instanceScores,
886
+ ...outputs === void 0 ? {} : { outputs }
887
+ });
832
888
  }
833
889
  /**
834
890
  * Copies everything mutable: a snapshot handed to `onCheckpoint` is a record
@@ -845,6 +901,7 @@ async function runGepa(args) {
845
901
  metricCalls: budget.spent(),
846
902
  reflectionCalls,
847
903
  cacheHits: evaluator.cacheHits(),
904
+ usage: evaluator.usage(),
848
905
  ...samplerState === void 0 ? {} : { sampler: samplerState },
849
906
  rejectedProposals: snapshotRejections({
850
907
  rejections: rejectedProposals,
@@ -873,6 +930,7 @@ async function runGepa(args) {
873
930
  ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
874
931
  trackOutputs: trackBestOutputs,
875
932
  cacheHits: resumeFrom?.cacheHits ?? 0,
933
+ ...resumeFrom?.usage === void 0 ? {} : { usage: resumeFrom.usage },
876
934
  ...signal === void 0 ? {} : { signal },
877
935
  onEvaluation: (event) => emit({
878
936
  type: "evaluation",
@@ -992,18 +1050,23 @@ async function runGepa(args) {
992
1050
  if (records.length === 0) {
993
1051
  const seedInstances = selectValInstances(seedCandidate);
994
1052
  if (!budget.canAfford(seedInstances.length)) throw new Error(`maxMetricCalls (${maxMetricCalls}) is smaller than the ${seedInstances.length} validation instances selected for scoring; the seed candidate cannot be scored`);
995
- addCandidate({
1053
+ const seedEvaluation = await evaluateValidation({
1054
+ candidate: seedCandidate,
1055
+ instances: seedInstances,
1056
+ phase: "seed",
1057
+ candidateId: 0
1058
+ });
1059
+ emitAccepted(addCandidate({
996
1060
  candidate: seedCandidate,
997
1061
  parentIds: [],
998
- evaluation: await evaluateValidation({
999
- candidate: seedCandidate,
1000
- instances: seedInstances,
1001
- phase: "seed",
1002
- candidateId: 0
1003
- }),
1062
+ evaluation: seedEvaluation,
1004
1063
  source: "seed",
1005
1064
  updatedComponents: []
1006
- });
1065
+ }));
1066
+ warnings.push(...seedScoreWarnings({
1067
+ scores: seedEvaluation.scores,
1068
+ perfectScore
1069
+ }));
1007
1070
  lastIterationAccepted = false;
1008
1071
  mergesDue = 0;
1009
1072
  await checkpoint();
@@ -1094,14 +1157,7 @@ async function runGepa(args) {
1094
1157
  });
1095
1158
  mergesDue -= 1;
1096
1159
  totalMergesTested += 1;
1097
- emit({
1098
- type: "candidateAccepted",
1099
- iteration,
1100
- candidateId: record.id,
1101
- parentIds: record.parentIds,
1102
- aggregateScore: record.aggregateScore,
1103
- source: "merge"
1104
- });
1160
+ emitAccepted(record);
1105
1161
  return "attempted";
1106
1162
  }
1107
1163
  /**
@@ -1346,21 +1402,13 @@ async function runGepa(args) {
1346
1402
  stop ??= "budgetExhausted";
1347
1403
  break;
1348
1404
  }
1349
- const record = addCandidate({
1405
+ emitAccepted(addCandidate({
1350
1406
  candidate: item.outcome.child,
1351
1407
  parentIds: [item.outcome.plan.parent.id],
1352
1408
  evaluation,
1353
1409
  source: "mutation",
1354
1410
  updatedComponents: componentNames(item.outcome.proposed)
1355
- });
1356
- emit({
1357
- type: "candidateAccepted",
1358
- iteration,
1359
- candidateId: record.id,
1360
- parentIds: record.parentIds,
1361
- aggregateScore: record.aggregateScore,
1362
- source: "mutation"
1363
- });
1411
+ }));
1364
1412
  }
1365
1413
  return stop;
1366
1414
  }
@@ -1444,7 +1492,7 @@ async function runGepa(args) {
1444
1492
  }
1445
1493
  const bestCandidateId = valEvaluationPolicy.bestCandidate(records);
1446
1494
  const best = records[bestCandidateId];
1447
- const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluateCached({
1495
+ const heldOut = testSet === void 0 ? void 0 : await evaluateCached({
1448
1496
  candidate: best.candidate,
1449
1497
  batch: testSet,
1450
1498
  ids: testIds,
@@ -1452,13 +1500,18 @@ async function runGepa(args) {
1452
1500
  phase: "test",
1453
1501
  candidateId: bestCandidateId,
1454
1502
  charge: false
1455
- }));
1503
+ });
1504
+ const testScore = heldOut === void 0 ? void 0 : measuredMean(heldOut);
1456
1505
  emit({
1457
1506
  type: "finish",
1458
1507
  reason: stopReason,
1508
+ warnings,
1459
1509
  bestCandidateId,
1510
+ bestScore: best.aggregateScore,
1460
1511
  metricCalls: budget.spent(),
1461
- ...testScore === void 0 ? {} : { testScore }
1512
+ ...testScore === void 0 ? {} : { testScore },
1513
+ ...heldOut === void 0 ? {} : { testInstanceScores: instanceRow(heldOut) },
1514
+ ...heldOut === void 0 || !trackBestOutputs ? {} : { testOutputs: heldOut.outputs }
1462
1515
  });
1463
1516
  const perObjectiveBest = collectPerObjectiveBest(records);
1464
1517
  const bestOutputs = outputsByCandidate.get(bestCandidateId);
@@ -1469,7 +1522,8 @@ async function runGepa(args) {
1469
1522
  bestCandidateId,
1470
1523
  ...testScore === void 0 ? {} : {
1471
1524
  testScore,
1472
- testMetricCalls: evaluator.unchargedCalls()
1525
+ testMetricCalls: evaluator.unchargedCalls(),
1526
+ testUsage: evaluator.unchargedUsage()
1473
1527
  },
1474
1528
  ...bestOutputs === void 0 ? {} : { bestOutputs },
1475
1529
  candidates: records,
@@ -1480,6 +1534,7 @@ async function runGepa(args) {
1480
1534
  reflectionCalls,
1481
1535
  cacheHits: evaluator.cacheHits(),
1482
1536
  iterations: iteration,
1537
+ warnings,
1483
1538
  stopReason,
1484
1539
  snapshot: takeSnapshot()
1485
1540
  };
@@ -1492,13 +1547,15 @@ async function runGepa(args) {
1492
1547
  function assertGepaConfig(config) {
1493
1548
  if (config.reflection?.buildPrompt !== void 0 && config.reflection.strategies !== void 0) throw new Error("reflection takes buildPrompt or strategies, not both");
1494
1549
  if (config.reflection?.strategies?.length === 0) throw new Error("reflection.strategies must not be empty");
1495
- const { minibatchSize = DEFAULT_MINIBATCH_SIZE, maxIterations = Number.POSITIVE_INFINITY, perfectScore = 1, rejectedProposalMemory = DEFAULT_REJECTED_PROPOSAL_MEMORY, proposals } = config;
1550
+ const { minibatchSize = DEFAULT_MINIBATCH_SIZE, maxIterations = Number.POSITIVE_INFINITY, acceptance, perfectScore = 1, rejectedProposalMemory = DEFAULT_REJECTED_PROPOSAL_MEMORY, proposals } = config;
1496
1551
  const proposalsPerIteration = proposals?.perIteration ?? 1;
1497
1552
  const proposalConcurrency = proposals?.concurrency ?? 1;
1498
1553
  if (!Number.isInteger(proposalsPerIteration) || proposalsPerIteration < 1) throw new Error(`proposals.perIteration must be a positive integer, received ${proposalsPerIteration}`);
1499
1554
  if (!Number.isInteger(proposalConcurrency) || proposalConcurrency < 1) throw new Error(`proposals.concurrency must be a positive integer, received ${proposalConcurrency}`);
1500
1555
  keepCount(proposals?.selection ?? "all");
1501
1556
  if (!Number.isInteger(minibatchSize) || minibatchSize < 1) throw new Error(`minibatchSize must be a positive integer, received ${minibatchSize}`);
1557
+ const minimumPairs = acceptance?.minimumPairs;
1558
+ if (minimumPairs !== void 0 && minibatchSize < minimumPairs) throw new Error(`this acceptance policy cannot accept anything on fewer than ${minimumPairs} instances, but minibatchSize is ${minibatchSize}; raise minibatchSize or loosen the policy`);
1502
1559
  if (!Number.isFinite(perfectScore)) throw new Error(`perfectScore must be a finite number, received ${perfectScore}`);
1503
1560
  if (!Number.isInteger(rejectedProposalMemory) || rejectedProposalMemory < 0) throw new Error(`rejectedProposalMemory must be a non-negative integer, received ${rejectedProposalMemory}`);
1504
1561
  if (maxIterations !== Number.POSITIVE_INFINITY && (!Number.isInteger(maxIterations) || maxIterations < 0)) throw new Error(`maxIterations must be a non-negative integer or Infinity, received ${maxIterations}`);
@@ -1617,17 +1674,6 @@ function collectDominatorIds(records) {
1617
1674
  return [...ids].sort((a, b) => a - b);
1618
1675
  }
1619
1676
  /**
1620
- * Names an instance by a hash of its content rather than by the content
1621
- * itself: the id ends up inside every cache key and inside the checkpoint
1622
- * fingerprint, and embedding whole examples there costs memory proportional to
1623
- * the dataset for no benefit. Data that will not serialize falls back to its
1624
- * position, which is stable for as long as the dataset order is.
1625
- */
1626
- function defaultInstanceId(args) {
1627
- const hash = stableHash(args.datum);
1628
- return hash === "" ? String(args.index) : hash;
1629
- }
1630
- /**
1631
1677
  * The two rollout sets restricted to the instances both of them measured.
1632
1678
  *
1633
1679
  * Screening is a paired comparison over one minibatch: a transient row is a
@@ -1649,4 +1695,4 @@ function pairMeasured(args) {
1649
1695
  };
1650
1696
  }
1651
1697
  //#endregion
1652
- export { GepaOptimizer, allComponentsSelector, buildGeneralizePrompt, buildReflectionPrompt, buildRewritePrompt, buildSimplifyPrompt, createDemoProposer, createPipelineAdapter, currentBestSelector, diverseReflectionStrategies, epsilonGreedySelector, fullEvaluationPolicy, improvementAcceptance, lowerBoundEvaluationPolicy, pairedPermutationAcceptance, paretoSelector, roundRobinComponentSelector, subsampledEvaluationPolicy, topKParetoSelector };
1698
+ export { GepaOptimizer, allComponentsSelector, buildGeneralizePrompt, buildReflectionPrompt, buildRewritePrompt, buildSimplifyPrompt, createDemoProposer, createPipelineAdapter, currentBestSelector, diverseReflectionStrategies, epsilonGreedySelector, fullEvaluationPolicy, improvementAcceptance, isCandidateAccepted, isRunFinished, lowerBoundEvaluationPolicy, pairedPermutationAcceptance, paretoSelector, roundRobinComponentSelector, subsampledEvaluationPolicy, topKParetoSelector };
package/dist/index.cjs CHANGED
@@ -1,11 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_evaluation = require("./evaluation-OZOp6TB7.cjs");
2
+ const require_warnings = require("./warnings-CWRJF-jA.cjs");
3
3
  const require_concurrency = require("./concurrency-C-cFzWW2.cjs");
4
- const require_math = require("./math-COOofUyv.cjs");
5
- const require_demos = require("./demos-B9BJiNKz.cjs");
4
+ const require_math = require("./math-BhlziRPc.cjs");
5
+ const require_demos = require("./demos-9v5ts7F3.cjs");
6
6
  const require_text = require("./text-CK_HB3su.cjs");
7
7
  //#region src/compare.ts
8
- const EXACT_LIMIT = 16;
8
+ const EXACT_LIMIT = 20;
9
+ /** Well above float subtraction noise (~1e-16), well below a real margin. */
10
+ const DEGENERACY_TOLERANCE = 1e-9;
9
11
  /**
10
12
  * Run several optimizers over the same seeds and report which one actually won.
11
13
  *
@@ -20,7 +22,7 @@ const EXACT_LIMIT = 16;
20
22
  * seed is constructor config and every optimizer here is deterministic given
21
23
  * one — comparing two entrants at a single seed compares two anecdotes. It also
22
24
  * leaves the task where the caller builds it, which is the only place the
23
- * optimizer-specific parts of it (`reflect`, `cache`, `onEvent`) are known.
25
+ * optimizer-specific parts of it (`reflect`, `cache`, `reporters`) are known.
24
26
  */
25
27
  async function compare(args) {
26
28
  const { entrants, seeds, concurrency = 1 } = args;
@@ -43,6 +45,8 @@ async function compare(args) {
43
45
  bestScore: result.bestScore,
44
46
  ...result.testScore === void 0 ? {} : { testScore: result.testScore },
45
47
  metricCalls: result.metricCalls,
48
+ cacheHits: result.cacheHits,
49
+ reflectionCalls: result.reflectionCalls ?? 0,
46
50
  costUsd: result.usage.costUsd,
47
51
  stopReason: result.stopReason
48
52
  };
@@ -53,17 +57,25 @@ async function compare(args) {
53
57
  runs: runs.filter((run) => run.entrant === entrant)
54
58
  }));
55
59
  const winner = summaries.reduce((best, summary) => summary.meanScore > best.meanScore ? summary : best);
60
+ const rawPValues = summaries.map((summary) => summary.entrant === winner.entrant ? void 0 : margin({
61
+ winner: winner.entrant,
62
+ entrant: summary.entrant,
63
+ runs,
64
+ seeds
65
+ }));
66
+ const holmAdjusted = holmAdjustSparse({
67
+ pValues: rawPValues,
68
+ familySize: names.length - 1
69
+ });
56
70
  return {
57
71
  winner: winner.entrant,
58
- summaries: summaries.map((summary) => summary.entrant === winner.entrant ? summary : {
72
+ summaries: summaries.map((summary, index) => ({
59
73
  ...summary,
60
- pValueVsWinner: margin({
61
- winner: winner.entrant,
62
- entrant: summary.entrant,
63
- runs,
64
- seeds
65
- })
66
- }),
74
+ ...rawPValues[index] === void 0 ? {} : {
75
+ pValueVsWinner: rawPValues[index],
76
+ pValueVsWinnerHolm: holmAdjusted[index]
77
+ }
78
+ })),
67
79
  runs
68
80
  };
69
81
  }
@@ -78,13 +90,28 @@ function summarize(args) {
78
90
  minScore: Math.min(...scores),
79
91
  maxScore: Math.max(...scores),
80
92
  meanMetricCalls: mean(runs.map((run) => run.metricCalls)),
81
- meanCostUsd: mean(runs.map((run) => run.costUsd))
93
+ meanCostUsd: mean(runs.map((run) => run.costUsd)),
94
+ meanCacheHits: mean(runs.map((run) => run.cacheHits)),
95
+ meanReflectionCalls: mean(runs.map((run) => run.reflectionCalls)),
96
+ distinctScores: new Set(scores).size
82
97
  };
83
98
  }
84
99
  /**
85
100
  * Paired across seeds rather than pooled: the same seed puts both entrants on
86
101
  * the same sampling order, so the difference at a seed is a comparison and the
87
102
  * spread between seeds is not.
103
+ *
104
+ * Returns `undefined` when every seed produced the exact same nonzero margin.
105
+ * That is not evidence of an n-seed-strong result — it is one realization the
106
+ * seed never varied, and a sign-flip p-value would report a precision from n
107
+ * independent trials that never happened. A margin of exactly zero every seed
108
+ * is not this case: `signFlipPValue` already reports that honestly as 1, no
109
+ * significance claimed either way, which is not a fabricated number.
110
+ *
111
+ * "Exact same" is judged within `DEGENERACY_TOLERANCE`, not `===`: subtracting
112
+ * two scores that are equal in substance can still land a few ULPs apart
113
+ * (0.95 - 0.55 and 0.9 - 0.5 differ at the 16th digit), and treating that as
114
+ * n real trials would be the same fabrication this check exists to prevent.
88
115
  */
89
116
  function margin(args) {
90
117
  const { winner, entrant, runs, seeds } = args;
@@ -99,6 +126,10 @@ function margin(args) {
99
126
  seed
100
127
  });
101
128
  });
129
+ const [first] = differences;
130
+ if (first !== void 0) {
131
+ if (differences.reduce((widest, difference) => Math.max(widest, Math.abs(difference - first)), 0) < DEGENERACY_TOLERANCE && Math.abs(first) > DEGENERACY_TOLERANCE) return;
132
+ }
102
133
  return require_math.signFlipPValue({
103
134
  differences,
104
135
  observed: differences.reduce((total, value) => total + value, 0),
@@ -109,6 +140,27 @@ function scoreOf(args) {
109
140
  const { runs, entrant, seed } = args;
110
141
  return runs.find((candidate) => candidate.entrant === entrant && candidate.seed === seed)?.score ?? 0;
111
142
  }
143
+ /**
144
+ * `holmAdjust` over the raw p-values that exist, skipping the slots a
145
+ * withheld comparison left `undefined` — those still count toward
146
+ * `familySize`, they just have nothing of their own to adjust.
147
+ */
148
+ function holmAdjustSparse(args) {
149
+ const { pValues, familySize } = args;
150
+ const present = pValues.map((pValue, index) => ({
151
+ pValue,
152
+ index
153
+ })).filter((entry) => entry.pValue !== void 0);
154
+ const adjusted = require_math.holmAdjust({
155
+ pValues: present.map((entry) => entry.pValue),
156
+ familySize
157
+ });
158
+ const result = new Array(pValues.length).fill(void 0);
159
+ present.forEach((entry, rank) => {
160
+ result[entry.index] = adjusted[rank];
161
+ });
162
+ return result;
163
+ }
112
164
  function mean(values) {
113
165
  return values.reduce((total, value) => total + value, 0) / values.length;
114
166
  }
@@ -119,6 +171,51 @@ function standardDeviation(values) {
119
171
  return Math.sqrt(variance);
120
172
  }
121
173
  //#endregion
174
+ //#region src/distillation.ts
175
+ const ROLES = /* @__PURE__ */ new Set([
176
+ "system",
177
+ "user",
178
+ "assistant"
179
+ ]);
180
+ /**
181
+ * Serialize harvested rollouts as JSONL, one training example per line.
182
+ *
183
+ * The chat-messages shape rather than any one vendor's: it is what Axolotl,
184
+ * Together, Fireworks and the Hugging Face trainers all ingest, and the
185
+ * providers that read it outlive the ones that do not.
186
+ *
187
+ * Returns the text rather than writing it. Only `file-cache` touches the
188
+ * filesystem, and a caller uploading this straight to a provider should not
189
+ * have to round-trip it through a file to do so.
190
+ */
191
+ function toTrainingJsonl(args) {
192
+ const { rollouts, render } = args;
193
+ const lines = [];
194
+ for (const [index, rollout] of rollouts.entries()) {
195
+ const example = render({
196
+ rollout,
197
+ index
198
+ });
199
+ if (example === null) continue;
200
+ assertUsable(example, index);
201
+ lines.push(JSON.stringify(example));
202
+ }
203
+ return lines.join("\n");
204
+ }
205
+ /**
206
+ * Checked here rather than left to the provider. A malformed line surfaces as
207
+ * a rejected upload hours later, naming a line number in a file the caller
208
+ * never wrote by hand; naming the rollout at the point it was rendered is the
209
+ * same error while it is still fixable.
210
+ */
211
+ function assertUsable(example, index) {
212
+ if (!Array.isArray(example.messages) || example.messages.length === 0) throw new Error(`rollout ${index} rendered no messages`);
213
+ for (const message of example.messages) {
214
+ if (!ROLES.has(message.role)) throw new Error(`rollout ${index} rendered the unknown role ${message.role}`);
215
+ if (typeof message.content !== "string") throw new Error(`rollout ${index} rendered non-text content`);
216
+ }
217
+ }
218
+ //#endregion
122
219
  //#region src/judge.ts
123
220
  const DEFAULT_SCALE = 5;
124
221
  const SCORE = /<score\s+name="([^"]+)"\s*>\s*([\d.]+)\s*<\/score>/g;
@@ -141,6 +238,11 @@ const FEEDBACK = /<feedback>([\s\S]*?)<\/feedback>/;
141
238
  function createJudge(args) {
142
239
  const { model, criteria, scale = DEFAULT_SCALE, renderInput = stringify, renderOutput = stringify, buildPrompt = buildJudgePrompt } = args;
143
240
  if (criteria.length === 0) throw new Error("createJudge requires at least one criterion");
241
+ if (!Number.isFinite(scale) || scale <= 0) throw new Error(`scale must be a positive number, received ${scale}`);
242
+ assertCriteria({
243
+ criteria,
244
+ scale
245
+ });
144
246
  return async ({ input, output, expected, signal }) => {
145
247
  return readVerdict({
146
248
  response: await model({
@@ -184,6 +286,7 @@ function buildJudgePrompt(args) {
184
286
  `Grade each criterion from 0 to ${scale}, where ${scale} is a perfect answer.`,
185
287
  "",
186
288
  "Then write feedback. It is read by a program that rewrites the system's instructions, not by a person reviewing this output, so say what the instructions should tell the system to do differently. Feedback about this particular answer is of no use to it.",
289
+ ...expected === void 0 ? [] : ["", "Do not restate the expected answer, or any fact drawn from it, in the feedback. The instruction it is rewritten into is reused on inputs whose answers you have not seen: a fact copied out of the expected answer becomes an answer key memorised in the prompt, which raises the score on this input and teaches the system nothing. Name the kind of thing the answer was missing, not the thing itself."],
187
290
  "",
188
291
  "Reply in exactly this format and nothing else:",
189
292
  ...criteria.map((criterion) => `<score name="${criterion.name}">…</score>`),
@@ -202,6 +305,9 @@ function readVerdict(args) {
202
305
  const graded = /* @__PURE__ */ new Map();
203
306
  for (const match of response.matchAll(SCORE)) graded.set(match[1], Number(match[2]));
204
307
  const objectiveScores = {};
308
+ let gated = false;
309
+ let weighted = 0;
310
+ let totalWeight = 0;
205
311
  for (const criterion of criteria) {
206
312
  const grade = graded.get(criterion.name);
207
313
  if (grade === void 0 || Number.isNaN(grade)) return {
@@ -209,15 +315,30 @@ function readVerdict(args) {
209
315
  feedback: `Judge did not grade "${criterion.name}".`,
210
316
  transient: true
211
317
  };
212
- objectiveScores[criterion.name] = clamp(grade / scale);
318
+ const normalized = clamp(grade / scale);
319
+ objectiveScores[criterion.name] = normalized;
320
+ if (criterion.gate !== void 0 && grade < criterion.gate) gated = true;
321
+ const weight = criterion.weight ?? 1;
322
+ weighted += normalized * weight;
323
+ totalWeight += weight;
213
324
  }
214
- const grades = Object.values(objectiveScores);
215
325
  return {
216
- score: grades.reduce((total, grade) => total + grade, 0) / grades.length,
326
+ score: gated ? 0 : weighted / totalWeight,
217
327
  feedback,
218
328
  objectiveScores
219
329
  };
220
330
  }
331
+ function assertCriteria(args) {
332
+ const { criteria, scale } = args;
333
+ let totalWeight = 0;
334
+ for (const { name, weight = 1, gate } of criteria) {
335
+ if (!Number.isFinite(weight) || weight < 0) throw new Error(`weight on criterion "${name}" must be a non-negative number, received ${weight}`);
336
+ totalWeight += weight;
337
+ if (gate !== void 0 && (!Number.isFinite(gate) || gate <= 0)) throw new Error(`gate on criterion "${name}" must be greater than 0, received ${gate}; no grade can fall below 0`);
338
+ if (gate !== void 0 && gate > scale) throw new Error(`gate on criterion "${name}" is ${gate}, above the scale of ${scale}; no grade can reach it`);
339
+ }
340
+ if (totalWeight <= 0) throw new Error("criteria weights must sum to more than 0; at least one criterion has to count towards the score");
341
+ }
221
342
  function stringify(value) {
222
343
  return typeof value === "string" ? value : JSON.stringify(value);
223
344
  }
@@ -243,24 +364,30 @@ function priceUsage(args) {
243
364
  };
244
365
  }
245
366
  //#endregion
246
- exports.BudgetExhausted = require_evaluation.BudgetExhausted;
247
- exports.assertResumable = require_evaluation.assertResumable;
248
- exports.bootstrapDemos = require_demos.bootstrapDemos;
367
+ exports.BudgetExhausted = require_warnings.BudgetExhausted;
368
+ exports.assertResumable = require_warnings.assertResumable;
249
369
  exports.buildJudgePrompt = buildJudgePrompt;
250
- exports.candidateFingerprint = require_evaluation.candidateFingerprint;
370
+ exports.candidateFingerprint = require_warnings.candidateFingerprint;
251
371
  exports.compare = compare;
252
- exports.componentNames = require_evaluation.componentNames;
253
- exports.costExhausted = require_evaluation.costExhausted;
254
- exports.createBudget = require_evaluation.createBudget;
255
- exports.createDeadline = require_evaluation.createDeadline;
256
- exports.createEvaluator = require_evaluation.createEvaluator;
372
+ exports.componentNames = require_warnings.componentNames;
373
+ exports.costExhausted = require_warnings.costExhausted;
374
+ exports.createBudget = require_warnings.createBudget;
375
+ exports.createDeadline = require_warnings.createDeadline;
376
+ exports.createEvaluator = require_warnings.createEvaluator;
257
377
  exports.createJudge = createJudge;
258
- exports.createMemoryCache = require_evaluation.createMemoryCache;
378
+ exports.createMemoryCache = require_warnings.createMemoryCache;
259
379
  exports.formatDemos = require_demos.formatDemos;
380
+ exports.harvestFewShotExamples = require_demos.harvestFewShotExamples;
381
+ exports.harvestRollouts = require_demos.harvestRollouts;
382
+ exports.isCandidateAccepted = require_warnings.isCandidateAccepted;
383
+ exports.isRunFinished = require_warnings.isRunFinished;
260
384
  exports.mapWithConcurrency = require_concurrency.mapWithConcurrency;
261
- exports.measuredMean = require_evaluation.measuredMean;
385
+ exports.measuredMean = require_warnings.measuredMean;
262
386
  exports.parseDemos = require_demos.parseDemos;
263
387
  exports.parseProposedText = require_text.parseProposedText;
264
388
  exports.priceUsage = priceUsage;
265
- exports.requireMeasuredMean = require_evaluation.requireMeasuredMean;
266
- exports.runFingerprint = require_evaluation.runFingerprint;
389
+ exports.requireMeasuredMean = require_warnings.requireMeasuredMean;
390
+ exports.resolveValidationSet = require_warnings.resolveValidationSet;
391
+ exports.runFingerprint = require_warnings.runFingerprint;
392
+ exports.seedScoreWarnings = require_warnings.seedScoreWarnings;
393
+ exports.toTrainingJsonl = toTrainingJsonl;