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,739 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_evaluation = require("../evaluation-OZOp6TB7.cjs");
3
+ const require_concurrency = require("../concurrency-C-cFzWW2.cjs");
4
+ const require_math = require("../math-COOofUyv.cjs");
5
+ const require_demos = require("../demos-B9BJiNKz.cjs");
6
+ const require_text = require("../text-CK_HB3su.cjs");
7
+ const require_rng = require("../rng-DbA_rPIo.cjs");
8
+ const require_sampling = require("../sampling-Dars7ctR.cjs");
9
+ //#region src/mipro/tpe.ts
10
+ const DEFAULT_SAMPLES = 24;
11
+ const GOOD_FRACTION = .1;
12
+ const MAX_GOOD = 25;
13
+ const DEFAULT_STARTUP = 10;
14
+ const DEFAULT_PRIOR_WEIGHT = 1;
15
+ /**
16
+ * Tree-structured Parzen Estimator over a categorical space.
17
+ *
18
+ * The search space here is a menu index per component. A TPE splits the
19
+ * observations into the good ones and the rest, models the density of each,
20
+ * and proposes the configuration that maximizes their ratio: sample where
21
+ * good configurations live, prefer where bad ones do not.
22
+ *
23
+ * The densities are mixtures with one kernel centred on each observation,
24
+ * rather than one histogram per component. That distinction is the whole
25
+ * reason this optimizer exists. Per-component histograms only record how often
26
+ * an option appears among good trials, so a space where every option is
27
+ * equally common and only the *pairing* matters looks completely flat to them.
28
+ * A kernel centred on an observed configuration keeps its components together,
29
+ * so "B works, but only alongside A" survives into the proposal. It is what
30
+ * Optuna calls a multivariate sampler, and what MIPROv2 turns on.
31
+ *
32
+ * What this buys over sampling every combination: a candidate with five
33
+ * components and four options each has 1024 configurations and a budget for
34
+ * perhaps thirty evaluations. Per-component hill climbing cannot see
35
+ * interactions at all; enumerating cannot afford to. This can do both, at the
36
+ * cost of being an estimate.
37
+ */
38
+ function proposeConfiguration(args) {
39
+ const { observations, menuSizes, gamma, samples = DEFAULT_SAMPLES, startupTrials = DEFAULT_STARTUP, priorWeight = DEFAULT_PRIOR_WEIGHT, multivariate = true, rng } = args;
40
+ for (const size of menuSizes) if (!Number.isInteger(size) || size < 1) throw new Error(`every component needs a menu of at least one option, received ${size}`);
41
+ if (observations.length < startupTrials) return menuSizes.map((size) => rng.nextInt(size));
42
+ const scores = observations.map((observation) => observation.score);
43
+ if (Math.max(...scores) === Math.min(...scores)) return menuSizes.map((size) => rng.nextInt(size));
44
+ const ranked = [...observations].sort((a, b) => b.score - a.score);
45
+ const requested = gamma === void 0 ? Math.min(Math.ceil(GOOD_FRACTION * ranked.length), MAX_GOOD) : Math.floor(gamma * ranked.length);
46
+ const goodCount = Math.min(ranked.length - 1, Math.max(1, requested));
47
+ const good = ranked.slice(0, goodCount);
48
+ const bad = ranked.slice(goodCount);
49
+ let best;
50
+ let bestScore = Number.NEGATIVE_INFINITY;
51
+ const goodModel = fit({
52
+ observations: good,
53
+ menuSizes,
54
+ priorWeight,
55
+ multivariate
56
+ });
57
+ const badModel = fit({
58
+ observations: bad,
59
+ menuSizes,
60
+ priorWeight,
61
+ multivariate
62
+ });
63
+ for (let sample = 0; sample < samples; sample += 1) {
64
+ const choices = goodModel.sample(rng);
65
+ const score = Math.log(goodModel.density(choices)) - Math.log(badModel.density(choices));
66
+ if (score > bestScore) {
67
+ bestScore = score;
68
+ best = choices;
69
+ }
70
+ }
71
+ return best;
72
+ }
73
+ /**
74
+ * Builds the density over a set of observations.
75
+ *
76
+ * Joint: a mixture with one kernel centred on each observation, plus a uniform
77
+ * kernel. Because a kernel keeps an observation's components together, the
78
+ * mixture assigns high density to combinations that were seen together — which
79
+ * is what carries a dependency between components.
80
+ *
81
+ * Independent: one smoothed histogram per component, multiplied. Evidence
82
+ * about a component generalizes across every combination it appears in, which
83
+ * converges far faster when the components really are independent, and cannot
84
+ * express a dependency at all.
85
+ *
86
+ * Both keep a uniform component — Optuna's `consider_prior` — so an option
87
+ * nobody has drawn never falls to zero probability and stays reachable.
88
+ */
89
+ function fit(args) {
90
+ const { observations, menuSizes, priorWeight, multivariate } = args;
91
+ const kernelCount = observations.length + 1;
92
+ if (multivariate) return {
93
+ density: (choices) => {
94
+ let total = uniformKernel(menuSizes);
95
+ for (const observation of observations) total += kernel({
96
+ choices,
97
+ center: observation.choices,
98
+ menuSizes,
99
+ priorWeight,
100
+ kernelCount
101
+ });
102
+ return total / kernelCount;
103
+ },
104
+ sample: (rng) => {
105
+ const picked = rng.nextInt(kernelCount);
106
+ if (picked === observations.length) return menuSizes.map((size) => rng.nextInt(size));
107
+ const center = observations[picked].choices;
108
+ const share = priorWeight / kernelCount;
109
+ return menuSizes.map((size, component) => drawWeighted(Array.from({ length: size }, (_, option) => option === center[component] ? share + 1 : share), rng));
110
+ }
111
+ };
112
+ const histograms = menuSizes.map((size, component) => {
113
+ const counts = new Array(size).fill(priorWeight);
114
+ for (const observation of observations) {
115
+ const choice = observation.choices[component];
116
+ if (choice !== void 0 && choice < size) counts[choice] = counts[choice] + 1;
117
+ }
118
+ const total = observations.length + priorWeight * size;
119
+ return counts.map((count) => count / total);
120
+ });
121
+ return {
122
+ density: (choices) => {
123
+ let product = 1;
124
+ for (let component = 0; component < histograms.length; component += 1) {
125
+ const histogram = histograms[component];
126
+ product *= histogram[choices[component]];
127
+ }
128
+ return product;
129
+ },
130
+ sample: (rng) => histograms.map((histogram) => drawWeighted(histogram, rng))
131
+ };
132
+ }
133
+ /**
134
+ * One kernel of the mixture, centred on a single observation.
135
+ *
136
+ * The prior is spread across the kernels rather than added whole to each, so
137
+ * `share` shrinks as the mixture grows and a kernel sits more sharply on what
138
+ * it observed: matched and unmatched options stand at `1 + kernelCount /
139
+ * priorWeight`, which is 2:1 for a lone kernel and 13:1 once twelve
140
+ * observations back it. Optuna spells the same thing as a weight matrix filled
141
+ * with `prior_weight / n_kernels` and incremented at each observed choice
142
+ * (`_calculate_categorical_distributions`); dividing the prior by the count is
143
+ * what makes accumulated evidence narrow the density instead of leaving it as
144
+ * flat as it was on the first trial.
145
+ */
146
+ function kernel(args) {
147
+ const { choices, center, menuSizes, priorWeight, kernelCount } = args;
148
+ const share = priorWeight / kernelCount;
149
+ let product = 1;
150
+ for (let component = 0; component < menuSizes.length; component += 1) {
151
+ const size = menuSizes[component];
152
+ const matched = choices[component] === center[component];
153
+ product *= (share + (matched ? 1 : 0)) / (share * size + 1);
154
+ }
155
+ return product;
156
+ }
157
+ function uniformKernel(menuSizes) {
158
+ let product = 1;
159
+ for (const size of menuSizes) product /= size;
160
+ return product;
161
+ }
162
+ function drawWeighted(weights, rng) {
163
+ return rng.weighted(weights.map((_, option) => option), weights);
164
+ }
165
+ //#endregion
166
+ //#region src/mipro/optimize.ts
167
+ const DEFAULT_INSTRUCTIONS = 3;
168
+ const DEFAULT_MINIBATCH_SIZE = 35;
169
+ const DEFAULT_MAX_TRIALS = 30;
170
+ const DEFAULT_FULL_EVAL_INTERVAL = 5;
171
+ const DEFAULT_DEMO_SETS = 3;
172
+ const DEFAULT_MAX_DEMOS = 4;
173
+ const DEFAULT_EXEMPLARS = 3;
174
+ const DEFAULT_SUMMARY_EXAMPLES = 10;
175
+ /**
176
+ * Style hints, one per generated instruction. Drawing four instructions from
177
+ * one prompt yields four rewordings of one idea; varying the hint is what
178
+ * makes the menu a spread of approaches instead.
179
+ */
180
+ const DEFAULT_TIPS = [
181
+ "Be concise. Say only what changes the output.",
182
+ "Be specific and detailed. Spell out the edge cases and the output format.",
183
+ "Describe the reasoning the component should do before it answers.",
184
+ "State the constraints as hard rules the component must never break.",
185
+ "Write it as a role description: who the component is and what it cares about."
186
+ ];
187
+ /**
188
+ * Joint search over a fixed menu, guided by a surrogate.
189
+ *
190
+ * The gap this fills is interaction between components. Reflective search
191
+ * updates one component per iteration and screens it in isolation, so a pair
192
+ * of components that only pay off together is invisible to it — a routing rule
193
+ * and the prompt it routes to, an output format and the instruction that
194
+ * assumes it. Merge recombines lineages after the fact but never proposes a
195
+ * joint move.
196
+ *
197
+ * This search proposes a menu of options per component up front, then treats
198
+ * the choice of one option per component as a single categorical
199
+ * configuration and lets a TPE decide which to spend a trial on. Trials run on
200
+ * minibatches; a configuration that beats the best minibatch reading earns a
201
+ * full sweep before it can become the incumbent, so the number reported is
202
+ * never a lucky minibatch.
203
+ *
204
+ * What it gives up is the ability to write text it did not think of at the
205
+ * start. The menu is fixed at trial one — reflective search keeps writing new
206
+ * text for the whole run. Neither dominates; they fail differently.
207
+ */
208
+ var MiproOptimizer = class {
209
+ #config;
210
+ constructor(config = {}) {
211
+ assertConfig(config);
212
+ this.#config = config;
213
+ }
214
+ async optimize(task) {
215
+ return runMipro({
216
+ config: this.#config,
217
+ task
218
+ });
219
+ }
220
+ };
221
+ function buildMiproPrompt(args) {
222
+ const { componentName, seedText, exemplars, tip, siblings = {}, datasetSummary } = args;
223
+ const others = Object.entries(siblings).filter(([, text]) => text.length > 0);
224
+ return [
225
+ `I am writing the "${componentName}" component of a larger system. Here is the instruction it currently uses:`,
226
+ "",
227
+ "<current_instruction>",
228
+ seedText,
229
+ "</current_instruction>",
230
+ ...others.length === 0 ? [] : [
231
+ "",
232
+ "The rest of the system reads as follows. Write something that fits alongside it rather than repeating or contradicting it:",
233
+ "",
234
+ "<system>",
235
+ others.map(([name, text]) => `<${name}>\n${text}\n</${name}>`).join("\n"),
236
+ "</system>"
237
+ ],
238
+ ...datasetSummary === void 0 ? [] : [
239
+ "",
240
+ "Here is what the data it runs on looks like:",
241
+ "",
242
+ "<dataset_summary>",
243
+ datasetSummary,
244
+ "</dataset_summary>"
245
+ ],
246
+ ...exemplars.length === 0 ? [] : [
247
+ "",
248
+ "Here are examples of the inputs this component receives:",
249
+ "",
250
+ "<inputs>",
251
+ exemplars.join("\n\n"),
252
+ "</inputs>"
253
+ ],
254
+ "",
255
+ "Write an alternative instruction for this component — a different approach to the same job, not an edit of the one above.",
256
+ `Follow this style: ${tip}`,
257
+ "",
258
+ "Return only the new instruction, inside a ``` block."
259
+ ].join("\n");
260
+ }
261
+ function buildDatasetSummaryPrompt(examples) {
262
+ return [
263
+ "Below are entries from a dataset a system is being tuned against.",
264
+ "",
265
+ "<examples>",
266
+ examples.join("\n\n"),
267
+ "</examples>",
268
+ "",
269
+ "Describe what this dataset is: what the inputs are, what varies between them, and what answering one well requires. Two or three sentences, concrete rather than generic.",
270
+ "",
271
+ "Return only the description, inside a ``` block."
272
+ ].join("\n");
273
+ }
274
+ async function runMipro(args) {
275
+ const { config, task } = args;
276
+ const { instructionsPerComponent = DEFAULT_INSTRUCTIONS, minibatchSize = DEFAULT_MINIBATCH_SIZE, maxTrials = DEFAULT_MAX_TRIALS, startupTrials, gamma, surrogateSamples, multivariate, fullEvalInterval = DEFAULT_FULL_EVAL_INTERVAL, demoSets = DEFAULT_DEMO_SETS, maxDemos = DEFAULT_MAX_DEMOS, demoMinScore, exemplars = DEFAULT_EXEMPLARS, datasetSummary = true, summaryExamples = DEFAULT_SUMMARY_EXAMPLES, concurrency = 1, seed = 0, buildPrompt = buildMiproPrompt, tips = DEFAULT_TIPS, trackBestOutputs = false, checkpointCache = true } = config;
277
+ const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, componentOptions, demoComponents, renderDemo, goldOutput, maxMetricCalls, renderDatum = renderDefault, batchSampler = require_sampling.createEpochShuffledSampler({ minibatchSize }), cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
278
+ const deadline = require_evaluation.createDeadline({ maxWallClockMs });
279
+ const components = require_evaluation.componentNames(seedCandidate);
280
+ if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
281
+ if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
282
+ if (components.length === 0) throw new Error("optimize requires a seed candidate with at least one component");
283
+ 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");
284
+ for (const name of demoComponents ?? []) if (componentOptions?.[name] !== void 0) throw new Error(`component "${name}" is listed in both demoComponents and componentOptions; pick one source for its menu`);
285
+ const trainingIds = trainingSet.map((datum, index) => instanceId({
286
+ datum,
287
+ index
288
+ }));
289
+ const validationIds = validationSet.map((datum, index) => instanceId({
290
+ datum,
291
+ index
292
+ }));
293
+ const testIds = testSet?.map((datum, index) => instanceId({
294
+ datum,
295
+ index
296
+ })) ?? [];
297
+ const fingerprint = require_evaluation.runFingerprint({
298
+ seedCandidate,
299
+ trainingIds,
300
+ validationIds,
301
+ seed,
302
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace }
303
+ });
304
+ require_evaluation.assertResumable({
305
+ fingerprint,
306
+ ...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
307
+ });
308
+ const rng = require_rng.createSeededRng(seed, resumeFrom?.rngState);
309
+ const budget = require_evaluation.createBudget({
310
+ maxMetricCalls,
311
+ spent: resumeFrom?.metricCalls ?? 0
312
+ });
313
+ const evaluationCache = cache === false ? void 0 : cache ?? require_evaluation.createMemoryCache();
314
+ const evaluator = require_evaluation.createEvaluator({
315
+ adapter,
316
+ budget,
317
+ ...retry === void 0 ? {} : { retry },
318
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace },
319
+ ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
320
+ trackOutputs: trackBestOutputs,
321
+ cacheHits: resumeFrom?.cacheHits ?? 0,
322
+ ...signal === void 0 ? {} : { signal },
323
+ onEvaluation: (event) => onEvent?.({
324
+ type: "evaluation",
325
+ ...event
326
+ })
327
+ });
328
+ evaluator.restore(resumeFrom?.cache ?? []);
329
+ if (resumeFrom?.sampler !== void 0) batchSampler.restore?.(resumeFrom.sampler);
330
+ onEvent?.({
331
+ type: "start",
332
+ components,
333
+ validationSetSize: validationSet.length
334
+ });
335
+ const shown = rng.sample(trainingSet, Math.min(exemplars, trainingSet.length)).map(renderDatum);
336
+ let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
337
+ const menu = {};
338
+ const demoNames = new Set(demoComponents ?? []);
339
+ const proposing = components.some((name) => !demoNames.has(name) && componentOptions?.[name] === void 0);
340
+ let summary;
341
+ if (resumeFrom === void 0 && datasetSummary && proposing && trainingSet.length > 0) {
342
+ reflectionCalls += 1;
343
+ summary = require_text.parseProposedText(await reflect({
344
+ prompt: buildDatasetSummaryPrompt(rng.sample(trainingSet, Math.min(summaryExamples, trainingSet.length)).map(renderDatum)),
345
+ ...signal === void 0 ? {} : { signal }
346
+ }));
347
+ }
348
+ let bootstrapMetricCalls = resumeFrom?.bootstrapMetricCalls ?? 0;
349
+ /**
350
+ * Builds a demo component's menu from rollouts the metric rewarded.
351
+ *
352
+ * Each set gets its own harvesting pass over a freshly shuffled training set, as
353
+ * MIPROv2 does. Drawing every set from a single pool would be cheaper and
354
+ * identical under deterministic scoring, but a system that answers at
355
+ * temperature does not give the same verdict twice: a second pass can turn a
356
+ * previously failing example into a demo, and one pass can never show it.
357
+ * Sizes vary across the sets because more demos is not monotonically better
358
+ * — a long block crowds out the instruction, and which length wins is
359
+ * exactly what the search settles.
360
+ *
361
+ * Not covered here: MIPROv2 also builds label-only sets from gold outputs,
362
+ * and pads bootstrapped sets with them. A gold output is something only the
363
+ * adapter knows, so there is nothing generic for this to read.
364
+ */
365
+ async function bootstrapMenu(name) {
366
+ const blocks = [];
367
+ if (goldOutput !== void 0) {
368
+ const labelled = rng.shuffle(trainingSet).map((datum) => ({
369
+ input: datum,
370
+ output: goldOutput(datum)
371
+ })).filter((demo) => demo.output !== void 0).slice(0, maxDemos);
372
+ if (labelled.length > 0) blocks.push(require_demos.formatDemos(labelled, renderDemo === void 0 ? {} : { render: renderDemo }));
373
+ }
374
+ for (let index = 0; index < demoSets; index += 1) {
375
+ const affordable = Math.min(trainingSet.length, budget.remaining() - validationSet.length);
376
+ if (affordable < 1) break;
377
+ const requested = demoSets === 1 ? maxDemos : Math.round(1 + index * (maxDemos - 1) / (demoSets - 1));
378
+ const harvest = await require_demos.bootstrapDemos({
379
+ adapter,
380
+ candidate: seedCandidate,
381
+ trainingSet,
382
+ ...demoMinScore === void 0 ? {} : { minScore: demoMinScore },
383
+ maxDemos: requested,
384
+ maxMetricCalls: affordable,
385
+ rng,
386
+ ...renderDemo === void 0 ? {} : { renderDemo },
387
+ ...signal === void 0 ? {} : { signal }
388
+ });
389
+ bootstrapMetricCalls += harvest.metricCalls;
390
+ budget.reserve(harvest.metricCalls);
391
+ if (harvest.demos.length > 0) blocks.push(require_demos.formatDemos(harvest.demos, renderDemo === void 0 ? {} : { render: renderDemo }));
392
+ }
393
+ return [.../* @__PURE__ */ new Set([
394
+ seedCandidate[name],
395
+ "",
396
+ ...blocks
397
+ ])].filter((text, index) => index === 0 || text.length === 0 || text.includes("<demo>"));
398
+ }
399
+ for (const name of components) {
400
+ const supplied = componentOptions?.[name];
401
+ const restored = resumeFrom?.menu[name];
402
+ if (restored !== void 0) {
403
+ menu[name] = [...restored];
404
+ continue;
405
+ }
406
+ if (demoNames.has(name)) {
407
+ menu[name] = await bootstrapMenu(name);
408
+ continue;
409
+ }
410
+ if (supplied !== void 0) {
411
+ menu[name] = [seedCandidate[name], ...supplied];
412
+ continue;
413
+ }
414
+ const drawn = await require_concurrency.mapWithConcurrency({
415
+ items: Array.from({ length: instructionsPerComponent }, (_, index) => index),
416
+ limit: concurrency,
417
+ signal,
418
+ task: async (index) => {
419
+ reflectionCalls += 1;
420
+ return require_text.parseProposedText(await reflect({
421
+ prompt: buildPrompt({
422
+ componentName: name,
423
+ seedText: seedCandidate[name],
424
+ exemplars: shown,
425
+ tip: tips[index % tips.length],
426
+ ...summary === void 0 || summary.length === 0 ? {} : { datasetSummary: summary },
427
+ siblings: Object.fromEntries(components.filter((other) => other !== name).map((other) => [other, seedCandidate[other]]))
428
+ }),
429
+ ...signal === void 0 ? {} : { signal }
430
+ }));
431
+ }
432
+ });
433
+ menu[name] = [seedCandidate[name], ...drawn.filter((text) => text.length > 0)];
434
+ }
435
+ onEvent?.({
436
+ type: "menu",
437
+ menu,
438
+ reflectionCalls
439
+ });
440
+ const menuSizes = components.map((name) => menu[name].length);
441
+ function candidateFor(choices) {
442
+ const candidate = {};
443
+ components.forEach((name, index) => {
444
+ candidate[name] = menu[name][choices[index]];
445
+ });
446
+ return candidate;
447
+ }
448
+ let trial = resumeFrom?.trial ?? 0;
449
+ let fullEvaluations = resumeFrom?.fullEvaluations ?? 0;
450
+ const observations = [...resumeFrom?.observations ?? []];
451
+ const surrogateInput = [...resumeFrom?.surrogateInput ?? []];
452
+ let stopReason = "maxTrials";
453
+ async function fullSweep(candidate, phase) {
454
+ return evaluator.evaluate({
455
+ candidate,
456
+ batch: validationSet,
457
+ ids: validationIds,
458
+ split: "val",
459
+ phase,
460
+ candidateId: null,
461
+ iteration: trial
462
+ });
463
+ }
464
+ const seedEvaluation = resumeFrom === void 0 ? await fullSweep(seedCandidate, "seed") : void 0;
465
+ const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : require_evaluation.requireMeasuredMean({
466
+ batch: seedEvaluation,
467
+ phase: "seed"
468
+ });
469
+ if (seedEvaluation !== void 0) fullEvaluations += 1;
470
+ if (resumeFrom === void 0) surrogateInput.push({
471
+ choices: menuSizes.map(() => 0),
472
+ score: seedScore
473
+ });
474
+ let best = resumeFrom?.best ?? seedCandidate;
475
+ let bestScore = resumeFrom?.bestScore ?? seedScore;
476
+ /** Absent on a resumed run until a sweep wins: outputs are not checkpointed. */
477
+ let bestOutputs = seedEvaluation?.outputs;
478
+ const readings = new Map(resumeFrom?.readings ?? []);
479
+ const swept = new Set(resumeFrom?.swept ?? []);
480
+ function takeSnapshot() {
481
+ const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
482
+ const samplerState = batchSampler.state?.();
483
+ return {
484
+ version: 1,
485
+ fingerprint,
486
+ menu: { ...menu },
487
+ best,
488
+ bestScore,
489
+ seedScore,
490
+ trial,
491
+ fullEvaluations,
492
+ reflectionCalls,
493
+ bootstrapMetricCalls,
494
+ metricCalls: budget.spent(),
495
+ cacheHits: evaluator.cacheHits(),
496
+ rngState: rng.state(),
497
+ observations: [...observations],
498
+ surrogateInput: surrogateInput.map((entry) => ({
499
+ choices: [...entry.choices],
500
+ score: entry.score
501
+ })),
502
+ readings: [...readings].map(([key, values]) => [key, [...values]]),
503
+ swept: [...swept],
504
+ ...samplerState === void 0 ? {} : { sampler: samplerState },
505
+ ...cached === void 0 ? {} : { cache: cached }
506
+ };
507
+ }
508
+ async function checkpoint() {
509
+ if (onCheckpoint === void 0) return;
510
+ await onCheckpoint(takeSnapshot());
511
+ }
512
+ await checkpoint();
513
+ /**
514
+ * Full-evaluates the strongest configuration by mean minibatch reading that
515
+ * has not been swept yet. Only a full sweep can move the incumbent, so a
516
+ * lucky minibatch buys a candidate a closer look and nothing more.
517
+ */
518
+ async function sweepBestUnswept() {
519
+ let bestKey;
520
+ let bestMean = Number.NEGATIVE_INFINITY;
521
+ for (const [key, values] of readings) {
522
+ if (swept.has(key)) continue;
523
+ const value = require_math.mean(values);
524
+ if (value > bestMean) {
525
+ bestMean = value;
526
+ bestKey = key;
527
+ }
528
+ }
529
+ if (bestKey === void 0) return "none";
530
+ if (!budget.canAfford(validationSet.length)) return "unaffordable";
531
+ const choices = bestKey.split(",").map(Number);
532
+ const candidate = candidateFor(choices);
533
+ swept.add(bestKey);
534
+ let evaluation;
535
+ try {
536
+ evaluation = await fullSweep(candidate, "validation");
537
+ } catch (err) {
538
+ if (err instanceof require_evaluation.BudgetExhausted) return "budgetExhausted";
539
+ if (signal?.aborted) return "aborted";
540
+ throw err;
541
+ }
542
+ fullEvaluations += 1;
543
+ const score = require_evaluation.measuredMean(evaluation);
544
+ if (score === void 0) return "swept";
545
+ surrogateInput.push({
546
+ choices,
547
+ score
548
+ });
549
+ for (const observation of observations) if (observation.choices.join(",") === bestKey) {
550
+ observation.promoted = true;
551
+ observation.score = score;
552
+ }
553
+ if (score > bestScore) {
554
+ best = candidate;
555
+ bestScore = score;
556
+ bestOutputs = evaluation.outputs;
557
+ onEvent?.({
558
+ type: "incumbent",
559
+ trial,
560
+ choices,
561
+ score
562
+ });
563
+ }
564
+ return "swept";
565
+ }
566
+ while (trial < maxTrials) {
567
+ if (signal?.aborted) {
568
+ stopReason = "aborted";
569
+ break;
570
+ }
571
+ if (require_evaluation.costExhausted({
572
+ usage: evaluator.usage(),
573
+ maxCostUsd
574
+ })) {
575
+ stopReason = "costExhausted";
576
+ break;
577
+ }
578
+ if (deadline.exceeded()) {
579
+ stopReason = "deadlineReached";
580
+ break;
581
+ }
582
+ if (!budget.canAfford(minibatchSize)) {
583
+ stopReason = "budgetExhausted";
584
+ break;
585
+ }
586
+ if (!budget.canAfford(validationSet.length)) {
587
+ stopReason = "budgetExhausted";
588
+ break;
589
+ }
590
+ const choices = proposeConfiguration({
591
+ observations: surrogateInput,
592
+ menuSizes,
593
+ ...gamma === void 0 ? {} : { gamma },
594
+ ...surrogateSamples === void 0 ? {} : { samples: surrogateSamples },
595
+ ...startupTrials === void 0 ? {} : { startupTrials },
596
+ ...multivariate === void 0 ? {} : { multivariate },
597
+ rng
598
+ });
599
+ const candidate = candidateFor(choices);
600
+ const batchIndices = batchSampler({
601
+ trainingSet,
602
+ iteration: trial,
603
+ rng
604
+ });
605
+ let minibatchScore;
606
+ try {
607
+ const evaluation = await evaluator.evaluate({
608
+ candidate,
609
+ batch: batchIndices.map((index) => trainingSet[index]),
610
+ ids: batchIndices.map((index) => trainingIds[index]),
611
+ split: "train",
612
+ phase: "minibatch",
613
+ candidateId: null,
614
+ iteration: trial
615
+ });
616
+ minibatchScore = require_evaluation.measuredMean(evaluation);
617
+ } catch (err) {
618
+ if (err instanceof require_evaluation.BudgetExhausted) {
619
+ stopReason = "budgetExhausted";
620
+ break;
621
+ }
622
+ if (signal?.aborted) {
623
+ stopReason = "aborted";
624
+ break;
625
+ }
626
+ throw err;
627
+ }
628
+ if (minibatchScore === void 0) {
629
+ trial += 1;
630
+ continue;
631
+ }
632
+ surrogateInput.push({
633
+ choices,
634
+ score: minibatchScore
635
+ });
636
+ const key = choices.join(",");
637
+ readings.set(key, [...readings.get(key) ?? [], minibatchScore]);
638
+ const observation = {
639
+ trial,
640
+ choices,
641
+ minibatchScore,
642
+ promoted: false
643
+ };
644
+ observations.push(observation);
645
+ onEvent?.({
646
+ type: "trial",
647
+ trial,
648
+ choices,
649
+ minibatchScore,
650
+ promoted: false
651
+ });
652
+ trial += 1;
653
+ await checkpoint();
654
+ if (trial % fullEvalInterval === 0) {
655
+ const outcome = await sweepBestUnswept();
656
+ if (outcome === "budgetExhausted" || outcome === "aborted") {
657
+ stopReason = outcome;
658
+ break;
659
+ }
660
+ if (outcome === "unaffordable") {
661
+ stopReason = "budgetExhausted";
662
+ break;
663
+ }
664
+ }
665
+ }
666
+ if (stopReason === "maxTrials" && !signal?.aborted) await sweepBestUnswept();
667
+ if (signal?.aborted) stopReason = "aborted";
668
+ const testScore = testSet === void 0 ? void 0 : require_evaluation.measuredMean(await evaluator.evaluate({
669
+ candidate: best,
670
+ batch: testSet,
671
+ ids: testIds,
672
+ split: "test",
673
+ phase: "test",
674
+ candidateId: null,
675
+ iteration: trial,
676
+ charge: false
677
+ }));
678
+ onEvent?.({
679
+ type: "finish",
680
+ reason: stopReason,
681
+ bestScore,
682
+ metricCalls: budget.spent(),
683
+ ...testScore === void 0 ? {} : { testScore }
684
+ });
685
+ return {
686
+ snapshot: takeSnapshot(),
687
+ bestCandidate: best,
688
+ bestScore,
689
+ usage: evaluator.usage(),
690
+ seedScore,
691
+ ...trackBestOutputs ? { bestOutputs } : {},
692
+ ...testScore === void 0 ? {} : {
693
+ testScore,
694
+ testMetricCalls: evaluator.unchargedCalls()
695
+ },
696
+ trials: trial,
697
+ menu,
698
+ observations,
699
+ fullEvaluations,
700
+ bootstrapMetricCalls,
701
+ metricCalls: budget.spent(),
702
+ reflectionCalls,
703
+ cacheHits: evaluator.cacheHits(),
704
+ stopReason
705
+ };
706
+ }
707
+ function renderDefault(datum) {
708
+ if (typeof datum === "string") return datum;
709
+ try {
710
+ return JSON.stringify(datum, null, 2) ?? String(datum);
711
+ } catch {
712
+ return String(datum);
713
+ }
714
+ }
715
+ function assertConfig(config) {
716
+ const positive = [
717
+ ["minibatchSize", config.minibatchSize],
718
+ ["maxTrials", config.maxTrials],
719
+ ["surrogateSamples", config.surrogateSamples],
720
+ ["concurrency", config.concurrency]
721
+ ];
722
+ 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}`);
723
+ const nonNegative = [
724
+ ["instructionsPerComponent", config.instructionsPerComponent],
725
+ ["startupTrials", config.startupTrials],
726
+ ["exemplars", config.exemplars]
727
+ ];
728
+ for (const [name, value] of nonNegative) if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new Error(`${name} must be a non-negative integer, received ${value}`);
729
+ if (config.gamma !== void 0 && (!Number.isFinite(config.gamma) || config.gamma <= 0 || config.gamma > 1)) throw new Error(`gamma must be within (0, 1], received ${config.gamma}`);
730
+ if (config.tips !== void 0 && config.tips.length === 0) throw new Error("tips must not be empty");
731
+ }
732
+ function defaultInstanceId(args) {
733
+ const hash = require_evaluation.stableHash(args.datum);
734
+ return hash === "" ? String(args.index) : hash;
735
+ }
736
+ //#endregion
737
+ exports.MiproOptimizer = MiproOptimizer;
738
+ exports.buildMiproPrompt = buildMiproPrompt;
739
+ exports.proposeConfiguration = proposeConfiguration;