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,1671 @@
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_rng = require("../rng-DbA_rPIo.cjs");
7
+ const require_reflection = require("../reflection-DRfbk6hu.cjs");
8
+ const require_sampling = require("../sampling-Dars7ctR.cjs");
9
+ //#region src/gepa/demos.ts
10
+ const DEFAULT_MAX_DEMOS = 4;
11
+ const DEFAULT_MIN_SCORE = 1;
12
+ /**
13
+ * A proposer that fills demonstration components from rollouts the run has
14
+ * already paid for.
15
+ *
16
+ * The reflective dataset carries every minibatch rollout's input, output and
17
+ * score, so the successful ones are a few-shot block sitting in memory. Taking
18
+ * them costs no rollout and no reflection call — the search buys demos as a
19
+ * side effect of the evaluations it was making anyway.
20
+ *
21
+ * A proposal appends to the block its parent already holds rather than
22
+ * replacing it: a block built from one minibatch alone would hold three or
23
+ * four examples and forget every earlier one. Accumulation therefore follows
24
+ * the accepted lineage — a demo only persists if the candidate carrying it
25
+ * beat its parent, which is the same bar every other component is held to.
26
+ */
27
+ function createDemoProposer(args) {
28
+ const { components, minScore = DEFAULT_MIN_SCORE, maxDemos = DEFAULT_MAX_DEMOS, render, fallback = require_reflection.createDefaultProposer() } = args;
29
+ if (components.length === 0) throw new Error("createDemoProposer requires at least one component");
30
+ const demoComponents = new Set(components);
31
+ return async (proposeArgs) => {
32
+ const { candidate, reflectiveDataset, componentsToUpdate } = proposeArgs;
33
+ const demoTargets = componentsToUpdate.filter((name) => demoComponents.has(name));
34
+ const others = componentsToUpdate.filter((name) => !demoComponents.has(name));
35
+ const patch = others.length === 0 ? {} : await fallback({
36
+ ...proposeArgs,
37
+ componentsToUpdate: others
38
+ });
39
+ for (const name of demoTargets) {
40
+ const harvested = harvestDemos({
41
+ records: reflectiveDataset[name] ?? [],
42
+ minScore
43
+ });
44
+ if (harvested.length === 0) continue;
45
+ const kept = mergeDemos({
46
+ existing: require_demos.parseDemos(candidate[name] ?? ""),
47
+ harvested,
48
+ maxDemos
49
+ });
50
+ const block = require_demos.formatDemos(kept, render === void 0 ? {} : { render });
51
+ if (block !== candidate[name]) patch[name] = block;
52
+ }
53
+ return patch;
54
+ };
55
+ }
56
+ function harvestDemos(args) {
57
+ const { records, minScore } = args;
58
+ return records.filter((record) => (record.score ?? Number.NEGATIVE_INFINITY) >= minScore).map((record) => ({
59
+ input: record.inputs,
60
+ output: record.generatedOutputs,
61
+ score: record.score
62
+ }));
63
+ }
64
+ /**
65
+ * Newest wins on overflow. A demo harvested later came from a stronger
66
+ * candidate, since a weaker one would not have scored highly enough to be
67
+ * harvested at all — so the tail of the block is the better end of it.
68
+ */
69
+ function mergeDemos(args) {
70
+ const { existing, harvested, maxDemos } = args;
71
+ const merged = [...existing];
72
+ const seen = new Set(existing.map((demo) => keyOf(demo.input)));
73
+ for (const demo of harvested) {
74
+ const key = keyOf(demo.input);
75
+ if (seen.has(key)) continue;
76
+ seen.add(key);
77
+ merged.push(demo);
78
+ }
79
+ return merged.slice(Math.max(0, merged.length - maxDemos));
80
+ }
81
+ function keyOf(input) {
82
+ try {
83
+ return JSON.stringify(input) ?? String(input);
84
+ } catch {
85
+ return String(input);
86
+ }
87
+ }
88
+ //#endregion
89
+ //#region src/gepa/pipeline.ts
90
+ const DEFAULT_CONCURRENCY = 1;
91
+ /**
92
+ * A GEPA adapter for a system built from several modules in sequence, where
93
+ * each module's instruction is its own candidate component.
94
+ *
95
+ * The work this saves is attribution. Reflection on a multi-module system is
96
+ * only as good as the evidence it sees, and the evidence a module needs is what
97
+ * *it* received and produced — not the pipeline's input and final answer, which
98
+ * is what an adapter written in a hurry ends up showing every component.
99
+ *
100
+ * The feedback is end-to-end, and every module sees the same string. That is
101
+ * the honest default rather than a shortcut: a metric scores the final output,
102
+ * so nothing in a score alone says which module lost the point. A caller who
103
+ * can attribute better should score the steps themselves — `score` is handed
104
+ * the whole trace for exactly that.
105
+ *
106
+ * Errors from a module are not caught. A helper cannot tell a rate limit from a
107
+ * bug in a module, and guessing wrong either buries the bug or fails the run
108
+ * over a blip. Classify inside `run` and return a transient `ScoreResult` from
109
+ * `score`, or let the optimizer's `raiseOnError` decide.
110
+ */
111
+ function createPipelineAdapter(args) {
112
+ const { modules, input = (datum) => datum, score, concurrency = DEFAULT_CONCURRENCY } = args;
113
+ if (modules.length === 0) throw new Error("createPipelineAdapter requires at least one module");
114
+ return {
115
+ evaluate: async ({ batch, candidate, captureTraces, signal }) => {
116
+ const rollouts = await require_concurrency.mapWithConcurrency({
117
+ items: batch,
118
+ limit: concurrency,
119
+ signal,
120
+ task: async (datum) => {
121
+ const trace = await runPipeline({
122
+ modules,
123
+ candidate,
124
+ datum,
125
+ input,
126
+ signal
127
+ });
128
+ const output = trace.steps.at(-1).output;
129
+ return {
130
+ trace,
131
+ output,
132
+ scored: await score({
133
+ datum,
134
+ output,
135
+ steps: trace.steps
136
+ })
137
+ };
138
+ }
139
+ });
140
+ const batchResult = {
141
+ outputs: rollouts.map((rollout) => rollout.output),
142
+ scores: rollouts.map((rollout) => rollout.scored.score),
143
+ feedback: rollouts.map((rollout) => rollout.scored.feedback ?? ""),
144
+ usage: rollouts.map((rollout) => rollout.scored.usage ?? {}),
145
+ transient: rollouts.map((rollout) => rollout.scored.transient ?? false),
146
+ objectiveScores: rollouts.map((rollout) => rollout.scored.objectiveScores ?? {})
147
+ };
148
+ return captureTraces ? {
149
+ ...batchResult,
150
+ trajectories: rollouts.map((r) => r.trace)
151
+ } : batchResult;
152
+ },
153
+ makeReflectiveDataset: ({ evaluation, componentsToUpdate }) => {
154
+ const dataset = {};
155
+ for (const component of componentsToUpdate) dataset[component] = recordsFor({
156
+ component,
157
+ evaluation
158
+ });
159
+ return dataset;
160
+ }
161
+ };
162
+ }
163
+ async function runPipeline(args) {
164
+ const { modules, candidate, datum, input, signal } = args;
165
+ const steps = [];
166
+ let carried = input(datum);
167
+ for (const module of modules) {
168
+ const output = await module.run({
169
+ instruction: candidate[module.component],
170
+ input: carried,
171
+ datum,
172
+ signal
173
+ });
174
+ steps.push({
175
+ component: module.component,
176
+ input: carried,
177
+ output
178
+ });
179
+ carried = output;
180
+ }
181
+ return { steps };
182
+ }
183
+ function recordsFor(args) {
184
+ const { component, evaluation } = args;
185
+ return (evaluation.trajectories ?? []).map((trace, index) => {
186
+ const step = trace.steps.find((entry) => entry.component === component);
187
+ return {
188
+ inputs: step?.input,
189
+ generatedOutputs: step?.output,
190
+ feedback: evaluation.feedback?.[index] ?? "",
191
+ score: evaluation.scores[index],
192
+ evidence: { component }
193
+ };
194
+ });
195
+ }
196
+ //#endregion
197
+ //#region src/gepa/merge.ts
198
+ const DEFAULT_MAX_ATTEMPTS = 10;
199
+ const DEFAULT_VAL_OVERLAP_FLOOR = 5;
200
+ /**
201
+ * System-aware merge (GEPA's crossover). Two dominator lineages that descend
202
+ * from a common ancestor neither of them regressed against can be recombined
203
+ * for free: for every component, take the version from whichever descendant
204
+ * actually moved it. Sampling is random rather than greedy — always merging the
205
+ * two strongest lineages collapses the frontier's diversity, which is the thing
206
+ * merge exists to exploit.
207
+ *
208
+ * Returns a proposal to be *tested*, not an accepted candidate: the caller
209
+ * still has to score it.
210
+ */
211
+ function proposeMerge(args) {
212
+ const { records, pool, rng, attempted, attemptedDescriptions, maxAttempts = DEFAULT_MAX_ATTEMPTS, valOverlapFloor = DEFAULT_VAL_OVERLAP_FLOOR } = args;
213
+ if (pool.length < 2 || records.length < 3) return null;
214
+ const ancestries = buildAncestries(records);
215
+ const existing = new Set(records.map((record) => fingerprint(record.candidate)));
216
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
217
+ const triplet = sampleTriplet({
218
+ records,
219
+ pool,
220
+ rng,
221
+ attempted,
222
+ ancestries,
223
+ maxAttempts,
224
+ valOverlapFloor
225
+ });
226
+ if (triplet === null) continue;
227
+ const { parentIds, ancestorId } = triplet;
228
+ const merged = mergeComponents({
229
+ records,
230
+ parentIds,
231
+ ancestorId,
232
+ rng
233
+ });
234
+ const descriptionKey = `${parentIds[0]}:${parentIds[1]}:${merged.sources.join(",")}`;
235
+ if (attemptedDescriptions.has(descriptionKey) || existing.has(fingerprint(merged.candidate))) continue;
236
+ return {
237
+ candidate: merged.candidate,
238
+ parentIds,
239
+ ancestorId,
240
+ attemptKey: `${parentIds[0]}:${parentIds[1]}:${ancestorId}`,
241
+ descriptionKey
242
+ };
243
+ }
244
+ return null;
245
+ }
246
+ /**
247
+ * Picks the validation instances a merge is judged on: up to `size` ids, drawn
248
+ * evenly from the instances each parent uniquely wins plus the ones they tie
249
+ * on. A uniform sample would usually miss the handful of instances that
250
+ * distinguish the parents at all, which is exactly where a merge either pays
251
+ * off or breaks.
252
+ */
253
+ function selectMergeSubsample(args) {
254
+ const { scores1, scores2, rng, size = 5 } = args;
255
+ const ids = scores1.map((_, index) => index).filter((index) => scores1[index] !== void 0 && scores2[index] !== void 0);
256
+ if (ids.length === 0) return [];
257
+ const firstWins = ids.filter((id) => scores1[id] > scores2[id]);
258
+ const secondWins = ids.filter((id) => scores2[id] > scores1[id]);
259
+ const ties = ids.filter((id) => !firstWins.includes(id) && !secondWins.includes(id));
260
+ const perBucket = Math.max(1, Math.ceil(size / 3));
261
+ const selected = [];
262
+ for (const bucket of [
263
+ firstWins,
264
+ secondWins,
265
+ ties
266
+ ]) {
267
+ if (selected.length >= size) break;
268
+ const available = bucket.filter((id) => !selected.includes(id));
269
+ const take = Math.min(available.length, perBucket, size - selected.length);
270
+ if (take > 0) selected.push(...rng.sample(available, take));
271
+ }
272
+ const remaining = size - selected.length;
273
+ if (remaining > 0) {
274
+ const unused = ids.filter((id) => !selected.includes(id));
275
+ if (unused.length >= remaining) selected.push(...rng.sample(unused, remaining));
276
+ else for (let index = 0; index < remaining; index += 1) selected.push(rng.pick(ids));
277
+ }
278
+ return selected.slice(0, size);
279
+ }
280
+ function sampleTriplet(args) {
281
+ const { records, pool, rng, attempted, ancestries, maxAttempts, valOverlapFloor } = args;
282
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
283
+ const [first, second] = rng.sample(pool, 2);
284
+ if (first === second) continue;
285
+ const left = Math.min(first, second);
286
+ const right = Math.max(first, second);
287
+ const leftAncestry = ancestries[left];
288
+ const rightAncestry = ancestries[right];
289
+ if (leftAncestry.has(right) || rightAncestry.has(left)) continue;
290
+ if (overlap({
291
+ records,
292
+ left,
293
+ right
294
+ }) < valOverlapFloor) continue;
295
+ const eligible = [...leftAncestry].filter((id) => rightAncestry.has(id)).filter((ancestorId) => isEligibleAncestor({
296
+ records,
297
+ left,
298
+ right,
299
+ ancestorId,
300
+ attempted
301
+ }));
302
+ if (eligible.length === 0) continue;
303
+ return {
304
+ parentIds: [left, right],
305
+ ancestorId: rng.weighted(eligible, eligible.map((id) => records[id].aggregateScore))
306
+ };
307
+ }
308
+ return null;
309
+ }
310
+ function overlap(args) {
311
+ const leftScores = args.records[args.left].instanceScores;
312
+ const rightScores = args.records[args.right].instanceScores;
313
+ let shared = 0;
314
+ for (let index = 0; index < leftScores.length; index += 1) if (leftScores[index] !== void 0 && rightScores[index] !== void 0) shared += 1;
315
+ return shared;
316
+ }
317
+ function isEligibleAncestor(args) {
318
+ const { records, left, right, ancestorId, attempted } = args;
319
+ if (attempted.has(`${left}:${right}:${ancestorId}`)) return false;
320
+ const ancestor = records[ancestorId];
321
+ const leftRecord = records[left];
322
+ const rightRecord = records[right];
323
+ if (ancestor.aggregateScore > leftRecord.aggregateScore || ancestor.aggregateScore > rightRecord.aggregateScore) return false;
324
+ return hasComplementaryComponent({
325
+ ancestor: ancestor.candidate,
326
+ left: leftRecord.candidate,
327
+ right: rightRecord.candidate
328
+ });
329
+ }
330
+ /**
331
+ * True when at least one component was changed by exactly one descendant. Two
332
+ * lineages that rewrote every shared component give the merge nothing to
333
+ * attribute an improvement to.
334
+ */
335
+ function hasComplementaryComponent(args) {
336
+ const { ancestor, left, right } = args;
337
+ return require_evaluation.componentNames(ancestor).some((name) => {
338
+ const base = ancestor[name];
339
+ return (base === left[name] || base === right[name]) && left[name] !== right[name];
340
+ });
341
+ }
342
+ function mergeComponents(args) {
343
+ const { records, parentIds, ancestorId, rng } = args;
344
+ const [leftId, rightId] = parentIds;
345
+ const ancestor = records[ancestorId].candidate;
346
+ const leftRecord = records[leftId];
347
+ const rightRecord = records[rightId];
348
+ const candidate = { ...ancestor };
349
+ const sources = [];
350
+ for (const name of require_evaluation.componentNames(ancestor)) {
351
+ const base = ancestor[name];
352
+ const leftText = leftRecord.candidate[name];
353
+ const rightText = rightRecord.candidate[name];
354
+ const sourceId = resolveComponentSource({
355
+ base,
356
+ leftText,
357
+ rightText,
358
+ leftRecord,
359
+ rightRecord,
360
+ rng
361
+ });
362
+ candidate[name] = records[sourceId].candidate[name];
363
+ sources.push(sourceId);
364
+ }
365
+ return {
366
+ candidate,
367
+ sources
368
+ };
369
+ }
370
+ function resolveComponentSource(args) {
371
+ const { base, leftText, rightText, leftRecord, rightRecord, rng } = args;
372
+ if ((base === leftText || base === rightText) && leftText !== rightText) return base === leftText ? rightRecord.id : leftRecord.id;
373
+ if (base !== leftText && base !== rightText) {
374
+ if (leftRecord.aggregateScore > rightRecord.aggregateScore) return leftRecord.id;
375
+ if (rightRecord.aggregateScore > leftRecord.aggregateScore) return rightRecord.id;
376
+ return rng.pick([leftRecord.id, rightRecord.id]);
377
+ }
378
+ return leftRecord.id;
379
+ }
380
+ /** Strict ancestors of each record, keyed by record id. */
381
+ function buildAncestries(records) {
382
+ const ancestries = [];
383
+ for (const record of records) {
384
+ const ancestry = /* @__PURE__ */ new Set();
385
+ for (const parentId of record.parentIds) {
386
+ ancestry.add(parentId);
387
+ for (const id of ancestries[parentId] ?? []) ancestry.add(id);
388
+ }
389
+ ancestries[record.id] = ancestry;
390
+ }
391
+ return ancestries;
392
+ }
393
+ function fingerprint(candidate) {
394
+ return JSON.stringify(require_evaluation.componentNames(candidate).sort().map((name) => [name, candidate[name]]));
395
+ }
396
+ //#endregion
397
+ //#region src/gepa/pareto.ts
398
+ function computeInstanceBests(scoreMatrix) {
399
+ const first = scoreMatrix[0];
400
+ if (first === void 0) return [];
401
+ const bests = first.map((score) => score ?? Number.NEGATIVE_INFINITY);
402
+ for (let candidate = 1; candidate < scoreMatrix.length; candidate += 1) {
403
+ const row = scoreMatrix[candidate];
404
+ for (let instance = 0; instance < bests.length; instance += 1) {
405
+ const score = row[instance] ?? Number.NEGATIVE_INFINITY;
406
+ if (score > bests[instance]) bests[instance] = score;
407
+ }
408
+ }
409
+ return bests;
410
+ }
411
+ /**
412
+ * For each instance, the set of candidates achieving the best score on it.
413
+ * Candidates within `epsilon` of the best count as tied, which keeps noisy
414
+ * metrics from collapsing the frontier to a single lineage.
415
+ */
416
+ function buildInstanceFronts(args) {
417
+ const { scoreMatrix, epsilon = 0 } = args;
418
+ return computeInstanceBests(scoreMatrix).map((best, instance) => {
419
+ const front = /* @__PURE__ */ new Set();
420
+ for (let candidate = 0; candidate < scoreMatrix.length; candidate += 1) {
421
+ const score = scoreMatrix[candidate][instance];
422
+ if (score !== void 0 && score >= best - epsilon) front.add(candidate);
423
+ }
424
+ return front;
425
+ });
426
+ }
427
+ /** The best value any candidate reached on each objective. */
428
+ function objectiveBests(objectiveScores) {
429
+ const bests = {};
430
+ for (const scores of objectiveScores) for (const [objective, value] of Object.entries(scores ?? {})) {
431
+ const best = bests[objective];
432
+ if (best === void 0 || value > best) bests[objective] = value;
433
+ }
434
+ return bests;
435
+ }
436
+ /**
437
+ * The objective-wise counterpart of `buildInstanceFronts`: one front per named
438
+ * objective, holding the candidates that lead it. Optimizing a system against
439
+ * several metrics at once — accuracy against cost, quality against latency —
440
+ * means the interesting candidates are the ones that lead *an* objective, which
441
+ * an average over instances hides.
442
+ */
443
+ function buildObjectiveFronts(args) {
444
+ const { objectiveScores, epsilon = 0 } = args;
445
+ const bests = objectiveBests(objectiveScores);
446
+ return Object.entries(bests).map(([objective, best]) => {
447
+ const front = /* @__PURE__ */ new Set();
448
+ for (let candidate = 0; candidate < objectiveScores.length; candidate += 1) {
449
+ const value = objectiveScores[candidate]?.[objective];
450
+ if (value !== void 0 && value >= best - epsilon) front.add(candidate);
451
+ }
452
+ return front;
453
+ });
454
+ }
455
+ /**
456
+ * Drop candidates that contribute nothing unique: a candidate is dominated when
457
+ * every instance it wins is also won by some surviving candidate. Candidates are
458
+ * considered in ascending aggregate score, so weaker duplicates are removed
459
+ * first. Mirrors `remove_dominated_programs` in the reference implementation.
460
+ */
461
+ function pruneDominatedFronts(args) {
462
+ const { fronts, aggregateScores } = args;
463
+ const frontsByCandidate = indexFrontsByCandidate(fronts);
464
+ const ordered = [...frontsByCandidate.keys()].sort((a, b) => (aggregateScores[a] ?? 0) - (aggregateScores[b] ?? 0));
465
+ const survivors = new Set(ordered);
466
+ for (const candidate of ordered) {
467
+ survivors.delete(candidate);
468
+ if (!frontsByCandidate.get(candidate).every((index) => hasSurvivor({
469
+ front: fronts[index],
470
+ survivors
471
+ }))) survivors.add(candidate);
472
+ }
473
+ return fronts.map((front) => new Set([...front].filter((candidate) => survivors.has(candidate))));
474
+ }
475
+ /**
476
+ * Sample a parent candidate with probability proportional to the number of
477
+ * instances it is best on, after dominated candidates are pruned.
478
+ */
479
+ function selectParetoCandidate(args) {
480
+ const { fronts, aggregateScores, rng } = args;
481
+ const pruned = pruneDominatedFronts({
482
+ fronts,
483
+ aggregateScores
484
+ });
485
+ const samplingPool = [];
486
+ for (const front of pruned) for (const candidate of front) samplingPool.push(candidate);
487
+ if (samplingPool.length === 0) return require_math.argmax(aggregateScores);
488
+ return rng.pick(samplingPool);
489
+ }
490
+ function indexFrontsByCandidate(fronts) {
491
+ const index = /* @__PURE__ */ new Map();
492
+ fronts.forEach((front, position) => {
493
+ for (const candidate of front) {
494
+ const owned = index.get(candidate);
495
+ if (owned === void 0) index.set(candidate, [position]);
496
+ else owned.push(position);
497
+ }
498
+ });
499
+ return index;
500
+ }
501
+ function hasSurvivor(args) {
502
+ const { front, survivors } = args;
503
+ for (const candidate of front) if (survivors.has(candidate)) return true;
504
+ return false;
505
+ }
506
+ //#endregion
507
+ //#region src/gepa/strategies.ts
508
+ /**
509
+ * Default parent selection: sample from the instance-wise Pareto frontier with
510
+ * probability proportional to how many validation instances a candidate is best
511
+ * on. This is what keeps GEPA from tunnelling into one lineage.
512
+ *
513
+ * `frontier` chooses what the fronts are taken over. "instance" is GEPA as
514
+ * published. "objective" tracks candidates leading each named objective the
515
+ * adapter reports, and "hybrid" pools both — a candidate then earns selection
516
+ * weight for every instance it wins *and* every objective it leads.
517
+ */
518
+ function paretoSelector(args = {}) {
519
+ const { epsilon = 0, frontier = "instance" } = args;
520
+ return ({ state, rng }) => {
521
+ const fronts = [];
522
+ if (frontier !== "objective") fronts.push(...buildInstanceFronts({
523
+ scoreMatrix: state.scoreMatrix,
524
+ epsilon
525
+ }));
526
+ if (frontier !== "instance") {
527
+ const objectiveFronts = buildObjectiveFronts({
528
+ objectiveScores: state.objectiveScores ?? [],
529
+ epsilon
530
+ });
531
+ if (objectiveFronts.length === 0) throw new Error(`paretoSelector frontier "${frontier}" needs objective scores, but no candidate has any; have the adapter return objectiveScores or use frontier "instance"`);
532
+ fronts.push(...objectiveFronts);
533
+ }
534
+ return selectParetoCandidate({
535
+ fronts,
536
+ aggregateScores: state.aggregateScores,
537
+ rng
538
+ });
539
+ };
540
+ }
541
+ /** Greedy hill climbing. Useful as an ablation baseline. */
542
+ function currentBestSelector() {
543
+ return ({ state }) => require_math.argmax(state.aggregateScores);
544
+ }
545
+ function epsilonGreedySelector(args) {
546
+ const { epsilon } = args;
547
+ return ({ state, rng }) => {
548
+ if (rng.next() < epsilon) return rng.nextInt(state.aggregateScores.length);
549
+ return require_math.argmax(state.aggregateScores);
550
+ };
551
+ }
552
+ /** Pareto selection restricted to the top k candidates by aggregate score. */
553
+ function topKParetoSelector(args) {
554
+ const { k, epsilon = 0 } = args;
555
+ return ({ state, rng }) => {
556
+ const ranked = state.aggregateScores.map((score, index) => ({
557
+ score,
558
+ index
559
+ })).sort((a, b) => b.score - a.score).slice(0, k).map((entry) => entry.index);
560
+ const allowed = new Set(ranked);
561
+ const nonEmpty = buildInstanceFronts({
562
+ scoreMatrix: state.scoreMatrix,
563
+ epsilon
564
+ }).map((front) => new Set([...front].filter((id) => allowed.has(id)))).filter((front) => front.size > 0);
565
+ if (nonEmpty.length === 0) return require_math.argmax(state.aggregateScores);
566
+ return selectParetoCandidate({
567
+ fronts: nonEmpty,
568
+ aggregateScores: state.aggregateScores,
569
+ rng
570
+ });
571
+ };
572
+ }
573
+ /**
574
+ * Score every accepted candidate on the whole validation set. This is GEPA as
575
+ * published: the frontier is exact, and the cost is one full sweep per
576
+ * acceptance.
577
+ */
578
+ function fullEvaluationPolicy() {
579
+ return {
580
+ selectInstances: ({ validationSet }) => validationSet.map((_, index) => index),
581
+ bestCandidate: bestByMeanThenCoverage
582
+ };
583
+ }
584
+ /**
585
+ * A full sweep, with the winner chosen by a lower confidence bound on its mean
586
+ * rather than by the mean itself: `mean - z * standardError`, over the
587
+ * instances the candidate was scored on.
588
+ *
589
+ * The reference picks the highest mean. That is an argmax over every candidate
590
+ * a run produced, all measured on the same instances, so the winner is
591
+ * systematically the one whose instance-level noise happened to land in its
592
+ * favour — the gap this library reports between `bestScore` and `testScore`.
593
+ * Penalising spread prefers a candidate that was even across the set to one
594
+ * carried by a few instances, at the cost of sometimes returning a genuinely
595
+ * better but less consistent candidate. It changes only which candidate is
596
+ * reported, never which ones the search explores.
597
+ */
598
+ function lowerBoundEvaluationPolicy(args = {}) {
599
+ const { z = 1 } = args;
600
+ return {
601
+ selectInstances: ({ validationSet }) => validationSet.map((_, index) => index),
602
+ bestCandidate: (records) => bestByLowerBound({
603
+ records,
604
+ z
605
+ })
606
+ };
607
+ }
608
+ /**
609
+ * Score each candidate on a random subset of the validation set. Cheaper per
610
+ * acceptance, at the cost of comparing candidates measured on different
611
+ * instances — coverage breaks ties, so a candidate cannot win by having been
612
+ * asked fewer questions.
613
+ */
614
+ function subsampledEvaluationPolicy(args) {
615
+ const { size } = args;
616
+ if (!Number.isFinite(size) || size <= 0) throw new Error(`subsampledEvaluationPolicy requires a positive size, received ${size}`);
617
+ return {
618
+ selectInstances: ({ validationSet, rng }) => rng.sample(validationSet.map((_, index) => index), size),
619
+ bestCandidate: bestByMeanThenCoverage
620
+ };
621
+ }
622
+ /**
623
+ * One component per selection, cycling in declaration order from the parent's
624
+ * own cursor. Updating a single component at a time is what makes the minibatch
625
+ * acceptance test attributable; keying off the parent's cursor rather than the
626
+ * global iteration is what guarantees every component of a rarely-selected
627
+ * lineage eventually gets a turn.
628
+ */
629
+ function roundRobinComponentSelector() {
630
+ return ({ candidate, cursor }) => {
631
+ const names = require_evaluation.componentNames(candidate);
632
+ if (names.length === 0) throw new Error("Candidate has no components to update");
633
+ return [names[cursor % names.length]];
634
+ };
635
+ }
636
+ /** Update every component in a single reflection call. */
637
+ function allComponentsSelector() {
638
+ return ({ candidate }) => require_evaluation.componentNames(candidate);
639
+ }
640
+ /**
641
+ * Accept a mutation only when it beats its parent on the same minibatch. Cheap
642
+ * gate that keeps full validation sweeps for candidates that showed a signal.
643
+ */
644
+ function improvementAcceptance(args = {}) {
645
+ const { minImprovement = 0 } = args;
646
+ return ({ parentScores, childScores }) => require_math.sum(childScores) > require_math.sum(parentScores) + minImprovement;
647
+ }
648
+ /**
649
+ * Accepts a child only when a paired permutation test over the minibatch says
650
+ * its gain is unlikely to be noise. Pairs are the same instances run by both
651
+ * candidates, so the test is over the per-instance differences.
652
+ *
653
+ * The reference accepts on any sum improvement. On a minibatch of three that
654
+ * promotes a candidate one lucky rollout ahead, which is what fills a pool
655
+ * with children the validation sweep then discards. The cost is that small
656
+ * batches cannot produce small p-values at all — with three instances the
657
+ * smallest attainable is 0.125 — so this accepts only a clean sweep there.
658
+ * That is the honest reading of three rollouts, not a limitation to tune
659
+ * around: raise `minibatchSize` to buy the power to detect smaller gains.
660
+ */
661
+ function pairedPermutationAcceptance(args = {}) {
662
+ const { alpha = .2, maxExact = 16 } = args;
663
+ return ({ parentScores, childScores }) => {
664
+ const differences = [];
665
+ for (let index = 0; index < parentScores.length; index += 1) differences.push(childScores[index] - parentScores[index]);
666
+ const observed = require_math.sum(differences);
667
+ if (observed <= 0) return false;
668
+ return require_math.signFlipPValue({
669
+ differences,
670
+ observed,
671
+ maxExact
672
+ }) <= alpha;
673
+ };
674
+ }
675
+ /**
676
+ * Highest mean over the instances it was scored on, with wider coverage
677
+ * winning a tie: a candidate measured on more instances has earned the same
678
+ * mean against more evidence.
679
+ */
680
+ function bestByMeanThenCoverage(records) {
681
+ let bestId = 0;
682
+ let bestScore = Number.NEGATIVE_INFINITY;
683
+ let bestCoverage = -1;
684
+ for (const record of records) {
685
+ const coverage = record.instanceScores.filter((score) => score !== void 0).length;
686
+ if (record.aggregateScore > bestScore || record.aggregateScore === bestScore && coverage > bestCoverage) {
687
+ bestId = record.id;
688
+ bestScore = record.aggregateScore;
689
+ bestCoverage = coverage;
690
+ }
691
+ }
692
+ return bestId;
693
+ }
694
+ /**
695
+ * Highest lower bound on the mean, over the instances each candidate was
696
+ * scored on. The bound is `mean - z * standardError`; a candidate scored on
697
+ * one instance has no spread to measure, so it is ranked on its mean alone
698
+ * rather than being flattered by an undefined variance.
699
+ */
700
+ function bestByLowerBound(args) {
701
+ const { records, z } = args;
702
+ let bestId = 0;
703
+ let bestBound = Number.NEGATIVE_INFINITY;
704
+ for (const record of records) {
705
+ const scored = record.instanceScores.filter((score) => score !== void 0);
706
+ if (scored.length === 0) continue;
707
+ const bound = scored.length < 2 ? record.aggregateScore : record.aggregateScore - z * standardError(scored);
708
+ if (bound > bestBound) {
709
+ bestBound = bound;
710
+ bestId = record.id;
711
+ }
712
+ }
713
+ return bestId;
714
+ }
715
+ function standardError(scores) {
716
+ const mean = scores.reduce((total, score) => total + score, 0) / scores.length;
717
+ const variance = scores.reduce((total, score) => total + (score - mean) ** 2, 0) / (scores.length - 1);
718
+ return Math.sqrt(variance / scores.length);
719
+ }
720
+ //#endregion
721
+ //#region src/gepa/optimize.ts
722
+ const DEFAULT_MINIBATCH_SIZE = 3;
723
+ const DEFAULT_REJECTED_PROPOSAL_MEMORY = 3;
724
+ const DEFAULT_MAX_MERGES = 5;
725
+ const MERGE_SUBSAMPLE_SIZE = 5;
726
+ var ReflectionBudgetExhausted = class extends Error {};
727
+ /**
728
+ * Reflective prompt evolution: propose, screen on a minibatch, promote what
729
+ * survives, and track the Pareto frontier of everything promoted.
730
+ *
731
+ * One instance is a configured search that can be run against any number of
732
+ * tasks. It holds no run state, so two runs never share a shuffle position, a
733
+ * budget or a candidate pool.
734
+ */
735
+ var GepaOptimizer = class {
736
+ #config;
737
+ constructor(config = {}) {
738
+ assertGepaConfig(config);
739
+ this.#config = config;
740
+ }
741
+ async optimize(task) {
742
+ return runGepa({
743
+ config: this.#config,
744
+ task
745
+ });
746
+ }
747
+ };
748
+ async function runGepa(args) {
749
+ const { config, task } = args;
750
+ const { minibatchSize = DEFAULT_MINIBATCH_SIZE, maxIterations = Number.POSITIVE_INFINITY, seed = 0, candidateSelector = paretoSelector(), acceptance = improvementAcceptance(), merge, skipPerfectScore = true, perfectScore = 1, rejectedProposalMemory = DEFAULT_REJECTED_PROPOSAL_MEMORY, proposals, reflection, checkpointCache = true, trackBestOutputs = false, raiseOnError = true } = config;
751
+ const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, maxMetricCalls, componentSelector = roundRobinComponentSelector(), batchSampler = require_sampling.createEpochShuffledSampler({ minibatchSize }), valEvaluationPolicy = fullEvaluationPolicy(), cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
752
+ const deadline = require_evaluation.createDeadline({ maxWallClockMs });
753
+ const seedComponents = require_evaluation.componentNames(seedCandidate);
754
+ const mergeConfig = {
755
+ enabled: merge?.enabled ?? seedComponents.length > 1,
756
+ maxInvocations: merge?.maxInvocations ?? DEFAULT_MAX_MERGES,
757
+ ...merge?.valOverlapFloor === void 0 ? {} : { valOverlapFloor: merge.valOverlapFloor }
758
+ };
759
+ const proposalsPerIteration = proposals?.perIteration ?? 1;
760
+ const proposalConcurrency = proposals?.concurrency ?? 1;
761
+ const survivorsPerIteration = keepCount(proposals?.selection ?? "all");
762
+ if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
763
+ if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet; the Pareto frontier is tracked over validation instances");
764
+ if (seedComponents.length === 0) throw new Error("optimize requires a seed candidate with at least one component");
765
+ 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");
766
+ const evaluationCache = cache === false ? void 0 : cache ?? require_evaluation.createMemoryCache();
767
+ const propose = adapter.proposeNewTexts?.bind(adapter) ?? require_reflection.createDefaultProposer({
768
+ ...reflection?.buildPrompt === void 0 ? {} : { buildPrompt: reflection.buildPrompt },
769
+ ...reflection?.strategies === void 0 ? {} : { strategies: reflection.strategies },
770
+ limits: {
771
+ ...reflection?.maxRecords === void 0 ? {} : { maxRecords: reflection.maxRecords },
772
+ ...reflection?.maxCharacters === void 0 ? {} : { maxCharacters: reflection.maxCharacters }
773
+ }
774
+ });
775
+ const trainingIds = trainingSet.map((datum, index) => instanceId({
776
+ datum,
777
+ index
778
+ }));
779
+ const validationIds = validationSet.map((datum, index) => instanceId({
780
+ datum,
781
+ index
782
+ }));
783
+ const testIds = testSet?.map((datum, index) => instanceId({
784
+ datum,
785
+ index
786
+ })) ?? [];
787
+ const fingerprint = require_evaluation.runFingerprint({
788
+ seedCandidate,
789
+ trainingIds,
790
+ validationIds,
791
+ seed,
792
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace }
793
+ });
794
+ require_evaluation.assertResumable({
795
+ fingerprint,
796
+ ...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
797
+ });
798
+ const rng = require_rng.createSeededRng(seed, resumeFrom?.rngState);
799
+ const budget = require_evaluation.createBudget({
800
+ maxMetricCalls,
801
+ spent: resumeFrom?.metricCalls ?? 0
802
+ });
803
+ if (resumeFrom?.sampler !== void 0) batchSampler.restore?.(resumeFrom.sampler);
804
+ let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
805
+ /**
806
+ * The reflection budget is enforced at the call, not at the proposal: an
807
+ * adapter's own proposer may make any number of calls, and a cap that only
808
+ * counted proposals would not bound it.
809
+ */
810
+ const countedReflect = async (args) => {
811
+ if (reflection?.maxCalls !== void 0 && reflectionCalls >= reflection.maxCalls) throw new ReflectionBudgetExhausted();
812
+ reflectionCalls += 1;
813
+ return reflect(args);
814
+ };
815
+ const records = restoreRecords({
816
+ records: resumeFrom?.records ?? [],
817
+ seedCandidate
818
+ });
819
+ const seenCandidates = new Set(records.map((record) => require_evaluation.candidateFingerprint(record.candidate)));
820
+ const outputsByCandidate = /* @__PURE__ */ new Map();
821
+ const rejectedProposals = restoreRejections({
822
+ rejections: resumeFrom?.rejectedProposals ?? {},
823
+ components: seedComponents
824
+ });
825
+ let iteration = resumeFrom?.iteration ?? 0;
826
+ const mergeAttempts = new Set(resumeFrom?.merge.attempts);
827
+ const mergeDescriptions = new Set(resumeFrom?.merge.descriptions);
828
+ let mergesDue = resumeFrom?.merge.due ?? 0;
829
+ let totalMergesTested = resumeFrom?.merge.tested ?? 0;
830
+ let lastIterationAccepted = resumeFrom?.merge.lastIterationAccepted ?? false;
831
+ function emit(event) {
832
+ onEvent?.(event);
833
+ }
834
+ /**
835
+ * Copies everything mutable: a snapshot handed to `onCheckpoint` is a record
836
+ * of that moment, and would otherwise keep growing as the run continues.
837
+ */
838
+ function takeSnapshot() {
839
+ const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
840
+ const samplerState = batchSampler.state?.();
841
+ return {
842
+ version: 1,
843
+ fingerprint,
844
+ records: copyRecords(records),
845
+ iteration,
846
+ metricCalls: budget.spent(),
847
+ reflectionCalls,
848
+ cacheHits: evaluator.cacheHits(),
849
+ ...samplerState === void 0 ? {} : { sampler: samplerState },
850
+ rejectedProposals: snapshotRejections({
851
+ rejections: rejectedProposals,
852
+ components: seedComponents
853
+ }),
854
+ rngState: rng.state(),
855
+ merge: {
856
+ attempts: [...mergeAttempts],
857
+ descriptions: [...mergeDescriptions],
858
+ due: mergesDue,
859
+ tested: totalMergesTested,
860
+ lastIterationAccepted
861
+ },
862
+ ...cached === void 0 ? {} : { cache: cached }
863
+ };
864
+ }
865
+ async function checkpoint() {
866
+ if (onCheckpoint === void 0) return;
867
+ await onCheckpoint(takeSnapshot());
868
+ }
869
+ const evaluator = require_evaluation.createEvaluator({
870
+ adapter,
871
+ budget,
872
+ ...retry === void 0 ? {} : { retry },
873
+ ...cacheNamespace === void 0 ? {} : { cacheNamespace },
874
+ ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
875
+ trackOutputs: trackBestOutputs,
876
+ cacheHits: resumeFrom?.cacheHits ?? 0,
877
+ ...signal === void 0 ? {} : { signal },
878
+ onEvaluation: (event) => emit({
879
+ type: "evaluation",
880
+ ...event
881
+ })
882
+ });
883
+ evaluator.restore(resumeFrom?.cache ?? []);
884
+ /**
885
+ * Evaluates a candidate on a batch at the current iteration. The cache, the
886
+ * budget and the transient-failure rules live in the shared evaluator; what
887
+ * belongs to GEPA is only which batch, and when.
888
+ */
889
+ async function evaluateCached(args) {
890
+ return evaluator.evaluate({
891
+ ...args,
892
+ iteration
893
+ });
894
+ }
895
+ /**
896
+ * Scores a candidate on the validation instances the policy selects, and
897
+ * spreads the result back over the full validation set — instances the
898
+ * policy skipped stay `undefined`, which every consumer reads as unknown
899
+ * rather than as a zero.
900
+ */
901
+ async function evaluateValidation(args) {
902
+ const { candidate, instances, phase, candidateId } = args;
903
+ const dense = await evaluateCached({
904
+ candidate,
905
+ batch: instances.map((index) => validationSet[index]),
906
+ ids: instances.map((index) => validationIds[index]),
907
+ split: "val",
908
+ phase,
909
+ candidateId
910
+ });
911
+ const scores = new Array(validationSet.length).fill(void 0);
912
+ const objectiveScores = new Array(validationSet.length).fill(void 0);
913
+ const outputs = new Array(validationSet.length).fill(void 0);
914
+ instances.forEach((instance, position) => {
915
+ if (dense.transient[position] === true) return;
916
+ scores[instance] = dense.scores[position];
917
+ objectiveScores[instance] = dense.objectiveScores[position];
918
+ outputs[instance] = dense.outputs[position];
919
+ });
920
+ return {
921
+ scores,
922
+ objectiveScores,
923
+ outputs
924
+ };
925
+ }
926
+ /** The validation instances this candidate should be scored on. */
927
+ function selectValInstances(candidate) {
928
+ const selected = valEvaluationPolicy.selectInstances({
929
+ validationSet,
930
+ candidate,
931
+ records,
932
+ iteration,
933
+ rng
934
+ });
935
+ if (selected.length === 0) throw new Error("valEvaluationPolicy selected no validation instances; a candidate cannot be scored");
936
+ return selected;
937
+ }
938
+ function addCandidate(args) {
939
+ const objectiveScores = meanObjectives({
940
+ rows: args.evaluation.objectiveScores,
941
+ scores: args.evaluation.scores
942
+ });
943
+ const record = {
944
+ id: records.length,
945
+ candidate: args.candidate,
946
+ parentIds: args.parentIds,
947
+ instanceScores: args.evaluation.scores,
948
+ aggregateScore: require_math.mean(args.evaluation.scores),
949
+ ...objectiveScores === void 0 ? {} : { objectiveScores },
950
+ source: args.source,
951
+ updatedComponents: args.updatedComponents,
952
+ iteration,
953
+ componentCursor: inheritedCursor(args.parentIds)
954
+ };
955
+ records.push(record);
956
+ seenCandidates.add(require_evaluation.candidateFingerprint(args.candidate));
957
+ if (trackBestOutputs) outputsByCandidate.set(record.id, args.evaluation.outputs);
958
+ if (args.source !== "merge") {
959
+ lastIterationAccepted = true;
960
+ if (mergeConfig.enabled && totalMergesTested < mergeConfig.maxInvocations) mergesDue += 1;
961
+ }
962
+ return record;
963
+ }
964
+ /** Keeps the most recent rejections per component, oldest dropped first. */
965
+ function rememberRejection(args) {
966
+ const { proposed, parentScore, childScore } = args;
967
+ if (rejectedProposalMemory <= 0) return;
968
+ for (const component of require_evaluation.componentNames(proposed)) {
969
+ const text = proposed[component];
970
+ if (text === void 0) continue;
971
+ const history = rejectedProposals[component] ?? [];
972
+ history.unshift({
973
+ text,
974
+ parentScore,
975
+ childScore
976
+ });
977
+ rejectedProposals[component] = history.slice(0, rejectedProposalMemory);
978
+ }
979
+ }
980
+ function inheritedCursor(parentIds) {
981
+ let cursor = 0;
982
+ for (const parentId of parentIds) {
983
+ const parent = records[parentId];
984
+ if (parent !== void 0 && parent.componentCursor > cursor) cursor = parent.componentCursor;
985
+ }
986
+ return cursor;
987
+ }
988
+ emit({
989
+ type: "start",
990
+ components: seedComponents,
991
+ validationSetSize: validationSet.length
992
+ });
993
+ if (records.length === 0) {
994
+ const seedInstances = selectValInstances(seedCandidate);
995
+ if (!budget.canAfford(seedInstances.length)) throw new Error(`maxMetricCalls (${maxMetricCalls}) is smaller than the ${seedInstances.length} validation instances selected for scoring; the seed candidate cannot be scored`);
996
+ addCandidate({
997
+ candidate: seedCandidate,
998
+ parentIds: [],
999
+ evaluation: await evaluateValidation({
1000
+ candidate: seedCandidate,
1001
+ instances: seedInstances,
1002
+ phase: "seed",
1003
+ candidateId: 0
1004
+ }),
1005
+ source: "seed",
1006
+ updatedComponents: []
1007
+ });
1008
+ lastIterationAccepted = false;
1009
+ mergesDue = 0;
1010
+ await checkpoint();
1011
+ }
1012
+ let stopReason = "budgetExhausted";
1013
+ /**
1014
+ * Proposes and gates one merge. Returns "none" when nothing was tested — the
1015
+ * iteration then falls through to reflective mutation, exactly as it would
1016
+ * have without merging enabled. A merge that cannot be afforded is skipped,
1017
+ * never treated as the end of the run.
1018
+ */
1019
+ async function tryMerge() {
1020
+ const proposal = proposeMerge({
1021
+ records,
1022
+ pool: collectDominatorIds(records),
1023
+ rng,
1024
+ attempted: mergeAttempts,
1025
+ attemptedDescriptions: mergeDescriptions,
1026
+ ...mergeConfig.valOverlapFloor === void 0 ? {} : { valOverlapFloor: mergeConfig.valOverlapFloor }
1027
+ });
1028
+ if (proposal === null) return "none";
1029
+ const [leftId, rightId] = proposal.parentIds;
1030
+ const left = records[leftId];
1031
+ const right = records[rightId];
1032
+ const subsample = selectMergeSubsample({
1033
+ scores1: left.instanceScores,
1034
+ scores2: right.instanceScores,
1035
+ rng,
1036
+ size: MERGE_SUBSAMPLE_SIZE
1037
+ });
1038
+ if (subsample.length === 0) return "none";
1039
+ const unique = [...new Set(subsample)];
1040
+ const uniqueIds = unique.map((index) => validationIds[index]);
1041
+ if (!budget.canAfford(evaluator.countUncached({
1042
+ candidate: proposal.candidate,
1043
+ ids: uniqueIds,
1044
+ split: "val"
1045
+ }))) return "none";
1046
+ emit({
1047
+ type: "iterationStart",
1048
+ iteration,
1049
+ parentIds: [...proposal.parentIds]
1050
+ });
1051
+ mergeAttempts.add(proposal.attemptKey);
1052
+ mergeDescriptions.add(proposal.descriptionKey);
1053
+ const uniqueEvaluation = await evaluateCached({
1054
+ candidate: proposal.candidate,
1055
+ batch: unique.map((index) => validationSet[index]),
1056
+ ids: uniqueIds,
1057
+ split: "val",
1058
+ phase: "minibatch",
1059
+ candidateId: null
1060
+ });
1061
+ const scoreByIndex = new Map(unique.map((index, position) => [index, uniqueEvaluation.scores[position]]));
1062
+ const mergedSum = require_math.sum(subsample.map((index) => scoreByIndex.get(index)));
1063
+ const parentBest = Math.max(require_math.sum(subsample.map((index) => left.instanceScores[index])), require_math.sum(subsample.map((index) => right.instanceScores[index])));
1064
+ if (mergedSum < parentBest) {
1065
+ emit({
1066
+ type: "candidateRejected",
1067
+ iteration,
1068
+ parentId: leftId,
1069
+ parentScore: parentBest,
1070
+ childScore: mergedSum,
1071
+ source: "merge",
1072
+ reason: "worse"
1073
+ });
1074
+ return "attempted";
1075
+ }
1076
+ const mergeInstances = selectValInstances(proposal.candidate);
1077
+ if (!budget.canAfford(evaluator.countUncached({
1078
+ candidate: proposal.candidate,
1079
+ ids: mergeInstances.map((index) => validationIds[index]),
1080
+ split: "val"
1081
+ }))) return "attempted";
1082
+ const evaluation = await evaluateValidation({
1083
+ candidate: proposal.candidate,
1084
+ instances: mergeInstances,
1085
+ phase: "validation",
1086
+ candidateId: records.length
1087
+ });
1088
+ const ancestor = records[proposal.ancestorId];
1089
+ const record = addCandidate({
1090
+ candidate: proposal.candidate,
1091
+ parentIds: [...proposal.parentIds],
1092
+ evaluation,
1093
+ source: "merge",
1094
+ updatedComponents: require_evaluation.componentNames(proposal.candidate).filter((name) => proposal.candidate[name] !== ancestor.candidate[name])
1095
+ });
1096
+ mergesDue -= 1;
1097
+ totalMergesTested += 1;
1098
+ emit({
1099
+ type: "candidateAccepted",
1100
+ iteration,
1101
+ candidateId: record.id,
1102
+ parentIds: record.parentIds,
1103
+ aggregateScore: record.aggregateScore,
1104
+ source: "merge"
1105
+ });
1106
+ return "attempted";
1107
+ }
1108
+ /**
1109
+ * Draws every proposal an iteration will make, before any of them runs.
1110
+ *
1111
+ * Each draw consumes the random stream — parent, minibatch, component — so
1112
+ * they all happen here, in order, on the same frontier snapshot. Doing it
1113
+ * inside the concurrent phase instead would make the whole run's trajectory
1114
+ * depend on which network call returned first.
1115
+ */
1116
+ function planProposals() {
1117
+ const state = {
1118
+ scoreMatrix: records.map((record) => record.instanceScores),
1119
+ aggregateScores: records.map((record) => record.aggregateScore),
1120
+ objectiveScores: records.map((record) => record.objectiveScores)
1121
+ };
1122
+ const plans = [];
1123
+ for (let slot = 0; slot < proposalsPerIteration; slot += 1) {
1124
+ const parent = records[candidateSelector({
1125
+ state,
1126
+ rng
1127
+ })];
1128
+ const batchIndices = batchSampler({
1129
+ trainingSet,
1130
+ iteration: iteration * proposalsPerIteration + slot,
1131
+ rng
1132
+ });
1133
+ const componentsToUpdate = componentSelector({
1134
+ candidate: parent.candidate,
1135
+ cursor: parent.componentCursor,
1136
+ iteration,
1137
+ rng
1138
+ });
1139
+ assertComponents({
1140
+ names: componentsToUpdate,
1141
+ candidate: parent.candidate,
1142
+ source: "componentSelector"
1143
+ });
1144
+ parent.componentCursor = (parent.componentCursor + 1) % Math.max(1, require_evaluation.componentNames(parent.candidate).length);
1145
+ plans.push({
1146
+ parent,
1147
+ batch: batchIndices.map((index) => trainingSet[index]),
1148
+ batchIds: batchIndices.map((index) => trainingIds[index]),
1149
+ componentsToUpdate,
1150
+ attempt: iteration * proposalsPerIteration + slot
1151
+ });
1152
+ }
1153
+ return plans;
1154
+ }
1155
+ /**
1156
+ * Reflects on one parent and screens the result on that parent's own
1157
+ * minibatch. Everything here is IO the run can overlap; nothing here mutates
1158
+ * the candidate pool, which is what makes overlapping it safe.
1159
+ */
1160
+ async function runProposal(plan) {
1161
+ const { parent, batch, batchIds, componentsToUpdate, attempt } = plan;
1162
+ const parentEvaluation = await evaluator.evaluateTraced({
1163
+ batch,
1164
+ candidate: parent.candidate,
1165
+ split: "train",
1166
+ phase: "minibatch",
1167
+ candidateId: parent.id,
1168
+ iteration
1169
+ });
1170
+ if (parentEvaluation === null) return { status: "budgetExhausted" };
1171
+ if (skipPerfectScore && parentEvaluation.scores.every((score) => score >= perfectScore)) return { status: "skipped" };
1172
+ const reflectiveDataset = await adapter.makeReflectiveDataset({
1173
+ candidate: parent.candidate,
1174
+ batch,
1175
+ evaluation: parentEvaluation,
1176
+ componentsToUpdate
1177
+ });
1178
+ let proposed;
1179
+ try {
1180
+ proposed = await propose({
1181
+ candidate: parent.candidate,
1182
+ reflectiveDataset,
1183
+ componentsToUpdate,
1184
+ rejectedProposals,
1185
+ attempt,
1186
+ reflect: countedReflect,
1187
+ signal
1188
+ });
1189
+ } catch (err) {
1190
+ if (err instanceof ReflectionBudgetExhausted) return { status: "reflectionExhausted" };
1191
+ throw err;
1192
+ }
1193
+ assertComponents({
1194
+ names: require_evaluation.componentNames(proposed),
1195
+ candidate: parent.candidate,
1196
+ source: "proposeNewTexts"
1197
+ });
1198
+ const child = {
1199
+ ...parent.candidate,
1200
+ ...proposed
1201
+ };
1202
+ const changed = require_evaluation.componentNames(proposed).length > 0 && !seenCandidates.has(require_evaluation.candidateFingerprint(child));
1203
+ emit({
1204
+ type: "proposal",
1205
+ iteration,
1206
+ parentId: parent.id,
1207
+ componentsToUpdate: [...componentsToUpdate],
1208
+ changed
1209
+ });
1210
+ if (!changed) return { status: "skipped" };
1211
+ let childEvaluation;
1212
+ try {
1213
+ childEvaluation = await evaluateCached({
1214
+ candidate: child,
1215
+ batch,
1216
+ ids: batchIds,
1217
+ split: "train",
1218
+ phase: "minibatch",
1219
+ candidateId: null
1220
+ });
1221
+ } catch (err) {
1222
+ if (err instanceof require_evaluation.BudgetExhausted) return { status: "budgetExhausted" };
1223
+ throw err;
1224
+ }
1225
+ const screened = pairMeasured({
1226
+ parent: parentEvaluation,
1227
+ child: childEvaluation
1228
+ });
1229
+ if (screened.parentScores.length === 0) return { status: "skipped" };
1230
+ return {
1231
+ status: "screened",
1232
+ plan,
1233
+ child,
1234
+ proposed,
1235
+ parentScore: require_math.mean(screened.parentScores),
1236
+ childScore: require_math.mean(screened.childScores),
1237
+ improvement: require_math.sum(screened.childScores) - require_math.sum(screened.parentScores),
1238
+ accepted: acceptance(screened)
1239
+ };
1240
+ }
1241
+ /**
1242
+ * Turns screened proposals into candidates: rejections first, then the
1243
+ * survivors the selection policy keeps, each paying for its own validation
1244
+ * sweep. Returns a stop reason when the iteration ran the run out of budget.
1245
+ */
1246
+ async function commitProposals(outcomes) {
1247
+ let stop;
1248
+ const improved = [];
1249
+ /**
1250
+ * Children two siblings converged on. Resolved here rather than while the
1251
+ * proposals were in flight: the first one in plan order keeps the child,
1252
+ * whichever of them finished first.
1253
+ */
1254
+ const claimed = /* @__PURE__ */ new Set();
1255
+ for (const outcome of outcomes) {
1256
+ if (outcome.status === "skipped") continue;
1257
+ if (outcome.status === "budgetExhausted") {
1258
+ stop ??= "budgetExhausted";
1259
+ continue;
1260
+ }
1261
+ if (outcome.status === "reflectionExhausted") {
1262
+ stop ??= "reflectionBudgetExhausted";
1263
+ continue;
1264
+ }
1265
+ const fingerprint = require_evaluation.candidateFingerprint(outcome.child);
1266
+ if (claimed.has(fingerprint)) continue;
1267
+ claimed.add(fingerprint);
1268
+ if (!outcome.accepted) {
1269
+ rememberRejection({
1270
+ proposed: outcome.proposed,
1271
+ parentScore: outcome.parentScore,
1272
+ childScore: outcome.childScore
1273
+ });
1274
+ emit({
1275
+ type: "candidateRejected",
1276
+ iteration,
1277
+ parentId: outcome.plan.parent.id,
1278
+ parentScore: outcome.parentScore,
1279
+ childScore: outcome.childScore,
1280
+ source: "mutation",
1281
+ reason: "worse"
1282
+ });
1283
+ continue;
1284
+ }
1285
+ improved.push(outcome);
1286
+ }
1287
+ const survivors = selectSurvivors(improved);
1288
+ for (const outcome of improved) {
1289
+ if (survivors.includes(outcome)) continue;
1290
+ emit({
1291
+ type: "candidateRejected",
1292
+ iteration,
1293
+ parentId: outcome.plan.parent.id,
1294
+ parentScore: outcome.parentScore,
1295
+ childScore: outcome.childScore,
1296
+ source: "mutation",
1297
+ reason: "notSelected"
1298
+ });
1299
+ }
1300
+ const baseId = records.length;
1301
+ const scheduled = [];
1302
+ let owed = 0;
1303
+ for (const outcome of survivors) {
1304
+ const instances = selectValInstances(outcome.child);
1305
+ const uncached = evaluator.countUncached({
1306
+ candidate: outcome.child,
1307
+ ids: instances.map((index) => validationIds[index]),
1308
+ split: "val"
1309
+ });
1310
+ if (!budget.canAfford(owed + uncached)) {
1311
+ stop ??= "budgetExhausted";
1312
+ break;
1313
+ }
1314
+ owed += uncached;
1315
+ scheduled.push({
1316
+ outcome,
1317
+ candidateId: baseId + scheduled.length,
1318
+ instances
1319
+ });
1320
+ }
1321
+ const scored = await require_concurrency.mapWithConcurrency({
1322
+ items: scheduled,
1323
+ limit: proposalConcurrency,
1324
+ signal,
1325
+ task: async (item) => {
1326
+ try {
1327
+ return {
1328
+ item,
1329
+ evaluation: await evaluateValidation({
1330
+ candidate: item.outcome.child,
1331
+ instances: item.instances,
1332
+ phase: "validation",
1333
+ candidateId: item.candidateId
1334
+ })
1335
+ };
1336
+ } catch (err) {
1337
+ if (err instanceof require_evaluation.BudgetExhausted) return {
1338
+ item,
1339
+ evaluation: void 0
1340
+ };
1341
+ throw err;
1342
+ }
1343
+ }
1344
+ });
1345
+ for (const { item, evaluation } of scored) {
1346
+ if (evaluation === void 0) {
1347
+ stop ??= "budgetExhausted";
1348
+ break;
1349
+ }
1350
+ const record = addCandidate({
1351
+ candidate: item.outcome.child,
1352
+ parentIds: [item.outcome.plan.parent.id],
1353
+ evaluation,
1354
+ source: "mutation",
1355
+ updatedComponents: require_evaluation.componentNames(item.outcome.proposed)
1356
+ });
1357
+ emit({
1358
+ type: "candidateAccepted",
1359
+ iteration,
1360
+ candidateId: record.id,
1361
+ parentIds: record.parentIds,
1362
+ aggregateScore: record.aggregateScore,
1363
+ source: "mutation"
1364
+ });
1365
+ }
1366
+ return stop;
1367
+ }
1368
+ /** The improving proposals an iteration keeps, in the order they were made. */
1369
+ function selectSurvivors(improved) {
1370
+ if (improved.length <= survivorsPerIteration) return [...improved];
1371
+ return improved.map((outcome, index) => ({
1372
+ outcome,
1373
+ index
1374
+ })).sort((a, b) => b.outcome.improvement - a.outcome.improvement || a.index - b.index).slice(0, survivorsPerIteration).sort((a, b) => a.index - b.index).map((entry) => entry.outcome);
1375
+ }
1376
+ while (true) {
1377
+ if (signal?.aborted) {
1378
+ stopReason = "aborted";
1379
+ break;
1380
+ }
1381
+ if (require_evaluation.costExhausted({
1382
+ usage: evaluator.usage(),
1383
+ maxCostUsd
1384
+ })) {
1385
+ stopReason = "costExhausted";
1386
+ break;
1387
+ }
1388
+ if (deadline.exceeded()) {
1389
+ stopReason = "deadlineReached";
1390
+ break;
1391
+ }
1392
+ if (iteration >= maxIterations) {
1393
+ stopReason = "maxIterations";
1394
+ break;
1395
+ }
1396
+ if (!budget.canAfford(proposalsPerIteration * minibatchSize * 2 + validationSet.length)) {
1397
+ stopReason = "budgetExhausted";
1398
+ break;
1399
+ }
1400
+ if (reflection?.maxCalls !== void 0 && reflectionCalls >= reflection.maxCalls) {
1401
+ stopReason = "reflectionBudgetExhausted";
1402
+ break;
1403
+ }
1404
+ const spentBeforeIteration = budget.spent();
1405
+ let pendingStop;
1406
+ try {
1407
+ const mergeScheduled = mergeConfig.enabled && mergesDue > 0 && lastIterationAccepted && totalMergesTested < mergeConfig.maxInvocations;
1408
+ lastIterationAccepted = false;
1409
+ if (!(mergeScheduled && await tryMerge() === "attempted")) {
1410
+ const plans = planProposals();
1411
+ emit({
1412
+ type: "iterationStart",
1413
+ iteration,
1414
+ parentIds: plans.map((plan) => plan.parent.id)
1415
+ });
1416
+ pendingStop = await commitProposals(await require_concurrency.mapWithConcurrency({
1417
+ items: plans,
1418
+ limit: proposalConcurrency,
1419
+ task: runProposal,
1420
+ signal
1421
+ }));
1422
+ }
1423
+ } catch (err) {
1424
+ if (signal?.aborted) {
1425
+ stopReason = "aborted";
1426
+ break;
1427
+ }
1428
+ if (err instanceof require_evaluation.BudgetExhausted) {
1429
+ stopReason = "budgetExhausted";
1430
+ break;
1431
+ }
1432
+ if (raiseOnError || budget.spent() === spentBeforeIteration) throw err;
1433
+ emit({
1434
+ type: "error",
1435
+ iteration,
1436
+ err
1437
+ });
1438
+ }
1439
+ iteration += 1;
1440
+ await checkpoint();
1441
+ if (pendingStop !== void 0) {
1442
+ stopReason = pendingStop;
1443
+ break;
1444
+ }
1445
+ }
1446
+ const bestCandidateId = valEvaluationPolicy.bestCandidate(records);
1447
+ const best = records[bestCandidateId];
1448
+ const testScore = testSet === void 0 ? void 0 : require_evaluation.measuredMean(await evaluateCached({
1449
+ candidate: best.candidate,
1450
+ batch: testSet,
1451
+ ids: testIds,
1452
+ split: "test",
1453
+ phase: "test",
1454
+ candidateId: bestCandidateId,
1455
+ charge: false
1456
+ }));
1457
+ emit({
1458
+ type: "finish",
1459
+ reason: stopReason,
1460
+ bestCandidateId,
1461
+ metricCalls: budget.spent(),
1462
+ ...testScore === void 0 ? {} : { testScore }
1463
+ });
1464
+ const perObjectiveBest = collectPerObjectiveBest(records);
1465
+ const bestOutputs = outputsByCandidate.get(bestCandidateId);
1466
+ return {
1467
+ bestCandidate: best.candidate,
1468
+ bestScore: best.aggregateScore,
1469
+ usage: evaluator.usage(),
1470
+ bestCandidateId,
1471
+ ...testScore === void 0 ? {} : {
1472
+ testScore,
1473
+ testMetricCalls: evaluator.unchargedCalls()
1474
+ },
1475
+ ...bestOutputs === void 0 ? {} : { bestOutputs },
1476
+ candidates: records,
1477
+ paretoFrontier: collectDominatorIds(records).map((id) => records[id]),
1478
+ ...perObjectiveBest === void 0 ? {} : { perObjectiveBest },
1479
+ scoreMatrix: records.map((record) => [...record.instanceScores]),
1480
+ metricCalls: budget.spent(),
1481
+ reflectionCalls,
1482
+ cacheHits: evaluator.cacheHits(),
1483
+ iterations: iteration,
1484
+ stopReason,
1485
+ snapshot: takeSnapshot()
1486
+ };
1487
+ }
1488
+ /**
1489
+ * Range checks on the search knobs, run at construction so a configuration
1490
+ * that could never terminate is refused before a task is ever handed to it.
1491
+ * Task-shaped checks stay in `runGepa`, where the data is.
1492
+ */
1493
+ function assertGepaConfig(config) {
1494
+ if (config.reflection?.buildPrompt !== void 0 && config.reflection.strategies !== void 0) throw new Error("reflection takes buildPrompt or strategies, not both");
1495
+ if (config.reflection?.strategies?.length === 0) throw new Error("reflection.strategies must not be empty");
1496
+ const { minibatchSize = DEFAULT_MINIBATCH_SIZE, maxIterations = Number.POSITIVE_INFINITY, perfectScore = 1, rejectedProposalMemory = DEFAULT_REJECTED_PROPOSAL_MEMORY, proposals } = config;
1497
+ const proposalsPerIteration = proposals?.perIteration ?? 1;
1498
+ const proposalConcurrency = proposals?.concurrency ?? 1;
1499
+ if (!Number.isInteger(proposalsPerIteration) || proposalsPerIteration < 1) throw new Error(`proposals.perIteration must be a positive integer, received ${proposalsPerIteration}`);
1500
+ if (!Number.isInteger(proposalConcurrency) || proposalConcurrency < 1) throw new Error(`proposals.concurrency must be a positive integer, received ${proposalConcurrency}`);
1501
+ keepCount(proposals?.selection ?? "all");
1502
+ if (!Number.isInteger(minibatchSize) || minibatchSize < 1) throw new Error(`minibatchSize must be a positive integer, received ${minibatchSize}`);
1503
+ if (!Number.isFinite(perfectScore)) throw new Error(`perfectScore must be a finite number, received ${perfectScore}`);
1504
+ if (!Number.isInteger(rejectedProposalMemory) || rejectedProposalMemory < 0) throw new Error(`rejectedProposalMemory must be a non-negative integer, received ${rejectedProposalMemory}`);
1505
+ if (maxIterations !== Number.POSITIVE_INFINITY && (!Number.isInteger(maxIterations) || maxIterations < 0)) throw new Error(`maxIterations must be a non-negative integer or Infinity, received ${maxIterations}`);
1506
+ }
1507
+ /**
1508
+ * The single narrowing point for a snapshot's candidate pool. A snapshot is
1509
+ * JSON that left the process and came back with plain string keys, so every
1510
+ * record is checked against the seed's components before it is read as one of
1511
+ * them — behind the fingerprint check, which has already established that the
1512
+ * snapshot belongs to this run.
1513
+ */
1514
+ function restoreRecords(args) {
1515
+ const { records, seedCandidate } = args;
1516
+ const known = new Set(require_evaluation.componentNames(seedCandidate));
1517
+ for (const record of records) {
1518
+ const named = [...Object.keys(record.candidate), ...record.updatedComponents];
1519
+ for (const name of named) if (!known.has(name)) throw new Error(`checkpoint names the component "${name}", which the seed candidate does not have (${[...known].join(", ")})`);
1520
+ }
1521
+ return copyRecords(records);
1522
+ }
1523
+ /**
1524
+ * Rejections arrive from a snapshot keyed by plain strings. Reading them
1525
+ * through the seed's own component names narrows them without an assertion,
1526
+ * and drops anything the seed no longer has.
1527
+ */
1528
+ function restoreRejections(args) {
1529
+ const { rejections, components } = args;
1530
+ const restored = {};
1531
+ for (const component of components) {
1532
+ const history = rejections[component];
1533
+ if (history !== void 0) restored[component] = history.map((entry) => ({ ...entry }));
1534
+ }
1535
+ return restored;
1536
+ }
1537
+ /** The inverse: back to the plain string keys a snapshot is written with. */
1538
+ function snapshotRejections(args) {
1539
+ const { rejections, components } = args;
1540
+ const copy = {};
1541
+ for (const component of components) {
1542
+ const history = rejections[component];
1543
+ if (history !== void 0) copy[component] = history.map((entry) => ({ ...entry }));
1544
+ }
1545
+ return copy;
1546
+ }
1547
+ /**
1548
+ * Patches are merged over the parent, so a name the candidate does not have is
1549
+ * added rather than refused: the run would go on optimizing text the system
1550
+ * under optimization never reads, and every descendant would carry it.
1551
+ */
1552
+ function assertComponents(args) {
1553
+ const { names, candidate, source } = args;
1554
+ for (const name of names) if (!Object.hasOwn(candidate, name)) throw new Error(`${source} named "${name}", which is not a component of the candidate (${Object.keys(candidate).join(", ")})`);
1555
+ }
1556
+ /** Copies everything a run mutates in place, so a snapshot never aliases one. */
1557
+ function copyRecords(records) {
1558
+ return records.map((record) => ({
1559
+ ...record,
1560
+ parentIds: [...record.parentIds],
1561
+ instanceScores: [...record.instanceScores],
1562
+ updatedComponents: [...record.updatedComponents],
1563
+ ...record.objectiveScores === void 0 ? {} : { objectiveScores: { ...record.objectiveScores } }
1564
+ }));
1565
+ }
1566
+ /** How many improving proposals an iteration is allowed to keep. */
1567
+ function keepCount(selection) {
1568
+ if (selection === "all") return Number.POSITIVE_INFINITY;
1569
+ if (selection === "best") return 1;
1570
+ if (!Number.isInteger(selection.keep) || selection.keep < 1) throw new Error(`proposals.selection.keep must be a positive integer, received ${selection.keep}`);
1571
+ return selection.keep;
1572
+ }
1573
+ /**
1574
+ * Mean of each objective over the instances this candidate was scored on.
1575
+ *
1576
+ * An objective only some of those instances reported is left out rather than
1577
+ * averaged over the ones that did: candidates are compared objective by
1578
+ * objective on the frontier, and a mean over one instance is not the same
1579
+ * measurement as a mean over forty.
1580
+ */
1581
+ function meanObjectives(args) {
1582
+ const { rows, scores } = args;
1583
+ const measured = rows.filter((_, index) => scores[index] !== void 0);
1584
+ const totals = /* @__PURE__ */ new Map();
1585
+ for (const row of measured) for (const [objective, value] of Object.entries(row ?? {})) {
1586
+ const running = totals.get(objective) ?? {
1587
+ total: 0,
1588
+ count: 0
1589
+ };
1590
+ running.total += value;
1591
+ running.count += 1;
1592
+ totals.set(objective, running);
1593
+ }
1594
+ const complete = [...totals].filter(([, { count }]) => count === measured.length);
1595
+ if (complete.length === 0) return;
1596
+ return Object.fromEntries(complete.map(([objective, { total, count }]) => [objective, total / count]));
1597
+ }
1598
+ function collectPerObjectiveBest(records) {
1599
+ const bests = objectiveBests(records.map((record) => record.objectiveScores));
1600
+ if (Object.keys(bests).length === 0) return;
1601
+ return Object.fromEntries(Object.entries(bests).map(([objective, score]) => [objective, {
1602
+ score,
1603
+ candidateIds: records.filter((record) => record.objectiveScores?.[objective] === score).map((record) => record.id)
1604
+ }]));
1605
+ }
1606
+ /**
1607
+ * Candidates that uniquely win at least one validation instance once dominated
1608
+ * lineages are pruned. This is both the reported frontier and the pool merge
1609
+ * draws its parents from.
1610
+ */
1611
+ function collectDominatorIds(records) {
1612
+ const fronts = pruneDominatedFronts({
1613
+ fronts: buildInstanceFronts({ scoreMatrix: records.map((record) => record.instanceScores) }),
1614
+ aggregateScores: records.map((record) => record.aggregateScore)
1615
+ });
1616
+ const ids = /* @__PURE__ */ new Set();
1617
+ for (const front of fronts) for (const id of front) ids.add(id);
1618
+ return [...ids].sort((a, b) => a - b);
1619
+ }
1620
+ /**
1621
+ * Names an instance by a hash of its content rather than by the content
1622
+ * itself: the id ends up inside every cache key and inside the checkpoint
1623
+ * fingerprint, and embedding whole examples there costs memory proportional to
1624
+ * the dataset for no benefit. Data that will not serialize falls back to its
1625
+ * position, which is stable for as long as the dataset order is.
1626
+ */
1627
+ function defaultInstanceId(args) {
1628
+ const hash = require_evaluation.stableHash(args.datum);
1629
+ return hash === "" ? String(args.index) : hash;
1630
+ }
1631
+ /**
1632
+ * The two rollout sets restricted to the instances both of them measured.
1633
+ *
1634
+ * Screening is a paired comparison over one minibatch: a transient row is a
1635
+ * rollout that never happened, and leaving it in scores the candidate that ran
1636
+ * against the infrastructure failure of the one that did not.
1637
+ */
1638
+ function pairMeasured(args) {
1639
+ const { parent, child } = args;
1640
+ const parentScores = [];
1641
+ const childScores = [];
1642
+ for (let index = 0; index < parent.scores.length; index += 1) {
1643
+ if (parent.transient?.[index] === true || child.transient?.[index] === true) continue;
1644
+ parentScores.push(parent.scores[index]);
1645
+ childScores.push(child.scores[index]);
1646
+ }
1647
+ return {
1648
+ parentScores,
1649
+ childScores
1650
+ };
1651
+ }
1652
+ //#endregion
1653
+ exports.GepaOptimizer = GepaOptimizer;
1654
+ exports.allComponentsSelector = allComponentsSelector;
1655
+ exports.buildGeneralizePrompt = require_reflection.buildGeneralizePrompt;
1656
+ exports.buildReflectionPrompt = require_reflection.buildReflectionPrompt;
1657
+ exports.buildRewritePrompt = require_reflection.buildRewritePrompt;
1658
+ exports.buildSimplifyPrompt = require_reflection.buildSimplifyPrompt;
1659
+ exports.createDemoProposer = createDemoProposer;
1660
+ exports.createPipelineAdapter = createPipelineAdapter;
1661
+ exports.currentBestSelector = currentBestSelector;
1662
+ exports.diverseReflectionStrategies = require_reflection.diverseReflectionStrategies;
1663
+ exports.epsilonGreedySelector = epsilonGreedySelector;
1664
+ exports.fullEvaluationPolicy = fullEvaluationPolicy;
1665
+ exports.improvementAcceptance = improvementAcceptance;
1666
+ exports.lowerBoundEvaluationPolicy = lowerBoundEvaluationPolicy;
1667
+ exports.pairedPermutationAcceptance = pairedPermutationAcceptance;
1668
+ exports.paretoSelector = paretoSelector;
1669
+ exports.roundRobinComponentSelector = roundRobinComponentSelector;
1670
+ exports.subsampledEvaluationPolicy = subsampledEvaluationPolicy;
1671
+ exports.topKParetoSelector = topKParetoSelector;