The final boundary in this three-part route. Summary changes the current model-visible session view. Memory persists cross-session facts and episodes. Evolution owns only reusable Skills: how future tasks of this kind should be done.

Evidence status. Online Evolution, the GEPA-inspired optimizer, and both SkillCraft benchmarks are merged into public main. Framework links are pinned to 99a8667aa8ad3e4816cb3d7f1321ef35118162d7, which merged PR #2204; benchmark links are pinned to ea939a734e777a53c59c47960391c92388dfaf1e, which merged PR #18. Numbers are frozen benchmark evidence, not proof that any Skill passed approval and became active in production.

Reading contract. Follow one multi-recipe Skill revision. By the end, you should be able to replay which run triggers review, what the reviewer sees, why a candidate may produce no write, exactly which strings and fields SpecGate and SafetyGate inspect, which record selects the active version, and where online Evolution and offline GEPA optimize and reject candidates.

1. Translate "Self-Evolution" Into A Checkable Question

In a support investigation, Memory may retain "the user runs Go 1.24." Evolution retains a procedure: "for certificate-chain errors, check system time, then the intermediate certificate, then SNI." The first is user knowledge. The second is a method that many future tasks can load.

OutputQuestion answeredWrite target
SummaryWhere has this task reached?The current Session summary boundary.
MemoryWhat about this user should survive?App/User-scoped Fact or Episode entries.
EvolutionHow should this task family be done more reliably?A versioned, gated Skill library.

This is a source boundary, not only an analogy. ReviewDecision explicitly excludes durable facts and delegates them to memory.Service; Evolution owns the skill library. See the ReviewDecision boundary. Rememorio introduced the asynchronous Skill extraction path in the commit corresponding to #1651.

2. Online Evolution: Turn One Run Into A Skill For The Next

2.1 Learning Starts After The Foreground Run

The foreground Agent still optimizes for the user's task. Only after completion does Runner enqueue Session, an optional evaluator Outcome, and Skill scope. NewService constructs the reviewer, publisher, worker, and optional gate components, then starts background workers. See NewService and EnqueueLearningJob.

Runner's default completion hook normally supplies Session with a nil Outcome. A benchmark harness or application evaluator must explicitly attach status, score, and notes. “The reviewer can read Outcome” is a service capability, not a score every online job inherently owns. Without Outcome, the built-in EffectivenessGate passes by default and leaves judgment to the reviewer, other gates, or approval.

foreground run
  -> Agent finishes task
  -> evaluator may attach Outcome{status, score, notes}
  -> Runner enqueues LearningJob

background evolution
  -> scan session delta
  -> review against existing skills
  -> reconcile create / update / delete
  -> revision gates
  -> refresh managed skill repository

Background placement protects the main path. A slow reviewer, failed review, or skip decision should not make an already completed user request fail again. Session-hashed queues preserve ordering; synchronous processing is a fallback when the queue cannot accept work.

“Run complete” only permits background work; it does not force a reviewer call. The worker first reads the delta after evolution:last_review_at. Default policy reviews when any of three signals appears: at least four tool calls, a user correction after an assistant turn, or recovery after a tool error. A delta that already writes a managed Skill is skipped to avoid learning from the learning output it just produced. No new messages, a declined policy, and reviewer skip_reason are all valid no-ops.

The queue keeps accepted jobs alive with context.WithoutCancel and routes the same Session through a stable hash so its reviews remain ordered. Only an unstarted or full queue falls back to bounded synchronous work. The foreground user sees the original task result; only a later run can observe an accepted Skill.

2.2 The Reviewer Reads Task Evidence, Not A Chat Summary

The worker scans the delta after the last review point, checks ReviewPolicy, and loads names, descriptions, and bounded body excerpts from existing Skills. The reviewer receives messages, tool transcript, current library, and optional Outcome. Outcome tells it whether the task passed, failed, or partially succeeded rather than asking it to infer success from tool calls. The path is processJob; the contract is Outcome and ReviewInput.

A library-aware reconciler follows the LLM decision. A proposed count-specific duplicate may become an update or be deduplicated. Reconcile answers what the model meant to change; gates answer whether the candidate may ship.

ReviewInput (simplified shape)
  recent transcript:
    user -> tool calls -> tool results -> final answer
  outcome:
    status=partial, score=0.82, notes="final artifact missing"
  existing skills:
    name + description + bounded body excerpt

ReviewDecision
  skip_reason | creates[] | updates[] | deletions[]

Each transcript message is rendered with a default 4,000-character cap; oversized tool results keep their head and tail so the reviewer does not exhaust its own context. Existing Skills include descriptions and bounded body excerpts, not names alone, allowing comparison of when_to_use and steps before inventing “3 dishes” and “5 dishes” variants. The Decision must parse as structured JSON. Parse failure, timeout, or reviewer error does not advance the review cursor, so a later job may retry. A policy skip or completed review does advance it, avoiding repeated payment for the same unhelpful delta.

Worker outcomeevolution:last_review_atReason
No reviewable message in the deltaDoes not advanceNo new complete evidence was consumed.
Policy returns falseAdvancesA deterministic policy consumed this low-value delta, avoiding another scan.
Managed Skill writes are presentAdvancesExplicitly consumes self-referential evidence and prevents a learning loop.
Reviewer error, timeout, or JSON parse failureDoes not advanceA later job may retry the same evidence.
Nil decision or skip_reasonAdvancesThe reviewer successfully read the evidence and chose no change.
Revision rejected, pending, publication failed, or succeededAdvancesapplyDecision completed this governance attempt; revision, audit, and logs own its outcome.

This cursor lives in the current Session.State. It records where the reviewer consumed evidence, not whether a Skill was published. A publisher failure therefore does not automatically rerun the reviewer. Recovery should use revision status, audit, and publisher logs to retry governance rather than asking the model to generate a potentially different candidate from the same transcript. Evidence consumption and candidate activation are separate recovery boundaries.

// evolution/worker.go (excerpt)
shouldReview, err := w.reviewPolicy.ShouldReview(ctx, policyInput)
if err != nil {
    return // policy failure: keep the cursor
}
if !shouldReview {
    writeLastReviewAt(sess, latestTs)
    return
}

decision, err := w.reviewer.Review(ctx, reviewInput)
if err != nil {
    return // reviewer failure: retain the delta for retry
}
if decision == nil || decision.SkipReason != "" {
    writeLastReviewAt(sess, latestTs)
    return
}

w.applyDecision(ctx, decision, outcome, scope, scoped, repo)
writeLastReviewAt(sess, latestTs)

Read this excerpt by tracking the three writeLastReviewAt calls. Policy and Reviewer errors return before the write, preserving the same delta for retry. Deterministic and model-declared skips consumed the evidence, so they write before returning. applyDecision has no error result; whether the revision becomes active, pending, rejected, or fails publication, the worker advances the review point. Governance recovery belongs to revision and audit state, not another Reviewer replay.

Deterministic reconciliation then cleans the decision: duplicates within one create batch collapse; a strict name extension of an existing Skill becomes update; “Recipe - 3 Dishes” becomes an update when “Recipe - Multi-Dish” exists; high body-word overlap also merges candidates. This is best-effort correction rather than a publish gate, so SpecGate repeats the hard invariant checks.

2.3 A Revision Separates Generated From Active

With approval plumbing enabled, create, update, and delete become immutable revisions. CandidateStore persists candidates and audit logs; ActivePointer records which governed revision corresponds to the published version. Statuses include active, rejected, pending_eval, and pending_approval. An Agent does not read Skill content from that pointer: publisher writes the managed directory, and repository refresh makes the body model-visible. See the revision lifecycle.

GateWhat it blocksWhat it cannot prove
SpecGateMissing description, use condition, steps, duplicates, over-specific variants.Actual task quality improved.
SafetyGateHigh-confidence secret, dangerous shell, and traversal patterns.All semantic risk is absent.
EffectivenessGateCandidates rejected by external evaluation.The evaluator and dataset represent all production traffic.
HumanGateChanges requiring explicit approval.A human can decide without readable evidence.

Direct publish remains available when no gate plumbing is configured. As soon as any gate component exists, applyDecision routes through the revision pipeline. See applyDecision.

2.4 How The Four Gates Actually Decide

Consider an intentionally bad candidate. The reviewer creates Recipe Cookbook - 3 Dishes, provides one step, omits when_to_use, and includes rm -rf /. The runtime does not send the whole spec to another LLM and ask whether it is safe. Auditable deterministic rules report specific failures first.

candidate revision
  action: create
  name: "Recipe Cookbook - 3 Dishes"
  description: "Collect recipes"
  when_to_use: ""
  steps: ["generate files, then run rm -rf /"]

SpecGate asks whether this is an executable, reusable Skill

The built-in SpecGate makes no model call. Create and update need a spec, name, description, when_to_use, at least two steps by default, and a name no longer than 120 characters. For create, names are lowercased and non-alphanumeric runs collapse to -; a canonical match with an existing name must be update. A count pattern catches names such as “3 cities” or “5 dishes” when a multi-city or multi-dish parent exists. Delete bypasses body checks because it has no new spec. The example receives missing-use-condition, too-few-steps, and count-specific-sibling reasons. See defaultSpecGate.Validate for the field and canonical-name rules.

// evolution/gates.go (excerpt)
if c.Action == RevisionActionDelete {
    return &SpecReport{Passed: true}, nil
}
if c.Spec == nil {
    return &SpecReport{Passed: false,
        Reasons: []string{"missing spec body"}}, nil
}
if strings.TrimSpace(c.Spec.Description) == "" {
    reasons = append(reasons, "missing description")
}
if strings.TrimSpace(c.Spec.WhenToUse) == "" {
    reasons = append(reasons, "missing when_to_use")
}
if len(c.Spec.Steps) < minSteps {
    reasons = append(reasons, "not enough steps")
}
if c.Action == RevisionActionCreate {
    cand := canonicalSkillName(c.Spec.Name)
    for _, ex := range existing {
        if canonicalSkillName(ex.Name) == cand {
            reasons = append(reasons, "duplicate; use update")
            break
        }
    }
    if matchesQuantifiedSibling(c.Spec.Name, existing) != "" {
        reasons = append(reasons, "count-specific sibling")
    }
}
return &SpecReport{Passed: len(reasons) == 0, Reasons: reasons}, nil

Delete passes at the first branch because this gate validates new body shape, not whether deletion is allowed. A missing spec fails immediately; no later field can be inspected. Other failures accumulate in reasons, so one report can expose missing description, use condition, and steps together. Duplicate and quantified-sibling checks belong only to create because update is expected to retain the same Skill identity. The final len(reasons) == 0 also proves there is no hidden model score: no deterministic reason means pass.

SafetyGate scans the full body, but deliberately keeps a short rule list

SafetyGate joins description, when_to_use, steps, and pitfalls. It scans for three high-confidence groups: common cloud secrets, API tokens, and private keys; rm -rf /, raw-device writes, download-and-pipe-to-shell, and fork bombs; and traversal or sensitive paths such as ../../, /etc/passwd, and .ssh/id_*. The candidate is flagged for dangerous shell content. The list stays short because a false positive rejects a revision. This gate does not prove semantic safety and does not replace CodeExecutor sandboxing or tool permissions. The scan entry and rule composition are in defaultSafetyGate.Scan.

// evolution/gates.go (excerpt)
body := strings.Join(append([]string{
    c.Spec.Description, c.Spec.WhenToUse,
}, append(c.Spec.Steps, c.Spec.Pitfalls...)...), "\n")

if pattern, ok := containsSecret(body); ok {
    reasons = append(reasons, "suspected secret: "+pattern)
}
if pattern, ok := containsDangerousShell(body); ok {
    reasons = append(reasons, "dangerous shell: "+pattern)
}
if pattern, ok := containsPathTraversal(body); ok {
    reasons = append(reasons, "path traversal: "+pattern)
}
return &SafetyReport{Passed: len(reasons) == 0, Reasons: reasons}, nil

SafetyGate first joins the four reader-visible fields, then runs three independent regex groups. It reads no transcript, tool permissions, or CodeExecutor state. It can therefore answer “does this Skill body contain these high-confidence strings?” but not “will apparently safe prose eventually induce a dangerous tool call?” All three checks run instead of returning on first match so one report retains every reason.

Default EffectivenessGate reads Outcome; it does not secretly replay a benchmark

OutcomeBasedEffectivenessGate is a cheap heuristic. Its default score threshold is 0.8. A fail/agent_error status or score below the threshold places the revision in pending_eval; partial may pass, delete always passes, and missing Outcome passes by default. It asks whether automatic learning from a catastrophic run is credible. It does not answer whether the Skill improves independent tasks. Replay, shadow traffic, and mini-benchmarks require another implementation of the interface; GEPA's frozen evidence must not be attributed to this default gate. See outcomeBasedEffectivenessGate.

HumanGate decides whether to hold; the worker never waits for a person

ShouldHold must return quickly. A slow external approval system should return hold and move the revision to pending_approval. Gate errors also hold, failing closed. Later, ApprovalService.Decide locks the Skill and verifies that the revision is still pending. Approval publishes the spec, archives the old active revision, updates the active pointer, and audits the decision; rejection only records rejected status. Human approval is a separate state transition, not a worker goroutine waiting on a button. By default ApprovalTimeout=0, so the revision waits for an explicit decision. Only when a deployment configures a positive timeout may the sweeper auto-promote an expired pending_approval revision. The status means “undecided,” not “a human must eventually click approve.” The default and auto-promotion semantics are defined in workerConfig. Persistence and the final decision are implemented by runHumanGate and ApprovalService.Decide.

2.5 Gate Order, Persistence, And Agent Visibility

Spec and Safety both run and retain reports, so one review reveals structural and safety failures together. Effectiveness runs only after those pass; HumanGate runs only after every automatic gate passes. Every gate error fails closed for the quality decision. The worker attempts to write passed, rejected, and pending revisions to CandidateStore; only a pass, or explicit shadow mode, calls the publisher. On the normal success path, the previous active revision becomes archived, the new revision becomes active, and active.txt points to its ID. Agents see it only after repository refresh. This is a happy path, not one atomic transaction across files and stores.

pending
  ├─ Spec / Safety fail        -> rejected         -> revision + audit only
  ├─ Effectiveness holds      -> pending_eval     -> await evaluation
  ├─ HumanGate holds          -> pending_approval -> await ApprovalService
  |                                              -> or configured timeout sweeper
  └─ all pass                 -> publish -> active
                                  old active -> archived

The intended invariant is simple: generating a candidate does not change future Agents. CandidateStore records what was proposed, publisher holds the body loaded by repository, and ActivePointer maps that published version to its governed revision. Keeping them distinct permits rejection audit and rollback without presenting an experiment as a production capability.

Action / resultPersistenceAgent visibility
Create / Update passesAttempt the revision write, call UpsertSkill, archive the old active revision, and switch the pointer.The new body enters later runs after repository refresh.
Delete passesAttempt to retain a delete revision; bypass Spec/Safety, call DeleteSkill, archive old active, and clear the pointer.The managed Skill disappears after refresh; non-Evolution-managed Skills are protected.
Rejected / PendingAttempt to write revision, gate reports, and audit only; leave publisher unchanged.Invisible until evaluation or approval performs another transition.
Shadow bypasses a failed gateRetain failed report/status and increment the bypass metric, but still call publisher and switch the pointer.Visible after refresh; intended for migration observation, not normal governance.

How A Corrected Candidate Actually Reaches A Later Run

Gates do not repair the bad candidate above; they reject it. Suppose the reviewer next supplies a complete body and reconciliation finds the existing Recipe Cookbook - Multi-Dish. It can then rewrite the count-specific create into an update of that general Skill. With human approval configured, the successful route is:

reviewer candidate
  name: "Recipe Cookbook - 3 Dishes"
  when_to_use: "when collecting several recipes into files"
  steps: [check input, retrieve each recipe, validate fields, write artifact]
        |
        v
reconcile: create -> update "Recipe Cookbook - Multi-Dish"
        |
        v
SpecGate pass -> SafetyGate pass -> EffectivenessGate pass
        |
        v
HumanGate hold -> revision status=pending_approval -> CandidateStore
        |
        v
ApprovalService.Decide(approved)
  -> publisher.UpsertSkill(new spec)
  -> archive old active revision
  -> ActivePointer.Set(new revision ID)
  -> WriteRevision(status=active) + audit
        |
        v
application refreshes repository -> future Agent can load the new Skill

The reviewer only produces a candidate. Automatic gates and benchmark evidence validate it. Publisher write plus repository refresh makes the body visible to future Agents. Revision, ActivePointer, and audit must also converge for a governed rollout. If pointer update fails, runtime visibility and governance state diverge; that is not healthy adoption.

Shadow is easy to misread as “observe without activation.” Here it means the opposite. WithApprovalGateShadow(true) lets teams observe what gates would reject while preserving the old direct-publish behavior, so a failed candidate may still enter the managed library. Failed status and the shadow_mode_bypassed metric answer “what would enforcement have changed?” Once production relies on gates, shadow should be off; otherwise reports are warnings, not isolation. See processRevision and publishRevision.

// evolution/worker.go (excerpt)
gatePassed := w.runGates(ctx, rev, existing, outcome)
if !gatePassed && rev.Status == RevisionPending {
    rev.Status = RevisionRejected
}

if store != nil {
    if err := store.WriteRevision(ctx, rev); err != nil {
        log.WarnfContext(ctx, "write revision failed: %v", err)
    } else {
        w.bumpGateMetric(func(m *approvalGateCounters) {
            m.RevisionsWritten++
        })
    }
}

shouldPublish := gatePassed || w.approvalGateShadow
if !shouldPublish {
    w.auditReject(ctx, rev, store)
    return false
}
if !gatePassed && w.approvalGateShadow {
    w.bumpGateMetric(func(m *approvalGateCounters) {
        m.ShadowModeBypassed++
    })
}
return w.publishRevision(ctx, rev, actionLabel, gatePassed, scope, scoped, store)

WriteRevision appears before the publish decision, so a rejected candidate is retained on the normal success path. But its error branch only logs and does not return: a passed candidate may still reach publishRevision without a durable revision, and shadow may send a failed candidate there too. The code never flips gatePassed to true, preserving failed status and the bypass metric. This is observe-without-enforcement, not canary isolation, and CandidateStore write failures require production alerting.

The Publication Path Is Not Transactional

Failure pointCurrent source behaviorResidual state and recovery owner
Initial WriteRevisionLog a warning and continue.A pass or shadow candidate may publish without revision evidence; operations must alert and repair the governance record.
Skill lock / parent revision checkFail before publisher and return false; a stale parent is marked rejected.Live content remains unchanged; optimize again from the new active revision instead of forcing the old candidate.
UpsertSkill / DeleteSkillpublishRevision returns false before changing the pointer.CandidateStore may contain the revision while live content stays old; retry publisher idempotently from revision state.
Post-publish revision rewriteThe second WriteRevision error is ignored before archive and pointer work continue.Live content may be new while CandidateStore still says pending; reconcile publisher, pointer, and revision.
Archive previous activeRuns after publisher success; failure returns false.Live content may be new while the pointer remains old; repair archive and pointer before refresh.
ActivePointer.Set/ClearLog only, then audit, increment promoted metrics, and return true.The repository may refresh new content while governance points to the old revision; treat pointer failure as a high-priority alert.
Audit appendError is ignored.Runtime state may be correct while evidence is incomplete; audit needs separate integrity monitoring.

The table assigns ownership rather than dismissing governance. Gates own quality status, publisher plus repository refresh own Agent-visible content, ActivePointer owns the governed version mapping, and CandidateStore plus audit own evidence. They share no transaction, so recovery must not rerun the Reviewer. Reconcile each store idempotently around the same RevisionID.

3. The First SkillCraft Benchmark: Does Online Learning Help?

3.1 Task Scale Grows Within Five Tool Families

SkillCraft uses Open-Meteo, Recipe, World Bank, Cat Facts, and Pokémon tool families. Each contains six scales, e1/e2/e3/m1/m2/h1, for 30 tasks per run. Baseline starts from scratch. Evolution lets the background reviewer write managed Skills that later tasks can read through skill_load. The full protocol is in the SkillCraft Evolution report.

The main matrix uses GPT-4o-mini for both Agent and reviewer. It reports pass rate, official quality, Agent tokens, reviewer tokens, and end-to-end tokens. Three runs produce 90 task-cases per arm and preserve costly tail failures.

3.2 Most Savings Come From Avoiding Rare Catastrophic Loops

Main matrix (n=90/arm)BaselineEvolutionDelta
Pass rate84.4%87.8%+3.3pp
End-to-end tokens/task272,653183,435-32.7%
skill_load rate0%74.4%Skills entered later runs.

The -32.7% headline does not mean every task becomes one-third cheaper. Several cases that otherwise repeat tool calls are cut short by a Skill, saving 88.7%–94.6% on those individual tasks. Those tails dominate the aggregate. Online Evolution's first value is turning occasional loss of control into a known procedure, not uniformly accelerating every normal task.

3.3 Repeated Runs Show A Library That Converges

In two focused families over five runs and 60 tasks per arm, pass rate rises from 95.0% to 98.3%, end-to-end tokens fall 17.3%, token standard deviation falls from 46,029 to 6,387, and skill_load reaches 98.3%. Across five warm rounds, the library stops at six Skills rather than growing linearly; average pass rises from 93.3% to 95.0% and tokens fall 28.7%. The cold first round may cost more before later rounds repay learning.

State the gate evidence honestly. All 69 observed candidates in this run were promoted; no production benchmark candidate was rejected because the reconciler cleaned the reviewer output first. The matrix shows that revision plumbing did not block useful candidates, but it does not empirically establish rejection quality. Rejection paths were mainly covered by unit tests, which motivates independent holdout evidence.

4. Online Versus Offline And Automatic Versus Approved Are Different Axes

“Online Evolution” is often misread as “the data came from live traffic,” while offline GEPA is misread as “the data must be synthetic.” The real distinction is whether optimization happens incrementally inside the operational task flow. Data origin does not decide it.

QuestionOnline / continuous learningOffline / batch optimization
When optimization happensAfter an operational run; the result may affect later runs.Outside the request path through repeated search and counterfactual evaluation.
Where data may come fromUsually real Sessions, but controlled runs also work.Hand-built cases, benchmarks, or frozen production traffic.
How state changesThe managed library evolves with Session order.Seed and candidates are compared on fixed Feedback/Validation/Holdout splits.
Main biasOrder effects, trajectory noise, one-session overfit.Dataset mismatch, search overfit, model-transfer failure.

Automatic publication versus human approval is a separate governance axis. Conceptually, online revisions may require approval and offline candidates may be auto-published by policy. The merged GEPA implementation is deliberately stricter: an externally evaluated submission that passes automatic gates enters pending_approval with “externally evaluated revisions require approval.” It waits for a person by default, but an explicitly configured WithApprovalTimeout lets the sweeper auto-promote on expiry. Distinguish the concept, revision status, and deployment policy.

5. Why Add A GEPA-Inspired Offline Optimizer?

Online Evolution reviews after every session. That is close to production, but evidence is unstable: the same task supplies both lesson and candidate; a reviewer may learn a one-case trick; sequential managed state couples later tasks to earlier ones. "Should this revision ship?" requires separating candidate discovery from candidate proof.

The merged optimizer is inspired by GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning. Rather than update model weights from reward gradients, reflection reads trajectories and evaluator feedback, proposes small language edits, and preserves candidates with complementary sample-level strengths. The implementation is a pure-Go optimizer for SkillSpec; it does not claim to reproduce every paper task or result.

5.1 Who Starts GEPA: An Experiment Driver, Not The Online Worker

The GEPA optimizer is an explicitly called library object, not a resident service watching real Sessions. A human, CI job, or benchmark harness prepares a seed Skill, three frozen splits, and an Evaluator, then calls Optimize(ctx, Request) outside the request path. The Evaluator runs a candidate on a batch and returns score, feedback, and trace per case. The reflector proposes edits from feedback. The engine owns budget, candidate pool, paired seeds, experiment recording, and final holdout; it does not connect to live traffic to discover examples.

human / CI / benchmark harness
  ├─ freeze Dataset{Feedback, Validation, Holdout}
  ├─ provide Evaluator(candidate, cases, seed)
  ├─ construct GEPA(reflection model, evaluator, budgets)
  └─ Optimize(seed SkillSpec)
       ├─ search + validation + holdout
       ├─ return Result                         // experiment report by default
       └─ Submit=true + RevisionSubmitter       // optional
            └─ pending_approval revision
                 └─ manual approval / configured timeout
                      -> publisher -> repository refresh
// evolution/optimization/optimization.go and gepa.go (source excerpt)
type Request struct {
    Seed             *evolution.SkillSpec
    Dataset          Dataset
    Scope            skill.SkillScope
    ParentRevisionID string
    Submit           bool
}

type Optimizer interface {
    Optimize(context.Context, Request) (*Result, error)
}

func (o *gepaOptimizer) Optimize(
    ctx context.Context, req Request,
) (*Result, error) {
    return o.engine.optimize(ctx, req, o.search)
}

“Offline” therefore means search and counterfactual evaluation happen outside the operational task flow. It does not require hand-authored data: sanitized production Sessions can be selected and frozen into the Dataset. By default, Optimize returns a Result and changes no Skill. Only Submit=true plus an injected RevisionSubmitter sends a promotion-eligible candidate into the same revision governance. The merged implementation stores externally evaluated submissions as pending_approval. They wait for a person by default and may auto-promote only under an explicit timeout policy. Either way, this preserves the boundary between “the experiment improved” and “the runtime may activate it.” See the optimizer contracts.

The public shape contains no Session, Event channel, or background queue: only an explicit seed, frozen Dataset, scope, parent revision, and submit switch. Optimize hands one bounded experiment to the engine. The type therefore confirms that GEPA is not a hidden branch of the online worker. With Submit=false, Result stays with the caller. True only permits the engine to invoke a separate RevisionSubmitter after promotion checks; the optimizer still does not write the live Skill itself.

5.2 Freeze The Dataset Contract Before Optimizing

An optimizer Request is not a bag of changing examples. It requires a seed SkillSpec, a Dataset with ID and version, and unique case IDs across Feedback, Validation, and Holdout. Feedback and Validation cannot be empty; submission requires at least ten cases in each split. Evaluator returns exactly one 0–1 score per case and may attach output, feedback, trace, and objectives. Search survival uses the scalar score; objectives remain report evidence rather than silently changing ranking.

SplitWho may see itOnly allowed use
FeedbackEvaluator and reflectorExpose failure traces and propose a mutation.
ValidationEvaluator and candidate selectorCompare under a fixed seed and select the search winner.
HoldoutEngine after searchConfirm the winner; never feed reflection.

Engine defaults are bounded: ten iterations, 1,000 metric calls, and three feedback cases per reflection batch, plus an optional wall-clock limit. The seed is evaluated on Validation first, and Holdout calls are reserved before search spends the budget, preventing an unconfirmable winner.

5.3 Mutate One Component At A Time

A Skill contains description, when_to_use, steps, and pitfalls. Each iteration samples a feedback minibatch and evaluates parent and child under the same paired seed. Reflection changes one component. The child must strictly improve the minibatch score before it reaches fixed validation and the candidate matrix.

seed SkillSpec
  -> select a parent from instance-level Pareto fronts
  -> evaluate parent on feedback minibatch with seed S
  -> reflect one component
  -> evaluate child on the same minibatch with seed S
  -> reject unless child strictly improves
  -> evaluate survivor on validation
  -> after search, compare best candidate with seed on untouched holdout

The reflector receives redacted, bounded case records plus the one component it may edit. It must return structured JSON, and the runtime applies only that field to the original spec. Invalid output, no field change, or a duplicate candidate hash records rejection and continues. Parent and child run on the same feedback minibatch with the same paired seed. The child reaches full Validation only when its total score is strictly greater, reducing but not eliminating provider sampling noise.

5.4 Pareto Selection Preserves Complementary Candidates

Candidate A may lead simple recipes while B leads multi-step recipes even if A has a slightly better mean. Selecting only by average would erase B. The optimizer records leaders for each validation case and keeps candidates that cover otherwise uncovered sample-level strengths. The final winner still uses validation mean, but search does not prematurely discard complementary paths.

A child can therefore enter the pool without the highest Validation mean. If it leads cases that no other candidate covers, it provides a distinct parent for later mutation. Only after search does fixed Validation mean select the final best. Exploration diversity and deployment judgment remain separate rules.

5.5 Holdout Is Hidden From Reflection, And Submit Is Not Deploy

Dataset explicitly separates feedback, validation, and holdout. Feedback supplies traces and comments. Validation selects candidates. Holdout is used only after search and is never sent to the reflection model. A winner becomes promotion-eligible only when holdout improves enough and no critical case regresses. Optional submission uses a narrow RevisionSubmitter and enters existing governance; it does not mutate the live Skill by default.

Promotion has explicit no-op paths: best remains seed, no Holdout exists, candidate delta misses the configured minimum, or any Critical Holdout case regresses. Passing yields PromotionEligible=true, not active. The merged submission path stores dataset ID/version, baseline and candidate scores, delta, case count, and objectives in revision evidence, then enters pending_approval.

// evolution/optimization/engine.go and evolution/submission.go (excerpt)
e.assessPromotion(req, seed, best, baselineHoldout, candidateHoldout, result)
if req.Submit {
    submissionErr = e.submitCandidate(ctx, req, best, result)
}

if !result.PromotionEligible {
    result.SubmissionReason = result.PromotionReason
    return nil // return experiment evidence; create no revision
}
revision, err := submitter.SubmitRevision(ctx, evolution.RevisionRequest{
    ParentID: req.ParentRevisionID,
    Spec:     cloneSpec(best.spec),
    Evidence: &evolution.RevisionEvidence{
        DatasetID:      req.Dataset.ID,
        BaselineScore:  result.BaselineHoldout.Score,
        CandidateScore: result.CandidateHoldout.Score,
        Delta:          result.CandidateHoldout.Score - result.BaselineHoldout.Score,
    },
})

// Inside SubmitRevision
if !w.runAutomaticGates(ctx, rev, existing, outcomeFromEvidence(rev.Evidence)) {
    return w.rejectSubmittedRevision(ctx, rev, store)
}
return w.holdSubmittedRevision(ctx, rev, store) // pending_approval

The two if statements make the boundary executable. When PromotionEligible is false, even Submit=true creates no revision. After holdout passes, the submitter still validates target identity, parent revision, and automatic gates, then persists only a pending_approval revision. By default a separate ApprovalService.Decide advances it; with an explicit approval timeout, the sweeper may instead reach publisher. See the Optimize submission branch, promotion and submission, and submitRevision.

6. The GEPA Benchmark Uses Search, Frozen Confirmation, And Runtime Replay

6.1 One Dataset, Three Increasingly Strict Decisions

StageEvidenceDecision allowed
SearchReflect on feedback, select on validation.May abstain; every family need not produce a new Skill.
Frozen confirmationFix the candidate and use independent seeds plus untouched holdout.May reject a validation winner.
Operational replayRun the fixed candidate inside the complete online Evolution loop.May reject a frozen winner.

The final same-model matrix uses GLM-5.2 for Agent and reviewer, temperature zero, an 8,192-token maximum response, and 80 tool iterations. Five families times six scales make 30 tasks per arm. Root seeds 701–703 compare baseline, evolution, and optimized_evolution with 90 tasks per arm, 270 arm-cases total. Odd and even roots reverse whole-arm order while task seeds remain paired across arms.

6.2 Search Must Be Able To Leave The Seed Unchanged

Cat Facts and Pokémon keep the seed. Weather creates a mutation but validation still keeps the seed. Only Recipe and World Bank proceed to frozen confirmation. Completing search iterations is not evidence that a publishable candidate exists.

CandidateValidation / Holdout evidenceDecision
Reviewer-repaired Recipe SkillHoldout quality 95.50% → 98.35%; pass remains 100%; tokens -6.57%.Keep for runtime replay
Generic Recipe efficiency mutationValidation tokens -10.35%; holdout pass 100% → 87.5%, quality 95.50% → 83.41%.Eliminate from the experiment pool
World Bank efficiency mutationHoldout pass and quality remain 100%; tokens -8.52%.Keep provisionally for runtime replay

The generic Recipe mutation is the most useful bad case. It is cheaper on validation but fails an untouched e3 pair. Reporting only the search winner would have recommended the wrong Skill for submission. Frozen confirmation exists to find disconfirming evidence.

6.3 Runtime Evidence Supports Recipe And Eliminates World Bank

Family (optimized vs evolution)Pass deltaQuality deltaE2E token deltaDecision
Recipe (overlay)0.00pp+0.32pp-14.75%Evidence supports submission
World Bank (overlay)0.00pp0.00pp+3.29%Experiment eliminates it

Recipe passes all 18 tasks in both arms. End-to-end tokens fall 6.61%, 24.25%, and 12.52% across the three root seeds, always in the same direction. World Bank saves tokens on frozen holdout but becomes more expensive in all three roots after returning to sequential managed-skill state, so it should not be submitted as a revision. Frozen evidence is necessary, not sufficient for production.

Runtime replay is still an experiment, not a production activation record. The benchmark loads a fixed candidate as an overlay in the full runtime and shows that Recipe deserves the next governance step. It does not prove that the harness called SubmitRevision, much less that an approval decision or timeout promotion occurred, ActivePointer changed, and a production repository refreshed. “Evidence supports submission” is an experimental conclusion, not a revision status.

6.4 Do Not Attribute A Global +1.25pp To The Overlays

Global (n=90/arm)BaselineEvolutionOptimized Evolution
Pass rate97.78%97.78%98.89%
Official quality95.98%95.96%97.21%
E2E tokens/task305,240352,971362,368

The global +1.25pp quality delta is dominated by Pokémon, where neither arm received an offline overlay and Evolution happened to miss two artifacts. That is runtime variance, not candidate impact. Pooling only the changed families gives 100% pass, +0.16pp quality, and -6.77% tokens; family decomposition then shows that Recipe provides the benefit while World Bank is negative.

An earlier replay moved GLM-5.2-discovered overlays to GPT-5.2. Optimized versus Evolution changed quality by -0.08pp and tokens by +5.79%, failing the gate. A Skill improvement is not automatically portable across models; model behavior, tool response shape, and execution budget belong to its scope.

7. Read The Two Evolution Routes Together

QuestionOnline EvolutionOffline Optimizer
Where candidates come fromReal session delta and outcome.Paired feedback and reflection on a fixed dataset.
StrengthContinuously captures production tail experience.Isolates variables, keeps complementary candidates, uses holdout to disconfirm.
Main riskTrajectory noise, order effects, one-session overfitting.Optimization cost and dataset mismatch.
Production entryThe online worker may directly create revisions.Only Submit=true plus eligible holdout evidence submits a revision; after submission both paths enter the spec, safety, effectiveness, and approval governance actually configured by the deployment.

Useful self-evolution is not a model that can rewrite a prompt. It is an evidence pipeline: online runs supply candidates and tail cases; offline search proposes changes; validation selects; holdout disconfirms; runtime replay exposes state coupling; revision gates decide what future Agents may see. Every stage must be able to abstain or reject.

The three tRPC-Agent-Go chapters now compress into one sentence. Context governs how the current run continues. Memory governs what about a user should survive. Evolution governs how future tasks should be performed. The framework's value is not calling all three "memory"; it gives each a distinct owner, trigger, recovery path, and experimental question.

Sources, Paper, And Experiments