Skip to main content
The edit calculus is the structural mutation half of the IR. An Overlay mutates parameter values over a fixed skeleton; an Edit mutates the skeleton itself — add a reasoning field, swap a Predict for an AgentLoop, wrap a flaky step in a Retry, remove a step. Edits are plain serde values — inspectable, diffable, replayable — and are only ever applied through Program::edited, which is pure: it clones the arenas, applies the edits in order, re-runs the same load-time validation the builder and loader use, and seals a new content hash. A program value is never mutated in place, so every hash-bound artifact (overlays, traces, caches) minted against the parent stays coherent. All items are exported from dspy_rs::ir: Edit, EditKind, SwapTarget, EditError, ApplyError, migrate_overlay.

The edits

An optimizer’s structural proposal is data, not code — it can be logged, replayed against the same parent, and diffed. SwapTarget is the target kind of SwapLeaf: Agent { tools, stop, budget } or Predict.

Program::edited

Applies edits in order to a clone of self and returns the sealed, validated result. The child gets a new content hash and lineage.parent set to the parent’s hash — exactly like Program::bake; the other provenance fields are left empty for the optimizer to fill (an edit is not an optimization run record). Behavior worth knowing:
  • NodeIds are positional handles against the parent. Within one edited() batch, ids stay stable (swaps happen in place, removals only detach); dead nodes, signatures, and params are garbage-collected once at the end. Ids in the child may therefore differ from the parent — re-locate leaves by name (Program::leaf_id(name), leaf names are program-unique and survive edits) and params by path.
  • Batch validation. Edits are validated as a sequence: intermediate states may be inconsistent (remove a producer, then its consumer); only the final program must pass validation. Apply-time errors cover what is checkable locally; everything data-flow shaped is deliberately left to the load-time validator, so the edit layer and the loader can never disagree.
  • Identity is preserved. edited(&[]) returns a program with the parent’s hash — only lineage differs, and lineage is outside the hash preimage. Signatures that were already unreferenced in the parent are kept; only newly orphaned ones are collected.
  • CoT re-sugars. When the prepended field is exactly the cot reasoning field on a Predict, the augmented signature copy keeps the base name so the canonical printer re-sugars it as cot <Sig>; otherwise it gets a fresh unique name (<Sig>_<field>).

Errors

ApplyError is the locally-checkable failure set: StaleNode, WrongKind (e.g. SetStop on a Predict), DuplicateField, UnknownTool, ToolCapsExceedProgram (a tool’s caps exceed the program ceiling), ToolAlreadyDeclared, ToolNotDeclared, NotInSeq (only Seq steps can be removed), Unparented.
The menu of edit kinds structurally admissible at a node — lightweight, serializable EditKind descriptors suitable for prompting an LLM proposer: The menu is purely structural — data-flow legality (whether a removal orphans a downstream binding) is still validate()’s call, surfaced by edited. A stale id yields an empty menu. This is exactly how the shipped Structural optimizer proposes edits: it serializes the menu, has a reflection LM choose one entry, applies the choice through edited, and gates the child against the parent on a shared minibatch.

migrate_overlay: carrying tuned values across an edit

An edit changes the program hash, so overlays minted against the parent no longer apply to the child. migrate_overlay carries value-level progress across the structural change: for every entry in the overlay, it re-mints the entry against the child when the child has a slot at the same path and kind whose owning leaf/tool still has a carrying signature — inputs identical (names and types, in order) and every parent output present in the child’s outputs. Outputs may widen: that is what lets instruction and demos survive AugmentSig (demo rows still map onto the base fields; the new field is simply absent from the row). ModelRef entries are re-minted by model name, not ordinal. ToolSet entries are re-minted by tool name and intersected with what the child’s agent still declares — partial survival carries the selection forward; a selection with no survivors is dropped. Entries that no longer fit are dropped; a base-mismatched overlay yields an empty result.

See also

  • Program and nodes: the value half — params, Overlay, and bake
  • Structural: the shipped optimizer over this calculus — LM-guided edit choice, migrate_overlay, minibatch gating
  • Optimizer engine: how candidates are evaluated; a structural optimizer proposes Edits where a prompt optimizer proposes overlays
  • Runtime: loading and running the edited program
  • The .dsrs file: the canonical text the child prints to