OpenEvolve
Island-model MAP-Elites evolutionary search with diff mutation (the open AlphaEvolve).
"""OpenEvolve Proposer component — the SEARCH/REPLACE diff operator.
Faithful port of OpenEvolve's diff mechanics (``utils/code_utils.py`` ``apply_diff`` / ``extract_diffs``):
diffs are applied by **whole-line slice equality** via the shared
:func:`~galapagos.components.proposer.apply_diff_wholeline`. A response with at least one parseable
SEARCH/REPLACE block is evaluated even if it does not match the parent (OpenEvolve admits the unchanged
child); a response with NO block is a no-op (OpenEvolve's "No valid diffs found" → discard). No
full-rewrite fence fallback in diff mode.
"""
from __future__ import annotations
import logging
from ...components.proposer import (DiffProposer, Env, apply_diff_wholeline,
extract_diffs, parse_full_rewrite, summarize_diff)
from ...models.base import Prompt
from ...records import Genome
log = logging.getLogger(__name__)
class OpenEvolveProposer(DiffProposer):
"""OpenEvolve's diff operator: one LLM call → SEARCH/REPLACE applied by whole-line matching.
Also records the immediate parent's metrics and a diff summary onto the child
(``metadata["parent_metrics"]`` / ``metadata["changes"]``), mirroring OpenEvolve's
``Program.metadata = {"changes": ..., "parent_metrics": parent.metrics}`` (iteration.py), which the
PromptBuilder renders in the "Program Evolution History" section.
"""
def propose(self, prompt: Prompt, env: Env) -> Genome:
parent = env.selection.parent
gen = env.model.generate(prompt)
if env.state is not None:
env.state.record_cost(gen.cost_usd, gen.prompt_tokens, gen.completion_tokens)
if self.diff_based: # whole-line SEARCH/REPLACE (no fence fallback)
new_code, _ = apply_diff_wholeline(parent.content, gen.text)
# OpenEvolve discards ONLY when NO diff block parses; a present-but-non-matching block still
# yields a child (the unchanged code) that is evaluated + admitted. So "changed" = "at least
# one block parsed", not "the text differs" (process_parallel.py:224-228 / 263).
changed = bool(extract_diffs(gen.text))
changes = summarize_diff(gen.text)
else: # full-rewrite mode: the whole response is the program
new_code, changed = parse_full_rewrite(parent.content, gen.text)
changes = "Full rewrite"
child = parent.child(new_code, generation=parent.metadata.get("generation", 0) + 1)
# a regular child is never itself a migrant copy — OpenEvolve gives children a fresh metadata
# dict; migrant copies are minted only in Population.migrate. Dropping the inherited flag lets
# descendants of a migrant migrate again (else a whole lineage is frozen out of migration).
child.metadata.pop("migrant", None)
# stamp the SAMPLING island on the child (OpenEvolve add(target_island=...)) — the policy
# publishes it via the signal rather than mutating the shared parent genome.
isl = (env.state.signals.get("openevolve", {}) if env.state is not None else {}).get("island")
if isinstance(isl, int):
child.metadata["island"] = isl
child.metadata["changed"] = changed
child.metadata["parent_metrics"] = dict(parent.scores)
child.metadata["changes"] = changes
child.artifacts["response"] = gen.text
log.debug("propose (openevolve %s) — parent=%s child=%s changed=%s",
"diff" if self.diff_based else "rewrite", parent.id, child.id, changed)
return child