textopt 0.0.0 → 0.1.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 (55) hide show
  1. package/README.md +44 -22
  2. package/dist/bootstrap-search/index.cjs +153 -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 +144 -66
  6. package/dist/{demos-B0pVQjYC.d.mts → demos-1b7JiUue.d.mts} +10 -3
  7. package/dist/{demos-BTuzFNsp.d.cts → demos-CU9dy2oT.d.cts} +10 -3
  8. package/dist/demos-D2o0qSSo.cjs +244 -0
  9. package/dist/demos-DE2oxNWX.mjs +215 -0
  10. package/dist/file-cache.cjs +11 -3
  11. package/dist/file-cache.mjs +11 -3
  12. package/dist/gepa/index.cjs +76 -71
  13. package/dist/gepa/index.d.cts +12 -6
  14. package/dist/gepa/index.d.mts +12 -6
  15. package/dist/gepa/index.mjs +49 -46
  16. package/dist/index.cjs +129 -27
  17. package/dist/index.d.cts +145 -7
  18. package/dist/index.d.mts +145 -7
  19. package/dist/index.mjs +113 -15
  20. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  21. package/dist/math-Dqme4rYz.mjs +123 -0
  22. package/dist/mipro/index.cjs +98 -70
  23. package/dist/mipro/index.d.cts +17 -14
  24. package/dist/mipro/index.d.mts +17 -14
  25. package/dist/mipro/index.mjs +84 -58
  26. package/dist/opro/index.cjs +130 -51
  27. package/dist/opro/index.d.cts +17 -9
  28. package/dist/opro/index.d.mts +17 -9
  29. package/dist/opro/index.mjs +115 -38
  30. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-Bh5DPRMH.d.cts} +50 -4
  31. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ck6-e_8o.d.mts} +50 -4
  32. package/dist/random-search/index.cjs +93 -49
  33. package/dist/random-search/index.d.cts +15 -13
  34. package/dist/random-search/index.d.mts +15 -13
  35. package/dist/random-search/index.mjs +83 -41
  36. package/dist/{reflection-CQToe-5B.d.cts → reflection-Dt3QrXhM.d.cts} +7 -11
  37. package/dist/{reflection-Cr_upzU0.d.mts → reflection-LRaAZP4e.d.mts} +7 -11
  38. package/dist/{evaluation-OZOp6TB7.cjs → reporting-CNHzbJC-.cjs} +165 -5
  39. package/dist/reporting-DQbAohc9.d.cts +240 -0
  40. package/dist/reporting-DQbAohc9.d.mts +240 -0
  41. package/dist/{evaluation-BV0nSZVx.mjs → reporting-DY-DC4HG.mjs} +124 -6
  42. package/dist/simba/index.cjs +210 -83
  43. package/dist/simba/index.d.cts +32 -11
  44. package/dist/simba/index.d.mts +32 -11
  45. package/dist/simba/index.mjs +200 -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/package.json +4 -3
  51. package/dist/demos-B9BJiNKz.cjs +0 -143
  52. package/dist/demos-Degx6UmP.mjs +0 -126
  53. package/dist/math-DhrDmpFS.mjs +0 -78
  54. package/dist/types-CWv4IQFF.d.cts +0 -129
  55. package/dist/types-CWv4IQFF.d.mts +0 -129
package/README.md CHANGED
@@ -23,7 +23,7 @@ This package has no runtime dependencies. For an overview of the algorithms and
23
23
  ```ts
24
24
  import {
25
25
  assertResumable,
26
- bootstrapDemos,
26
+ harvestFewShotExamples,
27
27
  buildJudgePrompt,
28
28
  compare,
29
29
  componentNames,
@@ -32,11 +32,13 @@ import {
32
32
  createJudge,
33
33
  createMemoryCache,
34
34
  formatDemos,
35
+ harvestRollouts,
35
36
  mapWithConcurrency,
36
37
  parseDemos,
37
38
  parseProposedText,
38
39
  priceUsage,
39
40
  runFingerprint,
41
+ toTrainingJsonl,
40
42
  } from "textopt";
41
43
  import type {
42
44
  Adapter,
@@ -93,9 +95,9 @@ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Outp
93
95
 
94
96
  **`transient`** marks scores caused by infrastructure failures such as rate limits, 5xx responses, or network errors. Transient scores are not cached.
95
97
 
96
- **`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`, and `stopReason`. Optimizer-specific task and result types extend these interfaces.
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.
97
99
 
98
- **`maxCostUsd`** and **`maxWallClockMs`** are checked between evaluations, so a run overruns by at most one of them. 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.
100
+ **`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.
99
101
 
100
102
  **`cacheNamespace`** scopes every cache key to the system the rollouts were measured under — model id, decoding settings, scorer version. Change it whenever anything outside the candidate text changes.
101
103
 
@@ -103,7 +105,7 @@ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Outp
103
105
 
104
106
  **`UsageTotals`** (`inputTokens`, `outputTokens`, `totalTokens`, `costUsd`, `rollouts`) is summed from the `RolloutUsage` entries an adapter reports. Zero throughout when the adapter reports none.
105
107
 
106
- **`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` and do not count against `maxMetricCalls`.
108
+ **`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.
107
109
 
108
110
  **`TextModel`** is the provider-independent interface `({ prompt, signal }) => Promise<string>`.
109
111
 
@@ -117,9 +119,13 @@ For Redis, SQLite, or file-backed caching, implement **`EvaluationCache`** with
117
119
 
118
120
  **`componentNames(candidate)`** returns `Object.keys(candidate)` while preserving the component-name union.
119
121
 
120
- **`createEvaluator({ adapter, budget, cache, cacheNamespace, retry, trackOutputs, onEvaluation, signal, cacheHits })`** handles adapter calls, caching, budget accounting, transient scores, and evaluation events. `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.
122
+ **`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.
121
123
 
122
- **`bootstrapDemos({ adapter, candidate, trainingSet, minScore, maxDemos, batchSize, maxMetricCalls, rng, renderDemo, signal })`** evaluates a candidate on `trainingSet` and keeps the rollouts the metric rewarded. Omit `minScore` to keep any rollout scoring above zero, as MIPROv2 does without a `metric_threshold`; pass a number to require at least that score. It returns the selected `demos`, a formatted `block`, and the metric calls used. It does not use the score cache because it needs rollout outputs.
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).
125
+
126
+ **`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
+
128
+ **`harvestFewShotExamples({ adapter, candidate, trainingSet, minScore, maxDemos, batchSize, maxMetricCalls, maxCostUsd, rng, renderDemo, signal })`** evaluates a candidate on `trainingSet` and keeps the rollouts the metric rewarded. Omit `minScore` to keep any rollout scoring above zero, as MIPROv2 does without a `metric_threshold`; pass a number to require at least that score. It returns the selected `demos`, a formatted `block`, and the metric calls and usage it spent. It does not use the score cache because it needs rollout outputs.
123
129
 
124
130
  **`formatDemos(demos, { render })`** and **`parseDemos(text)`** write and read the `<demo>`, `<input>`, and `<output>` block format.
125
131
 
@@ -137,6 +143,18 @@ For Redis, SQLite, or file-backed caching, implement **`EvaluationCache`** with
137
143
 
138
144
  **`BatchSampler<Datum>`** and **`Rng`** are type-only exports. Their default implementations are internal.
139
145
 
146
+ ### Reporting
147
+
148
+ Every optimizer takes **`reporters`**, an array of **`Reporter<Event>`** with an optional `onEvent` and an optional `flush`. `onEvent` is called synchronously on the search's hot path, so a reporter that ships anywhere over a network buffers there and uploads in `flush`, which is awaited once as the run ends — including when it ends by throwing. A reporter that throws is warned about and skipped: observability never fails a run. Persisting a run so it can be resumed is `onCheckpoint`, which is durability and a separate concern.
149
+
150
+ Each search emits its own discriminated union, but two members are shared. **`CandidateAccepted`** carries `candidateId`, the `candidate` text, its `aggregateScore`, its `instanceScores` over the validation set, and its `outputs` under `trackBestOutputs`. **`RunFinished`** carries `bestCandidateId`, `bestScore`, `metricCalls`, and the winner's held-out `testScore`, `testInstanceScores` and `testOutputs`. In both rows, `undefined` marks an instance nothing measured — the evaluation policy skipped it, or an infrastructure failure lost it — and reporting one as a zero shows as a regression that never happened.
151
+
152
+ `candidateAccepted` fires only when the incumbent moves and a full validation sweep measured it, so `instanceScores` never means "a subset, and you work out which". SIMBA accepts on a minibatch, which is too small a sample to name a row against; it reports those in its own `candidate` event and emits `candidateAccepted` when the sweep that confirms a step winner lands.
153
+
154
+ Every optimizer reports the seed as `candidateId` 0 once its own sweep lands, so a report opens with the baseline every later candidate is read against rather than with the first improvement. A resumed run does not re-emit it. Each search adds its own fields to the event — GEPA its `iteration`, `parentIds` and `source`, MIPRO its `trial`, SIMBA its `step`, OPRO and random search their `round`, bootstrap search its `source` and `demos` — and drops anything the seed could not honestly carry, since the events that name a component or a menu selection already carry it.
155
+
156
+ A reporter that reads only those two takes **`OptimizerEvent`** — `{ type: string }` — and narrows with **`isCandidateAccepted`** and **`isRunFinished`**. Because a literal tag is assignable to `string` and the parameter position is contravariant, one such reporter drops into any optimizer's `reporters` array. **`ReportableEvent`** names the union it reads. `@textopt/langsmith` is written this way.
157
+
140
158
  ## `textopt/gepa`
141
159
 
142
160
  ```ts
@@ -185,17 +203,17 @@ These options control search behavior and can be reused across runs.
185
203
 
186
204
  Required: `seedCandidate`, `trainingSet`, `adapter` (a `GepaAdapter`), `reflect` (a `TextModel`), and `maxMetricCalls`.
187
205
 
188
- | Option | Default |
189
- | --------------------- | ------------------------------------------------------------------------------------ |
190
- | `validationSet` | the trainingSet |
191
- | `testSet` | none. Held out of the search and scored once, on the winner |
192
- | `componentSelector` | `roundRobinComponentSelector()` |
193
- | `batchSampler` | an epoch-shuffled sampler over `minibatchSize` |
194
- | `valEvaluationPolicy` | `fullEvaluationPolicy()` |
195
- | `instanceId` | a content hash of the datum, falling back to its position when it will not serialize |
196
- | `cache` | a per-run memory cache. Pass `false` to disable |
206
+ | Option | Default |
207
+ | --------------------- | ---------------------------------------------------------------------------------- |
208
+ | `validationSet` | the trainingSet |
209
+ | `testSet` | none. Held out of the search and scored once, on the winner |
210
+ | `componentSelector` | `roundRobinComponentSelector()` |
211
+ | `batchSampler` | an epoch-shuffled sampler over `minibatchSize` |
212
+ | `valEvaluationPolicy` | `fullEvaluationPolicy()` |
213
+ | `instanceId` | a content hash of the datum, falling back to its position when JSON cannot read it |
214
+ | `cache` | a per-run memory cache. Pass `false` to disable |
197
215
 
198
- `onEvent`, `onCheckpoint`, `resumeFrom`, and `signal` have no defaults.
216
+ `reporters`, `onCheckpoint`, `resumeFrom`, and `signal` have no defaults.
199
217
 
200
218
  TypeScript infers component names and the datum type from `seedCandidate` and `trainingSet`. Other fields use `NoInfer` and are checked against those inferred types.
201
219
 
@@ -230,7 +248,7 @@ When `proposeNewTexts` is implemented, the adapter generates proposals without c
230
248
 
231
249
  Each export is a factory. Selector and acceptance interfaces accept custom functions. A `ValEvaluationPolicy` is an object with `selectInstances` and `bestCandidate` methods.
232
250
 
233
- `pairedPermutationAcceptance` and `lowerBoundEvaluationPolicy` exist for metrics whose readings vary between runs of the same text. Both are strictly more conservative than the defaults, and on a metric that does not vary that is pure cost: in the twenty-seed benchmark the pair drops GEPA from 0.729 to 0.175 on the noiseless task and ties it on the noisy one. 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.
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.
234
252
 
235
253
  ### Reflection prompts
236
254
 
@@ -270,7 +288,11 @@ Feedback is end-to-end and every module receives the same string. A metric score
270
288
 
271
289
  **`stopReason`** is one of `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"reflectionBudgetExhausted"`, `"aborted"`, or `"maxIterations"`.
272
290
 
273
- **`onEvent`** receives a discriminated `GepaEvent`: `start`, `iterationStart`, `evaluation`, `proposal`, `candidateAccepted`, `candidateRejected` (with `reason: "worse" | "notSelected"`), `error`, and `finish`.
291
+ **`reporters`** is an array of `Reporter<GepaEvent>`, each with an optional `onEvent` and an optional `flush`. `onEvent` receives a discriminated `GepaEvent`: `start`, `iterationStart`, `evaluation`, `proposal`, `candidateAccepted`, `candidateRejected` (with `reason: "worse" | "notSelected"`), `error`, and `finish`. It is called synchronously, on the search's hot path, so a reporter that ships anywhere over a network buffers in `onEvent` and uploads in `flush`, which is awaited once as the run ends — including when it ends by throwing. A reporter that throws is warned about and skipped: observability never fails a run.
292
+
293
+ `candidateAccepted` fires for the seed too, and carries the `candidate` text, its `instanceScores` over the validation set, and its `outputs` under `trackBestOutputs` — the row a candidate put on the frontier, not just the mean. `finish` carries the same for the held-out sweep in `testInstanceScores` and `testOutputs`. In both, `undefined` marks an instance nothing measured — the evaluation policy skipped it, or an infrastructure failure lost it — and reporting one as a zero shows as a regression that never happened.
294
+
295
+ Reporting is observability; persisting a run so it can be resumed is `onCheckpoint`, below.
274
296
 
275
297
  **`onCheckpoint`** runs after seed evaluation and each iteration with a JSON-serializable `GepaSnapshot`. Pass it as `resumeFrom` to continue. A fingerprint prevents resuming with a different seed candidate, instance set, or random seed. Every optimizer here has the same three: `onCheckpoint`, `resumeFrom`, and a `snapshot` on the result. A snapshot handed back as `resumeFrom` is copied, never mutated by the run that continues from it.
276
298
 
@@ -280,7 +302,7 @@ Feedback is end-to-end and every module receives the same string. A metric score
280
302
  import { SimbaOptimizer, buildAdvicePrompt, parseAdvice } from "textopt/simba";
281
303
  ```
282
304
 
283
- SIMBA uses the base `Adapter`: it reads outputs, scores, and feedback and builds its own evidence, so it needs no `makeReflectiveDataset`. `SimbaTask` adds `reflect`, `demoComponents`, `instructionComponents`, `renderDemo`, `buildAdvicePrompt`, `sampler`, `instanceId`, `cache`, `onEvent`, `onCheckpoint`, and `resumeFrom`.
305
+ SIMBA uses the base `Adapter`: it reads outputs, scores, and feedback and builds its own evidence, so it needs no `makeReflectiveDataset`. `SimbaTask` adds `reflect`, `demoComponents`, `instructionComponents`, `renderDemo`, `buildAdvicePrompt`, `sampler`, `instanceId`, `cache`, `reporters`, `onCheckpoint`, and `resumeFrom`.
284
306
 
285
307
  | Option | Default | Effect |
286
308
  | ---------------------- | -------------------- | -------------------------------------------------------------------------- |
@@ -320,7 +342,7 @@ Ported from DSPy's SIMBA with two deliberate changes. A trajectory sample runs o
320
342
  import { BootstrapSearchOptimizer } from "textopt/bootstrap-search";
321
343
  ```
322
344
 
323
- DSPy's `BootstrapFewShotWithRandomSearch`. It uses the base `Adapter` and no reflection model at all: every candidate is assembled from outputs the system itself produced, so the search costs rollouts and nothing else. `BootstrapSearchTask` adds `demoComponents` (required), `renderDemo`, `goldOutput`, `instanceId`, `cache`, `onEvent`, `onCheckpoint`, and `resumeFrom`.
345
+ DSPy's `BootstrapFewShotWithRandomSearch`. It uses the base `Adapter` and no reflection model at all: every candidate is assembled from outputs the system itself produced, so the search costs rollouts and nothing else. `BootstrapSearchTask` adds `demoComponents` (required), `renderDemo`, `goldOutput`, `instanceId`, `cache`, `reporters`, `onCheckpoint`, and `resumeFrom`.
324
346
 
325
347
  | Option | Default | Effect |
326
348
  | ------------------ | ------- | -------------------------------------------------------------------- |
@@ -352,7 +374,7 @@ Unlike DSPy, which bootstraps each predictor separately from the traces of one p
352
374
  import { OproOptimizer, buildOproPrompt } from "textopt/opro";
353
375
  ```
354
376
 
355
- OPRO uses the base `Adapter`. `OproTask` adds `reflect` and optional `renderDatum`, `instanceId`, `cache`, and `onEvent` fields.
377
+ OPRO uses the base `Adapter`. `OproTask` adds `reflect` and optional `renderDatum`, `instanceId`, `cache`, and `reporters` fields.
356
378
 
357
379
  | Option | Default | Effect |
358
380
  | -------------------- | ----------------- | ---------------------------------------------------------- |
@@ -399,7 +421,7 @@ For multi-component candidates, each attempt records the other components presen
399
421
  import { MiproOptimizer, proposeConfiguration } from "textopt/mipro";
400
422
  ```
401
423
 
402
- MIPRO uses the base `Adapter`. `MiproTask` adds `reflect`, `componentOptions`, `renderDatum`, `batchSampler`, `instanceId`, `cache`, and `onEvent`.
424
+ MIPRO uses the base `Adapter`. `MiproTask` adds `reflect`, `componentOptions`, `renderDatum`, `batchSampler`, `instanceId`, `cache`, and `reporters`.
403
425
 
404
426
  | Option | Default | Effect |
405
427
  | -------------------------- | ------------------ | ---------------------------------------------------------------------------------- |
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_evaluation = require("../evaluation-OZOp6TB7.cjs");
3
- const require_demos = require("../demos-B9BJiNKz.cjs");
2
+ const require_reporting = require("../reporting-CNHzbJC-.cjs");
3
+ const require_demos = require("../demos-D2o0qSSo.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;
@@ -34,21 +34,27 @@ const DEFAULT_MAX_LABELED_DEMOS = 16;
34
34
  var BootstrapSearchOptimizer = class {
35
35
  #config;
36
36
  constructor(config = {}) {
37
+ assertBootstrapSearchConfig(config);
37
38
  this.#config = config;
38
39
  }
39
- optimize(task) {
40
- return run({
41
- config: this.#config,
42
- task
43
- });
40
+ async optimize(task) {
41
+ try {
42
+ return await run({
43
+ config: this.#config,
44
+ task
45
+ });
46
+ } finally {
47
+ await require_reporting.flushReporters(task.reporters ?? []);
48
+ }
44
49
  }
45
50
  };
46
51
  async function run(args) {
47
52
  const { config, task } = args;
48
- const { candidates: shuffledHarvests = DEFAULT_CANDIDATES, maxDemos = DEFAULT_MAX_DEMOS, minDemos = DEFAULT_MIN_DEMOS, maxLabeledDemos = DEFAULT_MAX_LABELED_DEMOS, demoMinScore, stopAtScore, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
49
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, demoComponents, renderDemo, goldOutput, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
50
- const deadline = require_evaluation.createDeadline({ maxWallClockMs });
51
- const components = require_evaluation.componentNames(seedCandidate);
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);
52
58
  if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
53
59
  if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
54
60
  if (demoComponents.length === 0) throw new Error("optimize requires at least one demoComponent: this search has nothing to put demonstrations in otherwise");
@@ -61,7 +67,7 @@ async function run(args) {
61
67
  datum,
62
68
  index
63
69
  })) ?? [];
64
- const fingerprint = require_evaluation.runFingerprint({
70
+ const fingerprint = require_reporting.runFingerprint({
65
71
  seedCandidate,
66
72
  trainingIds: trainingSet.map((datum, index) => instanceId({
67
73
  datum,
@@ -71,17 +77,17 @@ async function run(args) {
71
77
  seed,
72
78
  ...cacheNamespace === void 0 ? {} : { cacheNamespace }
73
79
  });
74
- require_evaluation.assertResumable({
80
+ require_reporting.assertResumable({
75
81
  fingerprint,
76
82
  ...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
77
83
  });
78
84
  const rng = require_rng.createSeededRng(seed, resumeFrom?.rngState);
79
- const budget = require_evaluation.createBudget({
85
+ const budget = require_reporting.createBudget({
80
86
  maxMetricCalls,
81
87
  spent: resumeFrom?.metricCalls ?? 0
82
88
  });
83
- const evaluationCache = cache === false ? void 0 : cache ?? require_evaluation.createMemoryCache();
84
- const evaluator = require_evaluation.createEvaluator({
89
+ const evaluationCache = cache === false ? void 0 : cache ?? require_reporting.createMemoryCache();
90
+ const evaluator = require_reporting.createEvaluator({
85
91
  adapter,
86
92
  budget,
87
93
  ...retry === void 0 ? {} : { retry },
@@ -89,8 +95,9 @@ async function run(args) {
89
95
  ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
90
96
  trackOutputs: trackBestOutputs,
91
97
  cacheHits: resumeFrom?.cacheHits ?? 0,
98
+ ...resumeFrom?.usage === void 0 ? {} : { usage: resumeFrom.usage },
92
99
  ...signal === void 0 ? {} : { signal },
93
- onEvaluation: (event) => onEvent?.({
100
+ onEvaluation: (event) => emit({
94
101
  type: "evaluation",
95
102
  ...event
96
103
  })
@@ -100,7 +107,7 @@ async function run(args) {
100
107
  let drawn = resumeFrom?.drawn ?? 0;
101
108
  let bootstrapMetricCalls = resumeFrom?.bootstrapMetricCalls ?? 0;
102
109
  let stopReason = "candidatesExhausted";
103
- onEvent?.({
110
+ emit({
104
111
  type: "start",
105
112
  components,
106
113
  validationSetSize: validationSet.length
@@ -116,13 +123,25 @@ async function run(args) {
116
123
  iteration: evaluated.length
117
124
  });
118
125
  }
119
- const seedScore = resumeFrom?.seedScore ?? require_evaluation.requireMeasuredMean({
120
- batch: await sweep(seedCandidate, "seed"),
126
+ const seedEvaluation = resumeFrom === void 0 ? await sweep(seedCandidate, "seed") : void 0;
127
+ const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : require_reporting.requireMeasuredMean({
128
+ batch: seedEvaluation,
121
129
  phase: "seed"
122
130
  });
123
131
  let best = resumeFrom?.best ?? seedCandidate;
124
132
  let bestScore = resumeFrom?.bestScore ?? seedScore;
125
133
  let bestOutputs;
134
+ let acceptedCandidates = resumeFrom?.acceptedCandidates ?? 0;
135
+ if (seedEvaluation !== void 0) emit({
136
+ type: "candidateAccepted",
137
+ source: "zeroShot",
138
+ demos: 0,
139
+ candidateId: 0,
140
+ candidate: seedCandidate,
141
+ aggregateScore: seedScore,
142
+ instanceScores: require_reporting.instanceRow(seedEvaluation),
143
+ ...trackBestOutputs ? { outputs: seedEvaluation.outputs } : {}
144
+ });
126
145
  function takeSnapshot() {
127
146
  const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
128
147
  return {
@@ -136,6 +155,8 @@ async function run(args) {
136
155
  metricCalls: budget.spent(),
137
156
  bootstrapMetricCalls,
138
157
  cacheHits: evaluator.cacheHits(),
158
+ usage: evaluator.usage(),
159
+ acceptedCandidates,
139
160
  rngState: rng.state(),
140
161
  ...cached === void 0 ? {} : { cache: cached }
141
162
  };
@@ -148,12 +169,13 @@ async function run(args) {
148
169
  shuffledHarvests,
149
170
  labeled: goldOutput !== void 0
150
171
  });
151
- for (; drawn < plan.length; drawn += 1) {
172
+ const waveSize = stopAtScore === void 0 ? concurrency : 1;
173
+ while (drawn < plan.length) {
152
174
  if (signal?.aborted) {
153
175
  stopReason = "aborted";
154
176
  break;
155
177
  }
156
- if (require_evaluation.costExhausted({
178
+ if (require_reporting.costExhausted({
157
179
  usage: evaluator.usage(),
158
180
  maxCostUsd
159
181
  })) {
@@ -164,58 +186,95 @@ async function run(args) {
164
186
  stopReason = "deadlineReached";
165
187
  break;
166
188
  }
167
- if (!budget.canAfford(validationSet.length + 1)) {
168
- stopReason = "budgetExhausted";
169
- break;
170
- }
171
- const source = plan[drawn];
172
- const block = await buildBlock(source);
173
- const candidate = withDemos(block);
174
- let evaluation;
175
- try {
176
- evaluation = await sweep(candidate, "validation");
177
- } catch (err) {
178
- if (err instanceof require_evaluation.BudgetExhausted) {
179
- stopReason = "budgetExhausted";
180
- break;
181
- }
182
- if (signal?.aborted) {
183
- stopReason = "aborted";
189
+ const wave = [];
190
+ const inFlight = /* @__PURE__ */ new Map();
191
+ let waveStop;
192
+ while (wave.length < waveSize && drawn + wave.length < plan.length) {
193
+ if (!budget.canAfford(validationSet.length + 1)) {
194
+ waveStop = "budgetExhausted";
184
195
  break;
185
196
  }
186
- throw err;
197
+ const source = plan[drawn + wave.length];
198
+ const block = await buildBlock(source);
199
+ const candidate = withDemos(block);
200
+ const key = require_reporting.candidateHash(candidate);
201
+ const prior = inFlight.get(key);
202
+ const sweeping = prior === void 0 ? settled(sweep(candidate, "validation")) : prior.then(() => settled(sweep(candidate, "validation")));
203
+ inFlight.set(key, sweeping);
204
+ wave.push({
205
+ source,
206
+ candidate,
207
+ block,
208
+ sweep: sweeping
209
+ });
187
210
  }
188
- const score = require_evaluation.measuredMean(evaluation);
189
- if (score === void 0) continue;
190
- const accepted = score > bestScore;
191
- evaluated.push({
192
- candidate,
193
- source,
194
- demos: countDemos(block),
195
- score
196
- });
197
- onEvent?.({
198
- type: "candidate",
199
- index: evaluated.length - 1,
200
- source,
201
- demos: countDemos(block),
202
- score,
203
- accepted
204
- });
205
- if (accepted) {
206
- best = candidate;
207
- bestScore = score;
208
- bestOutputs = evaluation.outputs;
211
+ let waveError;
212
+ let draining = false;
213
+ for (const entry of wave) {
214
+ const outcome = await entry.sweep;
215
+ if (draining) continue;
216
+ drawn += 1;
217
+ if (outcome.failed === true) {
218
+ draining = true;
219
+ if (outcome.err instanceof require_reporting.BudgetExhausted) {
220
+ waveStop = "budgetExhausted";
221
+ continue;
222
+ }
223
+ if (signal?.aborted) {
224
+ waveStop = "aborted";
225
+ continue;
226
+ }
227
+ waveError = outcome.err;
228
+ continue;
229
+ }
230
+ const evaluation = outcome.evaluation;
231
+ const score = require_reporting.measuredMean(evaluation);
232
+ if (score === void 0) continue;
233
+ const accepted = score > bestScore;
234
+ evaluated.push({
235
+ candidate: entry.candidate,
236
+ source: entry.source,
237
+ demos: countDemos(entry.block),
238
+ score
239
+ });
240
+ emit({
241
+ type: "candidate",
242
+ index: evaluated.length - 1,
243
+ source: entry.source,
244
+ demos: countDemos(entry.block),
245
+ score,
246
+ accepted
247
+ });
248
+ if (accepted) {
249
+ acceptedCandidates += 1;
250
+ emit({
251
+ type: "candidateAccepted",
252
+ source: entry.source,
253
+ demos: countDemos(entry.block),
254
+ candidateId: acceptedCandidates,
255
+ candidate: entry.candidate,
256
+ aggregateScore: score,
257
+ instanceScores: require_reporting.instanceRow(evaluation),
258
+ ...trackBestOutputs ? { outputs: evaluation.outputs } : {}
259
+ });
260
+ best = entry.candidate;
261
+ bestScore = score;
262
+ bestOutputs = evaluation.outputs;
263
+ }
264
+ if (stopAtScore !== void 0 && score >= stopAtScore) {
265
+ waveStop = "scoreReached";
266
+ draining = true;
267
+ }
209
268
  }
269
+ if (waveError !== void 0) throw waveError;
210
270
  await checkpoint();
211
- if (stopAtScore !== void 0 && score >= stopAtScore) {
212
- stopReason = "scoreReached";
213
- drawn += 1;
271
+ if (waveStop !== void 0) {
272
+ stopReason = waveStop;
214
273
  break;
215
274
  }
216
275
  }
217
276
  if (signal?.aborted) stopReason = "aborted";
218
- const testScore = testSet === void 0 ? void 0 : require_evaluation.measuredMean(await evaluator.evaluate({
277
+ const heldOut = testSet === void 0 ? void 0 : await evaluator.evaluate({
219
278
  candidate: best,
220
279
  batch: testSet,
221
280
  ids: testIds,
@@ -224,13 +283,17 @@ async function run(args) {
224
283
  candidateId: null,
225
284
  iteration: evaluated.length,
226
285
  charge: false
227
- }));
228
- onEvent?.({
286
+ });
287
+ const testScore = heldOut === void 0 ? void 0 : require_reporting.measuredMean(heldOut);
288
+ emit({
229
289
  type: "finish",
230
290
  reason: stopReason,
291
+ bestCandidateId: acceptedCandidates,
231
292
  bestScore,
232
293
  metricCalls: budget.spent(),
233
- ...testScore === void 0 ? {} : { testScore }
294
+ ...testScore === void 0 ? {} : { testScore },
295
+ ...heldOut === void 0 ? {} : { testInstanceScores: require_reporting.instanceRow(heldOut) },
296
+ ...heldOut === void 0 || !trackBestOutputs ? {} : { testOutputs: heldOut.outputs }
234
297
  });
235
298
  return {
236
299
  bestCandidate: best,
@@ -245,7 +308,8 @@ async function run(args) {
245
308
  ...trackBestOutputs && bestOutputs !== void 0 ? { bestOutputs } : {},
246
309
  ...testScore === void 0 ? {} : {
247
310
  testScore,
248
- testMetricCalls: testSet?.length ?? 0
311
+ testMetricCalls: evaluator.unchargedCalls(),
312
+ testUsage: evaluator.unchargedUsage()
249
313
  },
250
314
  stopReason
251
315
  };
@@ -260,19 +324,21 @@ async function run(args) {
260
324
  const requested = source === "unshuffled" ? maxDemos : minDemos + rng.nextInt(Math.max(1, maxDemos - minDemos + 1));
261
325
  const affordable = Math.min(trainingSet.length, budget.remaining() - validationSet.length);
262
326
  if (affordable < 1) return "";
263
- const harvest = await require_demos.bootstrapDemos({
327
+ const harvest = await require_demos.harvestFewShotExamples({
264
328
  adapter,
265
329
  candidate: seedCandidate,
266
330
  trainingSet,
267
331
  ...demoMinScore === void 0 ? {} : { minScore: demoMinScore },
268
332
  maxDemos: requested,
269
333
  maxMetricCalls: affordable,
334
+ ...maxCostUsd === void 0 ? {} : { maxCostUsd: maxCostUsd - evaluator.usage().costUsd },
270
335
  ...source === "unshuffled" ? {} : { rng },
271
336
  ...renderDemo === void 0 ? {} : { renderDemo },
272
337
  ...signal === void 0 ? {} : { signal }
273
338
  });
274
339
  bootstrapMetricCalls += harvest.metricCalls;
275
340
  budget.reserve(harvest.metricCalls);
341
+ evaluator.absorbUsage(harvest.usage);
276
342
  return harvest.block;
277
343
  }
278
344
  function labeledBlock() {
@@ -298,11 +364,25 @@ function candidatePlan(args) {
298
364
  ...Array.from({ length: shuffledHarvests }, () => "bootstrapped")
299
365
  ];
300
366
  }
367
+ /** Turns a sweep into a value, so a dispatched one never rejects unobserved. */
368
+ function settled(sweeping) {
369
+ return sweeping.then((evaluation) => ({ evaluation }), (err) => ({
370
+ failed: true,
371
+ err
372
+ }));
373
+ }
374
+ /**
375
+ * Range checks on the search knobs, run at construction so a configuration
376
+ * that could never terminate is refused before a task is ever handed to it.
377
+ */
378
+ function assertBootstrapSearchConfig(config) {
379
+ const { concurrency = 1 } = config;
380
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`concurrency must be a positive integer, received ${concurrency}`);
381
+ }
301
382
  function countDemos(block) {
302
383
  return block.split("<demo>").length - 1;
303
384
  }
304
- function defaultInstanceId(args) {
305
- return String(args.index);
306
- }
307
385
  //#endregion
308
386
  exports.BootstrapSearchOptimizer = BootstrapSearchOptimizer;
387
+ exports.isCandidateAccepted = require_reporting.isCandidateAccepted;
388
+ exports.isRunFinished = require_reporting.isRunFinished;
@@ -1,7 +1,7 @@
1
- import { n as Candidate, t as Adapter } from "../types-CWv4IQFF.cjs";
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";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.cjs";
3
- import { r as DemoRenderer } from "../demos-BTuzFNsp.cjs";
4
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-B7SpRwl7.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";
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,
@@ -35,6 +35,18 @@ interface BootstrapSearchConfig {
35
35
  * every candidate, which is the reliable reading and the expensive one.
36
36
  */
37
37
  stopAtScore?: number;
38
+ /**
39
+ * How many candidates may be swept at once. Default 1.
40
+ *
41
+ * Harvesting stays in plan order however this is set — every harvest draws
42
+ * from the same random stream, and reordering them would make a seeded run
43
+ * unreproducible — so what overlaps is a sweep with the harvest of the
44
+ * candidates behind it. Two costs come with raising it: a checkpoint is
45
+ * taken per wave rather than per candidate, so a killed run loses up to this
46
+ * many candidates instead of one, and `stopAtScore` is honoured by sweeping
47
+ * one at a time, since a wave cannot know it has already passed the target.
48
+ */
49
+ concurrency?: number;
38
50
  seed?: number;
39
51
  trackBestOutputs?: boolean;
40
52
  /**
@@ -61,6 +73,13 @@ interface BootstrapSearchSnapshot {
61
73
  metricCalls: number;
62
74
  bootstrapMetricCalls: number;
63
75
  cacheHits: number;
76
+ /** Usage already spent, so a resumed run reports totals and honours ceilings. */
77
+ usage?: UsageTotals;
78
+ /**
79
+ * Candidates accepted so far. Reporters key rows by this id, so restarting it
80
+ * at zero makes a resumed run collide with the run it continues.
81
+ */
82
+ acceptedCandidates?: number;
64
83
  rngState: number;
65
84
  cache?: [string, CachedScore][];
66
85
  }
@@ -92,7 +111,8 @@ interface BootstrapSearchTask<Datum, Trajectory = unknown, Output = unknown, K e
92
111
  }) => string;
93
112
  /** Pass `false` to disable caching entirely. */
94
113
  cache?: EvaluationCache | false;
95
- onEvent?: (event: BootstrapSearchEvent<NoInfer<K>>) => void;
114
+ /** Observers of the run. Every one sees every event; none can fail it. */
115
+ reporters?: readonly Reporter<BootstrapSearchEvent<NoInfer<K>>>[];
96
116
  /** Called with a resumable snapshot after every candidate is scored. */
97
117
  onCheckpoint?: (snapshot: BootstrapSearchSnapshot) => void | Promise<void>;
98
118
  /** Snapshot to continue from. */
@@ -112,13 +132,15 @@ type BootstrapSearchEvent<K extends string = string> = {
112
132
  demos: number;
113
133
  score: number;
114
134
  accepted: boolean;
115
- } | {
135
+ } | ({
136
+ type: "candidateAccepted";
137
+ /** Which construction produced the demo block that won. */
138
+ source: DemoSource;
139
+ demos: number;
140
+ } & CandidateAccepted<K>) | ({
116
141
  type: "finish";
117
142
  reason: BootstrapSearchStopReason;
118
- bestScore: number;
119
- metricCalls: number;
120
- testScore?: number;
121
- };
143
+ } & RunFinished);
122
144
  interface BootstrapSearchResult<K extends string = string, Output = unknown> extends OptimizerResult<K, BootstrapSearchStopReason, Output> {
123
145
  /** The seed's score, so the lift the demos bought is readable directly. */
124
146
  seedScore: number;
@@ -159,4 +181,4 @@ declare class BootstrapSearchOptimizer implements Optimizer<BootstrapSearchStopR
159
181
  optimize<Datum, Trajectory = unknown, Output = unknown, const K extends string = string>(task: BootstrapSearchTask<Datum, Trajectory, Output, K>): Promise<BootstrapSearchResult<K, Output>>;
160
182
  }
161
183
  //#endregion
162
- export { type BootstrapCandidate, type BootstrapSearchConfig, type BootstrapSearchEvent, BootstrapSearchOptimizer, type BootstrapSearchResult, type BootstrapSearchSnapshot, type BootstrapSearchStopReason, type BootstrapSearchTask, type DemoSource };
184
+ export { type BootstrapCandidate, type BootstrapSearchConfig, type BootstrapSearchEvent, BootstrapSearchOptimizer, type BootstrapSearchResult, type BootstrapSearchSnapshot, type BootstrapSearchStopReason, type BootstrapSearchTask, type CandidateAccepted, type DemoSource, type OptimizerEvent, type ReportableEvent, type Reporter, type RunFinished, isCandidateAccepted, isRunFinished };