textopt 0.0.0 → 0.1.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 (55) hide show
  1. package/README.md +44 -22
  2. package/dist/bootstrap-search/index.cjs +153 -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 +144 -66
  6. package/dist/{demos-B0pVQjYC.d.mts → demos-1b7JiUue.d.mts} +10 -3
  7. package/dist/{demos-BTuzFNsp.d.cts → demos-CU9dy2oT.d.cts} +10 -3
  8. package/dist/demos-D2o0qSSo.cjs +244 -0
  9. package/dist/demos-DE2oxNWX.mjs +215 -0
  10. package/dist/file-cache.cjs +11 -3
  11. package/dist/file-cache.mjs +11 -3
  12. package/dist/gepa/index.cjs +76 -71
  13. package/dist/gepa/index.d.cts +12 -6
  14. package/dist/gepa/index.d.mts +12 -6
  15. package/dist/gepa/index.mjs +49 -46
  16. package/dist/index.cjs +129 -27
  17. package/dist/index.d.cts +145 -7
  18. package/dist/index.d.mts +145 -7
  19. package/dist/index.mjs +113 -15
  20. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  21. package/dist/math-Dqme4rYz.mjs +123 -0
  22. package/dist/mipro/index.cjs +98 -70
  23. package/dist/mipro/index.d.cts +17 -14
  24. package/dist/mipro/index.d.mts +17 -14
  25. package/dist/mipro/index.mjs +84 -58
  26. package/dist/opro/index.cjs +130 -51
  27. package/dist/opro/index.d.cts +17 -9
  28. package/dist/opro/index.d.mts +17 -9
  29. package/dist/opro/index.mjs +115 -38
  30. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-Bh5DPRMH.d.cts} +50 -4
  31. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ck6-e_8o.d.mts} +50 -4
  32. package/dist/random-search/index.cjs +93 -49
  33. package/dist/random-search/index.d.cts +15 -13
  34. package/dist/random-search/index.d.mts +15 -13
  35. package/dist/random-search/index.mjs +83 -41
  36. package/dist/{reflection-CQToe-5B.d.cts → reflection-Dt3QrXhM.d.cts} +7 -11
  37. package/dist/{reflection-Cr_upzU0.d.mts → reflection-LRaAZP4e.d.mts} +7 -11
  38. package/dist/{evaluation-OZOp6TB7.cjs → reporting-CNHzbJC-.cjs} +165 -5
  39. package/dist/reporting-DQbAohc9.d.cts +240 -0
  40. package/dist/reporting-DQbAohc9.d.mts +240 -0
  41. package/dist/{evaluation-BV0nSZVx.mjs → reporting-DY-DC4HG.mjs} +124 -6
  42. package/dist/simba/index.cjs +210 -83
  43. package/dist/simba/index.d.cts +32 -11
  44. package/dist/simba/index.d.mts +32 -11
  45. package/dist/simba/index.mjs +200 -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/package.json +4 -3
  51. package/dist/demos-B9BJiNKz.cjs +0 -143
  52. package/dist/demos-Degx6UmP.mjs +0 -126
  53. package/dist/math-DhrDmpFS.mjs +0 -78
  54. package/dist/types-CWv4IQFF.d.cts +0 -129
  55. package/dist/types-CWv4IQFF.d.mts +0 -129
@@ -0,0 +1,240 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * A candidate is a map of named text components to their current text. This is
4
+ * the unit of optimization — prompts, instructions, code, tool descriptions,
5
+ * anything expressible as a named string.
6
+ *
7
+ * `K` is the union of component names, inferred from the seed candidate, so a
8
+ * misspelled component is a compile error rather than a silent no-op.
9
+ */
10
+ type Candidate<K extends string = string> = Record<K, string>;
11
+ /**
12
+ * What one rollout consumed. Every field is optional because providers report
13
+ * different subsets, and a partial reading is still worth more than none.
14
+ */
15
+ interface RolloutUsage {
16
+ inputTokens?: number;
17
+ outputTokens?: number;
18
+ /** Defaults to the sum of the two token counts when they are reported. */
19
+ totalTokens?: number;
20
+ costUsd?: number;
21
+ }
22
+ /** Usage summed over a run, alongside the rollouts that produced it. */
23
+ interface UsageTotals {
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ totalTokens: number;
27
+ costUsd: number;
28
+ /** Fresh rollouts counted here. Cached instances buy nothing. */
29
+ rollouts: number;
30
+ }
31
+ /**
32
+ * Result of running a candidate over a batch of data instances.
33
+ *
34
+ * `scores` is the load-bearing field: one number per instance, higher is
35
+ * better. `feedback` is a per-instance textual diagnosis of what went wrong,
36
+ * which a reflective optimizer reads to write a better candidate.
37
+ */
38
+ interface EvaluationBatch<Trajectory = unknown, Output = unknown> {
39
+ outputs: Output[];
40
+ scores: number[];
41
+ /**
42
+ * What each rollout consumed. Rollout counts are the budget, but they are a
43
+ * poor proxy for spend: reflective search grows the text it optimizes, so
44
+ * the same rollout costs more late in a run than early in it.
45
+ */
46
+ usage?: RolloutUsage[];
47
+ feedback?: string[];
48
+ trajectories?: Trajectory[];
49
+ objectiveScores?: Record<string, number>[];
50
+ /**
51
+ * Per-instance: true when the score reflects an infrastructure failure
52
+ * rather than the candidate's behaviour. Transient scores are never written
53
+ * to the evaluation cache.
54
+ */
55
+ transient?: boolean[];
56
+ }
57
+ /**
58
+ * What a per-instance scorer returns. Shared by every adapter so scorers are
59
+ * portable between them — a Braintrust scorer works in a LangChain run.
60
+ */
61
+ interface ScoreResult {
62
+ score: number;
63
+ feedback?: string;
64
+ objectiveScores?: Record<string, number>;
65
+ /** What this rollout consumed, when the caller can see it. */
66
+ usage?: RolloutUsage;
67
+ /**
68
+ * Marks a score produced by an infrastructure failure — a rate limit, a
69
+ * network blip, a provider 5xx — rather than by the candidate. Without this
70
+ * the engine cannot tell such a zero from a genuine one, and would cache it
71
+ * permanently against the candidate.
72
+ */
73
+ transient?: boolean;
74
+ }
75
+ interface EvaluateArgs<Datum, K extends string = string> {
76
+ batch: readonly Datum[];
77
+ candidate: Candidate<K>;
78
+ captureTraces: boolean;
79
+ /**
80
+ * Where this batch sits in the run. Forward it to whatever tracing the
81
+ * system under optimization already has — without it a run is thousands of
82
+ * indistinguishable rollouts, and no trace can be tied back to the iteration
83
+ * whose score moved.
84
+ */
85
+ run: EvaluationContext;
86
+ signal?: AbortSignal;
87
+ }
88
+ /**
89
+ * Identifies one evaluation within a run. `candidateId` is null while the
90
+ * candidate is still a proposal being screened on a minibatch: it has no
91
+ * record, and inventing an id for it would collide with the one it gets if it
92
+ * is accepted.
93
+ */
94
+ interface EvaluationContext {
95
+ iteration: number;
96
+ phase: EvaluationPhase;
97
+ split: EvaluationSplit;
98
+ candidateId: number | null;
99
+ }
100
+ type EvaluationPhase = "seed" | "minibatch" | "validation" | "test";
101
+ /**
102
+ * Which dataset an instance id was drawn from. Each split numbers its ids
103
+ * independently, so the same id can name three different instances; the cache
104
+ * key has to keep them apart.
105
+ */
106
+ type EvaluationSplit = "train" | "val" | "test";
107
+ /**
108
+ * The single integration seam between an optimizer and a system under
109
+ * optimization. Everything framework-specific — LangChain, the AI SDK,
110
+ * Braintrust — lives in an implementation of this interface.
111
+ */
112
+ interface Adapter<Datum, Trajectory = unknown, Output = unknown, K extends string = string> {
113
+ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Output>> | EvaluationBatch<Trajectory, Output>;
114
+ }
115
+ /** Provider-agnostic text model: text in, text out. */
116
+ type TextModel = (args: {
117
+ prompt: string;
118
+ signal?: AbortSignal;
119
+ }) => Promise<string>;
120
+ /**
121
+ * The component names of a candidate, as the union they were inferred from.
122
+ *
123
+ * `Object.keys` widens a closed key union back to `string`. This is the one
124
+ * place that narrowing happens, so every other caller stays assertion-free.
125
+ * Accepts a partial so it also names the components of a component patch.
126
+ */
127
+ declare function componentNames<K extends string>(candidate: Partial<Candidate<K>>): K[];
128
+ //#endregion
129
+ //#region src/reporting.d.ts
130
+ /**
131
+ * Where a run's progress goes, for any optimizer. Observability only:
132
+ * persisting a run so it can be resumed is `onCheckpoint`, which is durability
133
+ * and a separate concern.
134
+ *
135
+ * Generic over the event union rather than one union covering every optimizer:
136
+ * a search emits what it actually has, and a reporter written against one
137
+ * optimizer still type-checks against the events it reads.
138
+ */
139
+ /**
140
+ * The least an optimizer's event satisfies. A reporter typed against this
141
+ * accepts every optimizer's union, because a literal tag is assignable to
142
+ * `string` and the parameter position is contravariant.
143
+ */
144
+ interface OptimizerEvent {
145
+ type: string;
146
+ }
147
+ interface Reporter<Event> {
148
+ /**
149
+ * Called on the search's hot path, synchronously. A reporter that ships
150
+ * anywhere over a network buffers here and uploads in `flush`, or it charges
151
+ * every iteration for its latency.
152
+ *
153
+ * A reporter that throws is warned about and skipped: observability never
154
+ * fails a run.
155
+ */
156
+ onEvent?: (event: Event) => void;
157
+ /** Awaited once as the run ends, including when it ends by throwing. */
158
+ flush?: () => Promise<void>;
159
+ }
160
+ /**
161
+ * The payload every optimizer's `candidateAccepted` carries, so one reporter
162
+ * can read an acceptance without knowing which search produced it. Each
163
+ * optimizer intersects its own fields onto this — GEPA its lineage, MIPRO its
164
+ * menu choices — the way the event unions already intersect `EvaluationEvent`.
165
+ *
166
+ * Emitted only when the incumbent moves and a full validation sweep measured
167
+ * it. An optimizer that accepts on a minibatch reports the acceptance in its
168
+ * own event and emits this one once the sweep that confirms it lands, so
169
+ * `instanceScores` never means "a subset, and you work out which".
170
+ */
171
+ interface CandidateAccepted<K extends string = string> {
172
+ /** Identifies the candidate within the run. */
173
+ candidateId: number;
174
+ /** The text that scored, so a move is readable next to the edit. */
175
+ candidate: Candidate<K>;
176
+ /** Mean over the validation set, which is what selection is decided on. */
177
+ aggregateScore: number;
178
+ /**
179
+ * Per-instance scores, aligned with the validation set. `undefined` marks an
180
+ * instance an infrastructure failure left unmeasured — unknown, not zero.
181
+ *
182
+ * Handed out by reference rather than copied: a run emits this once per
183
+ * accepted candidate, and copying a validation-set-sized array that often to
184
+ * guard against a listener that writes to it costs every run to protect a
185
+ * listener that should not exist.
186
+ */
187
+ instanceScores: readonly (number | undefined)[];
188
+ /** Aligned with `instanceScores`. Present only under `trackBestOutputs`. */
189
+ outputs?: readonly unknown[];
190
+ }
191
+ /**
192
+ * The payload every optimizer's `finish` carries. `reason` stays per-optimizer
193
+ * because the stop reasons genuinely differ — only GEPA can exhaust a
194
+ * reflection budget.
195
+ */
196
+ interface RunFinished {
197
+ /** The winner, named the way `CandidateAccepted.candidateId` names it. */
198
+ bestCandidateId: number;
199
+ bestScore: number;
200
+ metricCalls: number;
201
+ /** The winner's held-out score, when a testSet was given. */
202
+ testScore?: number;
203
+ /**
204
+ * The winner's per-instance held-out scores, aligned with the testSet and
205
+ * present whenever `testScore` is. `undefined` marks an instance an
206
+ * infrastructure failure left unmeasured, which is what `testScore` averages
207
+ * over too.
208
+ *
209
+ * The mean is the number selection never saw; this is where the gap below
210
+ * `bestScore` came from.
211
+ */
212
+ testInstanceScores?: readonly (number | undefined)[];
213
+ /** Aligned with `testInstanceScores`. Only under `trackBestOutputs`. */
214
+ testOutputs?: readonly unknown[];
215
+ }
216
+ /**
217
+ * The two events every optimizer emits with a payload a reporter can read
218
+ * without knowing which search produced it. A cross-optimizer reporter takes
219
+ * this as its event type: it is a supertype of every optimizer's own union, so
220
+ * one reporter drops into any optimizer's `reporters` array.
221
+ */
222
+ type ReportableEvent<K extends string = string> = ({
223
+ type: "candidateAccepted";
224
+ } & CandidateAccepted<K>) | ({
225
+ type: "finish";
226
+ } & RunFinished);
227
+ /**
228
+ * Narrows an event off any optimizer's union to an acceptance. The tag is
229
+ * enough: every optimizer's `candidateAccepted` intersects `CandidateAccepted`,
230
+ * so carrying the payload is a compile-time obligation rather than a hope.
231
+ */
232
+ declare function isCandidateAccepted<K extends string = string>(event: OptimizerEvent): event is {
233
+ type: "candidateAccepted";
234
+ } & CandidateAccepted<K>;
235
+ /** Narrows an event off any optimizer's union to the end of the run. */
236
+ declare function isRunFinished(event: OptimizerEvent): event is {
237
+ type: "finish";
238
+ } & RunFinished;
239
+ //#endregion
240
+ export { TextModel as _, RunFinished as a, Adapter as c, EvaluationBatch as d, EvaluationContext as f, ScoreResult as g, RolloutUsage as h, Reporter as i, Candidate as l, EvaluationSplit as m, OptimizerEvent as n, isCandidateAccepted as o, EvaluationPhase as p, ReportableEvent as r, isRunFinished as s, CandidateAccepted as t, EvaluateArgs as u, UsageTotals as v, componentNames as y };
@@ -30,6 +30,8 @@ function createBudget(args) {
30
30
  }
31
31
  //#endregion
32
32
  //#region src/cache.ts
33
+ /** What `stableHash` returns for any value that serializes to `{}`. */
34
+ const EMPTY_OBJECT_HASH = stableHash({});
33
35
  /**
34
36
  * The loop re-evaluates unchanged candidates against the same validation
35
37
  * instances constantly (every accepted child inherits most of its parent's
@@ -65,6 +67,20 @@ function stableHash(value) {
65
67
  }
66
68
  return `${hash32(serialized, 2166136261)}${hash32(serialized, 16777619)}`;
67
69
  }
70
+ /**
71
+ * Names one data instance for the evaluation cache: a content hash, so the same
72
+ * row is the same instance wherever it appears in a run.
73
+ *
74
+ * Falls back to the row's position when the datum carries nothing the hash can
75
+ * read. A Map, a Set and a class instance holding its state privately all
76
+ * serialize to `{}`, and an id two rows share serves each of them the score the
77
+ * other measured. Position is a weaker id — it is only stable while the data is
78
+ * — but it is one instance per row, which is what the cache needs to be sound.
79
+ */
80
+ function defaultInstanceId(args) {
81
+ const hash = stableHash(args.datum);
82
+ return hash === "" || hash === EMPTY_OBJECT_HASH ? String(args.index) : hash;
83
+ }
68
84
  function createMemoryCache(args = {}) {
69
85
  const { maxEntries = 1e5, entries: initial = [] } = args;
70
86
  const entries = new Map(initial);
@@ -172,25 +188,39 @@ const DEFAULT_RETRY = {
172
188
  */
173
189
  var BudgetExhausted = class extends Error {};
174
190
  function createEvaluator(args) {
175
- const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, retry, cacheNamespace } = args;
191
+ const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, usage: initialUsage, retry, cacheNamespace } = args;
176
192
  const { attempts: retryAttempts, delayMs: retryDelayMs } = {
177
193
  ...DEFAULT_RETRY,
178
194
  ...retry
179
195
  };
196
+ if (initialUsage !== void 0) assertUsage({
197
+ reading: initialUsage,
198
+ source: "Checkpoint carries"
199
+ });
180
200
  let cacheHits = initialCacheHits;
181
201
  let unchargedCalls = 0;
182
202
  const usage = {
203
+ inputTokens: 0,
204
+ outputTokens: 0,
205
+ totalTokens: 0,
206
+ costUsd: 0,
207
+ rollouts: 0,
208
+ ...initialUsage
209
+ };
210
+ /** What `charge: false` bought, kept out of the totals a ceiling reads. */
211
+ const unchargedUsage = {
183
212
  inputTokens: 0,
184
213
  outputTokens: 0,
185
214
  totalTokens: 0,
186
215
  costUsd: 0,
187
216
  rollouts: 0
188
217
  };
189
- /** Folds one adapter call's reported usage into the run's totals. */
218
+ /** Folds one adapter call's reported usage into the totals that bought it. */
190
219
  function recordUsage(args) {
191
- usage.rollouts += args.rollouts;
220
+ const totals = args.charge ? usage : unchargedUsage;
221
+ totals.rollouts += args.rollouts;
192
222
  for (const rollout of args.evaluation.usage ?? []) addUsage({
193
- totals: usage,
223
+ totals,
194
224
  rollout
195
225
  });
196
226
  }
@@ -252,7 +282,8 @@ function createEvaluator(args) {
252
282
  });
253
283
  recordUsage({
254
284
  evaluation,
255
- rollouts: rows.length
285
+ rollouts: rows.length,
286
+ charge
256
287
  });
257
288
  return evaluation;
258
289
  } catch (err) {
@@ -395,6 +426,18 @@ function createEvaluator(args) {
395
426
  cacheHits: () => cacheHits,
396
427
  unchargedCalls: () => unchargedCalls,
397
428
  usage: () => ({ ...usage }),
429
+ unchargedUsage: () => ({ ...unchargedUsage }),
430
+ absorbUsage: (spent) => {
431
+ assertUsage({
432
+ reading: spent,
433
+ source: "Absorbed usage carries"
434
+ });
435
+ usage.inputTokens += spent.inputTokens;
436
+ usage.outputTokens += spent.outputTokens;
437
+ usage.totalTokens += spent.totalTokens;
438
+ usage.costUsd += spent.costUsd;
439
+ usage.rollouts += spent.rollouts;
440
+ },
398
441
  entries: () => cache?.entries?.(),
399
442
  restore: (entries) => {
400
443
  for (const [key, cached] of entries) cache?.set(key, cached);
@@ -471,11 +514,29 @@ function requireMeasuredMean(args) {
471
514
  function addUsage(args) {
472
515
  const { totals, rollout } = args;
473
516
  const { inputTokens = 0, outputTokens = 0, costUsd = 0 } = rollout;
517
+ assertUsage({
518
+ reading: rollout,
519
+ source: "Adapter reported"
520
+ });
474
521
  totals.inputTokens += inputTokens;
475
522
  totals.outputTokens += outputTokens;
476
523
  totals.totalTokens += rollout.totalTokens ?? inputTokens + outputTokens;
477
524
  totals.costUsd += costUsd;
478
525
  }
526
+ /**
527
+ * Refuses a reading the totals cannot hold, where it enters rather than once it
528
+ * has been absorbed. A NaN folded in makes every later `maxCostUsd` comparison
529
+ * false, so the ceiling stops holding without saying so, and a value that is
530
+ * not a number at all concatenates onto the totals instead of adding to them.
531
+ * `RolloutUsage` binds TypeScript callers and nothing else.
532
+ */
533
+ function assertUsage(args) {
534
+ const { reading, source } = args;
535
+ for (const [field, value] of Object.entries(reading)) {
536
+ if (value === void 0) continue;
537
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${source} ${field} as ${value}; usage must be a non-negative finite number`);
538
+ }
539
+ }
479
540
  function transientIndices(evaluation) {
480
541
  const { transient } = evaluation;
481
542
  if (transient === void 0) return [];
@@ -518,4 +579,61 @@ function delay(milliseconds) {
518
579
  });
519
580
  }
520
581
  //#endregion
521
- export { requireMeasuredMean as a, candidateFingerprint as c, createMemoryCache as d, stableHash as f, measuredMean as i, runFingerprint as l, costExhausted as n, createDeadline as o, createBudget as p, createEvaluator as r, assertResumable as s, BudgetExhausted as t, componentNames as u };
582
+ //#region src/reporting.ts
583
+ /**
584
+ * Narrows an event off any optimizer's union to an acceptance. The tag is
585
+ * enough: every optimizer's `candidateAccepted` intersects `CandidateAccepted`,
586
+ * so carrying the payload is a compile-time obligation rather than a hope.
587
+ */
588
+ function isCandidateAccepted(event) {
589
+ return event.type === "candidateAccepted";
590
+ }
591
+ /** Narrows an event off any optimizer's union to the end of the run. */
592
+ function isRunFinished(event) {
593
+ return event.type === "finish";
594
+ }
595
+ /**
596
+ * Fans one event out to every reporter, absorbing whatever they throw. A
597
+ * reporter is an observer of the search, never a participant in it: a logging
598
+ * endpoint that is down must not decide a run's outcome.
599
+ */
600
+ function createEmitter(reporters) {
601
+ return function emit(event) {
602
+ for (const reporter of reporters) try {
603
+ reporter.onEvent?.(event);
604
+ } catch (err) {
605
+ console.warn("[textopt] reporter threw while handling an event", {
606
+ type: event.type,
607
+ err
608
+ });
609
+ }
610
+ };
611
+ }
612
+ /**
613
+ * Gives every reporter its one chance to upload what it buffered. Called from
614
+ * a `finally` rather than after the run: a reporter that buffers has the most
615
+ * to say about a run that aborted or threw, and that is exactly the run that
616
+ * never reaches its last line.
617
+ */
618
+ async function flushReporters(reporters) {
619
+ await Promise.all(reporters.map(async (reporter) => {
620
+ try {
621
+ await reporter.flush?.();
622
+ } catch (err) {
623
+ console.warn("[textopt] reporter threw while flushing", { err });
624
+ }
625
+ }));
626
+ }
627
+ /**
628
+ * A scored batch as the per-instance row a reporter should read: an instance
629
+ * an infrastructure failure left unmeasured becomes `undefined` rather than
630
+ * the zero the adapter reported for it.
631
+ *
632
+ * The same distinction `measuredMean` makes when it averages — a row of zeros
633
+ * and a row of unknowns describe very different runs.
634
+ */
635
+ function instanceRow(batch) {
636
+ return batch.scores.map((score, index) => batch.transient[index] === true ? void 0 : score);
637
+ }
638
+ //#endregion
639
+ export { createMemoryCache as _, isRunFinished as a, createBudget as b, createEvaluator as c, createDeadline as d, assertResumable as f, candidateHash as g, componentNames as h, isCandidateAccepted as i, measuredMean as l, runFingerprint as m, flushReporters as n, BudgetExhausted as o, candidateFingerprint as p, instanceRow as r, costExhausted as s, createEmitter as t, requireMeasuredMean as u, defaultInstanceId as v, stableHash as y };