/** * Darwin — GEPA-Style Reflective Optimizer (Phase 2 A2, S1185). * * Generation-loop wrapper around {@link Reflector} + {@link paretoSelect} * that produces N variants (default 3) per cycle and carries the * non-dominated set forward to the next generation. Opt-in via * `useGepa: true` in the evolution loop config. * * Pipeline per generation: * * 1. Take a parent variant (current prompt or last gen's winner). * 2. Generate N candidate mutations by calling the reflector with * different sliced feedback (forces variation while keeping each * mutation principled — not stochastic-only). * 3. Caller scores the candidates against their domain metric(s). * 4. paretoSelect keeps the non-dominated set (≤ maxCarry). * 5. Next generation reflects from the survivors. * * This file orchestrates only the GENERATION step. Scoring lives in * the caller (loop.ts → evaluator) because Darwin's existing * multi-critic pipeline is the authoritative source of variant scores. * * Why this design and not a full GEPA-engine port: * - Keep the Python lib (`gepa-ai/gepa`) out of our peer-dep graph. * - Reuse Darwin's existing multi-critic + safety + A/B machinery — * GepaOptimizer is purely about variant GENERATION + Pareto SELECTION. * - Smallest-possible-edit reflection is the part that empirically * matters most (per GEPA papers); the multi-objective search is the * other part. Together they cover the GEPA value-prop without a * Python dependency. * * **Inspired by, not lifted from** the official GEPA Python library / * arxiv 2507.19457 paper. Deliberate deviations (R1 Research Finding F1 * + F4 + F6, S1185): * - **N variants per generate() call:** GEPA Algorithm 1 produces 1 * offspring per iteration. We produce N=3-5 per call via three * `feedbackStrategy` modes (split / replicate / single). This is * our structural adaptation to TS-callsite ergonomics — call us * three times to recover the per-iteration GEPA budget. * - **`feedbackStrategy: "split"` is OUR adaptation** — GEPA paper * does not partition feedback across offspring. We do it to force * mutation diversity in a single `Promise.all` batch. * - **paretoSelect truncation has two strategies (V0.5.1):** * `"scalarised"` (V0.5.0 default, weighted-sum tie-break) and * `"crowding"` (NSGA-II Deb 2002 density-preserving truncation). * GEPA Algorithm 2's instance-proportional coverage sampling SHIPPED in * v0.7.0 — see `coverageFrontier`/`selectByCoverage`/`sampleByCoverage` * in `pareto.ts` and the `useCoverage` path in {@link GepaOptimizer#nextGeneration}. * - **GEPA+Merge (system-aware crossover from two Pareto-pool * ancestors, paper Appendix F)** SHIPPED in V0.5.1 via * {@link GepaOptimizer#merge}. Paper reports ~5% lift when run on * every K-th generation. * - **Stronger reflection LM** SHIPPED in V0.5.1 via * {@link GepaOptimizerOptions.reflectionRunPrompt}. Falls back to * the main `runPrompt` when omitted. * - **Instance-wise coverage sampling** (paper Algorithm 2) SHIPPED in * v0.7.0 (`coverageFrontier`/`selectByCoverage`/`sampleByCoverage` in * `pareto.ts`; opt-in via `NextGenerationOptions.useCoverage`). * * @example * ```ts * import { GepaOptimizer, DARWIN_DEFAULT_OBJECTIVES } from "darwin-agents"; * * const optimizer = new GepaOptimizer(myRunPromptFn); * * // Step 1: generate N variant mutations from feedback * const variants = await optimizer.generate(currentPrompt, feedbacks, { * numVariants: 3, * }); * * // Step 2: score each variant externally (via Darwin's multi-critic * // pipeline or any other evaluator) * const scored = await Promise.all( * variants.map(async (v) => ({ * ...v, * metrics: await scoreVariant(v.prompt), * })), * ); * * // Step 3: Pareto-select survivors for the next generation * const survivors = optimizer.nextGeneration(scored, { * objectives: DARWIN_DEFAULT_OBJECTIVES, * maxCarry: 3, * }); * ``` */ import type { ReflectiveFeedback, RunPromptFn } from "./reflector.js"; import { type ParetoObjective, type ParetoTruncationStrategy } from "./pareto.js"; /** One evaluated variant in a generation. */ export interface ScoredVariant { /** Variant identifier (e.g. "v3-gen2-cand1"). */ id: string; /** Mutated prompt text. */ prompt: string; /** Score map keyed by objective name — must include the keys in `objectives`. */ metrics: Record; /** * v0.7.0 — Optional per-frontier-key scores for GEPA Algorithm 2 coverage * sampling (higher is better; caller direction-normalises). A "key" is a * validation-example id, an objective, or an example×objective pair. When * present on every variant AND `NextGenerationOptions.useCoverage` is on, * survivors are chosen by coverage breadth (variants winning DIFFERENT keys) * instead of by aggregate Pareto truncation — preserving the diversity GEPA * relies on. Ignored when absent (falls back to the metrics-based path). */ perKeyScores?: Record; /** Optional text feedback collected for the next generation's reflector. */ textFeedback?: string; } /** Options for {@link GepaOptimizer#generate}. */ export interface GenerateOptions { /** Number of variants per generation. Default 3, clamped to [1, 10]. */ numVariants?: number; /** * Strategy for feeding feedback to the N reflector calls: * - `"split"` (default): split feedback array N ways; each variant * reflects on a different subset → encourages diversity. * - `"replicate"`: every variant sees every feedback → encourages * consistency at the cost of diversity. * - `"single"`: 1 reflection call, deduplicated to a single variant * (use when feedback set is tiny). */ feedbackStrategy?: "split" | "replicate" | "single"; } /** Options for {@link GepaOptimizer#nextGeneration}. */ export interface NextGenerationOptions extends GenerateOptions { /** * Pareto objectives over `ScoredVariant.metrics`. Required — * GepaOptimizer is multi-objective by design; if you only have one * objective, use the plain `PromptOptimizer` instead. */ objectives: ReadonlyArray>>; /** * Max variants to keep on the Pareto front. Default 3. */ maxCarry?: number; /** * V0.5.1 — strategy used when the Pareto front exceeds `maxCarry` * and needs truncation. Default `"scalarised"` (preserves V0.5.0 * behaviour). Switch to `"crowding"` for NSGA-II density-preserving * truncation that maintains diversity along the front. * * NEW V0.5.1 (S1235). */ truncationStrategy?: ParetoTruncationStrategy; /** * v0.7.0 — Opt into GEPA Algorithm 2 instance-level coverage selection. * When `true` AND every scored variant carries a `perKeyScores` map, * survivors are chosen by coverage breadth ({@link selectByCoverage}) — the * variants that win on the most DIFFERENT frontier keys — instead of the * aggregate-metrics Pareto truncation. This preserves the per-subset * diversity GEPA's reflection loop depends on (a candidate strong on a niche * of tasks is not crowded out by the global-average winner). When the * `perKeyScores` data is missing on any variant, this silently falls back to * the metrics-based Pareto path so existing callers are unaffected. * * Default `false` — V0.5.x behaviour unchanged. */ useCoverage?: boolean; } /** * v0.7.0 — Epoch-shuffled minibatch sampler (GEPA `reflection_minibatch_size` * + epoch-shuffled batch sampler, adapted to an online loop). * * Returns a deterministic rotating window of `size` items, offset by `epoch`, * wrapping around the array. Across consecutive epochs the window walks the * whole array, so every item is eventually reflected on — without the * persistent sampler state a true epoch shuffle needs, and without any RNG * (the module stays pure). When `size` is non-positive or ≥ the array length, * the full array is returned (no minibatching). * * @example * epochShuffledMinibatch(['a','b','c','d','e'], 2, 0) // ['a','b'] * epochShuffledMinibatch(['a','b','c','d','e'], 2, 1) // ['c','d'] * epochShuffledMinibatch(['a','b','c','d','e'], 2, 2) // ['e','a'] */ export declare function epochShuffledMinibatch(items: ReadonlyArray, size: number, epoch: number): T[]; /** * V0.5.1 options for {@link GepaOptimizer} constructor. * * R1 Research Finding F7 (S1185) closure — `reflectionRunPrompt` lets * callers route reflection / merge calls to a STRONGER LM than the task * LM, matching GEPA's official `reflection_lm` parameter. When omitted, * reflection and merge fall back to the main `runPrompt`. NEW V0.5.1. */ export interface GepaOptimizerOptions { /** * Stronger LM for reflection / merge calls. Pass a higher-tier * `RunPromptFn` (e.g. Claude Opus) here when your main task LM is a * cheaper one. Per GEPA paper guidance reflection is the leverage * point — a stronger reflector lifts mutation quality more than a * stronger task LM. * * When omitted, reflection + merge use the main `runPrompt`. * * NEW V0.5.1 (S1235). */ reflectionRunPrompt?: RunPromptFn; } /** * V0.5.1 options for {@link GepaOptimizer#merge}. */ export interface MergeOptions { /** * Override the merge prompt template. Default is the GEPA Appendix-F * system-aware merge template. Use a custom template when you want * different merge pressure (e.g. conservative-additive vs. selective). */ mergePromptTemplate?: string; /** * Hard upper bound on the returned merged-prompt length. Default * `Math.max(max(parents[].prompt.length) * 1.3, 3500)` — same growth * ceiling as {@link Reflector}. */ maxMergeLength?: number; } export declare class GepaOptimizer { private readonly reflector; private readonly reflectionRunPrompt; constructor(runPrompt: RunPromptFn, opts?: GepaOptimizerOptions); /** * Generate N variant mutations from the current prompt + feedback set. * Returns the raw mutated prompts — caller scores them (typically via * the existing multi-critic pipeline) before passing them through * {@link GepaOptimizer#nextGeneration}. */ generate(currentPrompt: string, feedbacks: ReadonlyArray, opts?: GenerateOptions): Promise>; /** * Given the scored variants of a generation, return the survivors * carried to the next generation (Pareto-front, capped at maxCarry). * * This is a pure-function wrapper around `paretoSelect` exposed on * the optimizer for ergonomic call-site grouping ("generate, score, * nextGeneration"). The actual selection logic lives in `pareto.ts`. */ nextGeneration(scored: ReadonlyArray, opts: NextGenerationOptions): ScoredVariant[]; /** * V0.5.1 (S1235) — GEPA+Merge (Paper Appendix F). * * Combines the strongest aspects of TWO Pareto-front parents into a * single mutated prompt via a reflection-LM call. The paper reports * ~+5% lift over generation-only optimisation when this is run on * every K-th generation (typically K=3-5). The merged prompt then * competes alongside the regular generation outputs in the next * `nextGeneration` Pareto-select step. * * Typical usage: * ```ts * // Every Kth generation, merge the top two Pareto-front members. * if (gen % 3 === 0 && survivors.length >= 2) { * const merged = await optimizer.merge( * [survivors[0], survivors[1]], * ); * // Score `merged.prompt` via your evaluator, then include the * // scored variant in the next nextGeneration() pool. * } * ``` * * Errors are thrown — `merge` is a deliberate optimisation step, the * caller chooses when to invoke it. No swallowing. */ merge(parents: readonly [ScoredVariant, ScoredVariant], opts?: MergeOptions): Promise<{ id: string; prompt: string; }>; /** Strip markdown fences + leading/trailing whitespace — mirrors `Reflector.cleanOutput`. */ private cleanOutput; /** Sentence-boundary truncation — mirrors `Reflector.truncateAtSentenceBoundary`. */ private truncateAtSentenceBoundary; /** Clamp variant count into the documented bounds. */ private clampN; /** Stable variant id: `gepa-cand-${i}` (caller can re-namespace). */ private makeId; /** V0.5.1 — stable merged-variant id: `gepa-merge-${idA}+${idB}`. */ private makeMergeId; } //# sourceMappingURL=optimizer-gepa.d.ts.map