Skip to content

Commit 2e6345e

Browse files
committed
add history-aware self-hosting search
1 parent 08d83ad commit 2e6345e

18 files changed

Lines changed: 2144 additions & 87 deletions

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,40 @@ OpenEvolve implements a sophisticated **evolutionary coding pipeline** that goes
215215
- **Adaptive Feature Dimensions**: Custom quality-diversity metrics
216216
- **Migration Patterns**: Ring topology with controlled gene flow
217217
- **Multi-Strategy Sampling**: Elite, diverse, and exploratory selection
218+
- **Evaluator-Defined Identity**: Set `program_identity_artifact` to reject
219+
structurally duplicate measured children across the complete run history
220+
- **Measured-Phenotype Identity**: Set `phenotype_identity_artifact` to reject
221+
structurally different children whose complete measured behavior is already
222+
present
223+
- **Observed-Result Allocation**: Optional `database.controller_scheduler`
224+
routes new calls using a domain-selected `score_metric`, validity, diversity,
225+
rejection rate, exploration, and token efficiency. Set `diversity_artifact`
226+
to count evaluator-defined niches, and reserve at least one island so live
227+
parallel scheduling retains a real allocation choice. Optionally set
228+
`adaptive_parallelism` below the worker count to use fast balanced warmup
229+
followed by a fresher-context quality phase. Set it to `1` when complete
230+
archive freshness matters more than late-stage throughput. The allocator
231+
reads retained seed and migrated-program scores as well as child results.
232+
Set `leader_score_band` to keep post-warmup calls within an absolute score
233+
distance of the best retained population. Set `parent_score_band` to keep
234+
post-warmup parents within a score distance of the selected population's
235+
retained leader
236+
- **Compact Measured-History Context**: Set
237+
`prompt.archive_context_artifact` to show the proposal model a deterministic,
238+
bounded inventory of one evaluator-defined artifact from measured programs.
239+
The append-only values survive MAP-Elites displacement and checkpoint resume
240+
- **Filtered Proposal Neighborhoods**: Set
241+
`prompt.proposal_neighborhood_artifact` when an evaluator can enumerate
242+
valid parent-local options. The controller removes identities measured
243+
anywhere in the complete persisted history, including behaviorally rejected
244+
or displaced programs, and renders the bounded remainder as
245+
`proposal-options.json` without embedding domain rules in OpenEvolve
246+
- **Prompt Artifact Selection**: Set ordered
247+
`prompt.artifact_include_names` to keep complete evaluator receipts in the
248+
archive while rendering only task-relevant artifacts to the proposal model
249+
- **Auditable Rejections**: Usage receipts distinguish measured candidates
250+
from archive admission and record the selected parent plus a provider-response
251+
digest without duplicating full prompt content
218252

219253
</details>
220254

@@ -859,14 +893,20 @@ Just set the `api_base` in your config to point to your endpoint.
859893

860894
**Multiple success metrics:**
861895

862-
1. **Primary Metric**: Your evaluator's `combined_score` or metric average
896+
1. **Primary Metric**: `selection_score` when supplied, then `combined_score`
863897
2. **Convergence**: Best score improvement over time
864898
3. **Diversity**: MAP-Elites grid coverage
865899
4. **Efficiency**: Iterations to reach target performance
866900
5. **Robustness**: Performance across different test cases
867901

868902
**Use the visualizer** to track all metrics in real-time and identify when evolution has converged.
869903

904+
For exact scientific searches, an evaluator can additionally return
905+
`selection_eligible`. A value at or below zero retains the attempt in lineage
906+
records but excludes it from parent selection, MAP-Elites cells, and the elite
907+
archive. This separates non-negotiable admission checks from the
908+
multi-objective quality score while preserving older evaluator behavior.
909+
870910
</details>
871911

872912
### **Contributors**

openevolve/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ async def _run_evolution_async(
175175
best_code = best_program.code
176176
metrics = best_program.metrics or {}
177177

178-
if "combined_score" in metrics:
178+
if "selection_score" in metrics:
179+
best_score = metrics["selection_score"]
180+
elif "combined_score" in metrics:
179181
best_score = metrics["combined_score"]
180182
elif metrics:
181183
numeric_metrics = [v for v in metrics.values() if isinstance(v, (int, float))]

openevolve/cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import argparse
66
import asyncio
7+
import json
78
import logging
89
import os
910
import sys
11+
from pathlib import Path
1012
from typing import Dict, List, Optional
1113

1214
from openevolve import OpenEvolve
@@ -25,6 +27,16 @@ def parse_args() -> argparse.Namespace:
2527
"evaluation_file", help="Path to the evaluation file containing an 'evaluate' function"
2628
)
2729

30+
parser.add_argument(
31+
"--seed-program",
32+
action="append",
33+
default=[],
34+
help=(
35+
"Additional evaluated starting parent. Repeat the option to seed "
36+
"multiple islands while retaining initial_program as the incumbent."
37+
),
38+
)
39+
2840
parser.add_argument("--config", "-c", help="Path to configuration file (YAML)", default=None)
2941

3042
parser.add_argument("--output", "-o", help="Output directory for results", default=None)
@@ -77,6 +89,10 @@ async def main_async() -> int:
7789
if not os.path.exists(args.evaluation_file):
7890
print(f"Error: Evaluation file '{args.evaluation_file}' not found")
7991
return 1
92+
missing_seeds = [path for path in args.seed_program if not os.path.exists(path)]
93+
if missing_seeds:
94+
print(f"Error: Seed program file not found: '{missing_seeds[0]}'")
95+
return 1
8096

8197
# Load base config from file or defaults
8298
config = load_config(args.config)
@@ -110,6 +126,7 @@ async def main_async() -> int:
110126
evaluation_file=args.evaluation_file,
111127
config=config,
112128
output_dir=args.output,
129+
seed_program_paths=args.seed_program,
113130
)
114131

115132
# Load from checkpoint if specified
@@ -161,6 +178,25 @@ async def main_async() -> int:
161178
print(f"\nLatest checkpoint saved at: {latest_checkpoint}")
162179
print(f"To resume, use: --checkpoint {latest_checkpoint}")
163180

181+
summary = {
182+
"schema_version": 1,
183+
"record_type": "openevolve_run_summary",
184+
"completion_reason": openevolve.completion_reason,
185+
"last_completed_iteration": openevolve.last_completed_iteration,
186+
"completed_iteration_count": openevolve.completed_iteration_count,
187+
"latest_checkpoint": latest_checkpoint,
188+
"best_program_id": best_program.id,
189+
"best_program_metrics": best_program.metrics,
190+
"llm_usage": openevolve.llm_usage,
191+
}
192+
summary_path = Path(openevolve.output_dir) / "run-summary.json"
193+
temporary = summary_path.with_name(f".{summary_path.name}.tmp")
194+
temporary.write_text(
195+
json.dumps(summary, indent=2, sort_keys=True) + "\n",
196+
encoding="utf-8",
197+
)
198+
temporary.replace(summary_path)
199+
164200
return 0
165201

166202
except Exception as e:

openevolve/config.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,19 @@ class PromptConfig:
282282
include_artifacts: bool = True
283283
max_artifact_bytes: int = 20 * 1024 # 20KB in prompt
284284
artifact_security_filter: bool = True
285+
# Optional ordered subset of evaluator artifacts rendered in prompts.
286+
# Stored artifacts remain complete.
287+
artifact_include_names: Optional[List[str]] = None
288+
# Optional evaluator artifact collected across the complete live archive
289+
# and rendered as one compact deterministic prompt artifact.
290+
archive_context_artifact: Optional[str] = None
291+
archive_context_max_items: int = 64
292+
# Optional evaluator-produced neighborhood. The artifact must be schema
293+
# version 1 JSON with an identity_artifact name and ordered options carrying
294+
# identity fields. The controller removes identities already retained in the
295+
# live archive and renders the remainder as proposal-options.json.
296+
proposal_neighborhood_artifact: Optional[str] = None
297+
proposal_options_max_items: int = 32
285298

286299
# Feature extraction and program labeling
287300
suggest_simplification_after_chars: Optional[int] = (
@@ -312,13 +325,33 @@ class ControllerSchedulerConfig:
312325
"""Optional observed-result island allocator for bounded live comparisons."""
313326

314327
enabled: bool = False
328+
score_metric: str = "combined_score"
329+
# Optional evaluator artifact used to count distinct measured phenotypes.
330+
# When absent, source-code hashes preserve the legacy definition.
331+
diversity_artifact: Optional[str] = None
315332
exploitation_weight: float = 0.0
316333
underexplored_weight: float = 0.0
317334
validity_weight: float = 0.0
318335
diversity_weight: float = 0.0
319336
token_efficiency_weight: float = 0.0
320337
rejection_penalty: float = 0.0
321338
minimum_calls: int = 1
339+
# Optional post-warmup quality band. When set, allocate only among islands
340+
# whose retained best score is within this absolute distance of the global
341+
# retained leader. None preserves unrestricted legacy allocation.
342+
leader_score_band: Optional[float] = None
343+
# Optional post-warmup parent band within the selected island. When set,
344+
# parent sampling is limited to retained programs whose score is within
345+
# this absolute distance of that island's retained leader.
346+
parent_score_band: Optional[float] = None
347+
# Keep this many islands outside the active worker frontier so a completed
348+
# call leaves the allocator a real choice. This does not reduce concurrency
349+
# when num_islands > parallel_evaluations.
350+
reserve_islands: int = 1
351+
# Optional lower in-flight limit after the balanced warmup proposal count
352+
# has been submitted. This trades some throughput for fresher archive
353+
# context during quality-focused adaptive search.
354+
adaptive_parallelism: Optional[int] = None
322355

323356

324357
@dataclass
@@ -461,6 +494,14 @@ class Config:
461494
strict_diff_application: bool = False
462495
enforce_evolve_blocks: bool = False
463496
max_diff_blocks: int = 32
497+
# Optional evaluator artifact whose exact value identifies the candidate's
498+
# behaviorally meaningful contents. When configured, a child duplicating
499+
# any program already known to the live archive is rejected.
500+
program_identity_artifact: Optional[str] = None
501+
# Optional second evaluator identity for exact measured behavior. This is
502+
# checked only after evaluation, so structurally different programs that
503+
# produce an already archived phenotype do not bloat parent selection.
504+
phenotype_identity_artifact: Optional[str] = None
464505

465506
# Early stopping settings
466507
early_stopping_patience: Optional[int] = None
@@ -544,9 +585,92 @@ def validate(self) -> None:
544585
)
545586
if self.max_diff_blocks < 1:
546587
raise ValueError("max_diff_blocks must be at least 1")
588+
if self.prompt.archive_context_artifact is not None and (
589+
not isinstance(self.prompt.archive_context_artifact, str)
590+
or not self.prompt.archive_context_artifact.strip()
591+
):
592+
raise ValueError(
593+
"prompt.archive_context_artifact must be a non-empty string or None"
594+
)
595+
if self.prompt.proposal_neighborhood_artifact is not None and (
596+
not isinstance(self.prompt.proposal_neighborhood_artifact, str)
597+
or not self.prompt.proposal_neighborhood_artifact.strip()
598+
):
599+
raise ValueError(
600+
"prompt.proposal_neighborhood_artifact must be a non-empty "
601+
"string or None"
602+
)
603+
artifact_names = self.prompt.artifact_include_names
604+
if artifact_names is not None:
605+
if not isinstance(artifact_names, list) or any(
606+
not isinstance(name, str) or not name.strip()
607+
for name in artifact_names
608+
):
609+
raise ValueError(
610+
"prompt.artifact_include_names must be a list of non-empty strings or None"
611+
)
612+
if len(set(artifact_names)) != len(artifact_names):
613+
raise ValueError(
614+
"prompt.artifact_include_names must not contain duplicates"
615+
)
616+
if (
617+
self.prompt.archive_context_artifact is not None
618+
and "archive-context.json" not in artifact_names
619+
):
620+
raise ValueError(
621+
"prompt.artifact_include_names must include archive-context.json "
622+
"when archive_context_artifact is configured"
623+
)
624+
if (
625+
self.prompt.proposal_neighborhood_artifact is not None
626+
and "proposal-options.json" not in artifact_names
627+
):
628+
raise ValueError(
629+
"prompt.artifact_include_names must include "
630+
"proposal-options.json when proposal_neighborhood_artifact "
631+
"is configured"
632+
)
633+
if (
634+
isinstance(self.prompt.archive_context_max_items, bool)
635+
or not isinstance(self.prompt.archive_context_max_items, int)
636+
or self.prompt.archive_context_max_items < 1
637+
):
638+
raise ValueError(
639+
"prompt.archive_context_max_items must be a positive integer"
640+
)
641+
if (
642+
isinstance(self.prompt.proposal_options_max_items, bool)
643+
or not isinstance(self.prompt.proposal_options_max_items, int)
644+
or self.prompt.proposal_options_max_items < 1
645+
):
646+
raise ValueError(
647+
"prompt.proposal_options_max_items must be a positive integer"
648+
)
649+
for field_name in (
650+
"program_identity_artifact",
651+
"phenotype_identity_artifact",
652+
):
653+
value = getattr(self, field_name)
654+
if value is not None and (
655+
not isinstance(value, str) or not value.strip()
656+
):
657+
raise ValueError(f"{field_name} must be a non-empty string or None")
547658
scheduler = self.database.controller_scheduler
548659
if not isinstance(scheduler.enabled, bool):
549660
raise ValueError("database.controller_scheduler.enabled must be boolean")
661+
if not isinstance(scheduler.score_metric, str) or not scheduler.score_metric.strip():
662+
raise ValueError(
663+
"database.controller_scheduler.score_metric "
664+
"must be a non-empty string"
665+
)
666+
if scheduler.diversity_artifact is not None and (
667+
not isinstance(scheduler.diversity_artifact, str)
668+
or not scheduler.diversity_artifact.strip()
669+
):
670+
raise ValueError(
671+
"database.controller_scheduler.diversity_artifact "
672+
"must be a non-empty string or None"
673+
)
550674
for field_name in (
551675
"exploitation_weight",
552676
"underexplored_weight",
@@ -566,6 +690,18 @@ def validate(self) -> None:
566690
f"database.controller_scheduler.{field_name} "
567691
"must be finite and nonnegative"
568692
)
693+
for field_name in ("leader_score_band", "parent_score_band"):
694+
value = getattr(scheduler, field_name)
695+
if value is not None and (
696+
isinstance(value, bool)
697+
or not isinstance(value, (int, float))
698+
or not math.isfinite(float(value))
699+
or float(value) < 0.0
700+
):
701+
raise ValueError(
702+
f"database.controller_scheduler.{field_name} "
703+
"must be finite and nonnegative when provided"
704+
)
569705
if (
570706
isinstance(scheduler.minimum_calls, bool)
571707
or not isinstance(scheduler.minimum_calls, int)
@@ -575,6 +711,36 @@ def validate(self) -> None:
575711
"database.controller_scheduler.minimum_calls "
576712
"must be a positive integer"
577713
)
714+
if (
715+
isinstance(scheduler.reserve_islands, bool)
716+
or not isinstance(scheduler.reserve_islands, int)
717+
or scheduler.reserve_islands < 0
718+
or (
719+
scheduler.enabled
720+
and scheduler.reserve_islands >= self.database.num_islands
721+
)
722+
):
723+
raise ValueError(
724+
"database.controller_scheduler.reserve_islands must be an integer "
725+
"in [0, database.num_islands) when the scheduler is enabled"
726+
)
727+
if scheduler.adaptive_parallelism is not None and (
728+
isinstance(scheduler.adaptive_parallelism, bool)
729+
or not isinstance(scheduler.adaptive_parallelism, int)
730+
or scheduler.adaptive_parallelism < 1
731+
or scheduler.adaptive_parallelism
732+
> self.evaluator.parallel_evaluations
733+
or (
734+
scheduler.enabled
735+
and scheduler.adaptive_parallelism
736+
> self.database.num_islands - scheduler.reserve_islands
737+
)
738+
):
739+
raise ValueError(
740+
"database.controller_scheduler.adaptive_parallelism must be a "
741+
"positive integer no greater than the evaluator worker count or "
742+
"selectable island count"
743+
)
578744

579745
def to_dict(self) -> Dict[str, Any]:
580746
return asdict(self)

0 commit comments

Comments
 (0)