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