Overview
Stop conditions (“stoppers”) control when the optimization loop terminates. GEPA provides several built-in stoppers that can be combined to create sophisticated stopping logic.
All stoppers implement the StopperProtocol: a callable that takes GEPAState and returns True when optimization should stop.
StopperProtocol
Base protocol for all stop conditions.
from gepa.utils import StopperProtocol
from gepa.core.state import GEPAState
class CustomStopper(StopperProtocol):
def __call__(self, gepa_state: GEPAState) -> bool:
# Return True to stop optimization
return should_stop
Parameters:
gepa_state (GEPAState): Current optimization state containing:
total_num_evals: Total evaluations performed
program_candidates: All candidates tracked
program_full_scores_val_set: Validation scores
i: Current iteration number
Returns: bool - True if optimization should stop
Built-in Stoppers
MaxMetricCallsStopper
Stops after a maximum number of evaluator calls.
This is the most common stopping condition - typically set via EngineConfig.max_metric_calls.
from gepa.utils import MaxMetricCallsStopper
from gepa.optimize_anything import GEPAConfig, EngineConfig
# Method 1: Via EngineConfig (recommended)
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=500)
)
# Method 2: Via stop_callbacks
stopper = MaxMetricCallsStopper(max_metric_calls=500)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
Maximum number of evaluator calls allowed.
Location: optimize_anything.py:1163
MaxCandidateProposalsStopper
Stops after a maximum number of candidate proposals.
from gepa.utils import MaxCandidateProposalsStopper
# Stop after generating 100 candidates
stopper = MaxCandidateProposalsStopper(max_proposals=100)
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=1000),
stop_callbacks=stopper,
)
Parameters:
Maximum number of candidate proposals.
Location: stop_condition.py:176
NoImprovementStopper
Stops after a specified number of iterations without improvement.
Useful for early stopping when the optimization plateaus.
from gepa.utils import NoImprovementStopper
# Stop if no improvement for 50 iterations
stopper = NoImprovementStopper(max_iterations_without_improvement=50)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
max_iterations_without_improvement
Number of iterations without score improvement before stopping.
Methods:
reset(): Reset the improvement counter (useful when manually improving the score)
Location: stop_condition.py:83
FileStopper
Stops when a specific file exists.
Automatically enabled when EngineConfig.run_dir is set - creates a stop file at {run_dir}/gepa.stop.
from gepa.utils import FileStopper
# Manual file stopper
stopper = FileStopper(stop_file_path="./my_stop_file.txt")
config = GEPAConfig(stop_callbacks=stopper)
# During optimization, create the file to stop gracefully:
# $ touch ./my_stop_file.txt
Parameters:
Path to the stop file. When this file exists, optimization stops.
Methods:
remove_stop_file(): Remove the stop file
Location: stop_condition.py:46
TimeoutStopCondition
Stops after a specified timeout.
from gepa.utils import TimeoutStopCondition
# Stop after 1 hour
stopper = TimeoutStopCondition(timeout_seconds=3600)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
Maximum runtime in seconds.
Location: stop_condition.py:34
ScoreThresholdStopper
Stops when a score threshold is reached.
Useful when you have a target score in mind.
from gepa.utils import ScoreThresholdStopper
# Stop when we achieve score >= 0.95
stopper = ScoreThresholdStopper(threshold=0.95)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
Score threshold. Stops when the best validation score reaches or exceeds this value.
Location: stop_condition.py:64
SignalStopper
Stops when a signal is received (e.g., SIGINT, SIGTERM).
Enables graceful shutdown on Ctrl+C or kill signals.
from gepa.utils import SignalStopper
import signal
# Stop on Ctrl+C or SIGTERM
stopper = SignalStopper(signals=[signal.SIGINT, signal.SIGTERM])
config = GEPAConfig(stop_callbacks=stopper)
# Clean up signal handlers when done
stopper.cleanup()
Parameters:
signals
list[int] | None
default:"[signal.SIGINT, signal.SIGTERM]"
List of signals to listen for. Defaults to SIGINT (Ctrl+C) and SIGTERM.
Methods:
cleanup(): Restore original signal handlers
Location: stop_condition.py:114
MaxTrackedCandidatesStopper
Stops after a maximum number of tracked candidates.
from gepa.utils import MaxTrackedCandidatesStopper
# Stop after tracking 200 candidates
stopper = MaxTrackedCandidatesStopper(max_tracked_candidates=200)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
Maximum number of candidates to track before stopping.
Location: stop_condition.py:150
CompositeStopper
Combines multiple stopping conditions.
Automatically used when multiple stoppers are provided to GEPAConfig.
from gepa.utils import (
CompositeStopper,
MaxMetricCallsStopper,
NoImprovementStopper,
TimeoutStopCondition,
)
# Stop when ANY condition is met
stopper = CompositeStopper(
MaxMetricCallsStopper(max_metric_calls=1000),
NoImprovementStopper(max_iterations_without_improvement=50),
TimeoutStopCondition(timeout_seconds=7200),
mode="any",
)
# Stop when ALL conditions are met
stopper = CompositeStopper(
MaxMetricCallsStopper(max_metric_calls=100),
ScoreThresholdStopper(threshold=0.9),
mode="all",
)
config = GEPAConfig(stop_callbacks=stopper)
Parameters:
Variable number of stopper instances to combine.
mode
Literal['any', 'all']
default:"'any'"
Combination mode:
"any": Stop when any stopper triggers (OR logic)
"all": Stop when all stoppers trigger (AND logic)
Location: stop_condition.py:193
Usage Examples
Single Stopper
from gepa.optimize_anything import optimize_anything, GEPAConfig, EngineConfig
from gepa.utils import NoImprovementStopper
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=1000),
stop_callbacks=NoImprovementStopper(max_iterations_without_improvement=30),
)
result = optimize_anything(
seed_candidate="initial code",
evaluator=my_evaluator,
config=config,
)
Multiple Stoppers (Automatic Composition)
from gepa.utils import (
NoImprovementStopper,
TimeoutStopCondition,
ScoreThresholdStopper,
)
# All stoppers in list are OR'd together
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=5000),
stop_callbacks=[
NoImprovementStopper(max_iterations_without_improvement=100),
TimeoutStopCondition(timeout_seconds=3600), # 1 hour
ScoreThresholdStopper(threshold=0.99), # 99% accuracy
],
)
# Stops when:
# - No improvement for 100 iterations, OR
# - 1 hour has elapsed, OR
# - Score reaches 0.99, OR
# - 5000 evaluations completed
Custom Stopper
from gepa.utils import StopperProtocol
from gepa.core.state import GEPAState
class CustomBusinessHoursStopper(StopperProtocol):
"""Stop optimization outside business hours."""
def __call__(self, gepa_state: GEPAState) -> bool:
from datetime import datetime
now = datetime.now()
# Stop if it's outside 9 AM - 5 PM
return now.hour < 9 or now.hour >= 17
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=10000),
stop_callbacks=CustomBusinessHoursStopper(),
)
Manual Stopper with AND Logic
from gepa.utils import (
CompositeStopper,
MaxMetricCallsStopper,
ScoreThresholdStopper,
)
# Only stop when BOTH conditions are met
stopper = CompositeStopper(
MaxMetricCallsStopper(max_metric_calls=200),
ScoreThresholdStopper(threshold=0.95),
mode="all", # AND logic
)
# Stops when:
# - At least 200 evaluations completed AND
# - Score reaches at least 0.95
config = GEPAConfig(stop_callbacks=stopper)
Graceful Shutdown with Signal Handler
from gepa.utils import SignalStopper
import signal
stopper = SignalStopper(signals=[signal.SIGINT, signal.SIGTERM])
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=10000),
stop_callbacks=stopper,
)
try:
result = optimize_anything(
seed_candidate=code,
evaluator=my_evaluator,
config=config,
)
finally:
# Restore original signal handlers
stopper.cleanup()
print("Optimization stopped gracefully")
File-Based Remote Control
from gepa.utils import FileStopper
stopper = FileStopper(stop_file_path="./optimization.stop")
config = GEPAConfig(
engine=EngineConfig(max_metric_calls=10000),
stop_callbacks=stopper,
)
result = optimize_anything(
seed_candidate=code,
evaluator=my_evaluator,
config=config,
)
# From another terminal or monitoring script:
# $ touch ./optimization.stop # Stops the optimization
# Clean up the stop file after optimization
stopper.remove_stop_file()
Best Practices
-
Always set a maximum budget: Use
EngineConfig.max_metric_calls to prevent runaway optimization.
-
Combine stoppers for safety: Use multiple conditions to ensure optimization terminates in reasonable time.
stop_callbacks=[
MaxMetricCallsStopper(10000), # Hard limit
TimeoutStopCondition(3600), # Time limit
NoImprovementStopper(100), # Early stopping
]
-
Use file stoppers for long runs: Enable manual intervention by setting
run_dir:
engine = EngineConfig(
run_dir="./experiments/run_001", # Enables gepa.stop file
max_metric_calls=10000,
)
-
Reset improvement counters carefully: Only call
NoImprovementStopper.reset() when you’ve externally validated an improvement.
-
Clean up signal handlers: Always call
SignalStopper.cleanup() after optimization to restore original handlers.