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.
| Output | Question answered | Write target |
|---|---|---|
| Summary | Where has this task reached? | The current Session summary boundary. |
| Memory | What about this user should survive? | App/User-scoped Fact or Episode entries. |
| Evolution | How 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 outcome | evolution:last_review_at | Reason |
|---|---|---|
| No reviewable message in the delta | Does not advance | No new complete evidence was consumed. |
| Policy returns false | Advances | A deterministic policy consumed this low-value delta, avoiding another scan. |
| Managed Skill writes are present | Advances | Explicitly consumes self-referential evidence and prevents a learning loop. |
| Reviewer error, timeout, or JSON parse failure | Does not advance | A later job may retry the same evidence. |
Nil decision or skip_reason | Advances | The reviewer successfully read the evidence and chose no change. |
| Revision rejected, pending, publication failed, or succeeded | Advances | applyDecision 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.
| Gate | What it blocks | What it cannot prove |
|---|---|---|
| SpecGate | Missing description, use condition, steps, duplicates, over-specific variants. | Actual task quality improved. |
| SafetyGate | High-confidence secret, dangerous shell, and traversal patterns. | All semantic risk is absent. |
| EffectivenessGate | Candidates rejected by external evaluation. | The evaluator and dataset represent all production traffic. |
| HumanGate | Changes 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 / result | Persistence | Agent visibility |
|---|---|---|
| Create / Update passes | Attempt the revision write, call UpsertSkill, archive the old active revision, and switch the pointer. | The new body enters later runs after repository refresh. |
| Delete passes | Attempt 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 / Pending | Attempt to write revision, gate reports, and audit only; leave publisher unchanged. | Invisible until evaluation or approval performs another transition. |
| Shadow bypasses a failed gate | Retain 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 point | Current source behavior | Residual state and recovery owner |
|---|---|---|
Initial WriteRevision | Log 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 check | Fail 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 / DeleteSkill | publishRevision 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 rewrite | The 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 active | Runs after publisher success; failure returns false. | Live content may be new while the pointer remains old; repair archive and pointer before refresh. |
ActivePointer.Set/Clear | Log 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 append | Error 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) | Baseline | Evolution | Delta |
|---|---|---|---|
| Pass rate | 84.4% | 87.8% | +3.3pp |
| End-to-end tokens/task | 272,653 | 183,435 | -32.7% |
skill_load rate | 0% | 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.
| Question | Online / continuous learning | Offline / batch optimization |
|---|---|---|
| When optimization happens | After an operational run; the result may affect later runs. | Outside the request path through repeated search and counterfactual evaluation. |
| Where data may come from | Usually real Sessions, but controlled runs also work. | Hand-built cases, benchmarks, or frozen production traffic. |
| How state changes | The managed library evolves with Session order. | Seed and candidates are compared on fixed Feedback/Validation/Holdout splits. |
| Main bias | Order 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.
| Split | Who may see it | Only allowed use |
|---|---|---|
| Feedback | Evaluator and reflector | Expose failure traces and propose a mutation. |
| Validation | Evaluator and candidate selector | Compare under a fixed seed and select the search winner. |
| Holdout | Engine after search | Confirm 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
| Stage | Evidence | Decision allowed |
|---|---|---|
| Search | Reflect on feedback, select on validation. | May abstain; every family need not produce a new Skill. |
| Frozen confirmation | Fix the candidate and use independent seeds plus untouched holdout. | May reject a validation winner. |
| Operational replay | Run 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.
| Candidate | Validation / Holdout evidence | Decision |
|---|---|---|
| Reviewer-repaired Recipe Skill | Holdout quality 95.50% → 98.35%; pass remains 100%; tokens -6.57%. | Keep for runtime replay |
| Generic Recipe efficiency mutation | Validation tokens -10.35%; holdout pass 100% → 87.5%, quality 95.50% → 83.41%. | Eliminate from the experiment pool |
| World Bank efficiency mutation | Holdout 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 delta | Quality delta | E2E token delta | Decision |
|---|---|---|---|---|
| Recipe (overlay) | 0.00pp | +0.32pp | -14.75% | Evidence supports submission |
| World Bank (overlay) | 0.00pp | 0.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) | Baseline | Evolution | Optimized Evolution |
|---|---|---|---|
| Pass rate | 97.78% | 97.78% | 98.89% |
| Official quality | 95.98% | 95.96% | 97.21% |
| E2E tokens/task | 305,240 | 352,971 | 362,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
| Question | Online Evolution | Offline Optimizer |
|---|---|---|
| Where candidates come from | Real session delta and outcome. | Paired feedback and reflection on a fixed dataset. |
| Strength | Continuously captures production tail experience. | Isolates variables, keeps complementary candidates, uses holdout to disconfirm. |
| Main risk | Trajectory noise, order effects, one-session overfitting. | Optimization cost and dataset mismatch. |
| Production entry | The 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
- tRPC-Agent-Go public source snapshot
- Evolution service
- Asynchronous review worker
- Revision store and lifecycle
- Spec, Safety, Effectiveness, and Human gates
- Merged SkillCraft online Evolution report
- Merged GEPA reflective optimization report
- GEPA paper (arXiv:2507.19457v2)
- Merged GEPA optimizer PR #2204 and merged benchmark PR #18
