textopt 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +65 -25
  2. package/dist/bootstrap-search/index.cjs +159 -73
  3. package/dist/bootstrap-search/index.d.cts +32 -10
  4. package/dist/bootstrap-search/index.d.mts +32 -10
  5. package/dist/bootstrap-search/index.mjs +150 -66
  6. package/dist/demos-9v5ts7F3.cjs +244 -0
  7. package/dist/{demos-B0pVQjYC.d.mts → demos-ASsSXYXA.d.mts} +10 -3
  8. package/dist/demos-Brobjfuc.mjs +215 -0
  9. package/dist/{demos-BTuzFNsp.d.cts → demos-ByaLZy-Z.d.cts} +10 -3
  10. package/dist/file-cache.cjs +27 -8
  11. package/dist/file-cache.d.cts +13 -0
  12. package/dist/file-cache.d.mts +13 -0
  13. package/dist/file-cache.mjs +27 -8
  14. package/dist/gepa/index.cjs +128 -80
  15. package/dist/gepa/index.d.cts +15 -7
  16. package/dist/gepa/index.d.mts +15 -7
  17. package/dist/gepa/index.mjs +101 -55
  18. package/dist/index.cjs +157 -30
  19. package/dist/index.d.cts +177 -7
  20. package/dist/index.d.mts +177 -7
  21. package/dist/index.mjs +139 -18
  22. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  23. package/dist/math-Dqme4rYz.mjs +123 -0
  24. package/dist/mipro/index.cjs +104 -70
  25. package/dist/mipro/index.d.cts +17 -14
  26. package/dist/mipro/index.d.mts +17 -14
  27. package/dist/mipro/index.mjs +90 -58
  28. package/dist/opro/index.cjs +136 -51
  29. package/dist/opro/index.d.cts +17 -9
  30. package/dist/opro/index.d.mts +17 -9
  31. package/dist/opro/index.mjs +121 -38
  32. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-4Zv-Zt2t.d.cts} +90 -5
  33. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ds5mzYjz.d.mts} +90 -5
  34. package/dist/random-search/index.cjs +99 -49
  35. package/dist/random-search/index.d.cts +15 -13
  36. package/dist/random-search/index.d.mts +15 -13
  37. package/dist/random-search/index.mjs +89 -41
  38. package/dist/{reflection-Cr_upzU0.d.mts → reflection-CMezGu6u.d.mts} +38 -14
  39. package/dist/{reflection-CQToe-5B.d.cts → reflection-D0A7eahD.d.cts} +38 -14
  40. package/dist/reporting-bq007_2z.d.cts +294 -0
  41. package/dist/reporting-bq007_2z.d.mts +294 -0
  42. package/dist/simba/index.cjs +216 -83
  43. package/dist/simba/index.d.cts +53 -13
  44. package/dist/simba/index.d.mts +53 -13
  45. package/dist/simba/index.mjs +206 -75
  46. package/dist/testing.cjs +1 -0
  47. package/dist/testing.d.cts +5 -3
  48. package/dist/testing.d.mts +5 -3
  49. package/dist/testing.mjs +1 -1
  50. package/dist/{evaluation-OZOp6TB7.cjs → warnings-CWRJF-jA.cjs} +228 -5
  51. package/dist/{evaluation-BV0nSZVx.mjs → warnings-OxvDi9kN.mjs} +175 -6
  52. package/docs/adapters.md +169 -0
  53. package/docs/benchmark.md +90 -0
  54. package/docs/data-prep.md +113 -0
  55. package/docs/distillation.md +128 -0
  56. package/docs/evaluation.md +87 -0
  57. package/docs/metric-preflight.md +132 -0
  58. package/docs/optimizers.md +293 -0
  59. package/docs/tuning.md +130 -0
  60. package/package.json +6 -4
  61. package/dist/demos-B9BJiNKz.cjs +0 -143
  62. package/dist/demos-Degx6UmP.mjs +0 -126
  63. package/dist/math-DhrDmpFS.mjs +0 -78
  64. package/dist/types-CWv4IQFF.d.cts +0 -129
  65. package/dist/types-CWv4IQFF.d.mts +0 -129
@@ -1,8 +1,8 @@
1
- import { n as Candidate, t as Adapter, u as TextModel } from "../types-CWv4IQFF.mjs";
1
+ import { S as UsageTotals, a as RunFinished, f as Adapter, i as Reporter, n as OptimizerEvent, o as isCandidateAccepted, p as Candidate, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted, x as TextModel } from "../reporting-bq007_2z.mjs";
2
2
  import { n as EvaluationCache, t as CachedScore } from "../cache-CuSo0NJ8.mjs";
3
3
  import { t as Rng } from "../rng-BR5MOedA.mjs";
4
- import { r as DemoRenderer } from "../demos-B0pVQjYC.mjs";
5
- import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-DqCoth_w.mjs";
4
+ import { r as DemoRenderer } from "../demos-ASsSXYXA.mjs";
5
+ import { n as OptimizerResult, r as OptimizerTask, s as EvaluationEvent, t as Optimizer } from "../optimizer-Ds5mzYjz.mjs";
6
6
  import { t as BatchSampler } from "../sampling-DFo_7RNJ.mjs";
7
7
  //#region src/simba/advice.d.ts
8
8
  interface AdviceTrajectory<Output = unknown> {
@@ -13,6 +13,12 @@ interface AdviceTrajectory<Output = unknown> {
13
13
  interface AdvicePromptArgs<Datum = unknown, Output = unknown> {
14
14
  /** Components the advice is wanted for, named so each can be addressed. */
15
15
  components: readonly string[];
16
+ /**
17
+ * What each of those components currently says. Advice is appended to this
18
+ * text rather than replacing it, so a proposer that cannot see it writes
19
+ * blind: it restates what is already there, and cannot correct it.
20
+ */
21
+ current: Record<string, string>;
16
22
  input: Datum;
17
23
  /** The higher scoring run of this instance, when there is one to contrast. */
18
24
  better?: AdviceTrajectory<Output>;
@@ -52,11 +58,41 @@ interface SimbaFinalist<K extends string = string> {
52
58
  step: number;
53
59
  }
54
60
  interface SimbaConfig {
55
- /** Instances per step. Default 32. */
61
+ /**
62
+ * Instances per step. Default 32.
63
+ *
64
+ * Minibatch defaults do not transfer between optimizers — GEPA's 3 and
65
+ * MIPRO's 35 mean different things, and SIMBA's ranks instances within a
66
+ * batch by how much its programs disagreed.
67
+ *
68
+ * @see docs/tuning.md
69
+ */
56
70
  minibatchSize?: number;
57
71
  /** Programs sampled per step, and candidates built from them. Default 6. */
58
72
  candidates?: number;
59
- /** Steps to run. Default 8. */
73
+ /**
74
+ * How many evaluations may be in flight at once. Default 1.
75
+ *
76
+ * Covers the two places a step's work is independent — scoring the
77
+ * candidates a step built, and sweeping the finalists at the end of the run
78
+ * — and nothing else. The trajectory samples and the mutations that read
79
+ * them are deliberately left in sequence: each of those reads state the one
80
+ * before it wrote, so overlapping them would make a seeded run depend on
81
+ * which call returned first.
82
+ */
83
+ concurrency?: number;
84
+ /**
85
+ * Steps to run. Default 8.
86
+ *
87
+ * A run has to be funded past the finalist reserve before any step happens:
88
+ * `min(candidates + 1, maxSteps + 1) * validationSet.length` rollouts are
89
+ * held back to sweep the finalists, and each step then costs
90
+ * `candidates * minibatchSize + minibatchSize`. Fund only the steps and the
91
+ * run stops after one, reporting `budgetExhausted` like any other exhausted
92
+ * budget.
93
+ *
94
+ * @see docs/tuning.md
95
+ */
60
96
  maxSteps?: number;
61
97
  /** Demos a candidate may hold before the loop starts dropping them. Default 4. */
62
98
  maxDemos?: number;
@@ -97,6 +133,8 @@ interface SimbaSnapshot {
97
133
  metricCalls: number;
98
134
  reflectionCalls: number;
99
135
  cacheHits: number;
136
+ /** Usage already spent, so a resumed run reports totals and honours ceilings. */
137
+ usage?: UsageTotals;
100
138
  rngState: number;
101
139
  sampler?: unknown;
102
140
  cache?: [string, CachedScore][];
@@ -121,7 +159,8 @@ interface SimbaTask<Datum, Trajectory = unknown, Output = unknown, K extends str
121
159
  index: number;
122
160
  }) => string;
123
161
  cache?: EvaluationCache | false;
124
- onEvent?: (event: SimbaEvent<NoInfer<K>>) => void;
162
+ /** Observers of the run. Every one sees every event; none can fail it. */
163
+ reporters?: readonly Reporter<SimbaEvent<NoInfer<K>>>[];
125
164
  onCheckpoint?: (snapshot: SimbaSnapshot) => void | Promise<void>;
126
165
  resumeFrom?: SimbaSnapshot;
127
166
  }
@@ -143,17 +182,18 @@ type SimbaEvent<K extends string = string> = {
143
182
  /** The program it was mutated from; 0 is the seed. */
144
183
  sourceProgram: number;
145
184
  minibatchScore: number;
146
- } | {
185
+ } | ({
186
+ type: "candidateAccepted";
187
+ /** The step whose winner this was. */
188
+ step: number;
189
+ } & CandidateAccepted<K>) | {
147
190
  type: "error";
148
191
  step: number;
149
192
  err: unknown;
150
- } | {
193
+ } | ({
151
194
  type: "finish";
152
195
  reason: SimbaStopReason;
153
- bestScore: number;
154
- metricCalls: number;
155
- testScore?: number;
156
- };
196
+ } & RunFinished);
157
197
  interface SimbaResult<K extends string = string, Output = unknown> extends OptimizerResult<K, SimbaStopReason, Output> {
158
198
  /** The seed's own validation score, so the lift the run bought is readable. */
159
199
  seedScore: number;
@@ -286,4 +326,4 @@ declare function evenlySpacedIndices(args: {
286
326
  count: number;
287
327
  }): number[];
288
328
  //#endregion
289
- export { type AdvicePromptArgs, type AdvicePromptBuilder, type AdviceTrajectory, type SimbaBucket, type SimbaConfig, type SimbaEvent, type SimbaFinalist, SimbaOptimizer, type SimbaResult, type SimbaRollout, type SimbaSample, type SimbaSnapshot, type SimbaStopReason, type SimbaStrategy, type SimbaTask, buildAdvicePrompt, buildBuckets, evenlySpacedIndices, parseAdvice, percentile, samplePoisson, softmaxWeights, topKPlusBaseline };
329
+ export { type AdvicePromptArgs, type AdvicePromptBuilder, type AdviceTrajectory, type CandidateAccepted, type OptimizerEvent, type ReportableEvent, type Reporter, type RunFinished, type SimbaBucket, type SimbaConfig, type SimbaEvent, type SimbaFinalist, SimbaOptimizer, type SimbaResult, type SimbaRollout, type SimbaSample, type SimbaSnapshot, type SimbaStopReason, type SimbaStrategy, type SimbaTask, buildAdvicePrompt, buildBuckets, evenlySpacedIndices, isCandidateAccepted, isRunFinished, parseAdvice, percentile, samplePoisson, softmaxWeights, topKPlusBaseline };
@@ -1,5 +1,6 @@
1
- import { d as createMemoryCache, i as measuredMean, l as runFingerprint, n as costExhausted, o as createDeadline, p as createBudget, r as createEvaluator, s as assertResumable, t as BudgetExhausted, u as componentNames } from "../evaluation-BV0nSZVx.mjs";
2
- import { n as formatDemos, r as parseDemos } from "../demos-Degx6UmP.mjs";
1
+ import { S as createBudget, _ as componentNames, a as instanceRow, b as defaultInstanceId, c as BudgetExhausted, d as measuredMean, g as runFingerprint, i as flushReporters, l as costExhausted, m as assertResumable, o as isCandidateAccepted, p as createDeadline, r as createEmitter, s as isRunFinished, t as resolveValidationSet, u as createEvaluator, v as candidateHash, y as createMemoryCache } from "../warnings-OxvDi9kN.mjs";
2
+ import { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
3
+ import { i as replaceDemos, r as parseDemos } from "../demos-Brobjfuc.mjs";
3
4
  import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
4
5
  import { t as createEpochShuffledSampler } from "../sampling-CfHt7Gue.mjs";
5
6
  //#region src/simba/advice.ts
@@ -14,7 +15,7 @@ const ADVICE_BLOCK = /<advice\s+component="([^"]+)"\s*>([\s\S]*?)<\/advice>/g;
14
15
  * behaviour rather than to the instance being easier.
15
16
  */
16
17
  function buildAdvicePrompt(args) {
17
- const { components, input, better, worse } = args;
18
+ const { components, current, input, better, worse } = args;
18
19
  return [
19
20
  "Two runs of the same system on the same input are shown below, with the reward each earned.",
20
21
  "Your job is to write advice that would make the system behave like the higher scoring run the next time it sees a similar input.",
@@ -25,12 +26,18 @@ function buildAdvicePrompt(args) {
25
26
  ...worse === void 0 ? [] : trajectoryBlock("worse", worse),
26
27
  ...better === void 0 ? [] : trajectoryBlock("better", better),
27
28
  "",
28
- "Write advice for each of these components:",
29
+ "Write advice for each of these components, shown with what it says now:",
29
30
  "",
30
31
  "<components>",
31
- components.join("\n"),
32
+ components.map((component) => [
33
+ `<component name="${component}">`,
34
+ current[component] ?? "",
35
+ "</component>"
36
+ ].join("\n")).join("\n"),
32
37
  "</components>",
33
38
  "",
39
+ "Your advice is appended to what the component already says. Do not restate advice it already carries; add what is missing or correct what is wrong.",
40
+ "",
34
41
  "The component will not have access to this example, so advice that only covers this input is wasted. State the general behaviour it should adopt, and be concrete about when it applies.",
35
42
  "Address each component's own sub-task rather than the system as a whole.",
36
43
  "Base the advice on what actually differed between the two runs. If nothing useful can be said for a component, leave it out.",
@@ -228,21 +235,32 @@ const DEMO_DROP_SCALE = 3;
228
235
  var SimbaOptimizer = class {
229
236
  #config;
230
237
  constructor(config = {}) {
238
+ assertSimbaConfig(config);
231
239
  this.#config = config;
232
240
  }
233
- optimize(task) {
234
- return run({
235
- config: this.#config,
236
- task
237
- });
241
+ async optimize(task) {
242
+ try {
243
+ return await run({
244
+ config: this.#config,
245
+ task
246
+ });
247
+ } finally {
248
+ await flushReporters(task.reporters ?? []);
249
+ }
238
250
  }
239
251
  };
240
252
  async function run(args) {
241
253
  const { config, task } = args;
242
- const { minibatchSize = DEFAULT_MINIBATCH_SIZE, candidates: candidateCount = DEFAULT_CANDIDATES, maxSteps = DEFAULT_MAX_STEPS, maxDemos = DEFAULT_MAX_DEMOS, samplingTemperature = DEFAULT_TEMPERATURE, candidateTemperature = DEFAULT_TEMPERATURE, strategies, maxReflectionCalls, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
243
- const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, demoComponents = [], instructionComponents, renderDemo, buildAdvicePrompt: buildPrompt = buildAdvicePrompt, sampler = createEpochShuffledSampler({ minibatchSize }), maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
254
+ const { minibatchSize = DEFAULT_MINIBATCH_SIZE, candidates: candidateCount = DEFAULT_CANDIDATES, concurrency = 1, maxSteps = DEFAULT_MAX_STEPS, maxDemos = DEFAULT_MAX_DEMOS, samplingTemperature = DEFAULT_TEMPERATURE, candidateTemperature = DEFAULT_TEMPERATURE, strategies, maxReflectionCalls, seed = 0, trackBestOutputs = false, checkpointCache = true } = config;
255
+ const { seedCandidate, trainingSet, validationSet: requestedValidationSet, testSet, adapter, reflect, demoComponents = [], instructionComponents, renderDemo, buildAdvicePrompt: buildPrompt = buildAdvicePrompt, sampler = createEpochShuffledSampler({ minibatchSize }), maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, reporters = [], onCheckpoint, resumeFrom, signal } = task;
256
+ const { validationSet, warnings } = resolveValidationSet({
257
+ validationSet: requestedValidationSet,
258
+ trainingSet
259
+ });
260
+ const emit = createEmitter(reporters);
244
261
  const components = componentNames(seedCandidate);
245
- const ruleComponents = instructionComponents ?? components.filter((name) => !demoComponents.includes(name));
262
+ const nonDemo = components.filter((name) => !demoComponents.includes(name));
263
+ const ruleComponents = instructionComponents ?? (nonDemo.length > 0 ? nonDemo : components);
246
264
  const enabled = strategies ?? (demoComponents.length > 0 ? ["appendDemo", "appendRule"] : ["appendRule"]);
247
265
  if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
248
266
  if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
@@ -251,6 +269,10 @@ async function run(args) {
251
269
  if (enabled.includes("appendDemo") && demoComponents.length === 0) throw new Error("the appendDemo strategy requires at least one entry in demoComponents");
252
270
  if (enabled.includes("appendRule") && ruleComponents.length === 0) throw new Error("the appendRule strategy requires at least one instruction component to write into");
253
271
  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");
272
+ const trainingIds = trainingSet.map((datum, index) => instanceId({
273
+ datum,
274
+ index
275
+ }));
254
276
  const validationIds = validationSet.map((datum, index) => instanceId({
255
277
  datum,
256
278
  index
@@ -261,10 +283,7 @@ async function run(args) {
261
283
  })) ?? [];
262
284
  const fingerprint = runFingerprint({
263
285
  seedCandidate,
264
- trainingIds: trainingSet.map((datum, index) => instanceId({
265
- datum,
266
- index
267
- })),
286
+ trainingIds,
268
287
  validationIds,
269
288
  seed,
270
289
  ...cacheNamespace === void 0 ? {} : { cacheNamespace }
@@ -288,8 +307,9 @@ async function run(args) {
288
307
  ...evaluationCache === void 0 ? {} : { cache: evaluationCache },
289
308
  trackOutputs: true,
290
309
  cacheHits: resumeFrom?.cacheHits ?? 0,
310
+ ...resumeFrom?.usage === void 0 ? {} : { usage: resumeFrom.usage },
291
311
  ...signal === void 0 ? {} : { signal },
292
- onEvaluation: (event) => onEvent?.({
312
+ onEvaluation: (event) => emit({
293
313
  type: "evaluation",
294
314
  ...event
295
315
  })
@@ -305,7 +325,7 @@ async function run(args) {
305
325
  let step = resumeFrom?.step ?? 0;
306
326
  let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
307
327
  let stopReason = "maxSteps";
308
- onEvent?.({
328
+ emit({
309
329
  type: "start",
310
330
  components,
311
331
  validationSetSize: validationSet.length
@@ -340,16 +360,17 @@ async function run(args) {
340
360
  scores: programs.map((_, index) => averageScore(index)),
341
361
  k: candidateCount
342
362
  });
343
- onEvent?.({
363
+ emit({
344
364
  type: "stepStart",
345
365
  step,
346
366
  poolSize: pool.length
347
367
  });
348
- const batch = sampler({
368
+ const batchIndices = sampler({
349
369
  trainingSet,
350
370
  iteration: step,
351
371
  rng
352
- }).map((index) => trainingSet[index]);
372
+ });
373
+ const batch = batchIndices.map((index) => trainingSet[index]);
353
374
  const samples = [];
354
375
  for (let slot = 0; slot < candidateCount; slot += 1) {
355
376
  const programIndex = softmaxSample({
@@ -411,7 +432,7 @@ async function run(args) {
411
432
  high
412
433
  });
413
434
  } catch (err) {
414
- onEvent?.({
435
+ emit({
415
436
  type: "error",
416
437
  step,
417
438
  err
@@ -426,36 +447,54 @@ async function run(args) {
426
447
  });
427
448
  }
428
449
  let stepBest;
450
+ const batchIds = batchIndices.map((index) => trainingIds[index]);
451
+ const scheduled = [];
452
+ let owed = 0;
429
453
  for (const entry of built) {
430
- if (!budget.canAfford(batch.length)) {
454
+ if (!budget.canAfford(owed + batch.length)) {
431
455
  stopReason = "budgetExhausted";
432
456
  break;
433
457
  }
434
- let score;
435
- try {
436
- score = measuredMean(await evaluator.evaluate({
437
- candidate: entry.candidate,
438
- batch,
439
- ids: batch.map((datum, index) => instanceId({
440
- datum,
441
- index
442
- })),
443
- split: "train",
444
- phase: "minibatch",
445
- candidateId: programs.length,
446
- iteration: step
447
- }));
448
- } catch (err) {
449
- if (err instanceof BudgetExhausted) {
450
- stopReason = "budgetExhausted";
451
- break;
458
+ owed += evaluator.countUncached({
459
+ candidate: entry.candidate,
460
+ ids: batchIds,
461
+ split: "train"
462
+ });
463
+ scheduled.push(entry);
464
+ }
465
+ const poolBase = programs.length;
466
+ const scored = await mapDistinct({
467
+ items: scheduled,
468
+ limit: concurrency,
469
+ key: (entry) => candidateHash(entry.candidate),
470
+ task: async (entry, index) => {
471
+ try {
472
+ return { score: measuredMean(await evaluator.evaluate({
473
+ candidate: entry.candidate,
474
+ batch,
475
+ ids: batchIds,
476
+ split: "train",
477
+ phase: "minibatch",
478
+ candidateId: poolBase + index,
479
+ iteration: step
480
+ })) };
481
+ } catch (err) {
482
+ if (err instanceof BudgetExhausted) return { stop: true };
483
+ throw err;
452
484
  }
453
- throw err;
454
485
  }
486
+ });
487
+ for (const [index, outcome] of scored.entries()) {
488
+ if (outcome.stop === true) {
489
+ stopReason = "budgetExhausted";
490
+ break;
491
+ }
492
+ const { score } = outcome;
455
493
  if (score === void 0) continue;
494
+ const entry = scheduled[index];
456
495
  programs.push(entry.candidate);
457
496
  programScores.push([score]);
458
- onEvent?.({
497
+ emit({
459
498
  type: "candidate",
460
499
  step,
461
500
  strategy: entry.strategy,
@@ -478,38 +517,78 @@ async function run(args) {
478
517
  }
479
518
  }
480
519
  if (signal?.aborted) stopReason = "aborted";
481
- const finalists = [];
520
+ const contenders = [];
521
+ let owedForFinalists = 0;
482
522
  for (const index of evenlySpacedIndices({
483
523
  length: winners.length,
484
524
  count: candidateCount + 1
485
525
  })) {
486
526
  const winner = winners[index];
487
- if (!budget.canAfford(evaluator.countUncached({
527
+ const uncached = evaluator.countUncached({
488
528
  candidate: winner.candidate,
489
529
  ids: validationIds,
490
530
  split: "val"
491
- }))) break;
492
- let score;
493
- try {
494
- score = measuredMean(await evaluator.evaluate({
495
- candidate: winner.candidate,
496
- batch: validationSet,
497
- ids: validationIds,
498
- split: "val",
499
- phase: index === 0 ? "seed" : "validation",
500
- candidateId: index,
501
- iteration: step
502
- }));
503
- } catch (err) {
504
- if (err instanceof BudgetExhausted || signal?.aborted) break;
505
- throw err;
506
- }
507
- if (score !== void 0) finalists.push({
531
+ });
532
+ if (!budget.canAfford(owedForFinalists + uncached)) break;
533
+ owedForFinalists += uncached;
534
+ contenders.push({
535
+ index,
508
536
  candidate: winner.candidate,
509
- score,
510
537
  step: winner.step
511
538
  });
512
539
  }
540
+ const sweeps = await mapDistinct({
541
+ items: contenders,
542
+ limit: concurrency,
543
+ key: (contender) => candidateHash(contender.candidate),
544
+ task: async (contender) => {
545
+ if (signal?.aborted) return { stop: true };
546
+ try {
547
+ const evaluation = await evaluator.evaluate({
548
+ candidate: contender.candidate,
549
+ batch: validationSet,
550
+ ids: validationIds,
551
+ split: "val",
552
+ phase: contender.index === 0 ? "seed" : "validation",
553
+ candidateId: contender.index,
554
+ iteration: step
555
+ });
556
+ return {
557
+ score: measuredMean(evaluation),
558
+ evaluation
559
+ };
560
+ } catch (err) {
561
+ if (err instanceof BudgetExhausted || signal?.aborted) return { stop: true };
562
+ throw err;
563
+ }
564
+ }
565
+ });
566
+ const finalists = [];
567
+ let acceptedCandidates = -1;
568
+ let acceptedScore = Number.NEGATIVE_INFINITY;
569
+ for (const [position, outcome] of sweeps.entries()) {
570
+ if (outcome.stop === true) break;
571
+ if (outcome.score === void 0) continue;
572
+ const contender = contenders[position];
573
+ finalists.push({
574
+ candidate: contender.candidate,
575
+ score: outcome.score,
576
+ step: contender.step
577
+ });
578
+ if (outcome.score > acceptedScore) {
579
+ acceptedScore = outcome.score;
580
+ acceptedCandidates += 1;
581
+ emit({
582
+ type: "candidateAccepted",
583
+ step: contender.step,
584
+ candidateId: acceptedCandidates,
585
+ candidate: contender.candidate,
586
+ aggregateScore: outcome.score,
587
+ instanceScores: instanceRow(outcome.evaluation),
588
+ ...trackBestOutputs ? { outputs: outcome.evaluation.outputs } : {}
589
+ });
590
+ }
591
+ }
513
592
  const seedScore = finalists[0]?.score ?? 0;
514
593
  finalists.sort((a, b) => b.score - a.score);
515
594
  const best = finalists[0] ?? {
@@ -525,7 +604,7 @@ async function run(args) {
525
604
  candidateId: null,
526
605
  iteration: step
527
606
  }))?.outputs;
528
- const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
607
+ const heldOut = testSet === void 0 ? void 0 : await evaluator.evaluate({
529
608
  candidate: best.candidate,
530
609
  batch: testSet,
531
610
  ids: testIds,
@@ -534,13 +613,18 @@ async function run(args) {
534
613
  candidateId: null,
535
614
  iteration: step,
536
615
  charge: false
537
- }));
538
- onEvent?.({
616
+ });
617
+ const testScore = heldOut === void 0 ? void 0 : measuredMean(heldOut);
618
+ emit({
539
619
  type: "finish",
540
620
  reason: stopReason,
621
+ warnings,
622
+ bestCandidateId: acceptedCandidates,
541
623
  bestScore: best.score,
542
624
  metricCalls: budget.spent(),
543
- ...testScore === void 0 ? {} : { testScore }
625
+ ...testScore === void 0 ? {} : { testScore },
626
+ ...heldOut === void 0 ? {} : { testInstanceScores: instanceRow(heldOut) },
627
+ ...heldOut === void 0 || !trackBestOutputs ? {} : { testOutputs: heldOut.outputs }
544
628
  });
545
629
  return {
546
630
  bestCandidate: best.candidate,
@@ -556,8 +640,10 @@ async function run(args) {
556
640
  ...bestOutputs === void 0 ? {} : { bestOutputs },
557
641
  ...testScore === void 0 ? {} : {
558
642
  testScore,
559
- testMetricCalls: testSet?.length ?? 0
643
+ testMetricCalls: evaluator.unchargedCalls(),
644
+ testUsage: evaluator.unchargedUsage()
560
645
  },
646
+ warnings,
561
647
  stopReason
562
648
  };
563
649
  /**
@@ -593,7 +679,11 @@ async function run(args) {
593
679
  const drops = /* @__PURE__ */ new Set();
594
680
  for (let draw = 0; draw < Math.min(wanted, held); draw += 1) drops.add(rng.nextInt(held));
595
681
  const next = { ...candidate };
596
- for (const [name, demos] of blocks) next[name] = formatDemos(demos.filter((_, index) => !drops.has(index)), renderDemo === void 0 ? {} : { render: renderDemo });
682
+ for (const [name, demos] of blocks) next[name] = replaceDemos({
683
+ text: next[name] ?? "",
684
+ demos: demos.filter((_, index) => !drops.has(index)),
685
+ ...renderDemo === void 0 ? {} : { render: renderDemo }
686
+ });
597
687
  return next;
598
688
  }
599
689
  /**
@@ -614,8 +704,12 @@ async function run(args) {
614
704
  };
615
705
  const next = { ...candidate };
616
706
  for (const name of demoComponents) {
617
- const kept = parseDemos(next[name] ?? "");
618
- next[name] = formatDemos([...kept, demo], renderDemo === void 0 ? {} : { render: renderDemo });
707
+ const text = next[name] ?? "";
708
+ next[name] = replaceDemos({
709
+ text,
710
+ demos: [...parseDemos(text), demo],
711
+ ...renderDemo === void 0 ? {} : { render: renderDemo }
712
+ });
619
713
  }
620
714
  return next;
621
715
  }
@@ -643,6 +737,7 @@ async function run(args) {
643
737
  const response = await reflect({
644
738
  prompt: buildPrompt({
645
739
  components: ruleComponents,
740
+ current: Object.fromEntries(ruleComponents.map((name) => [name, candidate[name] ?? ""])),
646
741
  input: bucket.datum,
647
742
  ...better === void 0 ? {} : { better },
648
743
  ...worse === void 0 ? {} : { worse }
@@ -683,6 +778,7 @@ async function run(args) {
683
778
  metricCalls: budget.spent(),
684
779
  reflectionCalls,
685
780
  cacheHits: evaluator.cacheHits(),
781
+ usage: evaluator.usage(),
686
782
  rngState: rng.state(),
687
783
  ...sampler.state === void 0 ? {} : { sampler: sampler.state() },
688
784
  ...cached === void 0 ? {} : { cache: cached }
@@ -693,8 +789,43 @@ async function run(args) {
693
789
  await onCheckpoint(takeSnapshot(completed));
694
790
  }
695
791
  }
696
- function defaultInstanceId(args) {
697
- return String(args.index);
792
+ /**
793
+ * Runs `items` concurrently, except that items sharing a key run one after the
794
+ * other.
795
+ *
796
+ * Two identical candidates cost one evaluation and one cache hit when they are
797
+ * scored in sequence, and two evaluations when they overlap: the cache is only
798
+ * written once a rollout returns. Serializing the duplicates is what keeps a
799
+ * fan-out from buying a second copy of a score the run has already paid for.
800
+ */
801
+ async function mapDistinct(args) {
802
+ const { items, limit, key, task } = args;
803
+ const groups = /* @__PURE__ */ new Map();
804
+ items.forEach((item, index) => {
805
+ const group = groups.get(key(item));
806
+ if (group === void 0) {
807
+ groups.set(key(item), [index]);
808
+ return;
809
+ }
810
+ group.push(index);
811
+ });
812
+ const results = new Array(items.length);
813
+ await mapWithConcurrency({
814
+ items: [...groups.values()],
815
+ limit,
816
+ task: async (indices) => {
817
+ for (const index of indices) results[index] = await task(items[index], index);
818
+ }
819
+ });
820
+ return results;
821
+ }
822
+ /**
823
+ * Range checks on the search knobs, run at construction so a configuration
824
+ * that could never terminate is refused before a task is ever handed to it.
825
+ */
826
+ function assertSimbaConfig(config) {
827
+ const { concurrency = 1 } = config;
828
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`concurrency must be a positive integer, received ${concurrency}`);
698
829
  }
699
830
  //#endregion
700
- export { SimbaOptimizer, buildAdvicePrompt, buildBuckets, evenlySpacedIndices, parseAdvice, percentile, samplePoisson, softmaxWeights, topKPlusBaseline };
831
+ export { SimbaOptimizer, buildAdvicePrompt, buildBuckets, evenlySpacedIndices, isCandidateAccepted, isRunFinished, parseAdvice, percentile, samplePoisson, softmaxWeights, topKPlusBaseline };
package/dist/testing.cjs CHANGED
@@ -147,6 +147,7 @@ function extractMissingTerms(prompt) {
147
147
  }
148
148
  //#endregion
149
149
  exports.KEYWORD_EXAMPLES = KEYWORD_EXAMPLES;
150
+ exports.SAMPLING_POOL = SAMPLING_POOL;
150
151
  exports.buildReflectionPrompt = require_reflection.buildReflectionPrompt;
151
152
  exports.createDegradingReflector = createDegradingReflector;
152
153
  exports.createHillClimbingReflector = createHillClimbingReflector;
@@ -1,5 +1,5 @@
1
- import { u as TextModel } from "./types-CWv4IQFF.cjs";
2
- import { i as buildReflectionPrompt, m as GepaAdapter } from "./reflection-CQToe-5B.cjs";
1
+ import { x as TextModel } from "./reporting-bq007_2z.cjs";
2
+ import { i as buildReflectionPrompt, m as GepaAdapter } from "./reflection-D0A7eahD.cjs";
3
3
  //#region src/testing.d.ts
4
4
  /**
5
5
  * A deterministic, LLM-free system under optimization.
@@ -20,6 +20,8 @@ interface KeywordTrajectory {
20
20
  missing: string[];
21
21
  }
22
22
  declare const KEYWORD_EXAMPLES: KeywordExample[];
23
+ /** Terms the keyword metric rewards, interleaved with terms it ignores. */
24
+ declare const SAMPLING_POOL: string[];
23
25
  declare function createKeywordAdapter(): GepaAdapter<KeywordExample, KeywordTrajectory, string>;
24
26
  /**
25
27
  * A deterministic stand-in for a reflection model: it reads the feedback in the
@@ -50,4 +52,4 @@ declare function createHillClimbingReflector(args?: {
50
52
  /** A reflection model that always proposes something strictly worse. */
51
53
  declare function createDegradingReflector(): TextModel;
52
54
  //#endregion
53
- export { KEYWORD_EXAMPLES, KeywordExample, KeywordTrajectory, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };
55
+ export { KEYWORD_EXAMPLES, KeywordExample, KeywordTrajectory, SAMPLING_POOL, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };
@@ -1,5 +1,5 @@
1
- import { u as TextModel } from "./types-CWv4IQFF.mjs";
2
- import { i as buildReflectionPrompt, m as GepaAdapter } from "./reflection-Cr_upzU0.mjs";
1
+ import { x as TextModel } from "./reporting-bq007_2z.mjs";
2
+ import { i as buildReflectionPrompt, m as GepaAdapter } from "./reflection-CMezGu6u.mjs";
3
3
  //#region src/testing.d.ts
4
4
  /**
5
5
  * A deterministic, LLM-free system under optimization.
@@ -20,6 +20,8 @@ interface KeywordTrajectory {
20
20
  missing: string[];
21
21
  }
22
22
  declare const KEYWORD_EXAMPLES: KeywordExample[];
23
+ /** Terms the keyword metric rewards, interleaved with terms it ignores. */
24
+ declare const SAMPLING_POOL: string[];
23
25
  declare function createKeywordAdapter(): GepaAdapter<KeywordExample, KeywordTrajectory, string>;
24
26
  /**
25
27
  * A deterministic stand-in for a reflection model: it reads the feedback in the
@@ -50,4 +52,4 @@ declare function createHillClimbingReflector(args?: {
50
52
  /** A reflection model that always proposes something strictly worse. */
51
53
  declare function createDegradingReflector(): TextModel;
52
54
  //#endregion
53
- export { KEYWORD_EXAMPLES, KeywordExample, KeywordTrajectory, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };
55
+ export { KEYWORD_EXAMPLES, KeywordExample, KeywordTrajectory, SAMPLING_POOL, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };
package/dist/testing.mjs CHANGED
@@ -145,4 +145,4 @@ function extractMissingTerms(prompt) {
145
145
  return [...terms];
146
146
  }
147
147
  //#endregion
148
- export { KEYWORD_EXAMPLES, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };
148
+ export { KEYWORD_EXAMPLES, SAMPLING_POOL, buildReflectionPrompt, createDegradingReflector, createHillClimbingReflector, createKeywordAdapter, createKeywordReflector, createSamplingReflector };