44
55import os
66import re
7+ import math
78from dataclasses import asdict , dataclass , field
89from pathlib import Path
910from typing import TYPE_CHECKING , Any , Callable , Dict , List , Optional , Union
@@ -306,6 +307,20 @@ class PromptConfig:
306307 )
307308
308309
310+ @dataclass
311+ class ControllerSchedulerConfig :
312+ """Optional observed-result island allocator for bounded live comparisons."""
313+
314+ enabled : bool = False
315+ exploitation_weight : float = 0.0
316+ underexplored_weight : float = 0.0
317+ validity_weight : float = 0.0
318+ diversity_weight : float = 0.0
319+ token_efficiency_weight : float = 0.0
320+ rejection_penalty : float = 0.0
321+ minimum_calls : int = 1
322+
323+
309324@dataclass
310325class DatabaseConfig :
311326 """Configuration for the program database"""
@@ -326,6 +341,9 @@ class DatabaseConfig:
326341 elite_selection_ratio : float = 0.1
327342 exploration_ratio : float = 0.2
328343 exploitation_ratio : float = 0.7
344+ controller_scheduler : ControllerSchedulerConfig = field (
345+ default_factory = ControllerSchedulerConfig
346+ )
329347 # Note: diversity_metric fixed to "edit_distance"
330348 diversity_metric : str = "edit_distance" # Options: "edit_distance", "feature_based"
331349
@@ -418,6 +436,10 @@ class Config:
418436
419437 # General settings
420438 max_iterations : int = 10000
439+ # Optional per-run proposal-model budgets. Calls are reserved before
440+ # submission; provider-token totals come from completed unique receipts.
441+ max_llm_calls : Optional [int ] = None
442+ max_total_provider_tokens : Optional [int ] = None
421443 checkpoint_interval : int = 100
422444 log_level : str = "INFO"
423445 log_dir : Optional [str ] = None
@@ -436,6 +458,9 @@ class Config:
436458 diff_based_evolution : bool = True
437459 max_code_length : int = 10000
438460 diff_pattern : str = r"<<<<<<< SEARCH\n(.*?)=======\n(.*?)>>>>>>> REPLACE"
461+ strict_diff_application : bool = False
462+ enforce_evolve_blocks : bool = False
463+ max_diff_blocks : int = 32
439464
440465 # Early stopping settings
441466 early_stopping_patience : Optional [int ] = None
@@ -489,13 +514,67 @@ def from_dict(cls, config_dict: Dict[str, Any]) -> "Config":
489514 if config .database .random_seed is None and config .random_seed is not None :
490515 config .database .random_seed = config .random_seed
491516
492- if config .prompt .programs_as_changes_description and not config .diff_based_evolution :
517+ config .validate ()
518+ return config
519+
520+ def validate (self ) -> None :
521+ """Validate combinations used by both YAML and programmatic callers."""
522+ for field_name in ("max_llm_calls" , "max_total_provider_tokens" ):
523+ value = getattr (self , field_name )
524+ if value is not None and (
525+ isinstance (value , bool ) or not isinstance (value , int ) or value < 1
526+ ):
527+ raise ValueError (f"{ field_name } must be a positive integer or None" )
528+ try :
529+ re .compile (self .diff_pattern )
530+ except re .error as error :
531+ raise ValueError (f"Invalid regex pattern in diff_pattern: { error } " ) from error
532+ if self .prompt .programs_as_changes_description and not self .diff_based_evolution :
493533 raise ValueError (
494534 "prompt.programs_as_changes_description=true requires diff_based_evolution=true "
495535 "(full rewrites cannot reliably update code and changes_description together)"
496536 )
497-
498- return config
537+ if self .enforce_evolve_blocks and not self .strict_diff_application :
538+ raise ValueError (
539+ "enforce_evolve_blocks=true requires strict_diff_application=true"
540+ )
541+ if self .enforce_evolve_blocks and not self .diff_based_evolution :
542+ raise ValueError (
543+ "enforce_evolve_blocks=true requires diff_based_evolution=true"
544+ )
545+ if self .max_diff_blocks < 1 :
546+ raise ValueError ("max_diff_blocks must be at least 1" )
547+ scheduler = self .database .controller_scheduler
548+ if not isinstance (scheduler .enabled , bool ):
549+ raise ValueError ("database.controller_scheduler.enabled must be boolean" )
550+ for field_name in (
551+ "exploitation_weight" ,
552+ "underexplored_weight" ,
553+ "validity_weight" ,
554+ "diversity_weight" ,
555+ "token_efficiency_weight" ,
556+ "rejection_penalty" ,
557+ ):
558+ value = getattr (scheduler , field_name )
559+ if (
560+ isinstance (value , bool )
561+ or not isinstance (value , (int , float ))
562+ or not math .isfinite (float (value ))
563+ or value < 0
564+ ):
565+ raise ValueError (
566+ f"database.controller_scheduler.{ field_name } "
567+ "must be finite and nonnegative"
568+ )
569+ if (
570+ isinstance (scheduler .minimum_calls , bool )
571+ or not isinstance (scheduler .minimum_calls , int )
572+ or scheduler .minimum_calls < 1
573+ ):
574+ raise ValueError (
575+ "database.controller_scheduler.minimum_calls "
576+ "must be a positive integer"
577+ )
499578
500579 def to_dict (self ) -> Dict [str , Any ]:
501580 return asdict (self )
0 commit comments