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.
- package/LICENSE +21 -0
- package/README.md +509 -0
- package/dist/bootstrap-search/index.cjs +308 -0
- package/dist/bootstrap-search/index.d.cts +162 -0
- package/dist/bootstrap-search/index.d.mts +162 -0
- package/dist/bootstrap-search/index.mjs +307 -0
- package/dist/cache-CuSo0NJ8.d.cts +24 -0
- package/dist/cache-CuSo0NJ8.d.mts +24 -0
- package/dist/concurrency-C-cFzWW2.cjs +44 -0
- package/dist/concurrency-D58PWeSk.mjs +39 -0
- package/dist/demos-B0pVQjYC.d.mts +88 -0
- package/dist/demos-B9BJiNKz.cjs +143 -0
- package/dist/demos-BTuzFNsp.d.cts +88 -0
- package/dist/demos-Degx6UmP.mjs +126 -0
- package/dist/evaluation-BV0nSZVx.mjs +521 -0
- package/dist/evaluation-OZOp6TB7.cjs +598 -0
- package/dist/file-cache.cjs +70 -0
- package/dist/file-cache.d.cts +21 -0
- package/dist/file-cache.d.mts +21 -0
- package/dist/file-cache.mjs +69 -0
- package/dist/gepa/index.cjs +1671 -0
- package/dist/gepa/index.d.cts +385 -0
- package/dist/gepa/index.d.mts +385 -0
- package/dist/gepa/index.mjs +1652 -0
- package/dist/index.cjs +266 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.mts +221 -0
- package/dist/index.mjs +245 -0
- package/dist/math-COOofUyv.cjs +101 -0
- package/dist/math-DhrDmpFS.mjs +78 -0
- package/dist/mipro/index.cjs +739 -0
- package/dist/mipro/index.d.cts +372 -0
- package/dist/mipro/index.d.mts +372 -0
- package/dist/mipro/index.mjs +736 -0
- package/dist/opro/index.cjs +487 -0
- package/dist/opro/index.d.cts +230 -0
- package/dist/opro/index.d.mts +230 -0
- package/dist/opro/index.mjs +485 -0
- package/dist/optimizer-B7SpRwl7.d.cts +288 -0
- package/dist/optimizer-DqCoth_w.d.mts +288 -0
- package/dist/random-search/index.cjs +321 -0
- package/dist/random-search/index.d.cts +156 -0
- package/dist/random-search/index.d.mts +156 -0
- package/dist/random-search/index.mjs +319 -0
- package/dist/reflection-CQToe-5B.d.cts +283 -0
- package/dist/reflection-Cr_upzU0.d.mts +283 -0
- package/dist/reflection-DRfbk6hu.cjs +249 -0
- package/dist/reflection-mwMhrjs_.mjs +214 -0
- package/dist/rng-BR5MOedA.d.cts +22 -0
- package/dist/rng-BR5MOedA.d.mts +22 -0
- package/dist/rng-DbA_rPIo.cjs +67 -0
- package/dist/rng-Dtc5eZ_W.mjs +62 -0
- package/dist/sampling-CfHt7Gue.mjs +59 -0
- package/dist/sampling-DFo_7RNJ.d.mts +23 -0
- package/dist/sampling-Dars7ctR.cjs +64 -0
- package/dist/sampling-axOwfZf5.d.cts +23 -0
- package/dist/simba/index.cjs +709 -0
- package/dist/simba/index.d.cts +289 -0
- package/dist/simba/index.d.mts +289 -0
- package/dist/simba/index.mjs +700 -0
- package/dist/testing.cjs +155 -0
- package/dist/testing.d.cts +53 -0
- package/dist/testing.d.mts +53 -0
- package/dist/testing.mjs +148 -0
- package/dist/text--v4Ffbus.mjs +21 -0
- package/dist/text-CK_HB3su.cjs +26 -0
- package/dist/types-CWv4IQFF.d.cts +129 -0
- package/dist/types-CWv4IQFF.d.mts +129 -0
- package/package.json +135 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { a as requireMeasuredMean, d as createMemoryCache, f as stableHash, 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 { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
|
|
3
|
+
import { t as parseProposedText } from "../text--v4Ffbus.mjs";
|
|
4
|
+
//#region src/random-search/optimize.ts
|
|
5
|
+
const DEFAULT_VARIANTS = 4;
|
|
6
|
+
/**
|
|
7
|
+
* The ablation baseline: propose blind, evaluate in full, keep what wins.
|
|
8
|
+
*
|
|
9
|
+
* It exists to be beaten. Reflective search costs a frontier-model call per
|
|
10
|
+
* proposal on top of its rollouts, and running the same budget through a
|
|
11
|
+
* search that cannot read feedback at all puts a floor under what that call
|
|
12
|
+
* has to buy. A GEPA run that does not clear this one is paying for machinery
|
|
13
|
+
* it is not using.
|
|
14
|
+
*
|
|
15
|
+
* What the gap measures is the whole of that machinery, not reflection alone:
|
|
16
|
+
* this drops feedback, Pareto parent selection, and minibatch screening
|
|
17
|
+
* together, and GEPA's own ablations put candidate selection at several points
|
|
18
|
+
* by itself. For reflection in isolation, keep GEPA and hand it a
|
|
19
|
+
* `reflection.buildPrompt` that withholds the evidence — same frontier, same
|
|
20
|
+
* screening, one variable.
|
|
21
|
+
*
|
|
22
|
+
* Not DSPy's `BootstrapFewShotWithRandomSearch`, which the literature usually
|
|
23
|
+
* means by "random search" and which searches bootstrapped demo sets; nor
|
|
24
|
+
* random search in the Bergstra–Bengio sense, since proposals are paraphrases
|
|
25
|
+
* of the incumbent rather than independent draws. No reference implements this
|
|
26
|
+
* baseline — it is original here, and the GEPA paper has no equivalent.
|
|
27
|
+
*/
|
|
28
|
+
var RandomSearchOptimizer = class {
|
|
29
|
+
#config;
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
assertConfig(config);
|
|
32
|
+
this.#config = config;
|
|
33
|
+
}
|
|
34
|
+
async optimize(task) {
|
|
35
|
+
return runRandomSearch({
|
|
36
|
+
config: this.#config,
|
|
37
|
+
task
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Adapted from the GEPA reflection prompt with every source of evidence
|
|
43
|
+
* removed. What is left is the ablation: the same model, the same component,
|
|
44
|
+
* the same output format, and nothing to reason from.
|
|
45
|
+
*/
|
|
46
|
+
function buildParaphrasePrompt(args) {
|
|
47
|
+
const { componentName, currentText, attempt } = args;
|
|
48
|
+
return [
|
|
49
|
+
`Here is the current instruction for the "${componentName}" component of a larger system:`,
|
|
50
|
+
"",
|
|
51
|
+
"<current_instruction>",
|
|
52
|
+
currentText,
|
|
53
|
+
"</current_instruction>",
|
|
54
|
+
"",
|
|
55
|
+
"Write a different instruction for this component.",
|
|
56
|
+
"You have no information about how the current one has performed, so do not guess at its weaknesses — vary it instead.",
|
|
57
|
+
`Make variation ${attempt + 1} distinct from the others: change the wording, the level of detail, or the strategy it describes.`,
|
|
58
|
+
"",
|
|
59
|
+
"Return only the new instruction, inside a ``` block."
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
async function runRandomSearch(args) {
|
|
63
|
+
const { config, task } = args;
|
|
64
|
+
const { variants = DEFAULT_VARIANTS, concurrency = 1, maxRounds = Number.POSITIVE_INFINITY, buildPrompt = buildParaphrasePrompt, trackBestOutputs = false, checkpointCache = true } = config;
|
|
65
|
+
const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, maxMetricCalls, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
|
|
66
|
+
const deadline = createDeadline({ maxWallClockMs });
|
|
67
|
+
const components = componentNames(seedCandidate);
|
|
68
|
+
if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
|
|
69
|
+
if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
|
|
70
|
+
if (components.length === 0) throw new Error("optimize requires a seed candidate with at least one component");
|
|
71
|
+
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");
|
|
72
|
+
const validationIds = validationSet.map((datum, index) => instanceId({
|
|
73
|
+
datum,
|
|
74
|
+
index
|
|
75
|
+
}));
|
|
76
|
+
const testIds = testSet?.map((datum, index) => instanceId({
|
|
77
|
+
datum,
|
|
78
|
+
index
|
|
79
|
+
})) ?? [];
|
|
80
|
+
const fingerprint = runFingerprint({
|
|
81
|
+
seedCandidate,
|
|
82
|
+
trainingIds: trainingSet.map((datum, index) => instanceId({
|
|
83
|
+
datum,
|
|
84
|
+
index
|
|
85
|
+
})),
|
|
86
|
+
validationIds,
|
|
87
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace }
|
|
88
|
+
});
|
|
89
|
+
assertResumable({
|
|
90
|
+
fingerprint,
|
|
91
|
+
...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
|
|
92
|
+
});
|
|
93
|
+
const budget = createBudget({
|
|
94
|
+
maxMetricCalls,
|
|
95
|
+
spent: resumeFrom?.metricCalls ?? 0
|
|
96
|
+
});
|
|
97
|
+
const evaluationCache = cache === false ? void 0 : cache ?? createMemoryCache();
|
|
98
|
+
const evaluator = createEvaluator({
|
|
99
|
+
adapter,
|
|
100
|
+
budget,
|
|
101
|
+
...retry === void 0 ? {} : { retry },
|
|
102
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace },
|
|
103
|
+
...evaluationCache === void 0 ? {} : { cache: evaluationCache },
|
|
104
|
+
trackOutputs: trackBestOutputs,
|
|
105
|
+
cacheHits: resumeFrom?.cacheHits ?? 0,
|
|
106
|
+
...signal === void 0 ? {} : { signal },
|
|
107
|
+
onEvaluation: (event) => onEvent?.({
|
|
108
|
+
type: "evaluation",
|
|
109
|
+
...event
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
evaluator.restore(resumeFrom?.cache ?? []);
|
|
113
|
+
let round = resumeFrom?.round ?? 0;
|
|
114
|
+
let variantsEvaluated = resumeFrom?.variantsEvaluated ?? 0;
|
|
115
|
+
let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
|
|
116
|
+
let stopReason = "maxRounds";
|
|
117
|
+
/**
|
|
118
|
+
* Consecutive rounds that neither spent a rollout nor improved on the
|
|
119
|
+
* incumbent. A proposer stuck on texts that are already cached costs
|
|
120
|
+
* nothing, so neither the metric budget nor the cost ceiling can end the
|
|
121
|
+
* run — without this the loop spins forever, burning reflection calls that
|
|
122
|
+
* no budget here bounds.
|
|
123
|
+
*/
|
|
124
|
+
let stalledRounds = 0;
|
|
125
|
+
onEvent?.({
|
|
126
|
+
type: "start",
|
|
127
|
+
components,
|
|
128
|
+
validationSetSize: validationSet.length
|
|
129
|
+
});
|
|
130
|
+
async function sweep(args) {
|
|
131
|
+
return evaluator.evaluate({
|
|
132
|
+
candidate: args.candidate,
|
|
133
|
+
batch: validationSet,
|
|
134
|
+
ids: validationIds,
|
|
135
|
+
split: "val",
|
|
136
|
+
phase: args.phase,
|
|
137
|
+
candidateId: null,
|
|
138
|
+
iteration: round
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function takeSnapshot() {
|
|
142
|
+
const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
|
|
143
|
+
return {
|
|
144
|
+
version: 1,
|
|
145
|
+
fingerprint,
|
|
146
|
+
best,
|
|
147
|
+
bestScore,
|
|
148
|
+
seedScore,
|
|
149
|
+
round,
|
|
150
|
+
variantsEvaluated,
|
|
151
|
+
reflectionCalls,
|
|
152
|
+
metricCalls: budget.spent(),
|
|
153
|
+
cacheHits: evaluator.cacheHits(),
|
|
154
|
+
...cached === void 0 ? {} : { cache: cached }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function checkpoint() {
|
|
158
|
+
if (onCheckpoint === void 0) return;
|
|
159
|
+
await onCheckpoint(takeSnapshot());
|
|
160
|
+
}
|
|
161
|
+
const seedEvaluation = resumeFrom === void 0 ? await sweep({
|
|
162
|
+
candidate: seedCandidate,
|
|
163
|
+
phase: "seed"
|
|
164
|
+
}) : void 0;
|
|
165
|
+
const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : requireMeasuredMean({
|
|
166
|
+
batch: seedEvaluation,
|
|
167
|
+
phase: "seed"
|
|
168
|
+
});
|
|
169
|
+
let best = resumeFrom?.best ?? seedCandidate;
|
|
170
|
+
let bestScore = resumeFrom?.bestScore ?? seedScore;
|
|
171
|
+
/** Absent on a resumed run until a variant wins: outputs are not checkpointed. */
|
|
172
|
+
let bestOutputs = seedEvaluation?.outputs;
|
|
173
|
+
await checkpoint();
|
|
174
|
+
while (round < maxRounds) {
|
|
175
|
+
if (signal?.aborted) {
|
|
176
|
+
stopReason = "aborted";
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (costExhausted({
|
|
180
|
+
usage: evaluator.usage(),
|
|
181
|
+
maxCostUsd
|
|
182
|
+
})) {
|
|
183
|
+
stopReason = "costExhausted";
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (deadline.exceeded()) {
|
|
187
|
+
stopReason = "deadlineReached";
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (!budget.canAfford(variants * validationSet.length)) {
|
|
191
|
+
stopReason = "budgetExhausted";
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
const component = components[round % components.length];
|
|
195
|
+
onEvent?.({
|
|
196
|
+
type: "roundStart",
|
|
197
|
+
round,
|
|
198
|
+
component
|
|
199
|
+
});
|
|
200
|
+
const spentBefore = budget.spent();
|
|
201
|
+
const scoreBefore = bestScore;
|
|
202
|
+
const currentText = best[component];
|
|
203
|
+
const drawn = await mapWithConcurrency({
|
|
204
|
+
items: Array.from({ length: variants }, (_, attempt) => attempt),
|
|
205
|
+
limit: concurrency,
|
|
206
|
+
signal,
|
|
207
|
+
task: async (attempt) => {
|
|
208
|
+
reflectionCalls += 1;
|
|
209
|
+
const response = await reflect({
|
|
210
|
+
prompt: buildPrompt({
|
|
211
|
+
componentName: component,
|
|
212
|
+
currentText,
|
|
213
|
+
attempt
|
|
214
|
+
}),
|
|
215
|
+
...signal === void 0 ? {} : { signal }
|
|
216
|
+
});
|
|
217
|
+
return parseProposedText(response);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const unique = [...new Set(drawn)].filter((text) => text.length > 0 && text !== currentText);
|
|
221
|
+
let roundStop;
|
|
222
|
+
for (const text of unique) {
|
|
223
|
+
const candidate = {
|
|
224
|
+
...best,
|
|
225
|
+
[component]: text
|
|
226
|
+
};
|
|
227
|
+
let evaluation;
|
|
228
|
+
try {
|
|
229
|
+
evaluation = await sweep({
|
|
230
|
+
candidate,
|
|
231
|
+
phase: "validation"
|
|
232
|
+
});
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (err instanceof BudgetExhausted) {
|
|
235
|
+
roundStop = "budgetExhausted";
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (signal?.aborted) {
|
|
239
|
+
roundStop = "aborted";
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
variantsEvaluated += 1;
|
|
245
|
+
const score = measuredMean(evaluation);
|
|
246
|
+
if (score !== void 0 && score > bestScore) {
|
|
247
|
+
onEvent?.({
|
|
248
|
+
type: "candidateAccepted",
|
|
249
|
+
round,
|
|
250
|
+
component,
|
|
251
|
+
score,
|
|
252
|
+
previousScore: bestScore
|
|
253
|
+
});
|
|
254
|
+
best = candidate;
|
|
255
|
+
bestScore = score;
|
|
256
|
+
bestOutputs = evaluation.outputs;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
round += 1;
|
|
260
|
+
stalledRounds = budget.spent() === spentBefore && bestScore === scoreBefore ? stalledRounds + 1 : 0;
|
|
261
|
+
await checkpoint();
|
|
262
|
+
if (stalledRounds >= components.length) {
|
|
263
|
+
stopReason = "proposerStalled";
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (roundStop !== void 0) {
|
|
267
|
+
stopReason = roundStop;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (signal?.aborted) stopReason = "aborted";
|
|
272
|
+
const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
|
|
273
|
+
candidate: best,
|
|
274
|
+
batch: testSet,
|
|
275
|
+
ids: testIds,
|
|
276
|
+
split: "test",
|
|
277
|
+
phase: "test",
|
|
278
|
+
candidateId: null,
|
|
279
|
+
iteration: round,
|
|
280
|
+
charge: false
|
|
281
|
+
}));
|
|
282
|
+
onEvent?.({
|
|
283
|
+
type: "finish",
|
|
284
|
+
reason: stopReason,
|
|
285
|
+
bestScore,
|
|
286
|
+
metricCalls: budget.spent(),
|
|
287
|
+
...testScore === void 0 ? {} : { testScore }
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
bestCandidate: best,
|
|
291
|
+
bestScore,
|
|
292
|
+
usage: evaluator.usage(),
|
|
293
|
+
seedScore,
|
|
294
|
+
snapshot: takeSnapshot(),
|
|
295
|
+
...trackBestOutputs ? { bestOutputs } : {},
|
|
296
|
+
...testScore === void 0 ? {} : {
|
|
297
|
+
testScore,
|
|
298
|
+
testMetricCalls: evaluator.unchargedCalls()
|
|
299
|
+
},
|
|
300
|
+
rounds: round,
|
|
301
|
+
variantsEvaluated,
|
|
302
|
+
metricCalls: budget.spent(),
|
|
303
|
+
reflectionCalls,
|
|
304
|
+
cacheHits: evaluator.cacheHits(),
|
|
305
|
+
stopReason
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function assertConfig(config) {
|
|
309
|
+
const { variants = DEFAULT_VARIANTS, concurrency = 1, maxRounds } = config;
|
|
310
|
+
if (!Number.isInteger(variants) || variants < 1) throw new Error(`variants must be a positive integer, received ${variants}`);
|
|
311
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`concurrency must be a positive integer, received ${concurrency}`);
|
|
312
|
+
if (maxRounds !== void 0 && (!Number.isInteger(maxRounds) || maxRounds < 1)) throw new Error(`maxRounds must be a positive integer, received ${maxRounds}`);
|
|
313
|
+
}
|
|
314
|
+
function defaultInstanceId(args) {
|
|
315
|
+
const hash = stableHash(args.datum);
|
|
316
|
+
return hash === "" ? String(args.index) : hash;
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
export { RandomSearchOptimizer, buildParaphrasePrompt };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { i as EvaluationBatch, n as Candidate, o as EvaluationPhase, s as EvaluationSplit, t as Adapter, u as TextModel } from "./types-CWv4IQFF.cjs";
|
|
2
|
+
import { t as CachedScore } from "./cache-CuSo0NJ8.cjs";
|
|
3
|
+
import { t as Rng } from "./rng-BR5MOedA.cjs";
|
|
4
|
+
//#region src/gepa/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* One piece of evidence reflection reads: what went in, what came out, and how
|
|
7
|
+
* it scored. `Evidence` is the adapter's own slot — trace steps, retrieved
|
|
8
|
+
* documents, errors — typed rather than smuggled in through an index signature.
|
|
9
|
+
*/
|
|
10
|
+
interface ReflectiveRecord<Evidence = unknown> {
|
|
11
|
+
inputs: unknown;
|
|
12
|
+
generatedOutputs: unknown;
|
|
13
|
+
feedback: string;
|
|
14
|
+
score?: number;
|
|
15
|
+
evidence?: Evidence;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Component name -> records shown to the reflection model for that component.
|
|
19
|
+
*
|
|
20
|
+
* Partial, not total: an adapter only fills the components it was asked to
|
|
21
|
+
* update, so a component with nothing to diagnose is simply absent.
|
|
22
|
+
*/
|
|
23
|
+
type ReflectiveDataset<K extends string = string> = Partial<Record<K, ReflectiveRecord[]>>;
|
|
24
|
+
interface MakeReflectiveDatasetArgs<Datum, Trajectory, Output, K extends string = string> {
|
|
25
|
+
candidate: Candidate<K>;
|
|
26
|
+
batch: readonly Datum[];
|
|
27
|
+
evaluation: EvaluationBatch<Trajectory, Output>;
|
|
28
|
+
componentsToUpdate: readonly K[];
|
|
29
|
+
}
|
|
30
|
+
/** New text for the subset of components a proposal actually changed. */
|
|
31
|
+
type ComponentPatch<K extends string = string> = Partial<Record<K, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* A component text that was proposed and lost on the minibatch. Showing these
|
|
34
|
+
* back to the reflection model is what stops a run from re-deriving the same
|
|
35
|
+
* dead end: only exact duplicates are filtered structurally, so without this
|
|
36
|
+
* the model can spend the whole budget circling one bad idea.
|
|
37
|
+
*/
|
|
38
|
+
interface RejectedProposal {
|
|
39
|
+
text: string;
|
|
40
|
+
parentScore: number;
|
|
41
|
+
childScore: number;
|
|
42
|
+
}
|
|
43
|
+
interface ProposeArgs<K extends string = string> {
|
|
44
|
+
candidate: Candidate<K>;
|
|
45
|
+
reflectiveDataset: ReflectiveDataset<K>;
|
|
46
|
+
componentsToUpdate: readonly K[];
|
|
47
|
+
/** Component name -> texts already tried and rejected for it. */
|
|
48
|
+
rejectedProposals?: Partial<Record<K, RejectedProposal[]>>;
|
|
49
|
+
/**
|
|
50
|
+
* Which proposal this is in the run, counting from 0. Assigned when the
|
|
51
|
+
* iteration is planned rather than when it executes, so concurrent
|
|
52
|
+
* proposals get stable, distinct values instead of racing for a counter.
|
|
53
|
+
* The default proposer rotates its strategies on it.
|
|
54
|
+
*/
|
|
55
|
+
attempt?: number;
|
|
56
|
+
reflect: TextModel;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An adapter GEPA can reflect against: evaluation, plus the traces reflection
|
|
61
|
+
* reads. `makeReflectiveDataset` is what turns a scored batch into the
|
|
62
|
+
* per-component evidence a reflection call is written from.
|
|
63
|
+
*/
|
|
64
|
+
interface GepaAdapter<Datum, Trajectory = unknown, Output = unknown, K extends string = string> extends Adapter<Datum, Trajectory, Output, K> {
|
|
65
|
+
makeReflectiveDataset(args: MakeReflectiveDatasetArgs<Datum, Trajectory, Output, K>): Promise<ReflectiveDataset<K>> | ReflectiveDataset<K>;
|
|
66
|
+
proposeNewTexts?(args: ProposeArgs<K>): Promise<ComponentPatch<K>> | ComponentPatch<K>;
|
|
67
|
+
}
|
|
68
|
+
type CandidateSource = "seed" | "mutation" | "merge";
|
|
69
|
+
interface CandidateRecord<K extends string = string> {
|
|
70
|
+
id: number;
|
|
71
|
+
candidate: Candidate<K>;
|
|
72
|
+
parentIds: number[];
|
|
73
|
+
/**
|
|
74
|
+
* One entry per validation instance. `undefined` marks an instance the
|
|
75
|
+
* evaluation policy did not select for this candidate — unknown, not zero.
|
|
76
|
+
*/
|
|
77
|
+
instanceScores: (number | undefined)[];
|
|
78
|
+
/** Mean over the instances that were scored. */
|
|
79
|
+
aggregateScore: number;
|
|
80
|
+
/** Mean of each objective over the evaluated validation instances. */
|
|
81
|
+
objectiveScores?: Record<string, number>;
|
|
82
|
+
source: CandidateSource;
|
|
83
|
+
updatedComponents: K[];
|
|
84
|
+
iteration: number;
|
|
85
|
+
/**
|
|
86
|
+
* Round-robin position this candidate resumes from the next time it is
|
|
87
|
+
* chosen as a parent. Per-candidate rather than global: a lineage that is
|
|
88
|
+
* only reselected every fifth iteration must still walk its own components
|
|
89
|
+
* one at a time instead of being pinned to whichever one the global counter
|
|
90
|
+
* happens to land on.
|
|
91
|
+
*/
|
|
92
|
+
componentCursor: number;
|
|
93
|
+
}
|
|
94
|
+
/** What a Pareto frontier is taken over. */
|
|
95
|
+
type ParetoFrontier = "instance" | "objective" | "hybrid";
|
|
96
|
+
/** Read-only view handed to candidate selectors. */
|
|
97
|
+
interface SelectionState {
|
|
98
|
+
scoreMatrix: readonly (readonly (number | undefined)[])[];
|
|
99
|
+
aggregateScores: readonly number[];
|
|
100
|
+
/** Per-candidate objective means, absent when the adapter reports none. */
|
|
101
|
+
objectiveScores?: readonly (Readonly<Record<string, number>> | undefined)[];
|
|
102
|
+
}
|
|
103
|
+
type CandidateSelector = (args: {
|
|
104
|
+
state: SelectionState;
|
|
105
|
+
rng: Rng;
|
|
106
|
+
}) => number;
|
|
107
|
+
type ComponentSelector<K extends string = string> = (args: {
|
|
108
|
+
candidate: Candidate<K>;
|
|
109
|
+
/** This candidate's own round-robin position, advanced after each selection. */
|
|
110
|
+
cursor: number;
|
|
111
|
+
iteration: number;
|
|
112
|
+
rng: Rng;
|
|
113
|
+
}) => K[];
|
|
114
|
+
type AcceptancePolicy = (args: {
|
|
115
|
+
parentScores: readonly number[];
|
|
116
|
+
childScores: readonly number[];
|
|
117
|
+
}) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Which validation instances a candidate is scored on, and how the best
|
|
120
|
+
* candidate is read back out of possibly partial coverage.
|
|
121
|
+
*
|
|
122
|
+
* A full sweep per accepted candidate is the published behaviour and the
|
|
123
|
+
* default. Swapping in a partial policy trades frontier fidelity for rollouts:
|
|
124
|
+
* candidates scored on different instances are no longer strictly comparable,
|
|
125
|
+
* which is why picking the best is the policy's job too.
|
|
126
|
+
*/
|
|
127
|
+
interface ValEvaluationPolicy<Datum = unknown, K extends string = string> {
|
|
128
|
+
selectInstances(args: {
|
|
129
|
+
validationSet: readonly Datum[];
|
|
130
|
+
candidate: Candidate<K>;
|
|
131
|
+
records: readonly CandidateRecord<K>[];
|
|
132
|
+
iteration: number;
|
|
133
|
+
rng: Rng;
|
|
134
|
+
}): number[];
|
|
135
|
+
bestCandidate(records: readonly CandidateRecord<K>[]): number;
|
|
136
|
+
}
|
|
137
|
+
type GepaStopReason = "budgetExhausted" | "costExhausted" | "deadlineReached" | "reflectionBudgetExhausted" | "aborted" | "maxIterations";
|
|
138
|
+
type GepaEvent<K extends string = string> = {
|
|
139
|
+
type: "start";
|
|
140
|
+
components: K[];
|
|
141
|
+
validationSetSize: number;
|
|
142
|
+
} | {
|
|
143
|
+
type: "iterationStart";
|
|
144
|
+
iteration: number;
|
|
145
|
+
parentIds: number[];
|
|
146
|
+
} | {
|
|
147
|
+
type: "evaluation";
|
|
148
|
+
iteration: number;
|
|
149
|
+
phase: EvaluationPhase;
|
|
150
|
+
split: EvaluationSplit;
|
|
151
|
+
candidateId: number | null;
|
|
152
|
+
metricCalls: number;
|
|
153
|
+
cacheHits: number;
|
|
154
|
+
meanScore: number;
|
|
155
|
+
} | {
|
|
156
|
+
type: "proposal";
|
|
157
|
+
iteration: number;
|
|
158
|
+
parentId: number;
|
|
159
|
+
componentsToUpdate: K[];
|
|
160
|
+
changed: boolean;
|
|
161
|
+
} | {
|
|
162
|
+
type: "candidateAccepted";
|
|
163
|
+
iteration: number;
|
|
164
|
+
candidateId: number;
|
|
165
|
+
parentIds: number[];
|
|
166
|
+
aggregateScore: number;
|
|
167
|
+
source: CandidateSource;
|
|
168
|
+
} | {
|
|
169
|
+
type: "candidateRejected";
|
|
170
|
+
iteration: number;
|
|
171
|
+
parentId: number;
|
|
172
|
+
parentScore: number;
|
|
173
|
+
childScore: number;
|
|
174
|
+
source: CandidateSource;
|
|
175
|
+
/**
|
|
176
|
+
* "worse" lost to its parent on the minibatch. "notSelected" beat its
|
|
177
|
+
* parent but lost to a stronger proposal from the same iteration — it is
|
|
178
|
+
* not a dead end, and is never fed back to reflection as one.
|
|
179
|
+
*/
|
|
180
|
+
reason: "worse" | "notSelected";
|
|
181
|
+
} | {
|
|
182
|
+
type: "error";
|
|
183
|
+
iteration: number;
|
|
184
|
+
err: unknown;
|
|
185
|
+
} | {
|
|
186
|
+
type: "finish";
|
|
187
|
+
reason: GepaStopReason;
|
|
188
|
+
bestCandidateId: number;
|
|
189
|
+
metricCalls: number;
|
|
190
|
+
/** The winner's held-out score, when a testSet was given. */
|
|
191
|
+
testScore?: number;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Everything needed to continue a run: the candidate pool with its scores, the
|
|
195
|
+
* budget already spent, the position of the random stream, and the bookkeeping
|
|
196
|
+
* that stops merges and proposals from being relitigated. Plain JSON — write it
|
|
197
|
+
* wherever you like and hand it back as `resumeFrom`.
|
|
198
|
+
*
|
|
199
|
+
* Deliberately not generic over component names. It leaves the process and
|
|
200
|
+
* comes back through `JSON.parse` with plain string keys, so the narrowing back
|
|
201
|
+
* to a run's own components happens once inside the engine, guarded by the
|
|
202
|
+
* fingerprint, rather than being a type the caller has to reconstruct.
|
|
203
|
+
*
|
|
204
|
+
* A resumed run follows the same trajectory an uninterrupted one would, as long
|
|
205
|
+
* as the batch sampler reports its state and the evaluation cache is either
|
|
206
|
+
* checkpointed or disabled — a cache that is neither leaves the resumed run
|
|
207
|
+
* paying again for rollouts the first one had already bought.
|
|
208
|
+
*/
|
|
209
|
+
interface GepaSnapshot {
|
|
210
|
+
version: 1;
|
|
211
|
+
/**
|
|
212
|
+
* Identifies the run this checkpoint came from — seed candidate, instance
|
|
213
|
+
* ids and seed. Resuming against a different setup is refused rather than
|
|
214
|
+
* silently scoring old candidates against new data.
|
|
215
|
+
*/
|
|
216
|
+
fingerprint: string;
|
|
217
|
+
records: CandidateRecord[];
|
|
218
|
+
iteration: number;
|
|
219
|
+
metricCalls: number;
|
|
220
|
+
reflectionCalls: number;
|
|
221
|
+
cacheHits: number;
|
|
222
|
+
rngState: number;
|
|
223
|
+
/** Whatever the batch sampler reports from `state()`, when it has one. */
|
|
224
|
+
sampler?: unknown;
|
|
225
|
+
rejectedProposals: Record<string, RejectedProposal[]>;
|
|
226
|
+
merge: {
|
|
227
|
+
attempts: string[];
|
|
228
|
+
descriptions: string[];
|
|
229
|
+
due: number;
|
|
230
|
+
tested: number;
|
|
231
|
+
lastIterationAccepted: boolean;
|
|
232
|
+
};
|
|
233
|
+
/** Cached instance scores, when the cache can enumerate them. */
|
|
234
|
+
cache?: [string, CachedScore][];
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/gepa/reflection.d.ts
|
|
238
|
+
interface ReflectionPromptArgs {
|
|
239
|
+
componentName: string;
|
|
240
|
+
currentText: string;
|
|
241
|
+
records: readonly ReflectiveRecord[];
|
|
242
|
+
/** Texts already tried for this component that lost to their parent. */
|
|
243
|
+
rejected?: readonly RejectedProposal[];
|
|
244
|
+
}
|
|
245
|
+
type ReflectionPromptBuilder = (args: ReflectionPromptArgs) => string;
|
|
246
|
+
/**
|
|
247
|
+
* Adapted from the reflection prompt in the GEPA paper (Agrawal et al., 2025).
|
|
248
|
+
* The instruction to mine domain facts out of the traces matters as much as the
|
|
249
|
+
* instruction to fix failures — most of the lift comes from the model writing
|
|
250
|
+
* down knowledge the traces revealed.
|
|
251
|
+
*/
|
|
252
|
+
declare function buildReflectionPrompt(args: ReflectionPromptArgs): string;
|
|
253
|
+
/**
|
|
254
|
+
* Cut rather than add. Reflective evolution only ever appends — every
|
|
255
|
+
* iteration diagnoses a failure and writes a rule preventing it — so an
|
|
256
|
+
* instruction grows monotonically until it is mostly edge cases that no longer
|
|
257
|
+
* fire. Nothing else in the loop ever removes one.
|
|
258
|
+
*/
|
|
259
|
+
declare function buildSimplifyPrompt(args: ReflectionPromptArgs): string;
|
|
260
|
+
/**
|
|
261
|
+
* Replace a rule that fits the instances it was written from with the
|
|
262
|
+
* principle behind it. Feedback is drawn from minibatches, so a rule written
|
|
263
|
+
* to fix three examples routinely encodes those three examples.
|
|
264
|
+
*/
|
|
265
|
+
declare function buildGeneralizePrompt(args: ReflectionPromptArgs): string;
|
|
266
|
+
/**
|
|
267
|
+
* Start from the evidence rather than from the incumbent. Every other strategy
|
|
268
|
+
* edits the current text, which anchors each proposal to whatever the search
|
|
269
|
+
* happened to reach first; this one is the only escape from a bad opening.
|
|
270
|
+
*/
|
|
271
|
+
declare function buildRewritePrompt(args: ReflectionPromptArgs): string;
|
|
272
|
+
/**
|
|
273
|
+
* A rotation covering the four directions a proposal can move in: fix what is
|
|
274
|
+
* broken, cut what is dead, widen what is too narrow, and start over.
|
|
275
|
+
*
|
|
276
|
+
* Drawing a proposal k times from one template samples one direction k times.
|
|
277
|
+
* Rotating costs nothing extra — same call count, same rollouts — and is the
|
|
278
|
+
* cheapest diversity available. Opt in via `reflection.strategies`; the
|
|
279
|
+
* default stays the published single prompt.
|
|
280
|
+
*/
|
|
281
|
+
declare function diverseReflectionStrategies(): ReflectionPromptBuilder[];
|
|
282
|
+
//#endregion
|
|
283
|
+
export { RejectedProposal as C, ReflectiveRecord as S, ValEvaluationPolicy as T, GepaStopReason as _, buildRewritePrompt as a, ProposeArgs as b, AcceptancePolicy as c, CandidateSource as d, ComponentPatch as f, GepaSnapshot as g, GepaEvent as h, buildReflectionPrompt as i, CandidateRecord as l, GepaAdapter as m, ReflectionPromptBuilder as n, buildSimplifyPrompt as o, ComponentSelector as p, buildGeneralizePrompt as r, diverseReflectionStrategies as s, ReflectionPromptArgs as t, CandidateSelector as u, MakeReflectiveDatasetArgs as v, SelectionState as w, ReflectiveDataset as x, ParetoFrontier as y };
|