textopt 0.1.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 (56) hide show
  1. package/README.md +24 -6
  2. package/dist/bootstrap-search/index.cjs +29 -23
  3. package/dist/bootstrap-search/index.d.cts +3 -3
  4. package/dist/bootstrap-search/index.d.mts +3 -3
  5. package/dist/bootstrap-search/index.mjs +9 -3
  6. package/dist/{demos-D2o0qSSo.cjs → demos-9v5ts7F3.cjs} +4 -4
  7. package/dist/{demos-1b7JiUue.d.mts → demos-ASsSXYXA.d.mts} +1 -1
  8. package/dist/{demos-DE2oxNWX.mjs → demos-Brobjfuc.mjs} +1 -1
  9. package/dist/{demos-CU9dy2oT.d.cts → demos-ByaLZy-Z.d.cts} +1 -1
  10. package/dist/file-cache.cjs +16 -5
  11. package/dist/file-cache.d.cts +13 -0
  12. package/dist/file-cache.d.mts +13 -0
  13. package/dist/file-cache.mjs +16 -5
  14. package/dist/gepa/index.cjs +87 -44
  15. package/dist/gepa/index.d.cts +8 -6
  16. package/dist/gepa/index.d.mts +8 -6
  17. package/dist/gepa/index.mjs +55 -12
  18. package/dist/index.cjs +44 -19
  19. package/dist/index.d.cts +37 -5
  20. package/dist/index.d.mts +37 -5
  21. package/dist/index.mjs +29 -6
  22. package/dist/mipro/index.cjs +31 -25
  23. package/dist/mipro/index.d.cts +3 -3
  24. package/dist/mipro/index.d.mts +3 -3
  25. package/dist/mipro/index.mjs +9 -3
  26. package/dist/opro/index.cjs +31 -25
  27. package/dist/opro/index.d.cts +2 -2
  28. package/dist/opro/index.d.mts +2 -2
  29. package/dist/opro/index.mjs +8 -2
  30. package/dist/{optimizer-Bh5DPRMH.d.cts → optimizer-4Zv-Zt2t.d.cts} +41 -2
  31. package/dist/{optimizer-Ck6-e_8o.d.mts → optimizer-Ds5mzYjz.d.mts} +41 -2
  32. package/dist/random-search/index.cjs +27 -21
  33. package/dist/random-search/index.d.cts +2 -2
  34. package/dist/random-search/index.d.mts +2 -2
  35. package/dist/random-search/index.mjs +8 -2
  36. package/dist/{reflection-LRaAZP4e.d.mts → reflection-CMezGu6u.d.mts} +32 -4
  37. package/dist/{reflection-Dt3QrXhM.d.cts → reflection-D0A7eahD.d.cts} +32 -4
  38. package/dist/{reporting-DQbAohc9.d.cts → reporting-bq007_2z.d.cts} +55 -1
  39. package/dist/{reporting-DQbAohc9.d.mts → reporting-bq007_2z.d.mts} +55 -1
  40. package/dist/simba/index.cjs +31 -25
  41. package/dist/simba/index.d.cts +24 -5
  42. package/dist/simba/index.d.mts +24 -5
  43. package/dist/simba/index.mjs +9 -3
  44. package/dist/testing.d.cts +2 -2
  45. package/dist/testing.d.mts +2 -2
  46. package/dist/{reporting-CNHzbJC-.cjs → warnings-CWRJF-jA.cjs} +63 -0
  47. package/dist/{reporting-DY-DC4HG.mjs → warnings-OxvDi9kN.mjs} +52 -1
  48. package/docs/adapters.md +169 -0
  49. package/docs/benchmark.md +90 -0
  50. package/docs/data-prep.md +113 -0
  51. package/docs/distillation.md +128 -0
  52. package/docs/evaluation.md +87 -0
  53. package/docs/metric-preflight.md +132 -0
  54. package/docs/optimizers.md +293 -0
  55. package/docs/tuning.md +130 -0
  56. package/package.json +3 -2
package/README.md CHANGED
@@ -4,6 +4,8 @@ Core interfaces and optimizers for textopt.
4
4
 
5
5
  This package has no runtime dependencies. For an overview of the algorithms and guidance on choosing one, see the [project README](https://github.com/ctdio/textopt#readme).
6
6
 
7
+ The long-form guides ship with the package, under `docs/`, so an installed copy documents the version installed rather than whatever `main` has become. Agent-facing guidance lives there and in the doc comments on the API itself.
8
+
7
9
  ## Entry points
8
10
 
9
11
  | Import | Contains |
@@ -95,7 +97,7 @@ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Outp
95
97
 
96
98
  **`transient`** marks scores caused by infrastructure failures such as rate limits, 5xx responses, or network errors. Transient scores are not cached.
97
99
 
98
- **`Optimizer<Stop extends string>`** defines `optimize(task: OptimizerTask) => Promise<OptimizerResult>`. `OptimizerTask` contains the shared run inputs: `seedCandidate`, `trainingSet`, `validationSet`, `testSet`, `adapter`, `maxMetricCalls`, `maxCostUsd`, `maxWallClockMs`, `cacheNamespace`, `retry`, and `signal`. `OptimizerResult` contains `bestCandidate`, `bestScore`, `bestOutputs`, `metricCalls`, `usage`, `testScore`, `testMetricCalls`, `testUsage`, and `stopReason`. Optimizer-specific task and result types extend these interfaces.
100
+ **`Optimizer<Stop extends string>`** defines `optimize(task: OptimizerTask) => Promise<OptimizerResult>`. `OptimizerTask` contains the shared run inputs: `seedCandidate`, `trainingSet`, `validationSet`, `testSet`, `adapter`, `maxMetricCalls`, `maxCostUsd`, `maxWallClockMs`, `cacheNamespace`, `retry`, and `signal`. `OptimizerResult` contains `bestCandidate`, `bestScore`, `bestOutputs`, `metricCalls`, `usage`, `testScore`, `testMetricCalls`, `testUsage`, `warnings`, and `stopReason`. Optimizer-specific task and result types extend these interfaces.
99
101
 
100
102
  **`maxCostUsd`** and **`maxWallClockMs`** are checked between evaluations, so a run overruns by whatever it had in flight when the ceiling was reached — one evaluation at the default concurrency, and up to `concurrency` of them above it. Neither bounds the held-out sweep, which runs once the search has already stopped: it is reported apart from the search as `testMetricCalls` and `testUsage`, and has to be budgeted for separately. Neither follows from `maxMetricCalls`: reflective search grows the text it optimizes, so late rollouts cost more than early ones, and a run behind a rate limit spends almost nothing while taking as long as the provider makes it take.
101
103
 
@@ -107,6 +109,10 @@ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Outp
107
109
 
108
110
  **`testSet`** is excluded from search and evaluated once against the winner. Because candidates are selected on `validationSet`, `bestScore` may be fitted to it. `testScore` measures held-out performance. Test rollouts are reported as `testMetricCalls` costing `testUsage`, outside `maxMetricCalls` and `maxCostUsd` both.
109
111
 
112
+ **`validationSet`** defaults to `trainingSet`, and a run that took that default reports a `validationSetReusesTraining` warning: the search selected candidates on the instances reflection read. Pass `validationSet: "reuseTraining"` to accept it by name and silence the warning.
113
+
114
+ **`warnings`** is a `RunWarning[]` of what a run cannot say about itself from its own numbers — selection that reused the training instances, a seed the metric scored identically on every instance. Never fatal, always present (empty when there is nothing to say), and repeated on the `finish` event so a reporter records them next to the score.
115
+
110
116
  **`TextModel`** is the provider-independent interface `({ prompt, signal }) => Promise<string>`.
111
117
 
112
118
  ### Values
@@ -121,7 +127,7 @@ For Redis, SQLite, or file-backed caching, implement **`EvaluationCache`** with
121
127
 
122
128
  **`createEvaluator({ adapter, budget, cache, cacheNamespace, retry, trackOutputs, onEvaluation, signal, cacheHits, usage })`** handles adapter calls, caching, budget accounting, transient scores, and evaluation events. `cacheHits` and `usage` seed the counters from a checkpoint, so a resumed run reports totals rather than deltas. `usage()` covers the charged rollouts a ceiling is checked against; `unchargedUsage()` covers what `charge: false` bought. `evaluate` returns a `ScoredBatch`. `evaluateTraced` returns an `EvaluationBatch`, or `null` when the remaining budget cannot cover the batch. A batch that exceeds the charged budget throws `BudgetExhausted`. All included optimizers use this evaluator.
123
129
 
124
- **`harvestRollouts({ adapter, candidate, data, minScore, maxRollouts, batchSize, maxMetricCalls, maxCostUsd, rng, signal })`** runs a candidate over `data` and returns the `Rollout`s the metric rewarded, alongside `metricCalls` and `attempted`. Omit `minScore` to keep any rollout scoring above zero; omit `maxRollouts` to sweep the whole pool. It carries its own budget and does not use the score cache, because it needs the outputs a cache hit cannot return. `maxCostUsd` bounds its dollars, checked between batches — a caller bounding spend cannot bound this pass from outside, since it runs on its own evaluator. Sweeping a validation set is the mistake to avoid — see [Distilling a run](../../docs/distillation.md).
130
+ **`harvestRollouts({ adapter, candidate, data, minScore, maxRollouts, batchSize, maxMetricCalls, maxCostUsd, rng, signal })`** runs a candidate over `data` and returns the `Rollout`s the metric rewarded, alongside `metricCalls` and `attempted`. Omit `minScore` to keep any rollout scoring above zero; omit `maxRollouts` to sweep the whole pool. It carries its own budget and does not use the score cache, because it needs the outputs a cache hit cannot return. `maxCostUsd` bounds its dollars, checked between batches — a caller bounding spend cannot bound this pass from outside, since it runs on its own evaluator. Sweeping a validation set is the mistake to avoid — see [Distilling a run](./docs/distillation.md).
125
131
 
126
132
  **`toTrainingJsonl({ rollouts, render })`** serializes harvested rollouts as one chat-messages example per line. `render` turns a rollout into `{ messages }` or returns `null` to skip it, and decides how much of the optimized candidate stays in the training input. Returns the text; writing it is the caller's job.
127
133
 
@@ -131,7 +137,11 @@ For Redis, SQLite, or file-backed caching, implement **`EvaluationCache`** with
131
137
 
132
138
  **`parseProposedText(text)`** extracts a proposal from a reflection response, including responses with fenced blocks or surrounding commentary.
133
139
 
134
- **`createJudge({ model, criteria, scale = 5, renderInput, renderOutput, buildPrompt })`** returns a `Judge<Datum, Output>`: `({ input, output, expected, signal }) => Promise<ScoreResult>`. Each `JudgeCriterion` is graded on a small integer scale and normalized; per-criterion values are returned as `objectiveScores` and the aggregate `score` is their mean. A criterion the judge failed to grade returns a transient score rather than a zero, so the instance is retried instead of recorded as a failure. **`buildJudgePrompt`** is the default template and implements `JudgePromptBuilder`.
140
+ **`createJudge({ model, criteria, scale = 5, renderInput, renderOutput, buildPrompt })`** returns a `Judge<Datum, Output>`: `({ input, output, expected, signal }) => Promise<ScoreResult>`. Each `JudgeCriterion` is graded on a small integer scale and normalized; per-criterion values are returned as `objectiveScores` and the aggregate `score` is their weighted mean. A criterion the judge failed to grade returns a transient score rather than a zero, so the instance is retried instead of recorded as a failure. **`buildJudgePrompt`** is the default template and implements `JudgePromptBuilder`.
141
+
142
+ A `JudgeCriterion` takes `weight` (default 1; 0 reports the criterion as an objective without letting it move the score) and `gate`. **`gate`** is the grade, on the judge's own scale, that a criterion must reach for the instance to score at all — below it the instance scores 0 whatever the rest said, and the per-criterion objectives still report which one failed it. A mean lets a search trade a hard requirement away: a candidate that tanks one non-negotiable criterion and aces three cosmetic ones outranks the incumbent that kept the rule. Anything you would not ship without is a gate rather than a term in the average.
143
+
144
+ When `expected` is passed, the default prompt forbids the feedback from restating it. Feedback is rewritten into a reusable instruction, so "the instruction never says to state the thirty-day refund window" is addressed to the instructions as asked and copies the answer key into the prompt.
135
145
 
136
146
  **`compare({ entrants, seeds, concurrency = 1 })`** runs each entrant over every seed and returns a `Comparison` of `winner`, `summaries`, and `runs`. Entrants are `({ seed }) => Promise<OptimizerResult>`, so the caller builds the optimizer-specific task. Ranking is on `testScore` where a run reports one, because the validation score is the number the search selected against. Each `ComparisonSummary` carries `meanScore`, `sdScore`, `minScore`, `maxScore`, `meanMetricCalls`, `meanCostUsd`, and a paired sign-flip `pValueVsWinner`.
137
147
 
@@ -222,11 +232,14 @@ TypeScript infers component names and the datum type from `seedCandidate` and `t
222
232
  Extends `Adapter` with what reflection needs:
223
233
 
224
234
  ```ts
235
+ evaluate(args: EvaluateArgs<Datum, K>): ReflectiveBatch<Trajectory, Output>
225
236
  makeReflectiveDataset(args: MakeReflectiveDatasetArgs<Datum, Trajectory, Output, K>): ReflectiveDataset<K>
226
237
  proposeNewTexts?(args: ProposeArgs<K>): ComponentPatch<K> // optional
227
238
  ```
228
239
 
229
- Both methods may be synchronous or asynchronous. `ReflectiveDataset` is a partial map from component names to `ReflectiveRecord[]`. Each record contains `inputs`, `generatedOutputs`, `feedback`, `score`, and a typed `evidence` field. Adapters only need to return records for the requested components.
240
+ A `ReflectiveBatch` is an `EvaluationBatch` with `feedback` required. It is optional on the shared type because the searches that never reflect have no use for it; here it is the input to the whole method. An adapter that returns scores and no prose reduces every rollout to a number, and reflection then rewrites the instruction from a prompt whose feedback blocks are empty — a run that spends its whole budget, reports a normal-looking `stopReason`, and has been doing blind search. Nothing in the result distinguishes that from a hard task, so it is a type error instead.
241
+
242
+ All three methods may be synchronous or asynchronous. `ReflectiveDataset` is a partial map from component names to `ReflectiveRecord[]`. Each record contains `inputs`, `generatedOutputs`, `feedback`, `score`, and a typed `evidence` field. Adapters only need to return records for the requested components.
230
243
 
231
244
  When `proposeNewTexts` is implemented, the adapter generates proposals without calling `reflect`. The task type still requires `reflect`, so offline runs can pass a stub.
232
245
 
@@ -248,7 +261,7 @@ When `proposeNewTexts` is implemented, the adapter generates proposals without c
248
261
 
249
262
  Each export is a factory. Selector and acceptance interfaces accept custom functions. A `ValEvaluationPolicy` is an object with `selectInstances` and `bestCandidate` methods.
250
263
 
251
- `pairedPermutationAcceptance` and `lowerBoundEvaluationPolicy` exist for metrics whose readings vary between runs of the same text. Both are strictly more conservative than the defaults. In the twenty-seed benchmark the pair scores 0.931 against plain GEPA's 0.920 on the noisy task and 0.945 against 0.947 on the noiseless one, neither gap significant over those seeds — conservative enough to be worth having on a metric that varies, cheap enough not to hurt on one that does not. A sign-flip test also needs a wide enough minibatch to say anything — over three instances the smallest p-value it can produce is 0.125, so at the default `minibatchSize` no proposal clears an `alpha` below that.
264
+ `pairedPermutationAcceptance` and `lowerBoundEvaluationPolicy` exist for metrics whose readings vary between runs of the same text. Both are strictly more conservative than the defaults. In the twenty-seed benchmark the pair scores 0.931 against plain GEPA's 0.920 on the noisy task and 0.945 against 0.947 on the noiseless one, neither gap significant over those seeds — conservative enough to be worth having on a metric that varies, cheap enough not to hurt on one that does not. A sign-flip test also needs a wide enough minibatch to say anything — over three instances the smallest p-value it can produce is 0.125, so at the default `minibatchSize` no proposal clears an `alpha` below that. `GepaOptimizer` refuses that combination at construction rather than letting the run discover it: an acceptance policy reports the smallest batch it could ever accept on as `minimumPairs`, and a `minibatchSize` below it throws.
252
265
 
253
266
  ### Reflection prompts
254
267
 
@@ -496,13 +509,18 @@ The paraphrase prompt receives no score or feedback. Compare random search with
496
509
  ```ts
497
510
  import { createFileCache } from "textopt/file-cache";
498
511
 
499
- const cache = createFileCache({ path: ".textopt/scores.jsonl" });
512
+ const cache = createFileCache({
513
+ path: ".textopt/scores.jsonl",
514
+ namespace: "gpt-4o-mini@t0/scorer-v3",
515
+ });
500
516
  ```
501
517
 
502
518
  An `EvaluationCache` that outlives the process, as an append-only JSONL log. A long run against a real provider is measured in hours and dollars, and an in-memory cache throws all of it away when the run ends.
503
519
 
504
520
  Append-only rather than rewritten: a score is never invalidated, because the key names the candidate, the instance, and the environment — and a log survives a process killed mid-write, which a file rewritten in place does not. A record that does not parse is dropped rather than fatal, since the last line of an interrupted log is routinely half-written. Later records win, so a re-measured instance replaces its earlier reading.
505
521
 
522
+ `namespace` is required and scopes every key to the system the scores measure — model id, decoding settings, scorer version. It is not optional the way `cacheNamespace` is, because this log outlives every part of that system a key does not name: an alias the provider upgraded under you, an edited scorer. Entries written under one namespace are never served to a run under another. Change it whenever anything outside the candidate text changes.
523
+
506
524
  `maxEntries` (default 1,000,000) bounds what is held in memory; the file itself is never trimmed. `entries()` is deliberately absent — it exists so a checkpoint can carry scores that would otherwise be lost, and these are already on disk.
507
525
 
508
526
  This is the only entry point that imports `node:fs`.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_reporting = require("../reporting-CNHzbJC-.cjs");
3
- const require_demos = require("../demos-D2o0qSSo.cjs");
2
+ const require_warnings = require("../warnings-CWRJF-jA.cjs");
3
+ const require_demos = require("../demos-9v5ts7F3.cjs");
4
4
  const require_rng = require("../rng-DbA_rPIo.cjs");
5
5
  //#region src/bootstrap-search/optimize.ts
6
6
  const DEFAULT_CANDIDATES = 16;
@@ -44,17 +44,21 @@ var BootstrapSearchOptimizer = class {
44
44
  task
45
45
  });
46
46
  } finally {
47
- await require_reporting.flushReporters(task.reporters ?? []);
47
+ await require_warnings.flushReporters(task.reporters ?? []);
48
48
  }
49
49
  }
50
50
  };
51
51
  async function run(args) {
52
52
  const { config, task } = args;
53
53
  const { candidates: shuffledHarvests = DEFAULT_CANDIDATES, maxDemos = DEFAULT_MAX_DEMOS, minDemos = DEFAULT_MIN_DEMOS, maxLabeledDemos = DEFAULT_MAX_LABELED_DEMOS, demoMinScore, stopAtScore, concurrency = 1, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
54
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, demoComponents, renderDemo, goldOutput, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = require_reporting.defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
55
- const emit = require_reporting.createEmitter(reporters);
56
- const deadline = require_reporting.createDeadline({ maxWallClockMs });
57
- const components = require_reporting.componentNames(seedCandidate);
54
+ const { seedCandidate, trainingSet, validationSet: requestedValidationSet, testSet, adapter, demoComponents, renderDemo, goldOutput, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = require_warnings.defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
55
+ const { validationSet, warnings } = require_warnings.resolveValidationSet({
56
+ validationSet: requestedValidationSet,
57
+ trainingSet
58
+ });
59
+ const emit = require_warnings.createEmitter(reporters);
60
+ const deadline = require_warnings.createDeadline({ maxWallClockMs });
61
+ const components = require_warnings.componentNames(seedCandidate);
58
62
  if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
59
63
  if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
60
64
  if (demoComponents.length === 0) throw new Error("optimize requires at least one demoComponent: this search has nothing to put demonstrations in otherwise");
@@ -67,7 +71,7 @@ async function run(args) {
67
71
  datum,
68
72
  index
69
73
  })) ?? [];
70
- const fingerprint = require_reporting.runFingerprint({
74
+ const fingerprint = require_warnings.runFingerprint({
71
75
  seedCandidate,
72
76
  trainingIds: trainingSet.map((datum, index) => instanceId({
73
77
  datum,
@@ -77,17 +81,17 @@ async function run(args) {
77
81
  seed,
78
82
  ...cacheNamespace === void 0 ? {} : { cacheNamespace }
79
83
  });
80
- require_reporting.assertResumable({
84
+ require_warnings.assertResumable({
81
85
  fingerprint,
82
86
  ...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
83
87
  });
84
88
  const rng = require_rng.createSeededRng(seed, resumeFrom?.rngState);
85
- const budget = require_reporting.createBudget({
89
+ const budget = require_warnings.createBudget({
86
90
  maxMetricCalls,
87
91
  spent: resumeFrom?.metricCalls ?? 0
88
92
  });
89
- const evaluationCache = cache === false ? void 0 : cache ?? require_reporting.createMemoryCache();
90
- const evaluator = require_reporting.createEvaluator({
93
+ const evaluationCache = cache === false ? void 0 : cache ?? require_warnings.createMemoryCache();
94
+ const evaluator = require_warnings.createEvaluator({
91
95
  adapter,
92
96
  budget,
93
97
  ...retry === void 0 ? {} : { retry },
@@ -124,7 +128,7 @@ async function run(args) {
124
128
  });
125
129
  }
126
130
  const seedEvaluation = resumeFrom === void 0 ? await sweep(seedCandidate, "seed") : void 0;
127
- const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : require_reporting.requireMeasuredMean({
131
+ const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : require_warnings.requireMeasuredMean({
128
132
  batch: seedEvaluation,
129
133
  phase: "seed"
130
134
  });
@@ -139,7 +143,7 @@ async function run(args) {
139
143
  candidateId: 0,
140
144
  candidate: seedCandidate,
141
145
  aggregateScore: seedScore,
142
- instanceScores: require_reporting.instanceRow(seedEvaluation),
146
+ instanceScores: require_warnings.instanceRow(seedEvaluation),
143
147
  ...trackBestOutputs ? { outputs: seedEvaluation.outputs } : {}
144
148
  });
145
149
  function takeSnapshot() {
@@ -175,7 +179,7 @@ async function run(args) {
175
179
  stopReason = "aborted";
176
180
  break;
177
181
  }
178
- if (require_reporting.costExhausted({
182
+ if (require_warnings.costExhausted({
179
183
  usage: evaluator.usage(),
180
184
  maxCostUsd
181
185
  })) {
@@ -197,7 +201,7 @@ async function run(args) {
197
201
  const source = plan[drawn + wave.length];
198
202
  const block = await buildBlock(source);
199
203
  const candidate = withDemos(block);
200
- const key = require_reporting.candidateHash(candidate);
204
+ const key = require_warnings.candidateHash(candidate);
201
205
  const prior = inFlight.get(key);
202
206
  const sweeping = prior === void 0 ? settled(sweep(candidate, "validation")) : prior.then(() => settled(sweep(candidate, "validation")));
203
207
  inFlight.set(key, sweeping);
@@ -216,7 +220,7 @@ async function run(args) {
216
220
  drawn += 1;
217
221
  if (outcome.failed === true) {
218
222
  draining = true;
219
- if (outcome.err instanceof require_reporting.BudgetExhausted) {
223
+ if (outcome.err instanceof require_warnings.BudgetExhausted) {
220
224
  waveStop = "budgetExhausted";
221
225
  continue;
222
226
  }
@@ -228,7 +232,7 @@ async function run(args) {
228
232
  continue;
229
233
  }
230
234
  const evaluation = outcome.evaluation;
231
- const score = require_reporting.measuredMean(evaluation);
235
+ const score = require_warnings.measuredMean(evaluation);
232
236
  if (score === void 0) continue;
233
237
  const accepted = score > bestScore;
234
238
  evaluated.push({
@@ -254,7 +258,7 @@ async function run(args) {
254
258
  candidateId: acceptedCandidates,
255
259
  candidate: entry.candidate,
256
260
  aggregateScore: score,
257
- instanceScores: require_reporting.instanceRow(evaluation),
261
+ instanceScores: require_warnings.instanceRow(evaluation),
258
262
  ...trackBestOutputs ? { outputs: evaluation.outputs } : {}
259
263
  });
260
264
  best = entry.candidate;
@@ -284,15 +288,16 @@ async function run(args) {
284
288
  iteration: evaluated.length,
285
289
  charge: false
286
290
  });
287
- const testScore = heldOut === void 0 ? void 0 : require_reporting.measuredMean(heldOut);
291
+ const testScore = heldOut === void 0 ? void 0 : require_warnings.measuredMean(heldOut);
288
292
  emit({
289
293
  type: "finish",
290
294
  reason: stopReason,
295
+ warnings,
291
296
  bestCandidateId: acceptedCandidates,
292
297
  bestScore,
293
298
  metricCalls: budget.spent(),
294
299
  ...testScore === void 0 ? {} : { testScore },
295
- ...heldOut === void 0 ? {} : { testInstanceScores: require_reporting.instanceRow(heldOut) },
300
+ ...heldOut === void 0 ? {} : { testInstanceScores: require_warnings.instanceRow(heldOut) },
296
301
  ...heldOut === void 0 || !trackBestOutputs ? {} : { testOutputs: heldOut.outputs }
297
302
  });
298
303
  return {
@@ -311,6 +316,7 @@ async function run(args) {
311
316
  testMetricCalls: evaluator.unchargedCalls(),
312
317
  testUsage: evaluator.unchargedUsage()
313
318
  },
319
+ warnings,
314
320
  stopReason
315
321
  };
316
322
  function withDemos(block) {
@@ -384,5 +390,5 @@ function countDemos(block) {
384
390
  }
385
391
  //#endregion
386
392
  exports.BootstrapSearchOptimizer = BootstrapSearchOptimizer;
387
- exports.isCandidateAccepted = require_reporting.isCandidateAccepted;
388
- exports.isRunFinished = require_reporting.isRunFinished;
393
+ exports.isCandidateAccepted = require_warnings.isCandidateAccepted;
394
+ exports.isRunFinished = require_warnings.isRunFinished;
@@ -1,7 +1,7 @@
1
- import { a as RunFinished, c as Adapter, i as Reporter, l as Candidate, n as OptimizerEvent, o as isCandidateAccepted, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted, v as UsageTotals } from "../reporting-DQbAohc9.cjs";
1
+ import { S as UsageTotals, a as RunFinished, f as Adapter, i as Reporter, n as OptimizerEvent, o as isCandidateAccepted, p as Candidate, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted } from "../reporting-bq007_2z.cjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.cjs";
3
- import { r as DemoRenderer } from "../demos-CU9dy2oT.cjs";
4
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-Bh5DPRMH.cjs";
3
+ import { r as DemoRenderer } from "../demos-ByaLZy-Z.cjs";
4
+ import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-4Zv-Zt2t.cjs";
5
5
  //#region src/bootstrap-search/optimize.d.ts
6
6
  /**
7
7
  * Where a candidate's demo block came from. `zeroShot` holds no demos at all,
@@ -1,7 +1,7 @@
1
- import { a as RunFinished, c as Adapter, i as Reporter, l as Candidate, n as OptimizerEvent, o as isCandidateAccepted, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted, v as UsageTotals } from "../reporting-DQbAohc9.mjs";
1
+ import { S as UsageTotals, a as RunFinished, f as Adapter, i as Reporter, n as OptimizerEvent, o as isCandidateAccepted, p as Candidate, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted } from "../reporting-bq007_2z.mjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.mjs";
3
- import { r as DemoRenderer } from "../demos-1b7JiUue.mjs";
4
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-Ck6-e_8o.mjs";
3
+ import { r as DemoRenderer } from "../demos-ASsSXYXA.mjs";
4
+ import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-Ds5mzYjz.mjs";
5
5
  //#region src/bootstrap-search/optimize.d.ts
6
6
  /**
7
7
  * Where a candidate's demo block came from. `zeroShot` holds no demos at all,
@@ -1,5 +1,5 @@
1
- import { _ as createMemoryCache, a as isRunFinished, b as createBudget, c as createEvaluator, d as createDeadline, f as assertResumable, g as candidateHash, h as componentNames, i as isCandidateAccepted, l as measuredMean, m as runFingerprint, n as flushReporters, o as BudgetExhausted, r as instanceRow, s as costExhausted, t as createEmitter, u as requireMeasuredMean, v as defaultInstanceId } from "../reporting-DY-DC4HG.mjs";
2
- import { n as harvestFewShotExamples, t as formatDemos } from "../demos-DE2oxNWX.mjs";
1
+ import { S as createBudget, _ as componentNames, a as instanceRow, b as defaultInstanceId, c as BudgetExhausted, d as measuredMean, f as requireMeasuredMean, g as runFingerprint, i as flushReporters, l as costExhausted, m as assertResumable, o as isCandidateAccepted, p as createDeadline, r as createEmitter, s as isRunFinished, t as resolveValidationSet, u as createEvaluator, v as candidateHash, y as createMemoryCache } from "../warnings-OxvDi9kN.mjs";
2
+ import { n as harvestFewShotExamples, t as formatDemos } from "../demos-Brobjfuc.mjs";
3
3
  import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
4
4
  //#region src/bootstrap-search/optimize.ts
5
5
  const DEFAULT_CANDIDATES = 16;
@@ -50,7 +50,11 @@ var BootstrapSearchOptimizer = class {
50
50
  async function run(args) {
51
51
  const { config, task } = args;
52
52
  const { candidates: shuffledHarvests = DEFAULT_CANDIDATES, maxDemos = DEFAULT_MAX_DEMOS, minDemos = DEFAULT_MIN_DEMOS, maxLabeledDemos = DEFAULT_MAX_LABELED_DEMOS, demoMinScore, stopAtScore, concurrency = 1, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
53
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, demoComponents, renderDemo, goldOutput, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
53
+ const { seedCandidate, trainingSet, validationSet: requestedValidationSet, testSet, adapter, demoComponents, renderDemo, goldOutput, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
54
+ const { validationSet, warnings } = resolveValidationSet({
55
+ validationSet: requestedValidationSet,
56
+ trainingSet
57
+ });
54
58
  const emit = createEmitter(reporters);
55
59
  const deadline = createDeadline({ maxWallClockMs });
56
60
  const components = componentNames(seedCandidate);
@@ -287,6 +291,7 @@ async function run(args) {
287
291
  emit({
288
292
  type: "finish",
289
293
  reason: stopReason,
294
+ warnings,
290
295
  bestCandidateId: acceptedCandidates,
291
296
  bestScore,
292
297
  metricCalls: budget.spent(),
@@ -310,6 +315,7 @@ async function run(args) {
310
315
  testMetricCalls: evaluator.unchargedCalls(),
311
316
  testUsage: evaluator.unchargedUsage()
312
317
  },
318
+ warnings,
313
319
  stopReason
314
320
  };
315
321
  function withDemos(block) {
@@ -1,4 +1,4 @@
1
- const require_reporting = require("./reporting-CNHzbJC-.cjs");
1
+ const require_warnings = require("./warnings-CWRJF-jA.cjs");
2
2
  //#region src/harvest.ts
3
3
  /**
4
4
  * Run a candidate over data and keep the rollouts the metric rewarded.
@@ -17,8 +17,8 @@ const require_reporting = require("./reporting-CNHzbJC-.cjs");
17
17
  async function harvestRollouts(args) {
18
18
  const { adapter, candidate, data, minScore, maxRollouts = Number.POSITIVE_INFINITY, batchSize = Math.min(maxRollouts, data.length), maxMetricCalls = data.length, maxCostUsd, rng, signal } = args;
19
19
  if (data.length === 0) throw new Error("harvestRollouts requires non-empty data");
20
- const budget = require_reporting.createBudget({ maxMetricCalls });
21
- const evaluator = require_reporting.createEvaluator({
20
+ const budget = require_warnings.createBudget({ maxMetricCalls });
21
+ const evaluator = require_warnings.createEvaluator({
22
22
  adapter,
23
23
  budget,
24
24
  ...signal === void 0 ? {} : { signal }
@@ -28,7 +28,7 @@ async function harvestRollouts(args) {
28
28
  let attempted = 0;
29
29
  for (let start = 0; start < order.length; start += batchSize) {
30
30
  if (rollouts.length >= maxRollouts || signal?.aborted) break;
31
- if (require_reporting.costExhausted({
31
+ if (require_warnings.costExhausted({
32
32
  usage: evaluator.usage(),
33
33
  maxCostUsd
34
34
  })) break;
@@ -1,4 +1,4 @@
1
- import { c as Adapter, l as Candidate, v as UsageTotals } from "./reporting-DQbAohc9.mjs";
1
+ import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.mjs";
2
2
  import { t as Rng } from "./rng-BR5MOedA.mjs";
3
3
  //#region src/demos.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as createBudget, c as createEvaluator, s as costExhausted } from "./reporting-DY-DC4HG.mjs";
1
+ import { S as createBudget, l as costExhausted, u as createEvaluator } from "./warnings-OxvDi9kN.mjs";
2
2
  //#region src/harvest.ts
3
3
  /**
4
4
  * Run a candidate over data and keep the rollouts the metric rewarded.
@@ -1,4 +1,4 @@
1
- import { c as Adapter, l as Candidate, v as UsageTotals } from "./reporting-DQbAohc9.cjs";
1
+ import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.cjs";
2
2
  import { t as Rng } from "./rng-BR5MOedA.cjs";
3
3
  //#region src/demos.d.ts
4
4
  /**
@@ -13,22 +13,33 @@ let node_path = require("node:path");
13
13
  * Append-only rather than rewritten: a score is never invalidated (the key
14
14
  * names the candidate, the instance, and the environment), and a log survives
15
15
  * a process killed mid-write, which a file rewritten in place does not.
16
+ *
17
+ * `namespace` is what makes that invariant true. A cached score measures a
18
+ * whole system, not a candidate, and this log outlives every part of that
19
+ * system a run does not pass through the key: the model id behind an alias the
20
+ * provider upgraded, the decoding settings, the scorer's own version. It is
21
+ * required rather than optional because the failure it prevents is silent —
22
+ * scores from one system served to a run of another, with a normal-looking
23
+ * result and no way to read afterwards that it happened.
16
24
  */
17
25
  function createFileCache(args) {
18
- const { path, maxEntries = 1e6 } = args;
26
+ const { path, namespace, maxEntries = 1e6 } = args;
27
+ if (namespace.trim() === "") throw new Error("createFileCache requires a non-empty namespace naming the system these scores measure");
19
28
  (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
20
29
  const log = readLog(path);
21
30
  const entries = log.entries;
31
+ const scope = (key) => `${namespace}\u0000${key}`;
22
32
  if (log.unterminated) (0, node_fs.appendFileSync)(path, "\n");
23
33
  return {
24
- get: (key) => entries.get(key),
34
+ get: (key) => entries.get(scope(key)),
25
35
  set: (key, cached) => {
26
- if (entries.size >= maxEntries && !entries.has(key)) {
36
+ const scoped = scope(key);
37
+ if (entries.size >= maxEntries && !entries.has(scoped)) {
27
38
  const oldest = entries.keys().next();
28
39
  if (!oldest.done) entries.delete(oldest.value);
29
40
  }
30
- entries.set(key, cached);
31
- (0, node_fs.appendFileSync)(path, `${JSON.stringify([key, cached])}\n`);
41
+ entries.set(scoped, cached);
42
+ (0, node_fs.appendFileSync)(path, `${JSON.stringify([scoped, cached])}\n`);
32
43
  }
33
44
  };
34
45
  }
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.cjs";
11
11
  * Append-only rather than rewritten: a score is never invalidated (the key
12
12
  * names the candidate, the instance, and the environment), and a log survives
13
13
  * a process killed mid-write, which a file rewritten in place does not.
14
+ *
15
+ * `namespace` is what makes that invariant true. A cached score measures a
16
+ * whole system, not a candidate, and this log outlives every part of that
17
+ * system a run does not pass through the key: the model id behind an alias the
18
+ * provider upgraded, the decoding settings, the scorer's own version. It is
19
+ * required rather than optional because the failure it prevents is silent —
20
+ * scores from one system served to a run of another, with a normal-looking
21
+ * result and no way to read afterwards that it happened.
14
22
  */
15
23
  declare function createFileCache(args: {
16
24
  path: string;
25
+ /**
26
+ * Names the system these scores measure — model id, decoding settings,
27
+ * scorer version. Change it whenever any of them changes.
28
+ */
29
+ namespace: string;
17
30
  /** Entries kept in memory. The file itself is never trimmed. */
18
31
  maxEntries?: number;
19
32
  }): EvaluationCache;
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.mjs";
11
11
  * Append-only rather than rewritten: a score is never invalidated (the key
12
12
  * names the candidate, the instance, and the environment), and a log survives
13
13
  * a process killed mid-write, which a file rewritten in place does not.
14
+ *
15
+ * `namespace` is what makes that invariant true. A cached score measures a
16
+ * whole system, not a candidate, and this log outlives every part of that
17
+ * system a run does not pass through the key: the model id behind an alias the
18
+ * provider upgraded, the decoding settings, the scorer's own version. It is
19
+ * required rather than optional because the failure it prevents is silent —
20
+ * scores from one system served to a run of another, with a normal-looking
21
+ * result and no way to read afterwards that it happened.
14
22
  */
15
23
  declare function createFileCache(args: {
16
24
  path: string;
25
+ /**
26
+ * Names the system these scores measure — model id, decoding settings,
27
+ * scorer version. Change it whenever any of them changes.
28
+ */
29
+ namespace: string;
17
30
  /** Entries kept in memory. The file itself is never trimmed. */
18
31
  maxEntries?: number;
19
32
  }): EvaluationCache;
@@ -12,22 +12,33 @@ import { dirname } from "node:path";
12
12
  * Append-only rather than rewritten: a score is never invalidated (the key
13
13
  * names the candidate, the instance, and the environment), and a log survives
14
14
  * a process killed mid-write, which a file rewritten in place does not.
15
+ *
16
+ * `namespace` is what makes that invariant true. A cached score measures a
17
+ * whole system, not a candidate, and this log outlives every part of that
18
+ * system a run does not pass through the key: the model id behind an alias the
19
+ * provider upgraded, the decoding settings, the scorer's own version. It is
20
+ * required rather than optional because the failure it prevents is silent —
21
+ * scores from one system served to a run of another, with a normal-looking
22
+ * result and no way to read afterwards that it happened.
15
23
  */
16
24
  function createFileCache(args) {
17
- const { path, maxEntries = 1e6 } = args;
25
+ const { path, namespace, maxEntries = 1e6 } = args;
26
+ if (namespace.trim() === "") throw new Error("createFileCache requires a non-empty namespace naming the system these scores measure");
18
27
  mkdirSync(dirname(path), { recursive: true });
19
28
  const log = readLog(path);
20
29
  const entries = log.entries;
30
+ const scope = (key) => `${namespace}\u0000${key}`;
21
31
  if (log.unterminated) appendFileSync(path, "\n");
22
32
  return {
23
- get: (key) => entries.get(key),
33
+ get: (key) => entries.get(scope(key)),
24
34
  set: (key, cached) => {
25
- if (entries.size >= maxEntries && !entries.has(key)) {
35
+ const scoped = scope(key);
36
+ if (entries.size >= maxEntries && !entries.has(scoped)) {
26
37
  const oldest = entries.keys().next();
27
38
  if (!oldest.done) entries.delete(oldest.value);
28
39
  }
29
- entries.set(key, cached);
30
- appendFileSync(path, `${JSON.stringify([key, cached])}\n`);
40
+ entries.set(scoped, cached);
41
+ appendFileSync(path, `${JSON.stringify([scoped, cached])}\n`);
31
42
  }
32
43
  };
33
44
  }