Skip to main content

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:
max_metric_calls
int
required
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:
max_proposals
int
required
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
int
required
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:
stop_file_path
str
required
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:
timeout_seconds
float
required
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:
threshold
float
required
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:
max_tracked_candidates
int
required
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:
*stoppers
StopperProtocol
required
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

  1. Always set a maximum budget: Use EngineConfig.max_metric_calls to prevent runaway optimization.
  2. 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
    ]
    
  3. 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,
    )
    
  4. Reset improvement counters carefully: Only call NoImprovementStopper.reset() when you’ve externally validated an improvement.
  5. Clean up signal handlers: Always call SignalStopper.cleanup() after optimization to restore original handlers.

Build docs developers (and LLMs) love