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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charlie Duong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,509 @@
1
+ # textopt
2
+
3
+ Core interfaces and optimizers for textopt.
4
+
5
+ This package has no runtime dependencies. For an overview of the algorithms and guidance on choosing one, see the [project README](https://github.com/ctdio/textopt#readme).
6
+
7
+ ## Entry points
8
+
9
+ | Import | Contains |
10
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `textopt` | Shared contracts, evaluator, judge, `compare()`, demo utilities, cache, and concurrency helper. Optimizer classes are not exported. |
12
+ | `textopt/gepa` | `GepaOptimizer`, GEPA types, events, checkpoints, the pipeline adapter, and configurable strategies. |
13
+ | `textopt/simba` | `SimbaOptimizer`, its advice prompt, and its bucket-ranking helpers. |
14
+ | `textopt/opro` | `OproOptimizer` and its types and events. |
15
+ | `textopt/mipro` | `MiproOptimizer`, its types, and the standalone `proposeConfiguration` TPE function. |
16
+ | `textopt/bootstrap-search` | `BootstrapSearchOptimizer` and its types. |
17
+ | `textopt/random-search` | `RandomSearchOptimizer` and its types. |
18
+ | `textopt/file-cache` | `createFileCache`, an append-only durable `EvaluationCache`. The only entry point that uses `node:fs`. |
19
+ | `textopt/testing` | Deterministic fixtures for testing optimizers and adapters without an LLM. |
20
+
21
+ ## `textopt`
22
+
23
+ ```ts
24
+ import {
25
+ assertResumable,
26
+ bootstrapDemos,
27
+ buildJudgePrompt,
28
+ compare,
29
+ componentNames,
30
+ createDeadline,
31
+ createEvaluator,
32
+ createJudge,
33
+ createMemoryCache,
34
+ formatDemos,
35
+ mapWithConcurrency,
36
+ parseDemos,
37
+ parseProposedText,
38
+ priceUsage,
39
+ runFingerprint,
40
+ } from "textopt";
41
+ import type {
42
+ Adapter,
43
+ BatchSampler,
44
+ BootstrapResult,
45
+ Candidate,
46
+ CachedScore,
47
+ Comparison,
48
+ ComparisonRun,
49
+ ComparisonSummary,
50
+ Deadline,
51
+ Demo,
52
+ DemoRenderer,
53
+ EvaluationEvent,
54
+ Evaluator,
55
+ ScoredBatch,
56
+ EvaluateArgs,
57
+ EvaluationBatch,
58
+ EvaluationCache,
59
+ EvaluationContext,
60
+ EvaluationPhase,
61
+ EvaluationSplit,
62
+ Judge,
63
+ JudgeCriterion,
64
+ JudgePromptBuilder,
65
+ Optimizer,
66
+ OptimizerResult,
67
+ OptimizerTask,
68
+ RetryPolicy,
69
+ Rng,
70
+ RolloutUsage,
71
+ ScoreResult,
72
+ TextModel,
73
+ TokenPricing,
74
+ UsageTotals,
75
+ } from "textopt";
76
+ ```
77
+
78
+ ### Contracts
79
+
80
+ **`Candidate<K extends string>`** is a `Record<K, string>` containing the text components to optimize. `K` is inferred from the seed candidate, so misspelled component names fail type checking.
81
+
82
+ **`Adapter<Datum, Trajectory, Output, K>`** connects an optimizer to the evaluated system:
83
+
84
+ ```ts
85
+ evaluate(args: EvaluateArgs<Datum, K>): Promise<EvaluationBatch<Trajectory, Output>> | EvaluationBatch<Trajectory, Output>
86
+ ```
87
+
88
+ `EvaluateArgs` contains the `batch`, `candidate`, `captureTraces`, an optional abort `signal`, and a `run` context with `iteration`, `phase`, `split`, and `candidateId`. The run context can be forwarded to a tracing system.
89
+
90
+ `EvaluationBatch` contains one output and score per instance. Higher scores are better. It may also include `feedback`, `trajectories`, `objectiveScores`, and `transient` flags. Reflective optimizers use `feedback` to generate revisions.
91
+
92
+ **`ScoreResult`** is the common return type for per-instance scorers: `score`, with optional `feedback`, `objectiveScores`, and `transient`.
93
+
94
+ **`transient`** marks scores caused by infrastructure failures such as rate limits, 5xx responses, or network errors. Transient scores are not cached.
95
+
96
+ **`Optimizer<Stop extends string>`** defines `optimize(task: OptimizerTask) => Promise<OptimizerResult>`. `OptimizerTask` contains the shared run inputs: `seedCandidate`, `trainingSet`, `validationSet`, `testSet`, `adapter`, `maxMetricCalls`, `maxCostUsd`, `maxWallClockMs`, `cacheNamespace`, `retry`, and `signal`. `OptimizerResult` contains `bestCandidate`, `bestScore`, `bestOutputs`, `metricCalls`, `usage`, `testScore`, `testMetricCalls`, and `stopReason`. Optimizer-specific task and result types extend these interfaces.
97
+
98
+ **`maxCostUsd`** and **`maxWallClockMs`** are checked between evaluations, so a run overruns by at most one of them. Neither follows from `maxMetricCalls`: reflective search grows the text it optimizes, so late rollouts cost more than early ones, and a run behind a rate limit spends almost nothing while taking as long as the provider makes it take.
99
+
100
+ **`cacheNamespace`** scopes every cache key to the system the rollouts were measured under — model id, decoding settings, scorer version. Change it whenever anything outside the candidate text changes.
101
+
102
+ **`retry`** is a `RetryPolicy` of `{ attempts = 2, delayMs = 500 }`. Instances the adapter marked `transient` are re-run, with the delay doubling per attempt. Retries are charged like any other rollout and never overdraw the budget.
103
+
104
+ **`UsageTotals`** (`inputTokens`, `outputTokens`, `totalTokens`, `costUsd`, `rollouts`) is summed from the `RolloutUsage` entries an adapter reports. Zero throughout when the adapter reports none.
105
+
106
+ **`testSet`** is excluded from search and evaluated once against the winner. Because candidates are selected on `validationSet`, `bestScore` may be fitted to it. `testScore` measures held-out performance. Test rollouts are reported as `testMetricCalls` and do not count against `maxMetricCalls`.
107
+
108
+ **`TextModel`** is the provider-independent interface `({ prompt, signal }) => Promise<string>`.
109
+
110
+ ### Values
111
+
112
+ **`createMemoryCache({ maxEntries = 100_000, entries })`** returns an in-memory `EvaluationCache`. Pass `entries` to restore checkpointed scores. When full, the cache evicts the oldest entry.
113
+
114
+ For Redis, SQLite, or file-backed caching, implement **`EvaluationCache`** with `get(key)`, `set(key, cached)`, and optional `entries()`. A durable store does not need `entries()`.
115
+
116
+ **`mapWithConcurrency({ items, limit, task, signal })`** maps items in order while limiting concurrent tasks. Running tasks settle before an error is propagated.
117
+
118
+ **`componentNames(candidate)`** returns `Object.keys(candidate)` while preserving the component-name union.
119
+
120
+ **`createEvaluator({ adapter, budget, cache, cacheNamespace, retry, trackOutputs, onEvaluation, signal, cacheHits })`** handles adapter calls, caching, budget accounting, transient scores, and evaluation events. `evaluate` returns a `ScoredBatch`. `evaluateTraced` returns an `EvaluationBatch`, or `null` when the remaining budget cannot cover the batch. A batch that exceeds the charged budget throws `BudgetExhausted`. All included optimizers use this evaluator.
121
+
122
+ **`bootstrapDemos({ adapter, candidate, trainingSet, minScore, maxDemos, batchSize, maxMetricCalls, rng, renderDemo, signal })`** evaluates a candidate on `trainingSet` and keeps the rollouts the metric rewarded. Omit `minScore` to keep any rollout scoring above zero, as MIPROv2 does without a `metric_threshold`; pass a number to require at least that score. It returns the selected `demos`, a formatted `block`, and the metric calls used. It does not use the score cache because it needs rollout outputs.
123
+
124
+ **`formatDemos(demos, { render })`** and **`parseDemos(text)`** write and read the `<demo>`, `<input>`, and `<output>` block format.
125
+
126
+ **`parseProposedText(text)`** extracts a proposal from a reflection response, including responses with fenced blocks or surrounding commentary.
127
+
128
+ **`createJudge({ model, criteria, scale = 5, renderInput, renderOutput, buildPrompt })`** returns a `Judge<Datum, Output>`: `({ input, output, expected, signal }) => Promise<ScoreResult>`. Each `JudgeCriterion` is graded on a small integer scale and normalized; per-criterion values are returned as `objectiveScores` and the aggregate `score` is their mean. A criterion the judge failed to grade returns a transient score rather than a zero, so the instance is retried instead of recorded as a failure. **`buildJudgePrompt`** is the default template and implements `JudgePromptBuilder`.
129
+
130
+ **`compare({ entrants, seeds, concurrency = 1 })`** runs each entrant over every seed and returns a `Comparison` of `winner`, `summaries`, and `runs`. Entrants are `({ seed }) => Promise<OptimizerResult>`, so the caller builds the optimizer-specific task. Ranking is on `testScore` where a run reports one, because the validation score is the number the search selected against. Each `ComparisonSummary` carries `meanScore`, `sdScore`, `minScore`, `maxScore`, `meanMetricCalls`, `meanCostUsd`, and a paired sign-flip `pValueVsWinner`.
131
+
132
+ **`priceUsage({ usage, pricing })`** fills in `costUsd` on one rollout's `RolloutUsage` from a `TokenPricing` table. Adapters call it so a run's `usage.costUsd` and its `maxCostUsd` ceiling have something to read; without it both stay zero.
133
+
134
+ **`createDeadline({ maxWallClockMs, now })`** returns a `Deadline` with `exceeded()` and `remainingMs()`. The optimizers build one from `maxWallClockMs`; `now` is injectable so a deadline can be tested without waiting.
135
+
136
+ **`runFingerprint({ seedCandidate, trainingIds, validationIds, seed, cacheNamespace })`** and **`assertResumable({ fingerprint, snapshot })`** are the shared checkpoint guard. Every optimizer stamps its snapshot with a fingerprint and refuses one that does not match, rather than silently scoring old candidates against new data. **`candidateFingerprint(candidate)`** hashes a candidate's text.
137
+
138
+ **`BatchSampler<Datum>`** and **`Rng`** are type-only exports. Their default implementations are internal.
139
+
140
+ ## `textopt/gepa`
141
+
142
+ ```ts
143
+ import { GepaOptimizer } from "textopt/gepa";
144
+
145
+ const gepa = new GepaOptimizer(config); // GepaConfig: stateless, reusable
146
+ const result = await gepa.optimize(task); // GepaTask: one problem
147
+ ```
148
+
149
+ Optimizer instances do not retain state between runs.
150
+
151
+ ### `GepaConfig`
152
+
153
+ These options control search behavior and can be reused across runs.
154
+
155
+ | Option | Default | Effect |
156
+ | -------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
157
+ | `minibatchSize` | `3` | Instances per screening batch. |
158
+ | `maxIterations` | `Infinity` | Iteration ceiling. The metric budget usually binds first. |
159
+ | `seed` | `0` | Seeds the run's random stream. |
160
+ | `candidateSelector` | `paretoSelector()` | Which candidate becomes the next parent. |
161
+ | `acceptance` | `improvementAcceptance()` | Whether a screened child beats its parent. |
162
+ | `merge.enabled` | components > 1 | System-aware merge of two lineages. |
163
+ | `merge.maxInvocations` | `5` | Ceiling on merges attempted per run. |
164
+ | `merge.valOverlapFloor` | `5` | Validation instances two lineages must share to be eligible. A validationSet smaller than this never merges. |
165
+ | `skipPerfectScore` | `true` | Skip reflection when the parent already scores perfectly on the minibatch. |
166
+ | `perfectScore` | `1` | The per-instance score treated as leaving no room. |
167
+ | `rejectedProposalMemory` | `3` | Rejected texts per component included in reflection. `0` disables it. This behavior is not in the GEPA paper. |
168
+ | `proposals.perIteration` | `1` | Mutations drawn per iteration, each with its own parent and minibatch. |
169
+ | `proposals.concurrency` | `1` | How many may be in flight at once. |
170
+ | `proposals.selection` | `"all"` | Which improving proposals to keep: `"all"`, `"best"`, or `{ keep: n }`. |
171
+ | `reflection.maxCalls` | unbounded | Hard ceiling on reflection calls. The run stops when it is reached. |
172
+ | `reflection.maxRecords` | unbounded | Records shown per component. The worst-scoring ones are kept. |
173
+ | `reflection.maxCharacters` | unbounded | Rough ceiling on the serialized records. |
174
+ | `reflection.buildPrompt` | `buildReflectionPrompt` | Replaces the prompt template. Custom proposers ignore it. |
175
+ | `reflection.strategies` | none | Rotates over several prompt templates, one per proposal slot. Mutually exclusive with `buildPrompt`. |
176
+ | `checkpointCache` | `true` | Include cached scores in every snapshot. |
177
+ | `trackBestOutputs` | `false` | Keep validation outputs so the winner's can be read back. |
178
+ | `raiseOnError` | `true` | Rethrow adapter failures instead of skipping the iteration. |
179
+
180
+ **What a run costs.** One validation sweep for the seed, then per iteration `proposals.perIteration × minibatchSize × 2` — every proposal screens its parent and its child on the same minibatch — plus `validationSet.length` for the sweep a surviving child earns — less than that where `valEvaluationPolicy` subsamples. All of it is reserved before the iteration begins, so a run never starts an iteration it cannot afford to promote out of.
181
+
182
+ ### `GepaTask`
183
+
184
+ `GepaTask` extends `OptimizerTask` with GEPA-specific run inputs.
185
+
186
+ Required: `seedCandidate`, `trainingSet`, `adapter` (a `GepaAdapter`), `reflect` (a `TextModel`), and `maxMetricCalls`.
187
+
188
+ | Option | Default |
189
+ | --------------------- | ------------------------------------------------------------------------------------ |
190
+ | `validationSet` | the trainingSet |
191
+ | `testSet` | none. Held out of the search and scored once, on the winner |
192
+ | `componentSelector` | `roundRobinComponentSelector()` |
193
+ | `batchSampler` | an epoch-shuffled sampler over `minibatchSize` |
194
+ | `valEvaluationPolicy` | `fullEvaluationPolicy()` |
195
+ | `instanceId` | a content hash of the datum, falling back to its position when it will not serialize |
196
+ | `cache` | a per-run memory cache. Pass `false` to disable |
197
+
198
+ `onEvent`, `onCheckpoint`, `resumeFrom`, and `signal` have no defaults.
199
+
200
+ TypeScript infers component names and the datum type from `seedCandidate` and `trainingSet`. Other fields use `NoInfer` and are checked against those inferred types.
201
+
202
+ ### `GepaAdapter`
203
+
204
+ Extends `Adapter` with what reflection needs:
205
+
206
+ ```ts
207
+ makeReflectiveDataset(args: MakeReflectiveDatasetArgs<Datum, Trajectory, Output, K>): ReflectiveDataset<K>
208
+ proposeNewTexts?(args: ProposeArgs<K>): ComponentPatch<K> // optional
209
+ ```
210
+
211
+ Both methods may be synchronous or asynchronous. `ReflectiveDataset` is a partial map from component names to `ReflectiveRecord[]`. Each record contains `inputs`, `generatedOutputs`, `feedback`, `score`, and a typed `evidence` field. Adapters only need to return records for the requested components.
212
+
213
+ When `proposeNewTexts` is implemented, the adapter generates proposals without calling `reflect`. The task type still requires `reflect`, so offline runs can pass a stub.
214
+
215
+ ### Strategies
216
+
217
+ | Strategy | Arguments | What it does |
218
+ | ----------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- |
219
+ | `paretoSelector` | `{ epsilon = 0, frontier = "instance" }` | Samples a parent from the frontier. `frontier` is `"instance"`, `"objective"`, or `"hybrid"`. |
220
+ | `currentBestSelector` | none | Always picks the highest aggregate score. |
221
+ | `epsilonGreedySelector` | `{ epsilon }` | The best candidate, with a uniform random pick at rate `epsilon`. |
222
+ | `topKParetoSelector` | `{ k, epsilon = 0 }` | Pareto selection restricted to the top `k` by aggregate score. |
223
+ | `roundRobinComponentSelector` | none | One component per iteration, walking a per-candidate cursor. |
224
+ | `allComponentsSelector` | none | Every component, every iteration. |
225
+ | `improvementAcceptance` | `{ minImprovement = 0 }` | Accepts a child whose minibatch total beats its parent's. |
226
+ | `pairedPermutationAcceptance` | `{ alpha = 0.2, maxExact = 16 }` | Accepts only when the paired improvement survives a sign-flip test. |
227
+ | `fullEvaluationPolicy` | none | Scores every validation instance per accepted candidate. |
228
+ | `subsampledEvaluationPolicy` | `{ size }` | Scores `size` instances, trading frontier fidelity for rollouts. |
229
+ | `lowerBoundEvaluationPolicy` | `{ z = 1 }` | Full coverage, but picks the winner by mean minus `z` standard errors. |
230
+
231
+ Each export is a factory. Selector and acceptance interfaces accept custom functions. A `ValEvaluationPolicy` is an object with `selectInstances` and `bestCandidate` methods.
232
+
233
+ `pairedPermutationAcceptance` and `lowerBoundEvaluationPolicy` exist for metrics whose readings vary between runs of the same text. Both are strictly more conservative than the defaults, and on a metric that does not vary that is pure cost: in the twenty-seed benchmark the pair drops GEPA from 0.729 to 0.175 on the noiseless task and ties it on the noisy one. A sign-flip test also needs a wide enough minibatch to say anything — over three instances the smallest p-value it can produce is 0.125, so at the default `minibatchSize` no proposal clears an `alpha` below that.
234
+
235
+ ### Reflection prompts
236
+
237
+ | Export | What it asks for |
238
+ | ----------------------------- | ------------------------------------------------------------- |
239
+ | `buildReflectionPrompt` | The default: read the failures, write better text. |
240
+ | `buildSimplifyPrompt` | A shorter version that retains behavior needed by the batch. |
241
+ | `buildGeneralizePrompt` | Text that avoids details specific to the current failures. |
242
+ | `buildRewritePrompt` | A replacement written without preserving the current wording. |
243
+ | `diverseReflectionStrategies` | All four builders for use with `reflection.strategies`. |
244
+
245
+ Each implements `ReflectionPromptBuilder`, defined as `(args: ReflectionPromptArgs) => string`.
246
+
247
+ ### `createDemoProposer`
248
+
249
+ ```ts
250
+ createDemoProposer({ components, minScore, maxDemos, render, fallback });
251
+ ```
252
+
253
+ This `proposeNewTexts` implementation fills selected components with few-shot examples from the reflective dataset. It uses existing record inputs, outputs, and scores, so it requires no additional rollouts or reflection calls. Examples are deduplicated by input and appended to the parent's block. `fallback` handles components not listed in `components`.
254
+
255
+ ### `createPipelineAdapter`
256
+
257
+ ```ts
258
+ createPipelineAdapter({ modules, input, score, concurrency });
259
+ ```
260
+
261
+ A `GepaAdapter` for a system built from several modules in sequence, where each module's instruction is its own candidate component. Each `PipelineModule` has a `component` and a `run({ instruction, input, datum, signal })`; the first module receives `input(datum)`, and each subsequent module receives the previous module's output. `score({ datum, output, steps })` returns a `ScoreResult` for the whole rollout.
262
+
263
+ The trajectory is a `PipelineTrace` of `PipelineStep` entries, and the reflective dataset gives each component only its own step. That attribution is the point: reflection is only as good as the evidence it sees, and a module needs what it received and produced, not the pipeline's input and final answer.
264
+
265
+ Feedback is end-to-end and every module receives the same string. A metric scores the final output, so nothing in a score alone says which module lost the point; `score` is handed the whole trace for callers who can attribute better. Errors from a module are not caught — a helper cannot tell a rate limit from a bug, so classify inside `run` and return a transient `ScoreResult`, or let `raiseOnError` decide.
266
+
267
+ ### Results, events, and resuming
268
+
269
+ **`GepaResult`** extends `OptimizerResult` with `bestCandidateId`, the complete candidate pool, lineage and per-instance scores, `paretoFrontier`, `scoreMatrix`, `perObjectiveBest`, `iterations`, `reflectionCalls`, `cacheHits`, and the final `snapshot`.
270
+
271
+ **`stopReason`** is one of `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"reflectionBudgetExhausted"`, `"aborted"`, or `"maxIterations"`.
272
+
273
+ **`onEvent`** receives a discriminated `GepaEvent`: `start`, `iterationStart`, `evaluation`, `proposal`, `candidateAccepted`, `candidateRejected` (with `reason: "worse" | "notSelected"`), `error`, and `finish`.
274
+
275
+ **`onCheckpoint`** runs after seed evaluation and each iteration with a JSON-serializable `GepaSnapshot`. Pass it as `resumeFrom` to continue. A fingerprint prevents resuming with a different seed candidate, instance set, or random seed. Every optimizer here has the same three: `onCheckpoint`, `resumeFrom`, and a `snapshot` on the result. A snapshot handed back as `resumeFrom` is copied, never mutated by the run that continues from it.
276
+
277
+ ## `textopt/simba`
278
+
279
+ ```ts
280
+ import { SimbaOptimizer, buildAdvicePrompt, parseAdvice } from "textopt/simba";
281
+ ```
282
+
283
+ SIMBA uses the base `Adapter`: it reads outputs, scores, and feedback and builds its own evidence, so it needs no `makeReflectiveDataset`. `SimbaTask` adds `reflect`, `demoComponents`, `instructionComponents`, `renderDemo`, `buildAdvicePrompt`, `sampler`, `instanceId`, `cache`, `onEvent`, `onCheckpoint`, and `resumeFrom`.
284
+
285
+ | Option | Default | Effect |
286
+ | ---------------------- | -------------------- | -------------------------------------------------------------------------- |
287
+ | `minibatchSize` | `32` | Instances per step. Must not exceed the trainingSet. |
288
+ | `candidates` | `6` | Programs sampled per step, and the ceiling on candidates built from them. |
289
+ | `maxSteps` | `8` | Steps to run. |
290
+ | `maxDemos` | `4` | Demos a candidate may hold before the loop starts dropping them. |
291
+ | `samplingTemperature` | `0.2` | Sharpness of the pick between programs when sampling trajectories. |
292
+ | `candidateTemperature` | `0.2` | Sharpness of the pick between programs when choosing what to mutate. |
293
+ | `strategies` | both, or rules alone | Pins the mutation to `"appendDemo"`, `"appendRule"`, or leaves both drawn. |
294
+ | `maxReflectionCalls` | unbounded | Advice calls the run may make. Bounded separately from rollouts. |
295
+ | `seed` | `0` | Seeds the run's random stream. |
296
+ | `checkpointCache` | `true` | Include cached scores in every snapshot. |
297
+ | `trackBestOutputs` | `false` | Keep the winner's validation outputs. |
298
+
299
+ **What a run costs.** `(candidates + 1) × minibatchSize` per step, on top of `min(candidates + 1, maxSteps + 1) × validationSet.length` reserved before the first step for the finalist sweeps. The seed is scored as one of those finalists rather than up front, so every rollout a SIMBA run makes is one of these two.
300
+
301
+ Each step samples `candidates` programs from the pool over one minibatch, groups the results by instance, and ranks the instances by how much the programs disagreed — max-to-min gap first, then best score, then max-to-avg gap. Wide disagreement is a controlled experiment with the input held fixed, so the difference in reward is attributable to behaviour rather than to difficulty.
302
+
303
+ Two mutations are drawn at random per instance. `appendDemo` keeps the winning rollout as a few-shot example and costs no model call; it requires `demoComponents` and is unavailable without one. `appendRule` shows the better and worse run to `reflect` and appends the returned advice to each instruction component. Neither replaces text, so demos are dropped at a Poisson rate to stop a growing block from crowding out the instruction. When the advice budget is spent, `appendDemo` carries the run alone, or the run stops with `"reflectionBudgetExhausted"` if it was the only mutation enabled.
304
+
305
+ Guards keep uninformative contrasts out: a winner below the batch's tenth percentile is not a success to imitate, and a loser above the ninetieth is not a failure to avoid. When the two runs tied, the uninformative side is withheld and the model advises from one trajectory.
306
+
307
+ Only the step winners are scored on the full validation set, sampled evenly across the run so early winners stay in the running — minibatch scores are noisy and the genuine best is often not the most recent. Those rollouts are reserved before the search starts, so a small `maxMetricCalls` buys fewer steps than the arithmetic suggests.
308
+
309
+ **`buildAdvicePrompt`** implements `AdvicePromptBuilder` and asks for one `<advice component="name">…</advice>` block per component. **`parseAdvice(response)`** reads them back, ignoring prose written around them; a component the model had nothing to say about is absent rather than empty.
310
+
311
+ `SimbaResult` adds `seedScore`, `steps`, `finalists` (the step winners with their validation scores, best first), `reflectionCalls`, and `cacheHits`. `stopReason` is `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"reflectionBudgetExhausted"`, `"maxSteps"`, or `"aborted"`.
312
+
313
+ `textopt/simba` also exports the pure functions the loop is built from: `buildBuckets`, `percentile`, `softmaxWeights`, `topKPlusBaseline`, `samplePoisson`, and `evenlySpacedIndices`.
314
+
315
+ Ported from DSPy's SIMBA with two deliberate changes. A trajectory sample runs one program across the whole minibatch rather than resampling a program per instance: the adapter owns decoding here, so there is no temperature knob to vary and the variability comes from the program pool. And the percentile guards are strict rather than inclusive, because on a step where every rollout ties an inclusive guard blocks every mutation and the run does nothing at all — the one case where the guard's own premise does not hold.
316
+
317
+ ## `textopt/bootstrap-search`
318
+
319
+ ```ts
320
+ import { BootstrapSearchOptimizer } from "textopt/bootstrap-search";
321
+ ```
322
+
323
+ DSPy's `BootstrapFewShotWithRandomSearch`. It uses the base `Adapter` and no reflection model at all: every candidate is assembled from outputs the system itself produced, so the search costs rollouts and nothing else. `BootstrapSearchTask` adds `demoComponents` (required), `renderDemo`, `goldOutput`, `instanceId`, `cache`, `onEvent`, `onCheckpoint`, and `resumeFrom`.
324
+
325
+ | Option | Default | Effect |
326
+ | ------------------ | ------- | -------------------------------------------------------------------- |
327
+ | `candidates` | `16` | Shuffled harvests attempted, beyond the fixed candidates. |
328
+ | `maxDemos` | `4` | Most demos a harvested set may hold. |
329
+ | `minDemos` | `1` | Fewest demos a shuffled harvest may ask for. |
330
+ | `maxLabeledDemos` | `16` | Most demos the labels-only candidate may hold. |
331
+ | `demoMinScore` | unset | Score a rollout must reach to be kept. Unset keeps any rewarded one. |
332
+ | `stopAtScore` | unset | Stop as soon as a candidate reaches this validation score. |
333
+ | `seed` | `0` | Seeds the run's random stream. |
334
+ | `checkpointCache` | `true` | Include cached scores in every snapshot. |
335
+ | `trackBestOutputs` | `false` | Keep the winner's validation outputs. |
336
+
337
+ **What a run costs.** One validation sweep per candidate — `candidates` shuffled harvests after the two or three fixed ones — plus the training rollouts each harvest spends looking for demos, bounded by the size of `trainingSet`. A stricter `demoMinScore` raises that second number rather than lowering it, because a harvest keeps rolling out instances until it holds `maxDemos` of them or has run out of set.
338
+
339
+ Candidates are tried cheapest and most reliable first, following DSPy's special seeds: zero-shot, then labels-only when `goldOutput` is supplied, then one unshuffled full-size harvest, then the shuffled ones. A run cut short by its budget therefore still has the baseline it needs to report against.
340
+
341
+ Zero-shot stays in the running throughout. Demonstrations can hurt, and a search that cannot return "no demos" has no baseline. The labels-only candidate costs no rollout to build and is the only candidate available at all to a system too weak to bootstrap from.
342
+
343
+ Sizes vary across the shuffled harvests because more demos is not monotonically better: a long block crowds out the instruction, and which length wins is what this search settles. A harvest and its validation sweep are treated as one purchase, so the run never harvests demos it cannot afford to score.
344
+
345
+ Unlike DSPy, which bootstraps each predictor separately from the traces of one pass, this adapter interface runs the whole system: a harvest is a set of end-to-end rollouts and every demo component receives the same block. For per-module demos, use `createPipelineAdapter` with GEPA.
346
+
347
+ `BootstrapSearchResult` adds `seedScore`, `candidates` (each with its `source`, demo count, and score), `bootstrapMetricCalls`, and `cacheHits`. `stopReason` is `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"scoreReached"`, `"candidatesExhausted"`, or `"aborted"`.
348
+
349
+ ## `textopt/opro`
350
+
351
+ ```ts
352
+ import { OproOptimizer, buildOproPrompt } from "textopt/opro";
353
+ ```
354
+
355
+ OPRO uses the base `Adapter`. `OproTask` adds `reflect` and optional `renderDatum`, `instanceId`, `cache`, and `onEvent` fields.
356
+
357
+ | Option | Default | Effect |
358
+ | -------------------- | ----------------- | ---------------------------------------------------------- |
359
+ | `proposalsPerRound` | `8` | Instructions drawn per round. |
360
+ | `concurrency` | `1` | How many may be in flight at once. |
361
+ | `maxRounds` | `Infinity` | Round ceiling. |
362
+ | `maxReflectionCalls` | unbounded | Bounded separately; no metric budget covers reflection. |
363
+ | `historySize` | `20` | Maximum scored attempts included in the prompt. |
364
+ | `exemplars` | `3` | Task inputs shown for grounding, redrawn each round. |
365
+ | `scoringSetSize` | unset | Instances drawn once from the trainingSet to screen on. |
366
+ | `fullEvalInterval` | `3` | Rounds between full validationSet sweeps of the incumbent. |
367
+ | `scoreScale` | `100` | What scores are multiplied by before being shown. |
368
+ | `seed` | `0` | Seeds the run's random stream. |
369
+ | `buildPrompt` | `buildOproPrompt` | Replaces the meta-prompt template. |
370
+
371
+ **What a run costs.** One validation sweep for the seed, then up to `proposalsPerRound` screenings per round, each over `validationSet` or over `scoringSetSize` instances when that is set, plus a full sweep of the incumbent every `fullEvalInterval` rounds. Rounds are also capped at `maxReflectionCalls / proposalsPerRound`, and a round advances one component in turn, so a candidate with several components needs proportionally more of them.
372
+
373
+ By default, every proposal is evaluated against the full `validationSet`; eight proposals on 500 instances require 4,000 rollouts. When `scoringSetSize` is set, proposals are screened on one fixed subset of `trainingSet`. The subset is not resampled because OPRO compares scores across proposals. Every `fullEvalInterval` rounds, the incumbent is evaluated on the full validation set.
374
+
375
+ Measured on 30 training and 30 validation instances over 8 rounds of 4 proposals, averaged across 10 seeds:
376
+
377
+ | `scoringSetSize` | Best score | Rollouts |
378
+ | ---------------------------- | ---------- | -------- |
379
+ | unset (whole validation set) | 1.000 | 738 |
380
+ | 12 | 1.000 | 383 |
381
+ | 6 | 0.950 | 217 |
382
+ | 3 | 0.862 | 137 |
383
+
384
+ In this sweep, 12 screening instances cut rollout count by 48% without changing the best score. Three instances reduced cost further but also reduced mean quality. Full-set scoring remains the default.
385
+
386
+ The reference implementation selects its winner by training-subset score. textopt instead returns the best candidate evaluated on the full validation set, so `bestScore` never falls below `seedScore`. Screening scores still guide the search but cannot determine the returned winner.
387
+
388
+ `OproResult` adds `seedScore`, `rounds`, `trajectory` (every candidate scored, in order), `reflectionCalls`, and `cacheHits`. `stopReason` is `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"reflectionBudgetExhausted"`, `"proposalsExhausted"`, `"maxRounds"`, or `"aborted"`.
389
+
390
+ `"proposalsExhausted"` means several rounds in a row produced only texts already tried. Such a round spends no rollouts, so without this the budget would never run down and a proposal model that has settled on one answer would loop forever.
391
+
392
+ History is sorted by ascending score so the strongest attempt appears nearest the request. Scores are shown as integers because models distinguish 41 from 68 more reliably than 0.41 from 0.68.
393
+
394
+ For multi-component candidates, each attempt records the other components present when it was scored. The prompt only includes attempts from the current component context, avoiding comparisons between scores obtained with different companion text. Single-component candidates require no filtering.
395
+
396
+ ## `textopt/mipro`
397
+
398
+ ```ts
399
+ import { MiproOptimizer, proposeConfiguration } from "textopt/mipro";
400
+ ```
401
+
402
+ MIPRO uses the base `Adapter`. `MiproTask` adds `reflect`, `componentOptions`, `renderDatum`, `batchSampler`, `instanceId`, `cache`, and `onEvent`.
403
+
404
+ | Option | Default | Effect |
405
+ | -------------------------- | ------------------ | ---------------------------------------------------------------------------------- |
406
+ | `instructionsPerComponent` | `3` | Menu entries generated per component, beyond the seed. |
407
+ | `minibatchSize` | `35` | Instances a trial is scored on. MIPROv2's `minibatch_size`. |
408
+ | `fullEvalInterval` | `5` | Trials between full evaluations. MIPROv2's `minibatch_full_eval_steps`. |
409
+ | `demoSets` | `3` | Bootstrapped demo sets generated per demo component. |
410
+ | `maxDemos` | `4` | Demos in the largest generated set. |
411
+ | `demoMinScore` | unset | Score a rollout must reach to be kept as a demo. Unset keeps any rewarded rollout. |
412
+ | `maxTrials` | `30` | Configurations evaluated. |
413
+ | `startupTrials` | `10` | Trials drawn uniformly before the surrogate takes over. |
414
+ | `gamma` | Optuna's rule | Observations assigned to the good density: `ceil(10%)`, capped at 25. |
415
+ | `surrogateSamples` | `24` | Candidate configurations sampled by the surrogate per trial. |
416
+ | `multivariate` | `true` | Model components jointly rather than one at a time. |
417
+ | `exemplars` | `3` | Task inputs shown when generating instructions. |
418
+ | `datasetSummary` | `true` | Generate a `trainingSet` summary for the proposer. Uses one reflection call. |
419
+ | `summaryExamples` | `10` | Training examples used to generate the summary. |
420
+ | `tips` | built-in | Style hints used when generating menu options. |
421
+ | `buildPrompt` | `buildMiproPrompt` | Replaces the proposal template. |
422
+
423
+ **What a run costs.** `maxTrials × minibatchSize` for the trials, plus `validationSet.length` for the seed and for each full evaluation — one every `fullEvalInterval` trials, and one at the end — plus up to `demoSets × trainingSet.length` to bootstrap the demo menus. The search stops as soon as a sweep is no longer affordable, because a trial that can never be promoted buys nothing. Trials should also scale with the menus they draw from: the space is their product, and the surrogate spends `startupTrials` of them sampling uniformly before it models anything.
424
+
425
+ The proposer receives the current text of other components, a generated summary of `trainingSet`, and task exemplars. The summary uses one reflection call and is skipped when all menus are supplied through `componentOptions`. Unlike DSPy's MIPROv2 implementation, textopt does not include program source because the adapter interface has no generic representation for it.
426
+
427
+ `componentOptions` adds menu entries without reflection calls. Each component's menu starts with its seed text, followed by these options.
428
+
429
+ `demoComponents` identifies components that contain few-shot blocks. Their menus are bootstrapped from successful `trainingSet` rollouts instead of generated by `reflect`. Each demo set uses a separate shuffled pass and a size between one and `maxDemos`; the zero-shot option is always included. Separate passes can produce different demos for stochastic systems but add redundant work for deterministic systems, matching MIPROv2's behavior. Bootstrap rollouts count against `maxMetricCalls` and are also reported as `bootstrapMetricCalls`.
430
+
431
+ When `goldOutput` is supplied, each demo component also includes a labels-only `LabeledFewShot` option. It requires no rollout because the expected output is provided by the caller, and remains available when the seed system cannot produce successful bootstrap examples.
432
+
433
+ Every `fullEvalInterval` trials, the unswept configuration with the highest average minibatch score receives a full evaluation. A final sweep runs when the search ends. Averaging repeated minibatch scores reduces sensitivity to individual samples.
434
+
435
+ The surrogate includes both minibatch and full-evaluation observations, matching DSPy. The seed's full score is registered before the first trial. In a ten-seed sweep, adding promoted configurations' full scores reduced repeat proposals of disproved configurations from 21 to 10. Repeated high minibatch scores can still outweigh one low validation score.
436
+
437
+ One difference from MIPROv2 is that textopt keeps labelled demo sets as separate menu options; MIPROv2 pads bootstrapped sets with labelled examples.
438
+
439
+ `MiproResult` adds `seedScore`, `trials`, `menu`, `observations`, `fullEvaluations`, `bootstrapMetricCalls`, `reflectionCalls`, and `cacheHits`. Only full validation evaluations update the incumbent; minibatch scores select configurations for full evaluation. `stopReason` is `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"maxTrials"`, or `"aborted"`.
440
+
441
+ A `MiproSnapshot` carries the option menus alongside the usual budget and RNG state. The menus matter most: building them is the expensive half of a run — a reflection call per instruction and a harvesting pass per demo set — and they are also what every trial's choice vector indexes into, so a resumed run that rebuilt them would both pay twice and reinterpret every observation it had already made.
442
+
443
+ **`proposeConfiguration({ observations, menuSizes, gamma, samples, startupTrials, multivariate, rng })`** exposes the TPE surrogate separately. It splits observations into good and remaining groups, models each density, samples from the good density, and ranks samples by the log density ratio.
444
+
445
+ With `multivariate: true`, each density is a mixture of kernels centered on observed configurations. This preserves dependencies between component options. With `multivariate: false`, each component uses an independent smoothed histogram, which learns from fewer trials but cannot represent interactions.
446
+
447
+ Measured on an objective where each component pays off only when the preceding one is also correct, across 15 seeds: in a space of 3,125 configurations with 60 trials, the joint model reached a mean best score of 0.91 and solved 10 runs, against 0.80 and 6 for the independent model. In a 16-configuration space with 30 trials over 20 seeds, the joint model found the optimum in every run, against 16 for the independent model. The multivariate model is the default because it leads on both.
448
+
449
+ Before `startupTrials`, or when all observations have the same score, sampling is uniform. TPE's good/bad split requires ranked observations; using it on tied scores can repeatedly select configurations already evaluated. This tie handling differs from published TPE and Optuna.
450
+
451
+ ## `textopt/random-search`
452
+
453
+ ```ts
454
+ import { RandomSearchOptimizer } from "textopt/random-search";
455
+ ```
456
+
457
+ | Option | Default | Effect |
458
+ | ------------- | ----------------------- | ------------------------------------------------- |
459
+ | `variants` | `4` | Variants drawn per round, each evaluated in full. |
460
+ | `concurrency` | `1` | How many may be in flight at once. |
461
+ | `maxRounds` | `Infinity` | Round ceiling. |
462
+ | `buildPrompt` | `buildParaphrasePrompt` | Replaces the paraphrase template. |
463
+
464
+ **What a run costs.** One validation sweep for the seed, then `variants × validationSet.length` per round. A round starts only when all of it is affordable, so a half-funded round never spends rollouts on variants the rest cannot be compared against.
465
+
466
+ `RandomSearchResult` adds `seedScore`, `rounds`, `variantsEvaluated`, `reflectionCalls`, and `cacheHits`. `stopReason` is `"budgetExhausted"`, `"costExhausted"`, `"deadlineReached"`, `"maxRounds"`, `"proposerStalled"`, or `"aborted"`.
467
+
468
+ `"proposerStalled"` is the guard against a livelock: a proposer that returns duplicates, or texts already in the cache, spends no budget at all, so with `maxRounds` unset the loop would spin forever burning reflection calls. A full pass over the components that buys neither a rollout nor an improvement ends the run.
469
+
470
+ The paraphrase prompt receives no score or feedback. Compare random search with a reflective optimizer under the same metric budget to measure the effect of reflection.
471
+
472
+ ## `textopt/file-cache`
473
+
474
+ ```ts
475
+ import { createFileCache } from "textopt/file-cache";
476
+
477
+ const cache = createFileCache({ path: ".textopt/scores.jsonl" });
478
+ ```
479
+
480
+ An `EvaluationCache` that outlives the process, as an append-only JSONL log. A long run against a real provider is measured in hours and dollars, and an in-memory cache throws all of it away when the run ends.
481
+
482
+ Append-only rather than rewritten: a score is never invalidated, because the key names the candidate, the instance, and the environment — and a log survives a process killed mid-write, which a file rewritten in place does not. A record that does not parse is dropped rather than fatal, since the last line of an interrupted log is routinely half-written. Later records win, so a re-measured instance replaces its earlier reading.
483
+
484
+ `maxEntries` (default 1,000,000) bounds what is held in memory; the file itself is never trimmed. `entries()` is deliberately absent — it exists so a checkpoint can carry scores that would otherwise be lost, and these are already on disk.
485
+
486
+ This is the only entry point that imports `node:fs`.
487
+
488
+ ## `textopt/testing`
489
+
490
+ Deterministic keyword-coverage fixtures for testing optimizers and adapters without network access.
491
+
492
+ | Export | Description |
493
+ | ------------------------------- | ----------------------------------------------------------------------------------- |
494
+ | `KEYWORD_EXAMPLES` | Four `KeywordExample` rows. |
495
+ | `createKeywordAdapter()` | A `GepaAdapter` scoring how many required terms the candidate text covers. |
496
+ | `createKeywordReflector()` | A `TextModel` that folds the missing terms into the current instruction. |
497
+ | `createSamplingReflector()` | A `TextModel` that ignores the prompt and appends a term from a fixed pool. |
498
+ | `createHillClimbingReflector()` | A `TextModel` that reads score history and extends the best attempt. |
499
+ | `SAMPLING_POOL` | Useful terms interleaved with distractors, the pool the two above draw from. |
500
+ | `createDegradingReflector()` | A `TextModel` that always proposes something strictly worse, for testing rejection. |
501
+ | `buildReflectionPrompt` | The default prompt template, also exported from `textopt/gepa`. |
502
+
503
+ ## Adapters
504
+
505
+ Prebuilt adapters for common stacks live alongside this package: [`@textopt/ai-sdk`](https://github.com/ctdio/textopt/tree/main/packages/ai-sdk), [`@textopt/langchain`](https://github.com/ctdio/textopt/tree/main/packages/langchain), and [`@textopt/braintrust`](https://github.com/ctdio/textopt/tree/main/packages/braintrust).
506
+
507
+ ## License
508
+
509
+ MIT