Skip to main content
Structural is the structural optimizer: where the other five strategies tune parameter values (instructions, demos) through overlays, Structural rewrites the program graph itself. Each generation it gathers the 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

Feedback is optional for Structural, but whatever the metric returns is what the reflection LM reads when choosing an edit, so specific feedback buys better proposals.

2. Configure and run

The closure argument supplies a fresh 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:
If you ran a value-level optimizer first (GEPA, COPRO), pass its winning overlay in and Structural carries it across every accepted edit:

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

  1. 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.
  2. Each generation:
    • Sample a shared minibatch (seeded RNG). The incumbent’s minibatch mean is the gate threshold.
    • Menu: gather legal_edits for every leaf; keep the materializable kinds.
    • Choose: the reflection LM reads the program’s canonical .dsrs text, the menu (one JSON object per line, each with an option number), and the incumbent’s per-example feedback, and answers with one option number.
    • Apply: Program::edited mints the child; migrate_overlay re-mints the incumbent overlay against it; the child loads through your RuntimeEnv factory.
    • 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.
  3. Return the incumbent program and its overlay.
Every rejection path degrades gracefully: an edit that fails to apply (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_size rollouts for the gate, plus the remaining examples.len() - minibatch_size only on promotion, plus one reflection call when a prompt_model is set.
Cap the spend with 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_overlay preserves the tuned text.
  • You have a labeled example set and budget for whole-program re-evaluation.
Prefer the value-level optimizers when instructions and demos are the lever: they are cheaper (cache-friendly, no program reloads) and search a denser space.

Troubleshooting

The run accepts nothing

The gate requires a strict minibatch win. Small minibatches are noisy; raise minibatch_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 needs examples.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::load and RuntimeEnv, which child loading goes through
  • Program and nodes: Overlay and Program::bake for keeping the winner