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,6 +1,6 @@
1
- import { n as Candidate, t as Adapter, u as TextModel } from "../types-CWv4IQFF.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, x as TextModel } from "../reporting-bq007_2z.cjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.cjs";
3
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-B7SpRwl7.cjs";
3
+ import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-4Zv-Zt2t.cjs";
4
4
  //#region src/opro/optimize.d.ts
5
5
  /** One instruction that was tried, and what it scored. */
6
6
  /** A history entry plus the system state its score was measured in. */
@@ -111,6 +111,13 @@ interface OproSnapshot {
111
111
  reflectionCalls: number;
112
112
  metricCalls: number;
113
113
  cacheHits: number;
114
+ /** Usage already spent, so a resumed run reports totals and honours ceilings. */
115
+ usage?: UsageTotals;
116
+ /**
117
+ * Candidates accepted so far. Reporters key rows by this id, so restarting it
118
+ * at zero makes a resumed run collide with the run it continues.
119
+ */
120
+ acceptedCandidates?: number;
114
121
  rngState: number;
115
122
  /** Component name -> every text tried for it, with what it scored. */
116
123
  histories: Record<string, RecordedAttempt[]>;
@@ -133,7 +140,8 @@ interface OproTask<Datum, Trajectory = unknown, Output = unknown, K extends stri
133
140
  }) => string;
134
141
  /** Pass `false` to disable caching entirely. */
135
142
  cache?: EvaluationCache | false;
136
- onEvent?: (event: OproEvent<NoInfer<K>>) => void;
143
+ /** Observers of the run. Every one sees every event; none can fail it. */
144
+ reporters?: readonly Reporter<OproEvent<NoInfer<K>>>[];
137
145
  /**
138
146
  * Called with a resumable snapshot after the seed is scored and after every
139
147
  * round. Persist it and a killed run costs the last round, not all of them.
@@ -161,13 +169,13 @@ type OproEvent<K extends string = string> = {
161
169
  score: number;
162
170
  /** True when this attempt became the new incumbent. */
163
171
  accepted: boolean;
164
- } | {
172
+ } | ({
173
+ type: "candidateAccepted";
174
+ round: number;
175
+ } & CandidateAccepted<K>) | ({
165
176
  type: "finish";
166
177
  reason: OproStopReason;
167
- bestScore: number;
168
- metricCalls: number;
169
- testScore?: number;
170
- };
178
+ } & RunFinished);
171
179
  interface OproAttempt<K extends string = string> {
172
180
  round: number;
173
181
  component: K;
@@ -227,4 +235,4 @@ declare function buildOproPrompt(args: {
227
235
  exemplars: readonly string[];
228
236
  }): string;
229
237
  //#endregion
230
- export { type OproAttempt, type OproConfig, type OproEvent, OproOptimizer, type OproPromptBuilder, type OproResult, type OproSnapshot, type OproStopReason, type OproTask, type ScoredAttempt, buildOproPrompt };
238
+ export { type CandidateAccepted, type OproAttempt, type OproConfig, type OproEvent, OproOptimizer, type OproPromptBuilder, type OproResult, type OproSnapshot, type OproStopReason, type OproTask, type OptimizerEvent, type ReportableEvent, type Reporter, type RunFinished, type ScoredAttempt, buildOproPrompt, isCandidateAccepted, isRunFinished };
@@ -1,6 +1,6 @@
1
- import { n as Candidate, t as Adapter, u as TextModel } from "../types-CWv4IQFF.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, x as TextModel } from "../reporting-bq007_2z.mjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.mjs";
3
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-DqCoth_w.mjs";
3
+ import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-Ds5mzYjz.mjs";
4
4
  //#region src/opro/optimize.d.ts
5
5
  /** One instruction that was tried, and what it scored. */
6
6
  /** A history entry plus the system state its score was measured in. */
@@ -111,6 +111,13 @@ interface OproSnapshot {
111
111
  reflectionCalls: number;
112
112
  metricCalls: number;
113
113
  cacheHits: number;
114
+ /** Usage already spent, so a resumed run reports totals and honours ceilings. */
115
+ usage?: UsageTotals;
116
+ /**
117
+ * Candidates accepted so far. Reporters key rows by this id, so restarting it
118
+ * at zero makes a resumed run collide with the run it continues.
119
+ */
120
+ acceptedCandidates?: number;
114
121
  rngState: number;
115
122
  /** Component name -> every text tried for it, with what it scored. */
116
123
  histories: Record<string, RecordedAttempt[]>;
@@ -133,7 +140,8 @@ interface OproTask<Datum, Trajectory = unknown, Output = unknown, K extends stri
133
140
  }) => string;
134
141
  /** Pass `false` to disable caching entirely. */
135
142
  cache?: EvaluationCache | false;
136
- onEvent?: (event: OproEvent<NoInfer<K>>) => void;
143
+ /** Observers of the run. Every one sees every event; none can fail it. */
144
+ reporters?: readonly Reporter<OproEvent<NoInfer<K>>>[];
137
145
  /**
138
146
  * Called with a resumable snapshot after the seed is scored and after every
139
147
  * round. Persist it and a killed run costs the last round, not all of them.
@@ -161,13 +169,13 @@ type OproEvent<K extends string = string> = {
161
169
  score: number;
162
170
  /** True when this attempt became the new incumbent. */
163
171
  accepted: boolean;
164
- } | {
172
+ } | ({
173
+ type: "candidateAccepted";
174
+ round: number;
175
+ } & CandidateAccepted<K>) | ({
165
176
  type: "finish";
166
177
  reason: OproStopReason;
167
- bestScore: number;
168
- metricCalls: number;
169
- testScore?: number;
170
- };
178
+ } & RunFinished);
171
179
  interface OproAttempt<K extends string = string> {
172
180
  round: number;
173
181
  component: K;
@@ -227,4 +235,4 @@ declare function buildOproPrompt(args: {
227
235
  exemplars: readonly string[];
228
236
  }): string;
229
237
  //#endregion
230
- export { type OproAttempt, type OproConfig, type OproEvent, OproOptimizer, type OproPromptBuilder, type OproResult, type OproSnapshot, type OproStopReason, type OproTask, type ScoredAttempt, buildOproPrompt };
238
+ export { type CandidateAccepted, type OproAttempt, type OproConfig, type OproEvent, OproOptimizer, type OproPromptBuilder, type OproResult, type OproSnapshot, type OproStopReason, type OproTask, type OptimizerEvent, type ReportableEvent, type Reporter, type RunFinished, type ScoredAttempt, buildOproPrompt, isCandidateAccepted, isRunFinished };
@@ -1,4 +1,4 @@
1
- import { a as requireMeasuredMean, 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, 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, x as stableHash, y as createMemoryCache } from "../warnings-OxvDi9kN.mjs";
2
2
  import { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
3
3
  import { t as parseProposedText } from "../text--v4Ffbus.mjs";
4
4
  import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
@@ -42,10 +42,14 @@ var OproOptimizer = class {
42
42
  this.#config = config;
43
43
  }
44
44
  async optimize(task) {
45
- return runOpro({
46
- config: this.#config,
47
- task
48
- });
45
+ try {
46
+ return await runOpro({
47
+ config: this.#config,
48
+ task
49
+ });
50
+ } finally {
51
+ await flushReporters(task.reporters ?? []);
52
+ }
49
53
  }
50
54
  };
51
55
  /**
@@ -90,7 +94,12 @@ function buildOproPrompt(args) {
90
94
  async function runOpro(args) {
91
95
  const { config, task } = args;
92
96
  const { proposalsPerRound = DEFAULT_PROPOSALS_PER_ROUND, concurrency = 1, maxRounds = Number.POSITIVE_INFINITY, maxReflectionCalls = Number.POSITIVE_INFINITY, seed = 0, historySize = DEFAULT_HISTORY_SIZE, exemplars = DEFAULT_EXEMPLARS, scoringSetSize, fullEvalInterval = DEFAULT_FULL_EVAL_INTERVAL, scoreScale = DEFAULT_SCORE_SCALE, buildPrompt = buildOproPrompt, trackBestOutputs = false, checkpointCache = true } = config;
93
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, maxMetricCalls, renderDatum = renderDefault, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
97
+ const { seedCandidate, trainingSet, validationSet: requestedValidationSet, testSet, adapter, reflect, maxMetricCalls, renderDatum = renderDefault, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
98
+ const { validationSet, warnings } = resolveValidationSet({
99
+ validationSet: requestedValidationSet,
100
+ trainingSet
101
+ });
102
+ const emit = createEmitter(reporters);
94
103
  const deadline = createDeadline({ maxWallClockMs });
95
104
  const components = componentNames(seedCandidate);
96
105
  if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
@@ -133,8 +142,9 @@ async function runOpro(args) {
133
142
  ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
134
143
  trackOutputs: trackBestOutputs,
135
144
  cacheHits: resumeFrom?.cacheHits ?? 0,
145
+ ...resumeFrom?.usage === void 0 ? {} : { usage: resumeFrom.usage },
136
146
  ...signal === void 0 ? {} : { signal },
137
- onEvaluation: (event) => onEvent?.({
147
+ onEvaluation: (event) => emit({
138
148
  type: "evaluation",
139
149
  ...event
140
150
  })
@@ -159,7 +169,7 @@ async function runOpro(args) {
159
169
  let round = resumeFrom?.round ?? 0;
160
170
  let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
161
171
  let stopReason = "maxRounds";
162
- onEvent?.({
172
+ emit({
163
173
  type: "start",
164
174
  components,
165
175
  validationSetSize: validationSet.length
@@ -192,14 +202,36 @@ async function runOpro(args) {
192
202
  iteration: round
193
203
  });
194
204
  }
205
+ /** What screening a candidate would cost, in rollouts nothing has cached. */
206
+ function screenCost(candidate) {
207
+ return scoringSet === void 0 ? evaluator.countUncached({
208
+ candidate,
209
+ ids: validationIds,
210
+ split: "val"
211
+ }) : evaluator.countUncached({
212
+ candidate,
213
+ ids: scoringIds,
214
+ split: "train"
215
+ });
216
+ }
195
217
  const seedEvaluation = resumeFrom === void 0 ? await sweep(seedCandidate, "seed") : void 0;
196
218
  const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : requireMeasuredMean({
197
219
  batch: seedEvaluation,
198
220
  phase: "seed"
199
221
  });
222
+ if (seedEvaluation !== void 0) emit({
223
+ type: "candidateAccepted",
224
+ round: 0,
225
+ candidateId: 0,
226
+ candidate: seedCandidate,
227
+ aggregateScore: seedScore,
228
+ instanceScores: instanceRow(seedEvaluation),
229
+ ...trackBestOutputs ? { outputs: seedEvaluation.outputs } : {}
230
+ });
200
231
  let best = resumeFrom?.best ?? seedCandidate;
201
232
  let bestScore = resumeFrom?.bestScore ?? seedScore;
202
233
  /** Absent on a resumed run until a sweep wins: outputs are not checkpointed. */
234
+ let acceptedCandidates = resumeFrom?.acceptedCandidates ?? 0;
203
235
  let bestOutputs = seedEvaluation?.outputs;
204
236
  let reported = resumeFrom?.reported ?? seedCandidate;
205
237
  let lastSwept = resumeFrom === void 0 ? seedCandidate : resumeFrom.incumbentSwept ? best : {};
@@ -214,6 +246,24 @@ async function runOpro(args) {
214
246
  * what gets reported is the best candidate a full sweep has actually seen —
215
247
  * never a subset number wearing a validation set label.
216
248
  */
249
+ /**
250
+ * The incumbent moved and a full sweep measured it. Emitted from the two
251
+ * places that can be true — a screening run with no scoring set, where the
252
+ * attempt's own evaluation is the sweep, and the cadence that confirms an
253
+ * incumbent later — because a payload assembled twice is one that drifts.
254
+ */
255
+ function emitAccepted(args) {
256
+ acceptedCandidates += 1;
257
+ emit({
258
+ type: "candidateAccepted",
259
+ round,
260
+ candidateId: acceptedCandidates,
261
+ candidate: args.candidate,
262
+ aggregateScore: args.score,
263
+ instanceScores: instanceRow(args.evaluation),
264
+ ...trackBestOutputs ? { outputs: args.evaluation.outputs } : {}
265
+ });
266
+ }
217
267
  async function refreshIncumbent() {
218
268
  if (best === lastSwept || !budget.canAfford(validationSet.length)) return "ok";
219
269
  try {
@@ -224,6 +274,11 @@ async function runOpro(args) {
224
274
  reported = best;
225
275
  bestScore = full;
226
276
  bestOutputs = evaluation.outputs;
277
+ emitAccepted({
278
+ candidate: best,
279
+ evaluation,
280
+ score: full
281
+ });
227
282
  }
228
283
  } catch (err) {
229
284
  if (err instanceof BudgetExhausted || signal?.aborted) return "stop";
@@ -259,6 +314,8 @@ async function runOpro(args) {
259
314
  reflectionCalls,
260
315
  metricCalls: budget.spent(),
261
316
  cacheHits: evaluator.cacheHits(),
317
+ usage: evaluator.usage(),
318
+ acceptedCandidates,
262
319
  rngState: rng.state(),
263
320
  histories: Object.fromEntries([...histories].map(([name, attempts]) => [name, [...attempts]])),
264
321
  ...scoringIndices === void 0 ? {} : { scoringIndices },
@@ -303,7 +360,7 @@ async function runOpro(args) {
303
360
  const history = histories.get(component);
304
361
  const context = contextOf(best, component);
305
362
  const comparable = history.filter((attempt) => attempt.context === context);
306
- onEvent?.({
363
+ emit({
307
364
  type: "roundStart",
308
365
  round,
309
366
  component,
@@ -332,25 +389,43 @@ async function runOpro(args) {
332
389
  const unique = [...new Set(drawn)].filter((text) => text.length > 0 && !tried.has(text));
333
390
  barrenRounds = unique.length === 0 ? barrenRounds + 1 : 0;
334
391
  let roundStop;
392
+ const scheduled = [];
393
+ let owed = 0;
335
394
  for (const text of unique) {
336
395
  const candidate = {
337
396
  ...best,
338
397
  [component]: text
339
398
  };
340
- let evaluation;
341
- try {
342
- evaluation = await screen(candidate, "validation");
343
- } catch (err) {
344
- if (err instanceof BudgetExhausted) {
345
- roundStop = "budgetExhausted";
346
- break;
347
- }
348
- if (signal?.aborted) {
349
- roundStop = "aborted";
350
- break;
399
+ const cost = screenCost(candidate);
400
+ if (!budget.canAfford(owed + cost)) {
401
+ roundStop = "budgetExhausted";
402
+ break;
403
+ }
404
+ owed += cost;
405
+ scheduled.push(candidate);
406
+ }
407
+ const screened = await mapWithConcurrency({
408
+ items: scheduled,
409
+ limit: concurrency,
410
+ task: async (candidate) => {
411
+ if (signal?.aborted) return { stop: "aborted" };
412
+ try {
413
+ return { evaluation: await screen(candidate, "validation") };
414
+ } catch (err) {
415
+ if (err instanceof BudgetExhausted) return { stop: "budgetExhausted" };
416
+ if (signal?.aborted) return { stop: "aborted" };
417
+ throw err;
351
418
  }
352
- throw err;
353
419
  }
420
+ });
421
+ for (const [index, outcome] of screened.entries()) {
422
+ if (outcome.stop !== void 0) {
423
+ roundStop = outcome.stop;
424
+ break;
425
+ }
426
+ const candidate = scheduled[index];
427
+ const text = candidate[component];
428
+ const evaluation = outcome.evaluation;
354
429
  const score = measuredMean(evaluation);
355
430
  if (score === void 0) continue;
356
431
  const accepted = score > bestSearchScore;
@@ -365,7 +440,7 @@ async function runOpro(args) {
365
440
  candidate,
366
441
  score
367
442
  });
368
- onEvent?.({
443
+ emit({
369
444
  type: "attempt",
370
445
  round,
371
446
  component,
@@ -380,6 +455,11 @@ async function runOpro(args) {
380
455
  lastSwept = candidate;
381
456
  bestScore = score;
382
457
  bestOutputs = evaluation.outputs;
458
+ emitAccepted({
459
+ candidate,
460
+ evaluation,
461
+ score
462
+ });
383
463
  }
384
464
  const scaled = scaleScore(score, scoreScale);
385
465
  for (const name of components) {
@@ -393,22 +473,22 @@ async function runOpro(args) {
393
473
  }
394
474
  }
395
475
  round += 1;
476
+ let cadenceStop;
477
+ if (roundStop === void 0 && scoringSet !== void 0 && round % fullEvalInterval === 0 && await refreshIncumbent() === "stop") cadenceStop = signal?.aborted ? "aborted" : "budgetExhausted";
396
478
  await checkpoint();
397
479
  if (roundStop !== void 0) {
398
480
  stopReason = roundStop;
399
481
  break;
400
482
  }
401
- if (scoringSet !== void 0 && round % fullEvalInterval === 0) {
402
- if (await refreshIncumbent() === "stop") {
403
- stopReason = signal?.aborted ? "aborted" : "budgetExhausted";
404
- break;
405
- }
483
+ if (cadenceStop !== void 0) {
484
+ stopReason = cadenceStop;
485
+ break;
406
486
  }
407
487
  }
408
488
  if (scoringSet !== void 0 && !signal?.aborted) await refreshIncumbent();
409
489
  if (signal?.aborted) stopReason = "aborted";
410
- const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
411
- candidate: best,
490
+ const heldOut = testSet === void 0 ? void 0 : await evaluator.evaluate({
491
+ candidate: reported,
412
492
  batch: testSet,
413
493
  ids: testIds,
414
494
  split: "test",
@@ -416,13 +496,18 @@ async function runOpro(args) {
416
496
  candidateId: null,
417
497
  iteration: round,
418
498
  charge: false
419
- }));
420
- onEvent?.({
499
+ });
500
+ const testScore = heldOut === void 0 ? void 0 : measuredMean(heldOut);
501
+ emit({
421
502
  type: "finish",
422
503
  reason: stopReason,
504
+ warnings,
505
+ bestCandidateId: acceptedCandidates,
423
506
  bestScore,
424
507
  metricCalls: budget.spent(),
425
- ...testScore === void 0 ? {} : { testScore }
508
+ ...testScore === void 0 ? {} : { testScore },
509
+ ...heldOut === void 0 ? {} : { testInstanceScores: instanceRow(heldOut) },
510
+ ...heldOut === void 0 || !trackBestOutputs ? {} : { testOutputs: heldOut.outputs }
426
511
  });
427
512
  return {
428
513
  snapshot: takeSnapshot(),
@@ -433,13 +518,15 @@ async function runOpro(args) {
433
518
  ...trackBestOutputs ? { bestOutputs } : {},
434
519
  ...testScore === void 0 ? {} : {
435
520
  testScore,
436
- testMetricCalls: evaluator.unchargedCalls()
521
+ testMetricCalls: evaluator.unchargedCalls(),
522
+ testUsage: evaluator.unchargedUsage()
437
523
  },
438
524
  rounds: round,
439
525
  trajectory,
440
526
  metricCalls: budget.spent(),
441
527
  reflectionCalls,
442
528
  cacheHits: evaluator.cacheHits(),
529
+ warnings,
443
530
  stopReason
444
531
  };
445
532
  }
@@ -477,9 +564,5 @@ function assertConfig(config) {
477
564
  for (const [name, value] of positive) if (value !== void 0 && (!Number.isInteger(value) || value < 1)) throw new Error(`${name} must be a positive integer, received ${value}`);
478
565
  if (config.exemplars !== void 0 && (!Number.isInteger(config.exemplars) || config.exemplars < 0)) throw new Error(`exemplars must be a non-negative integer, received ${config.exemplars}`);
479
566
  }
480
- function defaultInstanceId(args) {
481
- const hash = stableHash(args.datum);
482
- return hash === "" ? String(args.index) : hash;
483
- }
484
567
  //#endregion
485
- export { OproOptimizer, buildOproPrompt };
568
+ export { OproOptimizer, buildOproPrompt, isCandidateAccepted, isRunFinished };
@@ -1,4 +1,4 @@
1
- import { d as UsageTotals, i as EvaluationBatch, n as Candidate, o as EvaluationPhase, s as EvaluationSplit, t as Adapter } from "./types-CWv4IQFF.cjs";
1
+ import { S as UsageTotals, _ as EvaluationPhase, c as RunWarning, f as Adapter, h as EvaluationBatch, p as Candidate, v as EvaluationSplit } from "./reporting-bq007_2z.cjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "./cache-CuSo0NJ8.cjs";
3
3
  //#region src/budget.d.ts
4
4
  interface Budget {
@@ -106,8 +106,24 @@ interface Evaluator<Datum, Trajectory, Output, K extends string> {
106
106
  cacheHits(): number;
107
107
  /** Rollouts made with `charge: false`, tracked apart from the budget. */
108
108
  unchargedCalls(): number;
109
- /** Tokens and money the run has spent, as far as adapters have reported it. */
109
+ /**
110
+ * Tokens and money the search has spent, as far as adapters have reported
111
+ * it. `maxCostUsd` is checked against this, so what the ceiling does not
112
+ * bound is not counted in it: rollouts made with `charge: false` are in
113
+ * `unchargedUsage` instead.
114
+ */
110
115
  usage(): UsageTotals;
116
+ /**
117
+ * What the rollouts made with `charge: false` cost. Held-out measurement is
118
+ * taken after the search has stopped, so no ceiling bounds it and reporting
119
+ * it inside `usage` would describe a run as having overrun one.
120
+ */
121
+ unchargedUsage(): UsageTotals;
122
+ /**
123
+ * Folds in usage spent outside this evaluator — harvesting runs its own, and
124
+ * a cost ceiling that cannot see it bounds only part of the run.
125
+ */
126
+ absorbUsage(spent: UsageTotals): void;
111
127
  /** Cache contents for checkpointing, when the cache can enumerate them. */
112
128
  entries(): [string, CachedScore][] | undefined;
113
129
  restore(entries: Iterable<readonly [string, CachedScore]>): void;
@@ -140,6 +156,12 @@ declare function createEvaluator<Datum, Trajectory, Output, K extends string>(ar
140
156
  signal?: AbortSignal;
141
157
  /** Resumed counters, so a continued run reports totals rather than deltas. */
142
158
  cacheHits?: number;
159
+ /**
160
+ * Usage the interrupted run had already spent. Without it `maxCostUsd` is a
161
+ * ceiling on the segment rather than on the run, and resuming repeatedly
162
+ * spends it again each time.
163
+ */
164
+ usage?: UsageTotals;
143
165
  /**
144
166
  * Rate limits and 5xx responses are the common case in a long run, and a
145
167
  * transient row costs the instance whichever optimizer is driving: it is
@@ -192,8 +214,28 @@ declare function requireMeasuredMean(args: {
192
214
  */
193
215
  interface OptimizerTask<Datum, Trajectory = unknown, Output = unknown, K extends string = string> {
194
216
  seedCandidate: Candidate<K>;
217
+ /**
218
+ * Instances the search draws evidence from. Reflective optimizers mine these
219
+ * for what a candidate got wrong, so a training row earns its place by being
220
+ * diagnostic — a row every candidate already passes teaches the rewriter
221
+ * nothing.
222
+ *
223
+ * @see docs/data-prep.md
224
+ */
195
225
  trainingSet: readonly Datum[];
196
- validationSet?: readonly Datum[];
226
+ /**
227
+ * Instances the search selects candidates against. Defaults to
228
+ * `trainingSet`, which is the right default for a first run and the wrong
229
+ * number to report from one — the result carries a warning saying so.
230
+ * `"reuseTraining"` is that same default with the caller's name on it, and
231
+ * carries no warning.
232
+ *
233
+ * Split by group rather than by row: near-duplicate instances that straddle
234
+ * the boundary leak, and the run reports a score nothing earned.
235
+ *
236
+ * @see docs/data-prep.md
237
+ */
238
+ validationSet?: readonly Datum[] | "reuseTraining";
197
239
  /**
198
240
  * `NoInfer` keeps the adapter out of `K`'s inference: an adapter built by a
199
241
  * factory knows nothing about component names, and one inference candidate of
@@ -207,6 +249,18 @@ interface OptimizerTask<Datum, Trajectory = unknown, Output = unknown, K extends
207
249
  * number in a result that no candidate was ever selected against.
208
250
  */
209
251
  testSet?: readonly Datum[];
252
+ /**
253
+ * Scored rollouts the search may spend. Cache hits do not count, and test
254
+ * rollouts are outside it entirely.
255
+ *
256
+ * Spending it is only worth anything if the metric separates candidates
257
+ * first: a metric that scores every candidate alike turns the whole budget
258
+ * into ranked ties, and the run reports a stop reason that looks like any
259
+ * other.
260
+ *
261
+ * @see docs/metric-preflight.md
262
+ * @see docs/tuning.md
263
+ */
210
264
  maxMetricCalls: number;
211
265
  /**
212
266
  * Dollars the run may spend, as reported by the adapter's usage. Checked
@@ -252,8 +306,25 @@ interface OptimizerResult<K extends string, Stop extends string, Output = unknow
252
306
  bestOutputs?: (Output | undefined)[];
253
307
  metricCalls: number;
254
308
  /**
255
- * Tokens and dollars the run spent, summed from what the adapter reported.
256
- * Zero throughout when the adapter reports no usage.
309
+ * Rollouts served from the cache rather than charged to `metricCalls`. Every
310
+ * optimizer here caches by default, so the same `maxMetricCalls` can buy a
311
+ * search that revisits scored candidates a longer effective run than one
312
+ * that never does — a comparison over `metricCalls` alone hides that. Zero
313
+ * for a run with caching disabled.
314
+ */
315
+ cacheHits: number;
316
+ /**
317
+ * Calls made to a proposal or reflection model, which no metric budget
318
+ * covers — see each optimizer's own accounting for what a call costs there.
319
+ * Absent from a search that proposes nothing of its own: bootstrap search
320
+ * only accepts or rejects rollouts the metric already scored.
321
+ */
322
+ reflectionCalls?: number;
323
+ /**
324
+ * Tokens and dollars the search spent, summed from what the adapter reported.
325
+ * Zero throughout when the adapter reports no usage. `maxCostUsd` is checked
326
+ * against this, so the held-out sweep — which runs after the search has
327
+ * stopped, under no ceiling — is reported apart from it, in `testUsage`.
257
328
  */
258
329
  usage: UsageTotals;
259
330
  /**
@@ -267,7 +338,21 @@ interface OptimizerResult<K extends string, Stop extends string, Output = unknow
267
338
  * measurement rather than search, and so is not charged to `maxMetricCalls`.
268
339
  */
269
340
  testMetricCalls?: number;
341
+ /**
342
+ * Tokens and dollars the held-out sweep cost, for the same reason: no ceiling
343
+ * bounds it, so a caller adding up what a run spent has to see it as its own
344
+ * number rather than find it folded into one `maxCostUsd` was supposed to
345
+ * hold. Absent when no testSet was given.
346
+ */
347
+ testUsage?: UsageTotals;
270
348
  stopReason: Stop;
349
+ /**
350
+ * What this run cannot say about itself from its own numbers — selection
351
+ * that reused the training instances, a seed the metric could not separate.
352
+ * Empty when there is nothing to say. Never fatal, and repeated on the
353
+ * `finish` event so a reporter sees them next to the score.
354
+ */
355
+ warnings: RunWarning[];
271
356
  }
272
357
  /**
273
358
  * An optimizer: a task in, the best candidate it found out. Exactly one method.