legal_edits menu, has a reflection LM choose one edit from the serialized menu plus the incumbent’s evaluation feedback, applies it with Program::edited, carries the tuned overlay across the change with migrate_overlay, and keeps the child only if it beats the parent on a shared minibatch.
Structural runs on the program lane only: it needs an interpreter-loaded Program whose skeleton is data. Typed modules have no editable skeleton, so there is no compile_module here.
Overview
The edit calculus makes structural mutation safe: edits are serde values,Program::edited is pure and re-validates, and migrate_overlay re-mints tuned slot values against the child. Structural is the search loop on top: a GEPA-style reflection step chooses which edit to try, and the engine’s minibatch gate decides whether to keep the result. The moves it can propose:
SetStop and SetInstructionDefault appear in legal_edits but are excluded from Structural’s menu: they need free-form values, which is value-level work the other optimizers already own.
Quick start
1. Load a program and implement a ProgramMetric
2. Configure and run
RuntimeEnv every time an edited child needs loading. Only the host knows the live bindings (models, host tools, sandbox, capability grants), so child loading cannot be implicit; return the same bindings you loaded the incumbent with.
3. Keep the winner
The winner is returned, never installed: the interpreter you passed in is untouched. Bake the migrated overlay into the winning program to get a single self-contained artifact:Configuration options
Understanding Structural results
compile_program returns a StructuralReport:
Each
StructuralStep records generation, the targeted leaf, the concrete edit (serde data, replayable against parent_hash), parent_minibatch_score, child_minibatch_score (None when the child never scored), accepted, full_score (Some only when accepted), and rejection (why a child never scored, when it didn’t).
The loop
- Baseline the incumbent (plus overlay) over the full example set. This seeds the engine’s rollout cache, so every later parent minibatch read costs nothing.
- Each generation:
- Sample a shared minibatch (seeded RNG). The incumbent’s minibatch mean is the gate threshold.
- Menu: gather
legal_editsfor every leaf; keep the materializable kinds. - Choose: the reflection LM reads the program’s canonical
.dsrstext, the menu (one JSON object per line, each with anoptionnumber), and the incumbent’s per-example feedback, and answers with one option number. - Apply:
Program::editedmints the child;migrate_overlayre-mints the incumbent overlay against it; the child loads through yourRuntimeEnvfactory. - Gate: the child is scored on the same minibatch. Only a strict win promotes it to a full-set evaluation and makes it the new incumbent.
- Return the incumbent program and its overlay.
EditError), a child that fails validation or loading, or a reflection reply that does not parse is recorded in the step and skipped. A run only errors on the engine’s own failure modes (a metric error, an LM error during evaluation, a budget too small for the baseline pass).
Cost model
Every child is a fresh program: its hash keys fresh rollout-cache rows, so nothing it does is served from the parent’s cache. Per run:examples.len()rollouts for the baseline pass;- per generation,
minibatch_sizerollouts for the gate, plus the remainingexamples.len() - minibatch_sizeonly on promotion, plus one reflection call when aprompt_modelis set.
max_rollouts / max_lm_calls; the run stops cleanly when the next batch would not fit.
When to use Structural
- The program’s shape is the bottleneck: a leaf that should reason step by step, a step that should be an agent with tools (or should not be), a flaky node that needs a retry.
- After a value-level pass: tune instructions first, then let Structural search structure while
migrate_overlaypreserves the tuned text. - You have a labeled example set and budget for whole-program re-evaluation.
Troubleshooting
The run accepts nothing
The gate requires a strict minibatch win. Small minibatches are noisy; raiseminibatch_size for a better signal, and set a prompt_model so choices are informed rather than uniform.
Steps show rejection: Some("edit failed: ...")
Normal. The menu is structural, and data-flow legality is the validator’s call: removing a step whose outputs a later binding still references, for example, is refused by Program::edited and skipped. See the edit calculus.
The run errors with “budget too small for the baseline pass”
The baseline needsexamples.len() rollouts before the loop can start. Raise max_rollouts or shrink the example set.
Comparison with other optimizers
GEPA is the only optimizer that requires textual feedback from the metric (
Eval::with_feedback). The others use numerical scores alone. Full configuration tables for all six live in the optimizers reference.
See also
- The edit calculus:
Edit,Program::edited,legal_edits,migrate_overlay - Optimizers: the full configuration and report tables
- Optimizer engine: the shared evaluation core Structural gates through
- Runtime:
Interpreter::loadandRuntimeEnv, which child loading goes through - Program and nodes:
OverlayandProgram::bakefor keeping the winner
