textopt 0.0.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +509 -0
  3. package/dist/bootstrap-search/index.cjs +308 -0
  4. package/dist/bootstrap-search/index.d.cts +162 -0
  5. package/dist/bootstrap-search/index.d.mts +162 -0
  6. package/dist/bootstrap-search/index.mjs +307 -0
  7. package/dist/cache-CuSo0NJ8.d.cts +24 -0
  8. package/dist/cache-CuSo0NJ8.d.mts +24 -0
  9. package/dist/concurrency-C-cFzWW2.cjs +44 -0
  10. package/dist/concurrency-D58PWeSk.mjs +39 -0
  11. package/dist/demos-B0pVQjYC.d.mts +88 -0
  12. package/dist/demos-B9BJiNKz.cjs +143 -0
  13. package/dist/demos-BTuzFNsp.d.cts +88 -0
  14. package/dist/demos-Degx6UmP.mjs +126 -0
  15. package/dist/evaluation-BV0nSZVx.mjs +521 -0
  16. package/dist/evaluation-OZOp6TB7.cjs +598 -0
  17. package/dist/file-cache.cjs +70 -0
  18. package/dist/file-cache.d.cts +21 -0
  19. package/dist/file-cache.d.mts +21 -0
  20. package/dist/file-cache.mjs +69 -0
  21. package/dist/gepa/index.cjs +1671 -0
  22. package/dist/gepa/index.d.cts +385 -0
  23. package/dist/gepa/index.d.mts +385 -0
  24. package/dist/gepa/index.mjs +1652 -0
  25. package/dist/index.cjs +266 -0
  26. package/dist/index.d.cts +221 -0
  27. package/dist/index.d.mts +221 -0
  28. package/dist/index.mjs +245 -0
  29. package/dist/math-COOofUyv.cjs +101 -0
  30. package/dist/math-DhrDmpFS.mjs +78 -0
  31. package/dist/mipro/index.cjs +739 -0
  32. package/dist/mipro/index.d.cts +372 -0
  33. package/dist/mipro/index.d.mts +372 -0
  34. package/dist/mipro/index.mjs +736 -0
  35. package/dist/opro/index.cjs +487 -0
  36. package/dist/opro/index.d.cts +230 -0
  37. package/dist/opro/index.d.mts +230 -0
  38. package/dist/opro/index.mjs +485 -0
  39. package/dist/optimizer-B7SpRwl7.d.cts +288 -0
  40. package/dist/optimizer-DqCoth_w.d.mts +288 -0
  41. package/dist/random-search/index.cjs +321 -0
  42. package/dist/random-search/index.d.cts +156 -0
  43. package/dist/random-search/index.d.mts +156 -0
  44. package/dist/random-search/index.mjs +319 -0
  45. package/dist/reflection-CQToe-5B.d.cts +283 -0
  46. package/dist/reflection-Cr_upzU0.d.mts +283 -0
  47. package/dist/reflection-DRfbk6hu.cjs +249 -0
  48. package/dist/reflection-mwMhrjs_.mjs +214 -0
  49. package/dist/rng-BR5MOedA.d.cts +22 -0
  50. package/dist/rng-BR5MOedA.d.mts +22 -0
  51. package/dist/rng-DbA_rPIo.cjs +67 -0
  52. package/dist/rng-Dtc5eZ_W.mjs +62 -0
  53. package/dist/sampling-CfHt7Gue.mjs +59 -0
  54. package/dist/sampling-DFo_7RNJ.d.mts +23 -0
  55. package/dist/sampling-Dars7ctR.cjs +64 -0
  56. package/dist/sampling-axOwfZf5.d.cts +23 -0
  57. package/dist/simba/index.cjs +709 -0
  58. package/dist/simba/index.d.cts +289 -0
  59. package/dist/simba/index.d.mts +289 -0
  60. package/dist/simba/index.mjs +700 -0
  61. package/dist/testing.cjs +155 -0
  62. package/dist/testing.d.cts +53 -0
  63. package/dist/testing.d.mts +53 -0
  64. package/dist/testing.mjs +148 -0
  65. package/dist/text--v4Ffbus.mjs +21 -0
  66. package/dist/text-CK_HB3su.cjs +26 -0
  67. package/dist/types-CWv4IQFF.d.cts +129 -0
  68. package/dist/types-CWv4IQFF.d.mts +129 -0
  69. package/package.json +135 -0
@@ -0,0 +1,700 @@
1
+ import { d as createMemoryCache, 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";
2
+ import { n as formatDemos, r as parseDemos } from "../demos-Degx6UmP.mjs";
3
+ import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
4
+ import { t as createEpochShuffledSampler } from "../sampling-CfHt7Gue.mjs";
5
+ //#region src/simba/advice.ts
6
+ const ADVICE_BLOCK = /<advice\s+component="([^"]+)"\s*>([\s\S]*?)<\/advice>/g;
7
+ /**
8
+ * Adapted from SIMBA's `OfferFeedback` signature (Opsahl-Ong et al.'s DSPy).
9
+ *
10
+ * The framing that matters is "build up experience": the component will not
11
+ * see this instance again, so advice that only fixes this one input is wasted.
12
+ * Contrasting two runs of the *same* input is what makes that possible — the
13
+ * inputs are held constant, so the difference in reward is attributable to the
14
+ * behaviour rather than to the instance being easier.
15
+ */
16
+ function buildAdvicePrompt(args) {
17
+ const { components, input, better, worse } = args;
18
+ return [
19
+ "Two runs of the same system on the same input are shown below, with the reward each earned.",
20
+ "Your job is to write advice that would make the system behave like the higher scoring run the next time it sees a similar input.",
21
+ "",
22
+ "<input>",
23
+ serialize(input),
24
+ "</input>",
25
+ ...worse === void 0 ? [] : trajectoryBlock("worse", worse),
26
+ ...better === void 0 ? [] : trajectoryBlock("better", better),
27
+ "",
28
+ "Write advice for each of these components:",
29
+ "",
30
+ "<components>",
31
+ components.join("\n"),
32
+ "</components>",
33
+ "",
34
+ "The component will not have access to this example, so advice that only covers this input is wasted. State the general behaviour it should adopt, and be concrete about when it applies.",
35
+ "Address each component's own sub-task rather than the system as a whole.",
36
+ "Base the advice on what actually differed between the two runs. If nothing useful can be said for a component, leave it out.",
37
+ "",
38
+ "Return one block per component, in the form <advice component=\"name\">…</advice>, and nothing else."
39
+ ].join("\n");
40
+ }
41
+ /**
42
+ * Read the per-component advice out of the model's response, ignoring anything
43
+ * written around it. A component the model had nothing to say about is absent
44
+ * rather than empty, so the caller appends nothing instead of appending noise.
45
+ */
46
+ function parseAdvice(response) {
47
+ const advice = {};
48
+ for (const match of response.matchAll(ADVICE_BLOCK)) {
49
+ const component = match[1];
50
+ const text = (match[2] ?? "").trim();
51
+ if (text.length > 0) advice[component] = text;
52
+ }
53
+ return advice;
54
+ }
55
+ function trajectoryBlock(label, trajectory) {
56
+ return [
57
+ "",
58
+ `<${label}_trajectory>`,
59
+ `reward: ${trajectory.score}`,
60
+ "<output>",
61
+ serialize(trajectory.output),
62
+ "</output>",
63
+ ...trajectory.feedback === void 0 ? [] : [
64
+ "<feedback>",
65
+ trajectory.feedback,
66
+ "</feedback>"
67
+ ],
68
+ `</${label}_trajectory>`
69
+ ];
70
+ }
71
+ function serialize(value) {
72
+ if (typeof value === "string") return value;
73
+ try {
74
+ return JSON.stringify(value, null, 2) ?? String(value);
75
+ } catch {
76
+ return String(value);
77
+ }
78
+ }
79
+ //#endregion
80
+ //#region src/simba/strategies.ts
81
+ /**
82
+ * Group the step's samples by instance and rank the instances by how much the
83
+ * programs disagreed about them.
84
+ *
85
+ * Disagreement is the signal SIMBA runs on. An instance every program gets
86
+ * right teaches nothing, and one every program gets wrong is usually beyond
87
+ * the reach of a prompt edit; the instructive ones are where one program
88
+ * succeeded and another failed, because the pair is a controlled experiment
89
+ * the reflection model can read directly.
90
+ *
91
+ * Ranked on the max-to-min gap first, then the best score, then the max-to-avg
92
+ * gap: widest disagreement first, ties broken toward instances where something
93
+ * actually worked, since a bucket whose best rollout is bad has no success to
94
+ * generalize from.
95
+ */
96
+ function buildBuckets(args) {
97
+ const { batch, samples } = args;
98
+ return batch.map((datum, index) => {
99
+ const rollouts = samples.map((sample) => ({
100
+ programIndex: sample.programIndex,
101
+ score: sample.scores[index] ?? 0,
102
+ ...sample.outputs === void 0 ? {} : { output: sample.outputs[index] },
103
+ ...sample.feedback === void 0 ? {} : { feedback: sample.feedback[index] }
104
+ })).sort((a, b) => b.score - a.score);
105
+ const scores = rollouts.map((rollout) => rollout.score);
106
+ const maxScore = Math.max(...scores);
107
+ const minScore = Math.min(...scores);
108
+ const average = scores.reduce((total, score) => total + score, 0) / scores.length;
109
+ return {
110
+ index,
111
+ datum,
112
+ rollouts,
113
+ maxToMinGap: maxScore - minScore,
114
+ maxScore,
115
+ maxToAvgGap: maxScore - average
116
+ };
117
+ }).sort((a, b) => b.maxToMinGap - a.maxToMinGap || b.maxScore - a.maxScore || b.maxToAvgGap - a.maxToAvgGap);
118
+ }
119
+ /**
120
+ * Linear-interpolated percentile, matching numpy's default so the thresholds
121
+ * behave the way the reference implementation's do.
122
+ */
123
+ function percentile(values, p) {
124
+ if (values.length === 0) return 0;
125
+ const sorted = [...values].sort((a, b) => a - b);
126
+ const position = p / 100 * (sorted.length - 1);
127
+ const lower = Math.floor(position);
128
+ const upper = Math.ceil(position);
129
+ if (lower === upper) return sorted[lower];
130
+ return sorted[lower] + (position - lower) * (sorted[upper] - sorted[lower]);
131
+ }
132
+ /**
133
+ * Softmax weights over program scores, for picking which program to mutate
134
+ * next. Shifted by the maximum before exponentiating — the same distribution,
135
+ * but a score scale the caller chose freely cannot overflow it.
136
+ */
137
+ function softmaxWeights(scores, temperature) {
138
+ if (scores.length === 0) return [];
139
+ const highest = Math.max(...scores);
140
+ return scores.map((score) => Math.exp((score - highest) / temperature));
141
+ }
142
+ /**
143
+ * The `k` highest scoring programs, with the baseline forced into the pool.
144
+ *
145
+ * Keeping the baseline is what makes the search recoverable: every candidate
146
+ * descends from a program already in the pool, so a pool that has drifted into
147
+ * a bad region has nothing left to climb back from.
148
+ */
149
+ function topKPlusBaseline(args) {
150
+ const { scores, k } = args;
151
+ const ranked = scores.map((score, index) => ({
152
+ score,
153
+ index
154
+ })).sort((a, b) => b.score - a.score).slice(0, k).map((entry) => entry.index);
155
+ if (ranked.length > 0 && !ranked.includes(0)) ranked[ranked.length - 1] = 0;
156
+ return [...new Set(ranked)];
157
+ }
158
+ /**
159
+ * A Poisson draw by Knuth's method, used to decide how many demonstrations to
160
+ * drop before a mutation. Random rather than fixed so a candidate can shed a
161
+ * demo that is no longer earning its place — nothing else in the loop ever
162
+ * removes one, and a block that only grows eventually crowds out the
163
+ * instruction it was meant to support.
164
+ */
165
+ function samplePoisson(rng, lambda) {
166
+ if (lambda <= 0) return 0;
167
+ const limit = Math.exp(-lambda);
168
+ let draws = 0;
169
+ let product = 1;
170
+ do {
171
+ draws += 1;
172
+ product *= rng.next();
173
+ } while (product > limit);
174
+ return draws - 1;
175
+ }
176
+ /**
177
+ * Positions spread evenly across a sequence, first and last included.
178
+ *
179
+ * The step winners are held back and only a sample of them is scored on the
180
+ * full validation set at the end, because scoring every one of them costs more
181
+ * than the search did. Sampling evenly rather than taking the last few keeps
182
+ * early winners in the running: minibatch scores are noisy, and the run's
183
+ * genuine best is often not its most recent.
184
+ */
185
+ function evenlySpacedIndices(args) {
186
+ const { length, count } = args;
187
+ if (length <= 1 || count <= 1) return [0];
188
+ const last = length - 1;
189
+ const indices = Array.from({ length: count }, (_, position) => Math.round(position * last / (count - 1)));
190
+ return [...new Set(indices)];
191
+ }
192
+ //#endregion
193
+ //#region src/simba/optimize.ts
194
+ const DEFAULT_MINIBATCH_SIZE = 32;
195
+ const DEFAULT_CANDIDATES = 6;
196
+ const DEFAULT_MAX_STEPS = 8;
197
+ const DEFAULT_MAX_DEMOS = 4;
198
+ const DEFAULT_TEMPERATURE = .2;
199
+ /** What `maxDemos: 0` stands in as, so the drop rate stays defined. */
200
+ const DEMO_DROP_SCALE = 3;
201
+ /**
202
+ * SIMBA — stochastic introspective mini-batch ascent.
203
+ *
204
+ * Run several programs over the same minibatch, find the instances they
205
+ * disagree about most, and mutate toward whatever the winning run did. The
206
+ * disagreement is the point: an instance one program solved and another failed
207
+ * is a controlled experiment with the input held fixed, so the difference in
208
+ * reward is attributable to behaviour rather than to difficulty. GEPA reflects
209
+ * on failures; SIMBA reflects on the *contrast* between a success and a failure
210
+ * of the same input, which is a strictly stronger signal when it exists — and
211
+ * costs a pool of programs to produce.
212
+ *
213
+ * Two mutations, drawn at random per instance: append a demonstration the
214
+ * metric already rewarded, or ask a model what the better run did differently
215
+ * and append that as a rule. Neither replaces text, so a candidate accumulates;
216
+ * demos are dropped at a Poisson rate to keep the block from crowding out
217
+ * everything else.
218
+ *
219
+ * Ported from DSPy's SIMBA, with two deliberate changes. First, a trajectory
220
+ * sample runs one program across the whole minibatch rather than resampling a
221
+ * program per instance: the adapter here owns decoding, so there is no
222
+ * temperature knob to vary, and the variability comes from the program pool
223
+ * instead. Second, the percentile guards are strict rather than inclusive — on
224
+ * a step where every rollout ties, an inclusive guard blocks every mutation and
225
+ * the run does nothing at all, which is the one case where the guard's own
226
+ * premise does not hold.
227
+ */
228
+ var SimbaOptimizer = class {
229
+ #config;
230
+ constructor(config = {}) {
231
+ this.#config = config;
232
+ }
233
+ optimize(task) {
234
+ return run({
235
+ config: this.#config,
236
+ task
237
+ });
238
+ }
239
+ };
240
+ async function run(args) {
241
+ const { config, task } = args;
242
+ const { minibatchSize = DEFAULT_MINIBATCH_SIZE, candidates: candidateCount = DEFAULT_CANDIDATES, maxSteps = DEFAULT_MAX_STEPS, maxDemos = DEFAULT_MAX_DEMOS, samplingTemperature = DEFAULT_TEMPERATURE, candidateTemperature = DEFAULT_TEMPERATURE, strategies, maxReflectionCalls, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
243
+ const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, demoComponents = [], instructionComponents, renderDemo, buildAdvicePrompt: buildPrompt = buildAdvicePrompt, sampler = createEpochShuffledSampler({ minibatchSize }), maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
244
+ const components = componentNames(seedCandidate);
245
+ const ruleComponents = instructionComponents ?? components.filter((name) => !demoComponents.includes(name));
246
+ const enabled = strategies ?? (demoComponents.length > 0 ? ["appendDemo", "appendRule"] : ["appendRule"]);
247
+ if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
248
+ if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
249
+ if (minibatchSize > trainingSet.length) throw new Error(`optimize requires a minibatchSize no larger than the trainingSet: ${minibatchSize} > ${trainingSet.length}`);
250
+ if (enabled.length === 0) throw new Error("optimize requires at least one strategy");
251
+ if (enabled.includes("appendDemo") && demoComponents.length === 0) throw new Error("the appendDemo strategy requires at least one entry in demoComponents");
252
+ if (enabled.includes("appendRule") && ruleComponents.length === 0) throw new Error("the appendRule strategy requires at least one instruction component to write into");
253
+ if (testSet !== void 0 && testSet.length === 0) throw new Error("optimize requires a non-empty testSet when one is given; omit it to skip held-out evaluation");
254
+ const validationIds = validationSet.map((datum, index) => instanceId({
255
+ datum,
256
+ index
257
+ }));
258
+ const testIds = testSet?.map((datum, index) => instanceId({
259
+ datum,
260
+ index
261
+ })) ?? [];
262
+ const fingerprint = runFingerprint({
263
+ seedCandidate,
264
+ trainingIds: trainingSet.map((datum, index) => instanceId({
265
+ datum,
266
+ index
267
+ })),
268
+ validationIds,
269
+ seed,
270
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace }
271
+ });
272
+ assertResumable({
273
+ fingerprint,
274
+ ...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
275
+ });
276
+ const deadline = createDeadline({ maxWallClockMs });
277
+ const rng = createSeededRng(seed, resumeFrom?.rngState);
278
+ const budget = createBudget({
279
+ maxMetricCalls,
280
+ spent: resumeFrom?.metricCalls ?? 0
281
+ });
282
+ const evaluationCache = cache === false ? void 0 : cache ?? createMemoryCache();
283
+ const evaluator = createEvaluator({
284
+ adapter,
285
+ budget,
286
+ ...retry === void 0 ? {} : { retry },
287
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace },
288
+ ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
289
+ trackOutputs: true,
290
+ cacheHits: resumeFrom?.cacheHits ?? 0,
291
+ ...signal === void 0 ? {} : { signal },
292
+ onEvaluation: (event) => onEvent?.({
293
+ type: "evaluation",
294
+ ...event
295
+ })
296
+ });
297
+ evaluator.restore(resumeFrom?.cache ?? []);
298
+ if (resumeFrom?.sampler !== void 0) sampler.restore?.(resumeFrom.sampler);
299
+ const programs = [...resumeFrom?.programs ?? [seedCandidate]];
300
+ const programScores = (resumeFrom?.programScores ?? [[]]).map((scores) => [...scores]);
301
+ const winners = [...resumeFrom?.winners ?? [{
302
+ candidate: seedCandidate,
303
+ step: 0
304
+ }]];
305
+ let step = resumeFrom?.step ?? 0;
306
+ let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
307
+ let stopReason = "maxSteps";
308
+ onEvent?.({
309
+ type: "start",
310
+ components,
311
+ validationSetSize: validationSet.length
312
+ });
313
+ const finalistReserve = Math.min(candidateCount + 1, maxSteps + 1) * validationSet.length;
314
+ const stepCost = candidateCount * minibatchSize + minibatchSize;
315
+ for (; step < maxSteps; step += 1) {
316
+ if (signal?.aborted) {
317
+ stopReason = "aborted";
318
+ break;
319
+ }
320
+ if (costExhausted({
321
+ usage: evaluator.usage(),
322
+ maxCostUsd
323
+ })) {
324
+ stopReason = "costExhausted";
325
+ break;
326
+ }
327
+ if (deadline.exceeded()) {
328
+ stopReason = "deadlineReached";
329
+ break;
330
+ }
331
+ if (budget.remaining() - finalistReserve < stepCost) {
332
+ stopReason = "budgetExhausted";
333
+ break;
334
+ }
335
+ if (available().length === 0) {
336
+ stopReason = "reflectionBudgetExhausted";
337
+ break;
338
+ }
339
+ const pool = topKPlusBaseline({
340
+ scores: programs.map((_, index) => averageScore(index)),
341
+ k: candidateCount
342
+ });
343
+ onEvent?.({
344
+ type: "stepStart",
345
+ step,
346
+ poolSize: pool.length
347
+ });
348
+ const batch = sampler({
349
+ trainingSet,
350
+ iteration: step,
351
+ rng
352
+ }).map((index) => trainingSet[index]);
353
+ const samples = [];
354
+ for (let slot = 0; slot < candidateCount; slot += 1) {
355
+ const programIndex = softmaxSample({
356
+ pool,
357
+ temperature: samplingTemperature
358
+ });
359
+ const evaluation = await evaluator.evaluateTraced({
360
+ candidate: programs[programIndex],
361
+ batch,
362
+ split: "train",
363
+ phase: "minibatch",
364
+ candidateId: programIndex,
365
+ iteration: step
366
+ });
367
+ if (evaluation === null) break;
368
+ samples.push({
369
+ programIndex,
370
+ scores: evaluation.scores,
371
+ outputs: evaluation.outputs,
372
+ ...evaluation.feedback === void 0 ? {} : { feedback: evaluation.feedback }
373
+ });
374
+ programScores[programIndex].push(measuredMean(evaluation) ?? 0);
375
+ }
376
+ if (samples.length === 0) {
377
+ stopReason = "budgetExhausted";
378
+ break;
379
+ }
380
+ const buckets = buildBuckets({
381
+ batch,
382
+ samples
383
+ });
384
+ const allScores = samples.flatMap((sample) => [...sample.scores]);
385
+ const low = percentile(allScores, 10);
386
+ const high = percentile(allScores, 90);
387
+ const built = [];
388
+ for (const bucket of buckets) {
389
+ if (built.length >= candidateCount + 1) break;
390
+ const drawable = available();
391
+ if (drawable.length === 0) break;
392
+ const source = softmaxSample({
393
+ pool: topKPlusBaseline({
394
+ scores: programs.map((_, index) => averageScore(index)),
395
+ k: candidateCount
396
+ }),
397
+ temperature: candidateTemperature
398
+ });
399
+ const strategy = rng.pick(drawable);
400
+ const dropped = dropDemos(programs[source]);
401
+ let mutated;
402
+ try {
403
+ mutated = strategy === "appendDemo" ? appendDemo({
404
+ candidate: dropped,
405
+ bucket,
406
+ low
407
+ }) : await appendRule({
408
+ candidate: dropped,
409
+ bucket,
410
+ low,
411
+ high
412
+ });
413
+ } catch (err) {
414
+ onEvent?.({
415
+ type: "error",
416
+ step,
417
+ err
418
+ });
419
+ continue;
420
+ }
421
+ if (mutated === null || sameText(mutated, programs[source])) continue;
422
+ built.push({
423
+ candidate: mutated,
424
+ strategy,
425
+ source
426
+ });
427
+ }
428
+ let stepBest;
429
+ for (const entry of built) {
430
+ if (!budget.canAfford(batch.length)) {
431
+ stopReason = "budgetExhausted";
432
+ break;
433
+ }
434
+ let score;
435
+ try {
436
+ score = measuredMean(await evaluator.evaluate({
437
+ candidate: entry.candidate,
438
+ batch,
439
+ ids: batch.map((datum, index) => instanceId({
440
+ datum,
441
+ index
442
+ })),
443
+ split: "train",
444
+ phase: "minibatch",
445
+ candidateId: programs.length,
446
+ iteration: step
447
+ }));
448
+ } catch (err) {
449
+ if (err instanceof BudgetExhausted) {
450
+ stopReason = "budgetExhausted";
451
+ break;
452
+ }
453
+ throw err;
454
+ }
455
+ if (score === void 0) continue;
456
+ programs.push(entry.candidate);
457
+ programScores.push([score]);
458
+ onEvent?.({
459
+ type: "candidate",
460
+ step,
461
+ strategy: entry.strategy,
462
+ sourceProgram: entry.source,
463
+ minibatchScore: score
464
+ });
465
+ if (stepBest === void 0 || score > stepBest.score) stepBest = {
466
+ candidate: entry.candidate,
467
+ score
468
+ };
469
+ }
470
+ if (stepBest !== void 0) winners.push({
471
+ candidate: stepBest.candidate,
472
+ step: step + 1
473
+ });
474
+ await checkpoint(step + 1);
475
+ if (stopReason === "budgetExhausted") {
476
+ step += 1;
477
+ break;
478
+ }
479
+ }
480
+ if (signal?.aborted) stopReason = "aborted";
481
+ const finalists = [];
482
+ for (const index of evenlySpacedIndices({
483
+ length: winners.length,
484
+ count: candidateCount + 1
485
+ })) {
486
+ const winner = winners[index];
487
+ if (!budget.canAfford(evaluator.countUncached({
488
+ candidate: winner.candidate,
489
+ ids: validationIds,
490
+ split: "val"
491
+ }))) break;
492
+ let score;
493
+ try {
494
+ score = measuredMean(await evaluator.evaluate({
495
+ candidate: winner.candidate,
496
+ batch: validationSet,
497
+ ids: validationIds,
498
+ split: "val",
499
+ phase: index === 0 ? "seed" : "validation",
500
+ candidateId: index,
501
+ iteration: step
502
+ }));
503
+ } catch (err) {
504
+ if (err instanceof BudgetExhausted || signal?.aborted) break;
505
+ throw err;
506
+ }
507
+ if (score !== void 0) finalists.push({
508
+ candidate: winner.candidate,
509
+ score,
510
+ step: winner.step
511
+ });
512
+ }
513
+ const seedScore = finalists[0]?.score ?? 0;
514
+ finalists.sort((a, b) => b.score - a.score);
515
+ const best = finalists[0] ?? {
516
+ candidate: seedCandidate,
517
+ score: seedScore
518
+ };
519
+ let bestOutputs;
520
+ if (trackBestOutputs) bestOutputs = (await evaluator.evaluateTraced({
521
+ candidate: best.candidate,
522
+ batch: validationSet,
523
+ split: "val",
524
+ phase: "validation",
525
+ candidateId: null,
526
+ iteration: step
527
+ }))?.outputs;
528
+ const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
529
+ candidate: best.candidate,
530
+ batch: testSet,
531
+ ids: testIds,
532
+ split: "test",
533
+ phase: "test",
534
+ candidateId: null,
535
+ iteration: step,
536
+ charge: false
537
+ }));
538
+ onEvent?.({
539
+ type: "finish",
540
+ reason: stopReason,
541
+ bestScore: best.score,
542
+ metricCalls: budget.spent(),
543
+ ...testScore === void 0 ? {} : { testScore }
544
+ });
545
+ return {
546
+ bestCandidate: best.candidate,
547
+ bestScore: best.score,
548
+ usage: evaluator.usage(),
549
+ seedScore,
550
+ steps: step,
551
+ finalists,
552
+ reflectionCalls,
553
+ cacheHits: evaluator.cacheHits(),
554
+ metricCalls: budget.spent(),
555
+ snapshot: takeSnapshot(step),
556
+ ...bestOutputs === void 0 ? {} : { bestOutputs },
557
+ ...testScore === void 0 ? {} : {
558
+ testScore,
559
+ testMetricCalls: testSet?.length ?? 0
560
+ },
561
+ stopReason
562
+ };
563
+ /**
564
+ * The mutations still open. `appendRule` closes once the advice budget is
565
+ * spent; `appendDemo` never closes, because harvesting costs no model call.
566
+ */
567
+ function available() {
568
+ if (maxReflectionCalls === void 0 || reflectionCalls < maxReflectionCalls) return enabled;
569
+ return enabled.filter((strategy) => strategy !== "appendRule");
570
+ }
571
+ function averageScore(index) {
572
+ const observed = programScores[index] ?? [];
573
+ if (observed.length === 0) return 0;
574
+ return observed.reduce((total, score) => total + score, 0) / observed.length;
575
+ }
576
+ function softmaxSample(args) {
577
+ const { pool, temperature } = args;
578
+ return rng.weighted(pool, softmaxWeights(pool.map((index) => averageScore(index)), temperature));
579
+ }
580
+ /**
581
+ * Drops a Poisson-distributed number of demos, always at least one once the
582
+ * block is at its ceiling. Sampled with replacement, matching the reference:
583
+ * a repeated draw drops fewer than the count suggests, which biases the loop
584
+ * toward keeping demos rather than shedding them.
585
+ */
586
+ function dropDemos(candidate) {
587
+ if (demoComponents.length === 0) return { ...candidate };
588
+ const blocks = new Map(demoComponents.map((name) => [name, parseDemos(candidate[name] ?? "")]));
589
+ const held = Math.max(...[...blocks.values()].map((demos) => demos.length));
590
+ if (held === 0) return { ...candidate };
591
+ const scale = maxDemos > 0 ? maxDemos : DEMO_DROP_SCALE;
592
+ const wanted = Math.max(samplePoisson(rng, held / scale), held >= scale ? 1 : 0);
593
+ const drops = /* @__PURE__ */ new Set();
594
+ for (let draw = 0; draw < Math.min(wanted, held); draw += 1) drops.add(rng.nextInt(held));
595
+ const next = { ...candidate };
596
+ for (const [name, demos] of blocks) next[name] = formatDemos(demos.filter((_, index) => !drops.has(index)), renderDemo === void 0 ? {} : { render: renderDemo });
597
+ return next;
598
+ }
599
+ /**
600
+ * Keep the winning rollout of this instance as a demonstration.
601
+ *
602
+ * Declines when the winner is below the batch's tenth percentile: a demo is
603
+ * an assertion that this is what good looks like, and the worst rollouts of a
604
+ * bad step are not that.
605
+ */
606
+ function appendDemo(args) {
607
+ const { candidate, bucket, low } = args;
608
+ const winner = bucket.rollouts[0];
609
+ if (winner === void 0 || winner.score < low || winner.output === void 0) return null;
610
+ const demo = {
611
+ input: bucket.datum,
612
+ output: winner.output,
613
+ score: winner.score
614
+ };
615
+ const next = { ...candidate };
616
+ for (const name of demoComponents) {
617
+ const kept = parseDemos(next[name] ?? "");
618
+ next[name] = formatDemos([...kept, demo], renderDemo === void 0 ? {} : { render: renderDemo });
619
+ }
620
+ return next;
621
+ }
622
+ /**
623
+ * Ask the reflection model what the better run did differently, and append
624
+ * its answer to each instruction component.
625
+ *
626
+ * Declines when the contrast is not informative: a winner below the tenth
627
+ * percentile is not a success to imitate, and a loser above the ninetieth is
628
+ * not a failure to avoid. When the two runs tied, the uninformative side is
629
+ * withheld and the model advises from one trajectory — a tie at a low score
630
+ * is shown as a failure, a tie at a high score as a success.
631
+ */
632
+ async function appendRule(args) {
633
+ const { candidate, bucket, low, high } = args;
634
+ if (reflect === void 0) throw new Error("the appendRule strategy requires a reflect model");
635
+ const good = bucket.rollouts[0];
636
+ const bad = bucket.rollouts[bucket.rollouts.length - 1];
637
+ if (good === void 0 || bad === void 0) return null;
638
+ if (good.score < low || bad.score > high) return null;
639
+ const tied = good.score <= bad.score;
640
+ const better = tied && good.score <= high ? void 0 : trajectory(good);
641
+ const worse = tied && good.score > high ? void 0 : trajectory(bad);
642
+ if (better === void 0 && worse === void 0) return null;
643
+ const response = await reflect({
644
+ prompt: buildPrompt({
645
+ components: ruleComponents,
646
+ input: bucket.datum,
647
+ ...better === void 0 ? {} : { better },
648
+ ...worse === void 0 ? {} : { worse }
649
+ }),
650
+ ...signal === void 0 ? {} : { signal }
651
+ });
652
+ reflectionCalls += 1;
653
+ const advice = parseAdvice(response);
654
+ const next = { ...candidate };
655
+ let changed = false;
656
+ for (const name of ruleComponents) {
657
+ const rule = advice[name];
658
+ if (rule === void 0) continue;
659
+ next[name] = [next[name] ?? "", rule].filter(Boolean).join("\n\n");
660
+ changed = true;
661
+ }
662
+ return changed ? next : null;
663
+ }
664
+ function trajectory(rollout) {
665
+ return {
666
+ output: rollout.output,
667
+ score: rollout.score,
668
+ ...rollout.feedback === void 0 ? {} : { feedback: rollout.feedback }
669
+ };
670
+ }
671
+ function sameText(a, b) {
672
+ return components.every((name) => a[name] === b[name]);
673
+ }
674
+ function takeSnapshot(completed) {
675
+ const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
676
+ return {
677
+ version: 1,
678
+ fingerprint,
679
+ programs: [...programs],
680
+ programScores: programScores.map((scores) => [...scores]),
681
+ winners: [...winners],
682
+ step: completed,
683
+ metricCalls: budget.spent(),
684
+ reflectionCalls,
685
+ cacheHits: evaluator.cacheHits(),
686
+ rngState: rng.state(),
687
+ ...sampler.state === void 0 ? {} : { sampler: sampler.state() },
688
+ ...cached === void 0 ? {} : { cache: cached }
689
+ };
690
+ }
691
+ async function checkpoint(completed) {
692
+ if (onCheckpoint === void 0) return;
693
+ await onCheckpoint(takeSnapshot(completed));
694
+ }
695
+ }
696
+ function defaultInstanceId(args) {
697
+ return String(args.index);
698
+ }
699
+ //#endregion
700
+ export { SimbaOptimizer, buildAdvicePrompt, buildBuckets, evenlySpacedIndices, parseAdvice, percentile, samplePoisson, softmaxWeights, topKPlusBaseline };