/** * Darwin — Evolution Loop * * The core evolution cycle. Called after every agent run, it: * 1. Records the experiment * 2. Checks for failure rollback * 3. Manages A/B tests between prompt versions * 4. Triggers prompt optimization when enough data exists * * This is the brain of Darwin's self-evolution. */ import type { AgentDefinition, DarwinExperiment, DarwinPattern, MemoryProvider } from '../types.js'; import type { ExperimentTracker } from './tracker.js'; import type { PromptOptimizer } from './optimizer.js'; import type { SafetyGate } from './safety.js'; import type { PatternDetector } from './patterns.js'; import type { NotificationConfig } from './notifications.js'; import type { GepaOptimizer } from './optimizer-gepa.js'; import { type EmbedFn } from './alignment.js'; import { type MetricsSink } from '../metrics/sink.js'; export interface EvolutionResult { patternsFound: DarwinPattern[]; promptEvolved: boolean; abTestStarted: boolean; abTestCompleted: boolean; rolledBack: boolean; newVersion?: string; message: string; } interface DarwinLoopDeps { memory: MemoryProvider; tracker: ExperimentTracker; optimizer: PromptOptimizer; safety: SafetyGate; patterns: PatternDetector; /** Agent definition — used to pass tool context to optimizer */ agent?: AgentDefinition; /** Notification config (Telegram alerts) — auto-loaded from env if not set */ notifications?: NotificationConfig; /** * v0.6.0 — Optional GEPA-style reflective optimizer. When present AND the * agent has `evolution.useGepa === true`, variant generation routes through * the reflector (rich text feedback → smallest-possible-edit) instead of * the legacy stats-meta-prompt optimizer. Omit it (or leave `useGepa` * false) to keep the legacy single-shot path — behaviour is unchanged. */ gepa?: GepaOptimizer; /** * v0.7.0 — Optional batch embedder. When present, the GEPA mutation path * upgrades its alignment guard from keyword-count to the semantic * (embedding-distance) check: a safety constraint that was REWORDED (not * removed) is accepted instead of triggering a false-positive rejection. * When omitted, the guard stays keyword-only (fail-closed). Darwin keeps * zero hard deps — you inject the embedder. */ embed?: EmbedFn; /** * v0.7.0 — Cosine-similarity threshold for the semantic alignment guard * (only used when `embed` is set). Default 0.82. */ alignmentSimilarityThreshold?: number; /** * v0.10.0 — Random source in [0, 1) for the stochastic parent-selection * strategies (`candidateSelection: 'pareto' | 'epsilon-greedy'`). Injected * for deterministic tests; default `Math.random`. Never consulted on the * default `'active'` path. */ rng?: () => number; /** * v0.14.0 — Optional metrics sink. Every evolution decision (run recorded, * A/B started/completed/timeout, rollback) is emitted as a typed event via * {@link emitMetric}, which swallows sink errors — observability must never * break the loop. Omit for zero overhead; `buildEvolutionLoop` wires the * JSONL sink from `DARWIN_METRICS_JSONL` automatically. */ metrics?: MetricsSink; } export declare class DarwinLoop { private memory; private tracker; private optimizer; private safety; private patterns; private agent?; private notifications; private gepa?; private embed?; private alignmentSimilarityThreshold?; private rng?; private metrics?; constructor(deps: DarwinLoopDeps); /** * Called AFTER every agent run. Drives the evolution cycle. * * Flow: * 0. Detect incomplete runs (skip them) * 1. Record experiment * 2. Rollback check (consecutive failures) * 3. A/B test management (if active) * 4. Validate data quality before evolving * 5. Evolution trigger (if enough data and no active test) */ afterRun(experiment: DarwinExperiment): Promise; /** * Manual / on-demand evolution trigger — the engine behind * `darwin evolve --force`. * * Runs the SAME variant-generation + A/B-start path as the automatic loop's * Step 5, but WITHOUT the "enough runs / actionable patterns / data-quality" * gates. Use it to deliberately kick off an optimisation from the current * best prompt using the experiments collected so far, instead of waiting for * the loop to decide on its own. * * It still refuses the cases where evolution is genuinely impossible or * unsafe: * - no active prompt seeded yet (nothing to mutate from), * - no recorded experiments (the optimizer has nothing to learn from), * - an A/B test already running (can't start a second concurrent test). * * Patterns are still DETECTED (so the change reason and the GEPA/legacy * feedback are meaningful) — they are simply not used as a gate. */ forceEvolve(agentName: string): Promise; /** * Shared tail of the evolution cycle: generate one challenger prompt (GEPA * reflective path when opted in, else the legacy meta-prompt optimizer), * persist it as a new version, and start an A/B test against the incumbent. * Called by both the gated automatic loop ({@link afterRun}) and the * on-demand {@link forceEvolve}. Mutates and returns the passed `result`. */ private generateAndStartABTest; private handleABTest; /** * The wall-clock budget (days) governing this test: the budget snapshotted * at test start when present (v0.13.1), else the agent's CURRENT config — * the fallback keeps two pre-snapshot behaviours working: tests started * before v0.13.1 under a persisted budget, and budgets added AFTER a test * was already running. */ private effectiveTestBudget; /** * Has this A/B test outlived its wall-clock budget? * * No effective budget → never expires (the default). An unparsable * `startedAt` also never expires: a clock we cannot read is no reason to * abandon a test that may be collecting good data. */ private isTestExpired; /** * Close a timed-out A/B test in favour of the incumbent. * * Deliberately NOT a `rollback()`: nothing failed, and the incumbent is * already the active version — re-activating it keeps the persisted state * self-consistent for callers that read `activeVersions` without re-deriving * it. `lastKnownGood` is left untouched, since a timeout produced no new * evidence about which version is good. */ private concludeInconclusive; /** * Roll back after consecutive failures. Returns true if a rollback was * performed. * * Two-stage target resolution (v0.14.0): * 1. `lastKnownGood` when it differs from the active version — divergent * state WITHOUT an open test (manual state surgery, legacy blobs); * with a test open, no rollback of any kind runs (guard below). * 2. Otherwise the ACTIVE version's parent in the version lineage. This * closes a real gap (cross-model review): `handleABTest` promotes a * winner by setting `activeVersions` AND `lastKnownGood` to the same * label, so from that moment `current === lastGood` and stage 1 can * never fire — the advertised failure rollback was dead exactly when * a freshly-promoted prompt started degrading in real traffic (model * update, tool drift). One step up the lineage per rollback, and * `lastKnownGood` moves along, so repeated failure bursts can walk * further back — v1 (no parent) is the floor. */ private rollback; /** * Activate a specific prompt version and deactivate all others. */ private activateVersion; /** * v0.7.0 — Configurable feedback window (default 15, was a hard-coded 5). * A larger window gives both the legacy optimizer and the GEPA reflector * more of the recent behaviour to learn from. Clamped to ≥ 1. */ private feedbackWindow; /** * v0.7.0 — Parse the integer out of a "vN" version string for use as the * epoch-shuffled-minibatch epoch. "v1"→1, "v12"→12; non-parsable→0. */ private versionInt; /** * Extract recent critic feedback reports from experiments. * * Returns up to `limit` feedback report texts from the most recent experiments * that have critic feedback. Experiments are already ordered by started_at DESC * from loadExperiments(), so we just filter for ones with feedback. */ private getRecentFeedback; /** * v0.11.0 — Resolved perfect-score threshold for {@link * EvolutionConfig.skipPerfectFeedback} (finite, within the critic 1–10 * scale, else default 10). */ private perfectFeedbackScore; /** * v0.6.0 — Generate the next prompt variant via the GEPA reflective path. * * Builds rich {@link ReflectiveFeedback} from recent critic reports + * execution trajectories and asks the {@link GepaOptimizer} for ONE * smallest-possible-edit mutation (the online loop carries a single * challenger into the A/B test; the N-variant + Pareto + merge surfaces * are for offline/batch optimisation). The mutation is then run through * the SHARED alignment guard — the same check the legacy optimizer uses — * so the GEPA path cannot ship a prompt that erodes safety keywords. * * Returns `null` (→ caller falls back to the legacy optimizer) when: * - no GepaOptimizer is wired in, * - there is no critic feedback yet (cold start — the reflector has * nothing to reflect on), * - the reflector throws or returns an empty mutation, or * - the mutation fails the alignment guard. */ private generateVariantGepa; /** * v0.7.0 — Shared alignment guard for every GEPA-path mutation (reflective * OR merge). Returns the candidate unchanged when it preserves the safety * keywords, or `null` when it erodes one (→ caller rejects / falls back). * * Uses the semantic (embedding-distance) guard when an embedder is injected * — a REWORDED safety constraint is accepted — and the strict keyword guard * otherwise. Fail-closed: no embedder ⇒ keyword-only. */ private runAlignmentGuard; /** * v0.7.0 — Merge cadence (every K-th cycle). Default 3, clamped ≥ 1. */ private mergeEveryK; /** * v0.11.0 — Resolved lifetime merge cap (GEPA `max_merge_invocations`), or * `null` when uncapped. A non-finite / negative value is treated as "no cap" * rather than silently disabling merge; a non-integer cap is floored (mirrors * `mergeEveryK`) so a hand-edited `2.5` behaves as 2, not 3. */ private mergeCap; /** * v0.11.0 — Lifetime merge budget check (GEPA `max_merge_invocations`). * Returns `true` (merge may fire) when no cap is configured, or when the * per-agent count of merge-derived challengers is still below the cap. */ private mergeBudgetAvailable; /** * v0.11.0 — Increment this agent's lifetime merge-invocation count. Called * once per merge challenger that passes the alignment guard and is carried * into an A/B test, and ONLY when a cap is configured — an uncapped useMerge * agent never writes the counter, so its persisted state is unchanged from * v0.10. Initialises the map lazily for state rows that predate the field. * * The check (`mergeBudgetAvailable`) and this increment are separate state * reads, so two cycles running concurrently for the SAME agent could each * observe `used = cap-1` and overshoot by one. The overshoot is bounded by * the concurrency and sits inside the pre-existing concurrent-A/B-start * envelope (afterRun's "an A/B test is already running" guard is likewise * check-then-act) — acceptable for a soft lifetime budget, not a hard limit. */ private recordMergeInvocation; /** * v0.10.0 — Demo-injection cadence (every K-th cycle). Default 4, clamped * ≥ 1. Deliberately offset from the merge default (3) so the two * non-reflective challenger sources don't collide on the same cycles. */ private demoEveryK; /** * v0.10.0 — Build a SIMBA-style demo challenger: current prompt + a * marker-delimited "Demonstrations" section harvested from this agent's * highest-scoring past runs. Pure selection + rendering — no LLM call. * * Returns `null` (→ caller falls through to GEPA/legacy generation) when * no run qualifies (score/threshold/length filters) or when the rendered * demo set is byte-identical to what the prompt already carries — an A/B * test of a version against itself would be pointless. */ private tryDemoVariant; /** * v0.10.0 — Scored version history for parent selection AND merge: every * prompt version that has text and at least one run's worth of averaged * metrics, as {@link ScoredVariant}s. One getAverageMetrics * (→ loadExperiments) call per version. O(N) backend round-trips, but N is * the agent's prompt-version count (≤ ~20 in practice) and this only runs * on selection/merge cadences, so it is cheap — same pattern handleABTest * already uses for the Pareto gate. (Extracted from tryMergeVariant, * behaviour unchanged.) */ private buildScoredHistory; /** * v0.7.0 — GEPA system-aware MERGE (paper Appendix-D). Builds scored * variants from this agent's prompt-version history (each version's prompt * text + its averaged objective vector), takes the two best Pareto-front * members, and asks the {@link GepaOptimizer} to combine their complementary * strengths into one challenger prompt. * * Returns the merged prompt, or `null` (→ caller falls back to reflective) * when: no optimizer is wired, fewer than two versions carry metric data, * the Pareto front has fewer than two members, or the merge call throws. * The returned prompt has NOT yet passed the alignment guard — the caller * runs {@link runAlignmentGuard} on it. */ private tryMergeVariant; /** * v0.7.0 — GEPA Algorithm 2 instance-wise coverage selection. * * Builds a {@link ScoredVariant} per RUN prompt-version (its averaged * objective vector + its per-task-type composite map from {@link * ExperimentTracker.getPerKeyScoresByCategory}), then asks the * {@link GepaOptimizer#nextGeneration} coverage path to pick the survivor * that wins on the MOST DIFFERENT task types. Returns that survivor's prompt * text — the version to reflect the next challenger from. * * Returns `null` (→ caller reflects from the active prompt as before) when no * optimizer is wired or fewer than two versions carry per-task-type data — * coverage selection is meaningless with a single covered version. This is * the ONLINE adaptation: it selects the reflection PARENT among already-run * versions by their real per-task-type scores. (Per-challenger coverage of * unrun candidates needs the offline scored-pool path and is out of scope.) */ private selectCoverageParent; /** * v0.6.0 — Build GEPA {@link ReflectiveFeedback} from the most recent * experiments that carry critic feedback. Each becomes a (variantId, * score, textFeedback, trace) tuple the reflector uses to synthesise the * mutation. `loadExperiments` returns newest-first, so we take the first * `limit` that have a feedback report. */ private getReflectiveFeedback; /** * Increment version string: "v1" -> "v2", "v12" -> "v13". */ private nextVersion; /** * Pick a label for a new challenger that collides with NOTHING already in * the agent's version history. * * `nextVersion(active)` on its own is unsafe. When a challenger LOSES its * A/B test the incumbent stays active, so the next evolution cycle derives * the very same label again ("v1" active -> "v2", twice). * `savePromptVersion` upserts on (agentName, version), so the second * challenger overwrites the first one's row: its prompt text is gone, and * `createdAt`/`parentVersion` are left describing a prompt that no longer * exists. Every reader of the archive — merge-parent selection, Pareto * candidate selection, `darwin status` — then works off a fabricated * history of two versions instead of the N challengers actually tried. * * Numbering therefore continues above the HIGHEST version in history rather * than above the active one. When the active version already IS the highest * (the healthy case, and the only case the pre-existing suite constructs) * this returns exactly what `nextVersion(active)` returned. */ private nextFreeVersion; /** * `nextVersion`, hardened to ALWAYS return a different label than its * input. `nextVersion` alone self-maps once `parseInt` saturates at 2^53 * (`nextVersion("v9007199254740992") === "v9007199254740992"`), which would * pin the probe walk on one taken candidate forever and hand the upsert a * colliding label — found by the round-2 cross-model review. On a * non-progressing step we switch to the append strategy, which strictly * grows and restores the collision-freedom proof without a carve-out. */ private progressStep; /** * Check if a run is incomplete (agent ran out of turns or produced no real output). * Incomplete runs are NOT recorded as experiments to avoid poisoning the data. * * Checks output length regardless of success flag — a 300-char "successful" run * is still garbage data that shouldn't influence evolution. */ private isIncompleteRun; /** * Validate that experiment data is clean enough for evolution. * Prevents garbage-in-garbage-out (e.g., broken search backend producing 0 sources). */ private validateDataQuality; /** * Build a human-readable change reason from detected patterns. */ private buildChangeReason; } export {}; //# sourceMappingURL=loop.d.ts.map