Skip to content

Domain

domain

Domain layer for gepa-adk evolution engine.

This module exports the core domain models for the GEPA-ADK evolution engine.

ATTRIBUTE DESCRIPTION
EvolutionConfig

Configuration parameters for evolution runs.

TYPE: class

EvolutionResult

Outcome of a completed evolution run.

TYPE: class

Candidate

Instruction candidate being evolved.

TYPE: class

IterationRecord

Metrics for a single evolution iteration.

TYPE: class

TokenRollup

Token usage summed over evaluated rows.

TYPE: class

Score

Type alias for normalized scores.

TYPE: type

ComponentName

Type alias for component identifiers.

TYPE: type

ModelName

Type alias for model identifiers.

TYPE: type

TrajectoryConfig

Configuration for trajectory extraction.

TYPE: class

StopReason

Why an evolution run terminated.

TYPE: enum

OnIterationCallback

Callback invoked after each iteration.

TYPE: type

ProposalValidator

Check run on each proposed component text.

TYPE: type

DEFAULT_SENSITIVE_KEYS

Default keys for trajectory redaction.

TYPE: tuple

CURRENT_SCHEMA_VERSION

Current result schema version.

TYPE: int

EvolutionError

Base exception for all gepa-adk errors.

TYPE: class

ConfigurationError

Raised when configuration validation fails.

TYPE: class

EvaluationError

Raised when batch evaluation fails.

TYPE: class

AdapterError

Raised when adapter operations fail.

TYPE: class

EmptyProposalError

Raised when reflection stays empty after one retry.

TYPE: class

ReflectionTimeoutError

Raised when the reflection agent exceeds its timeout.

TYPE: class

IncompleteProposalError

Raised when the reflection output was cut off or opens a reasoning tag it never closes.

TYPE: class

ReflectionError

Raised when the reflection function raises; retryable errors are skipped, others abort the run.

TYPE: class

MultiAgentEvolutionResult

Outcome of a completed multi-agent evolution run.

TYPE: class

ParetoState

Tracks evolution state for Pareto-aware selection.

TYPE: class

ParetoFrontier

Tracks non-dominated candidates across multiple frontier dimensions.

TYPE: class

StopperState

Immutable snapshot of evolution state for stopper decisions.

TYPE: class

ADKTrajectory

Execution trace from ADK agent evaluation.

TYPE: class

MultiAgentTrajectory

Execution trace from multi-agent pipeline evaluation.

TYPE: class

ToolCallRecord

Record of a single tool call during agent execution.

TYPE: class

TokenUsage

Token usage statistics from LLM calls.

TYPE: class

COMPONENT_INSTRUCTION

Name of the instruction component.

TYPE: str

COMPONENT_OUTPUT_SCHEMA

Name of the output schema component.

TYPE: str

COMPONENT_GENERATE_CONFIG

Name of the generate_content_config component.

TYPE: str

DEFAULT_COMPONENT_NAME

Component evolved when none is named (instruction).

TYPE: str

FrontierType

Supported frontier tracking strategies for Pareto selection.

TYPE: Enum

MultiAgentCandidate

Mapping of qualified component names to their text.

TYPE: type

MergeAttempt

Merged candidate with its parent and ancestor indices, or None.

TYPE: type

AncestorLog

Parent and ancestor indices of an attempted merge.

TYPE: type

SchemaConstraints

Constraints for output schema evolution.

TYPE: class

ScoringError

Base exception for all scoring-related errors.

TYPE: class

CriticOutputParseError

Raised when critic agent output cannot be parsed as valid JSON.

TYPE: class

MissingScoreFieldError

Raised when score field is missing or null in parsed output.

TYPE: class

MultiAgentValidationError

Raised when multi-agent configuration validation fails.

TYPE: class

NoCandidateAvailableError

Raised when no candidates are available for selection.

TYPE: class

VideoValidationError

Raised when video file validation fails.

TYPE: class

Examples:

Basic usage with configuration and records:

from gepa_adk.domain import EvolutionConfig, IterationRecord

config = EvolutionConfig(max_iterations=20)
record = IterationRecord(
    iteration_number=1,
    score=0.85,
    component_text="Test",
    evolved_component="instruction",
    accepted=True,
)
See Also
Notes

This package contains pure domain logic with no external dependencies. All models follow hexagonal architecture principles (ADR-000).

CURRENT_SCHEMA_VERSION module-attribute

CURRENT_SCHEMA_VERSION = 7

Schema version for evolution result serialization.

Incremented when the result schema changes in a way that requires migration logic in from_dict().

COMPONENT_GENERATE_CONFIG module-attribute

COMPONENT_GENERATE_CONFIG: ComponentName = (
    "generate_content_config"
)

Component name for LLM generation configuration (temperature, top_p, etc.).

COMPONENT_INSTRUCTION module-attribute

COMPONENT_INSTRUCTION: ComponentName = 'instruction'

Component name for agent instructions (same as DEFAULT_COMPONENT_NAME).

COMPONENT_OUTPUT_SCHEMA module-attribute

COMPONENT_OUTPUT_SCHEMA: ComponentName = 'output_schema'

Component name for Pydantic output schema definitions.

DEFAULT_COMPONENT_NAME module-attribute

DEFAULT_COMPONENT_NAME: ComponentName = 'instruction'

Default component name for single-component evolution.

This constant provides a single source of truth for the default component name used when evolving a single component (typically an agent's instruction). Use this constant instead of hardcoding 'instruction' throughout the codebase.

DEFAULT_SENSITIVE_KEYS module-attribute

DEFAULT_SENSITIVE_KEYS: tuple[str, ...] = (
    "api_key",
    "token",
    "secret",
    "password",
    "credential",
    "authorization",
    "bearer",
)

Default keys treated as sensitive during trajectory redaction.

Used by TrajectoryConfig.sensitive_keys as the default value. Pass sensitive_keys=() to disable key-based redaction.

AncestorLog module-attribute

AncestorLog: TypeAlias = tuple[int, int, int]

Type alias for tracking attempted merges.

Represents a merge attempt triplet that has been tried, preventing duplicate merges.

Type

tuple[int, int, int]: (parent1_idx, parent2_idx, ancestor_idx)

Examples:

from gepa_adk.domain.types import AncestorLog

# Log of attempted merge
log: AncestorLog = (5, 8, 2)  # (parent1_idx, parent2_idx, ancestor_idx)
Notes

Type alias used by MergeProposer to track which merge combinations have already been attempted, preventing redundant merge operations.

ComponentName module-attribute

ComponentName: TypeAlias = str

Name of a candidate component (e.g., 'instruction', 'output_schema').

MergeAttempt module-attribute

MergeAttempt: TypeAlias = (
    tuple["Candidate", int, int, int] | None
)

Type alias for merge attempt results.

Represents a successful merge attempt with the merged candidate and parent/ancestor indices, or None if merge was not possible.

Type:
    tuple[Candidate, int, int, int] | None: (merged_candidate, parent1_idx,
        parent2_idx, ancestor_idx) or None

Examples:

from gepa_adk.domain.types import MergeAttempt
from gepa_adk.domain.models import Candidate

# Successful merge
attempt: MergeAttempt = (
    Candidate(components={"instruction": "..."}),
    5,  # parent1_idx
    8,  # parent2_idx
    2,  # ancestor_idx
)

# Failed merge
failed: MergeAttempt = None
Notes

Type alias reserved for future merge reporting; currently unused but kept for parity with the merge-proposer design docs.

ModelName module-attribute

ModelName: TypeAlias = str

Model identifier (e.g., 'ollama_chat/gpt-oss:20b', 'gemini-3.8-flash', 'gpt-4o').

MultiAgentCandidate module-attribute

MultiAgentCandidate: TypeAlias = dict[str, str]

Type alias for multi-agent candidate structure.

Maps qualified component names to their values using dot-separated format: {agent_name}.{component_name} as the key.

See ADR-012 for the addressing scheme rationale.

Examples:

Basic multi-agent candidate:

from gepa_adk.domain.types import MultiAgentCandidate, ComponentSpec

# Using ComponentSpec for type-safe construction
gen_inst = ComponentSpec(agent="generator", component="instruction")
critic_schema = ComponentSpec(agent="critic", component="output_schema")

candidate: MultiAgentCandidate = {
    gen_inst.qualified: "Generate Python code...",
    critic_schema.qualified: "Review code output schema...",
}

# Equivalent direct construction
candidate: MultiAgentCandidate = {
    "generator.instruction": "Generate Python code...",
    "critic.output_schema": "Review code output schema...",
}
Notes

This type alias is compatible with GEPA's Candidate type (dict[str, str]), enabling seamless integration with existing mutation proposers and evolution engine components.

OnIterationCallback module-attribute

OnIterationCallback: TypeAlias = Callable[
    ["IterationRecord", str | None], Awaitable[None] | None
]

Callback the engine invokes after each iteration record is appended.

Type

Callable[[IterationRecord, str | None], Awaitable[None] | None]: Receives the iteration's record and the id of the candidate it concerns (None for an empty_proposal skip). May be sync or async.

Examples:

from gepa_adk.domain.types import OnIterationCallback


def log_iteration(record, candidate_id):
    print(record.iteration_number, record.score, candidate_id)


callback: OnIterationCallback = log_iteration
Notes

Set it as EvolutionConfig.on_iteration. An awaitable return value is awaited before the loop continues; exceptions propagate out of run().

ProposalValidator module-attribute

ProposalValidator: TypeAlias = Callable[
    [str, str], str | None
]

Check the engine runs on each proposed component text before evaluation.

Type

Callable[[str, str], str | None]: Receives the component name and the proposed text. Returns None to accept the proposal, or a short reason string to reject it.

Examples:

from gepa_adk.domain.types import ProposalValidator


def require_placeholder(component, text):
    if "{question}" not in text:
        return "dropped the {question} placeholder"
    return None


validator: ProposalValidator = require_placeholder
Notes

Set it as EvolutionConfig.proposal_validator. A rejected proposal is recorded with skip_reason="proposal_rejected" and the reason, costs no evaluation and counts toward patience. Exceptions propagate out of run().

Score module-attribute

Score: TypeAlias = float

Normalized score, typically in [0.0, 1.0].

AdapterError

Bases: EvaluationError


              flowchart TD
              gepa_adk.domain.AdapterError[AdapterError]
              gepa_adk.domain.exceptions.EvaluationError[EvaluationError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvaluationError --> gepa_adk.domain.AdapterError
                                gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.exceptions.EvaluationError
                



              click gepa_adk.domain.AdapterError href "" "gepa_adk.domain.AdapterError"
              click gepa_adk.domain.exceptions.EvaluationError href "" "gepa_adk.domain.exceptions.EvaluationError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when an adapter operation fails.

This exception is used by adapter implementations (e.g., ADKAdapter) for adapter-specific failures such as session errors or configuration issues.

Examples:

Raising an adapter error:

from gepa_adk.domain.exceptions import AdapterError

if not self._session_service:
    raise AdapterError(
        "Session service unavailable",
        adapter="ADKAdapter",
        operation="evaluate",
    )
Notes

AdapterError is a subclass of EvaluationError, so callers can catch either for different granularity of error handling.

Source code in src/gepa_adk/domain/exceptions.py
class AdapterError(EvaluationError):
    """Raised when an adapter operation fails.

    This exception is used by adapter implementations (e.g., ADKAdapter)
    for adapter-specific failures such as session errors or configuration
    issues.

    Examples:
        Raising an adapter error:

        ```python
        from gepa_adk.domain.exceptions import AdapterError

        if not self._session_service:
            raise AdapterError(
                "Session service unavailable",
                adapter="ADKAdapter",
                operation="evaluate",
            )
        ```

    Notes:
        AdapterError is a subclass of EvaluationError, so callers can
        catch either for different granularity of error handling.
    """

ConfigurationError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.ConfigurationError[ConfigurationError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.ConfigurationError
                


              click gepa_adk.domain.ConfigurationError href "" "gepa_adk.domain.ConfigurationError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when configuration validation fails.

This exception is raised during EvolutionConfig initialization when a parameter violates its validation constraints.

ATTRIBUTE DESCRIPTION
field

The name of the configuration field that failed validation.

TYPE: str | None

value

The invalid value that was provided.

TYPE: object

constraint

Description of the validation constraint that was violated.

TYPE: str | None

Examples:

Creating a configuration error with context:

from gepa_adk.domain.exceptions import ConfigurationError

error = ConfigurationError(
    "max_iterations must be non-negative",
    field="max_iterations",
    value=-5,
    constraint=">= 0",
)
print(error.field, error.value, error.constraint)
# Output: max_iterations -5 >= 0
Notes

Arises from user-provided invalid settings, not programming errors. Should be caught and reported with clear guidance on valid values.

Source code in src/gepa_adk/domain/exceptions.py
class ConfigurationError(EvolutionError):
    """Raised when configuration validation fails.

    This exception is raised during EvolutionConfig initialization
    when a parameter violates its validation constraints.

    Attributes:
        field (str | None): The name of the configuration field that failed
            validation.
        value (object): The invalid value that was provided.
        constraint (str | None): Description of the validation constraint that
            was violated.

    Examples:
        Creating a configuration error with context:

        ```python
        from gepa_adk.domain.exceptions import ConfigurationError

        error = ConfigurationError(
            "max_iterations must be non-negative",
            field="max_iterations",
            value=-5,
            constraint=">= 0",
        )
        print(error.field, error.value, error.constraint)
        # Output: max_iterations -5 >= 0
        ```

    Notes:
        Arises from user-provided invalid settings, not programming errors.
        Should be caught and reported with clear guidance on valid values.
    """

    def __init__(
        self,
        message: str,
        *,
        field: str | None = None,
        value: object = None,
        constraint: str | None = None,
    ) -> None:
        """Initialize ConfigurationError with context.

        Args:
            message: Human-readable error description.
            field: Name of the invalid configuration field.
            value: The invalid value provided.
            constraint: Description of the validation constraint.

        """
        super().__init__(message)
        self.field = field
        self.value = value
        self.constraint = constraint

    def __str__(self) -> str:
        """Return string representation with context.

        Returns:
            Formatted error message including field and value context.

        Notes:
            Outputs formatted error message with field and value context
            when available, preserving base message structure.
        """
        base = super().__str__()
        context_parts = []
        if self.field is not None:
            context_parts.append(f"field={self.field!r}")
        if self.value is not None:
            context_parts.append(f"value={self.value!r}")
        if context_parts:
            return f"{base} [{', '.join(context_parts)}]"
        return base

__init__

__init__(
    message: str,
    *,
    field: str | None = None,
    value: object = None,
    constraint: str | None = None,
) -> None

Initialize ConfigurationError with context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

field

Name of the invalid configuration field.

TYPE: str | None DEFAULT: None

value

The invalid value provided.

TYPE: object DEFAULT: None

constraint

Description of the validation constraint.

TYPE: str | None DEFAULT: None

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    field: str | None = None,
    value: object = None,
    constraint: str | None = None,
) -> None:
    """Initialize ConfigurationError with context.

    Args:
        message: Human-readable error description.
        field: Name of the invalid configuration field.
        value: The invalid value provided.
        constraint: Description of the validation constraint.

    """
    super().__init__(message)
    self.field = field
    self.value = value
    self.constraint = constraint

__str__

__str__() -> str

Return string representation with context.

RETURNS DESCRIPTION
str

Formatted error message including field and value context.

Notes

Outputs formatted error message with field and value context when available, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string representation with context.

    Returns:
        Formatted error message including field and value context.

    Notes:
        Outputs formatted error message with field and value context
        when available, preserving base message structure.
    """
    base = super().__str__()
    context_parts = []
    if self.field is not None:
        context_parts.append(f"field={self.field!r}")
    if self.value is not None:
        context_parts.append(f"value={self.value!r}")
    if context_parts:
        return f"{base} [{', '.join(context_parts)}]"
    return base

CriticOutputParseError

Bases: ScoringError


              flowchart TD
              gepa_adk.domain.CriticOutputParseError[CriticOutputParseError]
              gepa_adk.domain.exceptions.ScoringError[ScoringError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.ScoringError --> gepa_adk.domain.CriticOutputParseError
                                gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.exceptions.ScoringError
                



              click gepa_adk.domain.CriticOutputParseError href "" "gepa_adk.domain.CriticOutputParseError"
              click gepa_adk.domain.exceptions.ScoringError href "" "gepa_adk.domain.exceptions.ScoringError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when critic agent output cannot be parsed as valid JSON.

This exception indicates that the critic agent returned output that could not be parsed as JSON or did not conform to the expected schema.

ATTRIBUTE DESCRIPTION
raw_output

The unparseable output from critic.

TYPE: str

parse_error

Description of parsing failure.

TYPE: str

Examples:

Handling parse errors:

from gepa_adk.domain.exceptions import CriticOutputParseError

try:
    score, metadata = await scorer.async_score(...)
except CriticOutputParseError as e:
    print(f"Invalid JSON: {e.raw_output}")
    print(f"Error: {e.parse_error}")
Notes

Arises when critic agent output cannot be parsed as valid JSON. Typically occurs when LLM output doesn't follow structured format despite output_schema being set.

Source code in src/gepa_adk/domain/exceptions.py
class CriticOutputParseError(ScoringError):
    """Raised when critic agent output cannot be parsed as valid JSON.

    This exception indicates that the critic agent returned output that
    could not be parsed as JSON or did not conform to the expected schema.

    Attributes:
        raw_output (str): The unparseable output from critic.
        parse_error (str): Description of parsing failure.

    Examples:
        Handling parse errors:

        ```python
        from gepa_adk.domain.exceptions import CriticOutputParseError

        try:
            score, metadata = await scorer.async_score(...)
        except CriticOutputParseError as e:
            print(f"Invalid JSON: {e.raw_output}")
            print(f"Error: {e.parse_error}")
        ```

    Notes:
        Arises when critic agent output cannot be parsed as valid JSON.
        Typically occurs when LLM output doesn't follow structured format
        despite output_schema being set.
    """

    def __init__(
        self,
        message: str,
        *,
        raw_output: str,
        parse_error: str,
        cause: Exception | None = None,
    ) -> None:
        """Initialize CriticOutputParseError with context.

        Args:
            message: Human-readable error description.
            raw_output: The unparseable output string from critic.
            parse_error: Description of the parsing failure.
            cause: Original exception that caused this error.

        """
        super().__init__(message, cause=cause)
        self.raw_output = raw_output
        self.parse_error = parse_error

    def __str__(self) -> str:
        """Return string with parse error details.

        Returns:
            Formatted message including parse error and raw output preview.

        Notes:
            Outputs formatted error message with parse error and raw output
            preview (the first 100 chars followed by ``...`` when longer),
            preserving base message structure.
        """
        base = super().__str__()
        output_preview = (
            self.raw_output[:100] + "..."
            if len(self.raw_output) > 100  # noqa: PLR2004  # preview length local to this __str__
            else self.raw_output
        )
        return (
            f"{base} [parse_error={self.parse_error!r}, raw_output={output_preview!r}]"
        )

__init__

__init__(
    message: str,
    *,
    raw_output: str,
    parse_error: str,
    cause: Exception | None = None,
) -> None

Initialize CriticOutputParseError with context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

raw_output

The unparseable output string from critic.

TYPE: str

parse_error

Description of the parsing failure.

TYPE: str

cause

Original exception that caused this error.

TYPE: Exception | None DEFAULT: None

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    raw_output: str,
    parse_error: str,
    cause: Exception | None = None,
) -> None:
    """Initialize CriticOutputParseError with context.

    Args:
        message: Human-readable error description.
        raw_output: The unparseable output string from critic.
        parse_error: Description of the parsing failure.
        cause: Original exception that caused this error.

    """
    super().__init__(message, cause=cause)
    self.raw_output = raw_output
    self.parse_error = parse_error

__str__

__str__() -> str

Return string with parse error details.

RETURNS DESCRIPTION
str

Formatted message including parse error and raw output preview.

Notes

Outputs formatted error message with parse error and raw output preview (the first 100 chars followed by ... when longer), preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string with parse error details.

    Returns:
        Formatted message including parse error and raw output preview.

    Notes:
        Outputs formatted error message with parse error and raw output
        preview (the first 100 chars followed by ``...`` when longer),
        preserving base message structure.
    """
    base = super().__str__()
    output_preview = (
        self.raw_output[:100] + "..."
        if len(self.raw_output) > 100  # noqa: PLR2004  # preview length local to this __str__
        else self.raw_output
    )
    return (
        f"{base} [parse_error={self.parse_error!r}, raw_output={output_preview!r}]"
    )

EmptyProposalError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.EmptyProposalError[EmptyProposalError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.EmptyProposalError
                


              click gepa_adk.domain.EmptyProposalError href "" "gepa_adk.domain.EmptyProposalError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when reflection returns an empty proposal after one retry.

The engine treats this error as a failed iteration rather than a fatal one: it records the iteration as skipped and continues the loop.

ATTRIBUTE DESCRIPTION
component

Name of the component the reflection was proposing text for.

TYPE: str

Examples:

raise EmptyProposalError("instruction")
Notes

Raised by the mutation proposer after the reflection function has returned an empty or whitespace-only string twice in a row.

Source code in src/gepa_adk/domain/exceptions.py
class EmptyProposalError(EvolutionError):
    """Raised when reflection returns an empty proposal after one retry.

    The engine treats this error as a failed iteration rather than a fatal
    one: it records the iteration as skipped and continues the loop.

    Attributes:
        component (str): Name of the component the reflection was proposing
            text for.

    Examples:
        ```python
        raise EmptyProposalError("instruction")
        ```

    Notes:
        Raised by the mutation proposer after the reflection function has
        returned an empty or whitespace-only string twice in a row.
    """

    def __init__(self, component: str) -> None:
        """Initialize EmptyProposalError with the component name.

        Args:
            component: Name of the component whose reflection was empty.
        """
        super().__init__(
            f"Reflection agent returned empty string for component "
            f"{component!r} after one retry. Expected non-empty string "
            "with proposed component text."
        )
        self.component = component

__init__

__init__(component: str) -> None

Initialize EmptyProposalError with the component name.

PARAMETER DESCRIPTION
component

Name of the component whose reflection was empty.

TYPE: str

Source code in src/gepa_adk/domain/exceptions.py
def __init__(self, component: str) -> None:
    """Initialize EmptyProposalError with the component name.

    Args:
        component: Name of the component whose reflection was empty.
    """
    super().__init__(
        f"Reflection agent returned empty string for component "
        f"{component!r} after one retry. Expected non-empty string "
        "with proposed component text."
    )
    self.component = component

EvaluationError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.EvaluationError[EvaluationError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.EvaluationError
                


              click gepa_adk.domain.EvaluationError href "" "gepa_adk.domain.EvaluationError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when batch evaluation fails.

This exception indicates failures during agent evaluation, such as agent execution errors, timeout, or malformed output.

ATTRIBUTE DESCRIPTION
cause

Original exception that caused this error.

TYPE: Exception | None

context

Additional context for debugging.

TYPE: dict

Examples:

Wrapping an ADK error:

from gepa_adk.domain.exceptions import EvaluationError

try:
    result = await runner.run_async(...)
except ADKError as e:
    raise EvaluationError(
        "Agent execution failed",
        cause=e,
        agent_name="my_agent",
    ) from e
Notes

Always preserves the original cause for debugging while providing a consistent interface for error handling.

Source code in src/gepa_adk/domain/exceptions.py
class EvaluationError(EvolutionError):
    """Raised when batch evaluation fails.

    This exception indicates failures during agent evaluation,
    such as agent execution errors, timeout, or malformed output.

    Attributes:
        cause (Exception | None): Original exception that caused this error.
        context (dict): Additional context for debugging.

    Examples:
        Wrapping an ADK error:

        ```python
        from gepa_adk.domain.exceptions import EvaluationError

        try:
            result = await runner.run_async(...)
        except ADKError as e:
            raise EvaluationError(
                "Agent execution failed",
                cause=e,
                agent_name="my_agent",
            ) from e
        ```

    Notes:
        Always preserves the original cause for debugging while providing
        a consistent interface for error handling.
    """

    def __init__(
        self,
        message: str,
        *,
        cause: Exception | None = None,
        **context: object,
    ) -> None:
        """Initialize EvaluationError with cause and context.

        Args:
            message: Human-readable error description.
            cause: Original exception that caused this error.
            **context: Arbitrary key-value pairs stored as exception attributes
                for debugging context.

        Notes:
            Context is passed via keyword arguments. Positional arguments
            after message are not allowed.
        """
        super().__init__(message)
        self.cause = cause
        self.context = context

    def __str__(self) -> str:
        """Return string with cause chain if present.

        Returns:
            Formatted message with context and cause information.

        Notes:
            Outputs formatted error message with context dict and cause chain
            when available, preserving base message structure.
        """
        base = super().__str__()
        if self.context:
            ctx_str = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
            base = f"{base} [{ctx_str}]"
        if self.cause:
            base = f"{base} (caused by: {self.cause})"
        return base

__init__

__init__(
    message: str,
    *,
    cause: Exception | None = None,
    **context: object,
) -> None

Initialize EvaluationError with cause and context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

cause

Original exception that caused this error.

TYPE: Exception | None DEFAULT: None

**context

Arbitrary key-value pairs stored as exception attributes for debugging context.

TYPE: object DEFAULT: {}

Notes

Context is passed via keyword arguments. Positional arguments after message are not allowed.

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    cause: Exception | None = None,
    **context: object,
) -> None:
    """Initialize EvaluationError with cause and context.

    Args:
        message: Human-readable error description.
        cause: Original exception that caused this error.
        **context: Arbitrary key-value pairs stored as exception attributes
            for debugging context.

    Notes:
        Context is passed via keyword arguments. Positional arguments
        after message are not allowed.
    """
    super().__init__(message)
    self.cause = cause
    self.context = context

__str__

__str__() -> str

Return string with cause chain if present.

RETURNS DESCRIPTION
str

Formatted message with context and cause information.

Notes

Outputs formatted error message with context dict and cause chain when available, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string with cause chain if present.

    Returns:
        Formatted message with context and cause information.

    Notes:
        Outputs formatted error message with context dict and cause chain
        when available, preserving base message structure.
    """
    base = super().__str__()
    if self.context:
        ctx_str = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
        base = f"{base} [{ctx_str}]"
    if self.cause:
        base = f"{base} (caused by: {self.cause})"
    return base

EvolutionError

Bases: Exception


              flowchart TD
              gepa_adk.domain.EvolutionError[EvolutionError]

              

              click gepa_adk.domain.EvolutionError href "" "gepa_adk.domain.EvolutionError"
            

Base exception for all gepa-adk errors.

All custom exceptions in gepa-adk should inherit from this class to allow for unified exception handling.

ATTRIBUTE DESCRIPTION
partial_result

Partial result the engine attaches when it aborts a run after the baseline was scored, built with StopReason.ERROR from the iterations recorded so far. None when the error was raised before the baseline score existed or outside an engine run.

TYPE: EvolutionResult | None

Examples:

Catching evolution errors:

from gepa_adk.domain.exceptions import EvolutionError

try:
    raise EvolutionError("Evolution failed unexpectedly")
except EvolutionError as e:
    print(f"Caught: {e}")
# Output: Caught: Evolution failed unexpectedly
Notes

Always use this base class or its subclasses for domain errors. Standard Python exceptions should still be raised for programming errors (e.g., TypeError, ValueError for developer mistakes).

Source code in src/gepa_adk/domain/exceptions.py
class EvolutionError(Exception):
    """Base exception for all gepa-adk errors.

    All custom exceptions in gepa-adk should inherit from this class
    to allow for unified exception handling.

    Attributes:
        partial_result (EvolutionResult | None): Partial result the engine
            attaches when it aborts a run after the baseline was scored,
            built with ``StopReason.ERROR`` from the iterations recorded so
            far. ``None`` when the error was raised before the baseline
            score existed or outside an engine run.

    Examples:
        Catching evolution errors:

        ```python
        from gepa_adk.domain.exceptions import EvolutionError

        try:
            raise EvolutionError("Evolution failed unexpectedly")
        except EvolutionError as e:
            print(f"Caught: {e}")
        # Output: Caught: Evolution failed unexpectedly
        ```

    Notes:
        Always use this base class or its subclasses for domain errors.
        Standard Python exceptions should still be raised for programming
        errors (e.g., TypeError, ValueError for developer mistakes).
    """

    partial_result: "EvolutionResult | None" = None

IncompleteProposalError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.IncompleteProposalError[IncompleteProposalError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.IncompleteProposalError
                


              click gepa_adk.domain.IncompleteProposalError href "" "gepa_adk.domain.IncompleteProposalError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when the reflection output is incomplete.

A proposal is incomplete when the last model response behind it reports a length stop (the output-token limit), or when its text opens a reasoning tag such as <think> that it never closes. The engine treats this error as a skipped iteration: it records the iteration with skip_reason="incomplete_proposal", keeps the raw text as the record's component_text, counts it toward patience and continues the loop without evaluating. The proposer does not retry it.

ATTRIBUTE DESCRIPTION
component

Name of the component the reflection was proposing text for.

TYPE: str

finish_reason

Why the proposal is incomplete: the finish reason name (for example "MAX_TOKENS") or "unterminated_<tag>" for an unclosed reasoning tag.

TYPE: str

raw_text

The incomplete text the reflection produced.

TYPE: str

output_length

Length of raw_text in characters.

TYPE: int

Examples:

raise IncompleteProposalError(
    "instruction", finish_reason="MAX_TOKENS", raw_text="Be concise and"
)
Notes

Raised by the ADK reflection function after a successful execution, before the empty-response handling, so a length stop with no text is still reported as incomplete.

Source code in src/gepa_adk/domain/exceptions.py
class IncompleteProposalError(EvolutionError):
    """Raised when the reflection output is incomplete.

    A proposal is incomplete when the last model response behind it reports
    a length stop (the output-token limit), or when its text opens a
    reasoning tag such as ``<think>`` that it never closes. The engine
    treats this error as a skipped iteration: it records the iteration with
    ``skip_reason="incomplete_proposal"``, keeps the raw text as the
    record's ``component_text``, counts it toward patience and continues
    the loop without evaluating. The proposer does not retry it.

    Attributes:
        component (str): Name of the component the reflection was proposing
            text for.
        finish_reason (str): Why the proposal is incomplete: the finish
            reason name (for example ``"MAX_TOKENS"``) or
            ``"unterminated_<tag>"`` for an unclosed reasoning tag.
        raw_text (str): The incomplete text the reflection produced.
        output_length (int): Length of ``raw_text`` in characters.

    Examples:
        ```python
        raise IncompleteProposalError(
            "instruction", finish_reason="MAX_TOKENS", raw_text="Be concise and"
        )
        ```

    Notes:
        Raised by the ADK reflection function after a successful execution,
        before the empty-response handling, so a length stop with no text
        is still reported as incomplete.
    """

    def __init__(self, component: str, *, finish_reason: str, raw_text: str) -> None:
        """Initialize IncompleteProposalError with the incomplete output.

        Args:
            component: Name of the component whose proposal is incomplete.
            finish_reason: Finish reason name or ``"unterminated_<tag>"``.
            raw_text: The incomplete text the reflection produced.
        """
        output_length = len(raw_text)
        super().__init__(
            f"Reflection output for component {component!r} is incomplete "
            f"(finish_reason={finish_reason}, {output_length} characters)."
        )
        self.component = component
        self.finish_reason = finish_reason
        self.raw_text = raw_text
        self.output_length = output_length

__init__

__init__(
    component: str, *, finish_reason: str, raw_text: str
) -> None

Initialize IncompleteProposalError with the incomplete output.

PARAMETER DESCRIPTION
component

Name of the component whose proposal is incomplete.

TYPE: str

finish_reason

Finish reason name or "unterminated_<tag>".

TYPE: str

raw_text

The incomplete text the reflection produced.

TYPE: str

Source code in src/gepa_adk/domain/exceptions.py
def __init__(self, component: str, *, finish_reason: str, raw_text: str) -> None:
    """Initialize IncompleteProposalError with the incomplete output.

    Args:
        component: Name of the component whose proposal is incomplete.
        finish_reason: Finish reason name or ``"unterminated_<tag>"``.
        raw_text: The incomplete text the reflection produced.
    """
    output_length = len(raw_text)
    super().__init__(
        f"Reflection output for component {component!r} is incomplete "
        f"(finish_reason={finish_reason}, {output_length} characters)."
    )
    self.component = component
    self.finish_reason = finish_reason
    self.raw_text = raw_text
    self.output_length = output_length

MissingScoreFieldError

Bases: ScoringError


              flowchart TD
              gepa_adk.domain.MissingScoreFieldError[MissingScoreFieldError]
              gepa_adk.domain.exceptions.ScoringError[ScoringError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.ScoringError --> gepa_adk.domain.MissingScoreFieldError
                                gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.exceptions.ScoringError
                



              click gepa_adk.domain.MissingScoreFieldError href "" "gepa_adk.domain.MissingScoreFieldError"
              click gepa_adk.domain.exceptions.ScoringError href "" "gepa_adk.domain.exceptions.ScoringError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when score field is missing or null in parsed output.

This exception indicates that the output was successfully parsed and validated, but the required score field is missing or null.

ATTRIBUTE DESCRIPTION
parsed_output

The parsed output without valid score.

TYPE: dict[str, Any]

available_fields

Fields that were present.

TYPE: list[str]

Examples:

Handling missing score field:

from gepa_adk.domain.exceptions import MissingScoreFieldError

try:
    score, metadata = await scorer.async_score(...)
except MissingScoreFieldError as e:
    print(f"Missing score. Available fields: {e.available_fields}")
Notes

Applies when parsed output lacks a valid score value. The parsed_output may contain other valid fields that will be preserved in metadata if score is found.

Source code in src/gepa_adk/domain/exceptions.py
class MissingScoreFieldError(ScoringError):
    """Raised when score field is missing or null in parsed output.

    This exception indicates that the output was successfully parsed
    and validated, but the required `score` field is missing or null.

    Attributes:
        parsed_output (dict[str, Any]): The parsed output without valid score.
        available_fields (list[str]): Fields that were present.

    Examples:
        Handling missing score field:

        ```python
        from gepa_adk.domain.exceptions import MissingScoreFieldError

        try:
            score, metadata = await scorer.async_score(...)
        except MissingScoreFieldError as e:
            print(f"Missing score. Available fields: {e.available_fields}")
        ```

    Notes:
        Applies when parsed output lacks a valid score value.
        The parsed_output may contain other valid fields that will be
        preserved in metadata if score is found.
    """

    def __init__(
        self,
        message: str,
        *,
        parsed_output: dict[str, Any],
        cause: Exception | None = None,
    ) -> None:
        """Initialize MissingScoreFieldError with parsed output.

        Args:
            message: Human-readable error description.
            parsed_output: The parsed dict without valid score field.
            cause: Original exception that caused this error.

        """
        super().__init__(message, cause=cause)
        self.parsed_output = parsed_output
        self.available_fields = list(parsed_output.keys())

    def __str__(self) -> str:
        """Return string with available fields.

        Returns:
            Formatted message including list of available fields.

        Notes:
            Outputs formatted error message with list of available fields
            from parsed output, preserving base message structure.
        """
        base = super().__str__()
        return f"{base} [available_fields={self.available_fields}]"

__init__

__init__(
    message: str,
    *,
    parsed_output: dict[str, Any],
    cause: Exception | None = None,
) -> None

Initialize MissingScoreFieldError with parsed output.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

parsed_output

The parsed dict without valid score field.

TYPE: dict[str, Any]

cause

Original exception that caused this error.

TYPE: Exception | None DEFAULT: None

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    parsed_output: dict[str, Any],
    cause: Exception | None = None,
) -> None:
    """Initialize MissingScoreFieldError with parsed output.

    Args:
        message: Human-readable error description.
        parsed_output: The parsed dict without valid score field.
        cause: Original exception that caused this error.

    """
    super().__init__(message, cause=cause)
    self.parsed_output = parsed_output
    self.available_fields = list(parsed_output.keys())

__str__

__str__() -> str

Return string with available fields.

RETURNS DESCRIPTION
str

Formatted message including list of available fields.

Notes

Outputs formatted error message with list of available fields from parsed output, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string with available fields.

    Returns:
        Formatted message including list of available fields.

    Notes:
        Outputs formatted error message with list of available fields
        from parsed output, preserving base message structure.
    """
    base = super().__str__()
    return f"{base} [available_fields={self.available_fields}]"

MultiAgentValidationError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.MultiAgentValidationError[MultiAgentValidationError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.MultiAgentValidationError
                


              click gepa_adk.domain.MultiAgentValidationError href "" "gepa_adk.domain.MultiAgentValidationError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when multi-agent configuration validation fails.

This exception is raised during MultiAgentAdapter initialization when a parameter violates its validation constraints.

ATTRIBUTE DESCRIPTION
field

The name of the configuration field that failed validation.

TYPE: str

value

The invalid value that was provided.

TYPE: object

constraint

Description of the validation constraint that was violated.

TYPE: str

Examples:

Creating a multi-agent validation error:

from gepa_adk.domain.exceptions import MultiAgentValidationError

error = MultiAgentValidationError(
    "agents list cannot be empty",
    field="agents",
    value=[],
    constraint="len >= 1",
)
print(error.field, error.value)  # agents []
Notes

Arises from user-provided invalid multi-agent settings, not programming errors. Should be caught and reported with clear guidance on valid values.

Source code in src/gepa_adk/domain/exceptions.py
class MultiAgentValidationError(EvolutionError):
    """Raised when multi-agent configuration validation fails.

    This exception is raised during MultiAgentAdapter initialization
    when a parameter violates its validation constraints.

    Attributes:
        field (str): The name of the configuration field that failed
            validation.
        value (object): The invalid value that was provided.
        constraint (str): Description of the validation constraint that
            was violated.

    Examples:
        Creating a multi-agent validation error:

        ```python
        from gepa_adk.domain.exceptions import MultiAgentValidationError

        error = MultiAgentValidationError(
            "agents list cannot be empty",
            field="agents",
            value=[],
            constraint="len >= 1",
        )
        print(error.field, error.value)  # agents []
        ```

    Notes:
        Arises from user-provided invalid multi-agent settings, not
        programming errors. Should be caught and reported with clear
        guidance on valid values.
    """

    def __init__(
        self,
        message: str,
        *,
        field: str,
        value: object,
        constraint: str,
    ) -> None:
        """Initialize MultiAgentValidationError with context.

        Args:
            message: Human-readable error description.
            field: Name of the invalid configuration field.
            value: The invalid value provided.
            constraint: Description of the validation constraint.

        """
        super().__init__(message)
        self.field = field
        self.value = value
        self.constraint = constraint

    def __str__(self) -> str:
        """Return string representation with context.

        Returns:
            Formatted error message including field and value context.

        Notes:
            Outputs formatted error message with field, value, and constraint
            context when available, preserving base message structure.
        """
        base = super().__str__()
        return (
            f"{base} [field={self.field!r}, value={self.value!r}, "
            f"constraint={self.constraint!r}]"
        )

__init__

__init__(
    message: str,
    *,
    field: str,
    value: object,
    constraint: str,
) -> None

Initialize MultiAgentValidationError with context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

field

Name of the invalid configuration field.

TYPE: str

value

The invalid value provided.

TYPE: object

constraint

Description of the validation constraint.

TYPE: str

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    field: str,
    value: object,
    constraint: str,
) -> None:
    """Initialize MultiAgentValidationError with context.

    Args:
        message: Human-readable error description.
        field: Name of the invalid configuration field.
        value: The invalid value provided.
        constraint: Description of the validation constraint.

    """
    super().__init__(message)
    self.field = field
    self.value = value
    self.constraint = constraint

__str__

__str__() -> str

Return string representation with context.

RETURNS DESCRIPTION
str

Formatted error message including field and value context.

Notes

Outputs formatted error message with field, value, and constraint context when available, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string representation with context.

    Returns:
        Formatted error message including field and value context.

    Notes:
        Outputs formatted error message with field, value, and constraint
        context when available, preserving base message structure.
    """
    base = super().__str__()
    return (
        f"{base} [field={self.field!r}, value={self.value!r}, "
        f"constraint={self.constraint!r}]"
    )

NoCandidateAvailableError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.NoCandidateAvailableError[NoCandidateAvailableError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.NoCandidateAvailableError
                


              click gepa_adk.domain.NoCandidateAvailableError href "" "gepa_adk.domain.NoCandidateAvailableError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when no candidates are available for selection.

ATTRIBUTE DESCRIPTION
cause

Original exception that caused this error.

TYPE: Exception | None

context

Extra context (candidate_idx, frontier_type).

TYPE: dict[str, object]

Examples:

raise NoCandidateAvailableError(
    "No candidates available",
    frontier_type="instance",
)
Notes

Arises when candidate selector cannot find any valid candidates from the Pareto frontier, typically due to empty frontier or filtering constraints.

Source code in src/gepa_adk/domain/exceptions.py
class NoCandidateAvailableError(EvolutionError):
    """Raised when no candidates are available for selection.

    Attributes:
        cause (Exception | None): Original exception that caused this error.
        context (dict[str, object]): Extra context (candidate_idx, frontier_type).

    Examples:
        ```python
        raise NoCandidateAvailableError(
            "No candidates available",
            frontier_type="instance",
        )
        ```

    Notes:
        Arises when candidate selector cannot find any valid candidates
        from the Pareto frontier, typically due to empty frontier or
        filtering constraints.
    """

    def __init__(
        self,
        message: str,
        *,
        cause: Exception | None = None,
        **context: object,
    ) -> None:
        """Initialize NoCandidateAvailableError with context.

        Args:
            message: Human-readable error description.
            cause: Original exception that caused this error.
            **context: Arbitrary key-value pairs stored as exception attributes
                for debugging context.
        """
        super().__init__(message)
        self.cause = cause
        self.context = context

    def __str__(self) -> str:
        """Return string with context and cause details.

        Returns:
            Formatted error message including context and cause information.

        Notes:
            Outputs formatted error message with context dict and cause chain
            when available, preserving base message structure.
        """
        base = super().__str__()
        if self.context:
            ctx_str = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
            base = f"{base} [{ctx_str}]"
        if self.cause:
            base = f"{base} (caused by: {self.cause})"
        return base

__init__

__init__(
    message: str,
    *,
    cause: Exception | None = None,
    **context: object,
) -> None

Initialize NoCandidateAvailableError with context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

cause

Original exception that caused this error.

TYPE: Exception | None DEFAULT: None

**context

Arbitrary key-value pairs stored as exception attributes for debugging context.

TYPE: object DEFAULT: {}

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    cause: Exception | None = None,
    **context: object,
) -> None:
    """Initialize NoCandidateAvailableError with context.

    Args:
        message: Human-readable error description.
        cause: Original exception that caused this error.
        **context: Arbitrary key-value pairs stored as exception attributes
            for debugging context.
    """
    super().__init__(message)
    self.cause = cause
    self.context = context

__str__

__str__() -> str

Return string with context and cause details.

RETURNS DESCRIPTION
str

Formatted error message including context and cause information.

Notes

Outputs formatted error message with context dict and cause chain when available, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string with context and cause details.

    Returns:
        Formatted error message including context and cause information.

    Notes:
        Outputs formatted error message with context dict and cause chain
        when available, preserving base message structure.
    """
    base = super().__str__()
    if self.context:
        ctx_str = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
        base = f"{base} [{ctx_str}]"
    if self.cause:
        base = f"{base} (caused by: {self.cause})"
    return base

ReflectionError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.ReflectionError[ReflectionError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.ReflectionError
                


              click gepa_adk.domain.ReflectionError href "" "gepa_adk.domain.ReflectionError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when the reflection function raises an unexpected exception.

The proposer wraps any non-EvolutionError exception from the reflection function in this error and classifies it. A retryable error (quota, availability or connection failure) is retried once; when the retry fails too, the engine records the iteration with skip_reason="reflection_error", counts it toward patience and continues the loop. A non-retryable error aborts the run.

ATTRIBUTE DESCRIPTION
component

Name of the component the reflection was proposing text for.

TYPE: str

cause

The exception the reflection function raised on the last attempt.

TYPE: BaseException

retryable

Whether the cause is a transient provider failure.

TYPE: bool

attempts

Number of reflection calls made before giving up.

TYPE: int

Examples:

raise ReflectionError(
    "instruction",
    cause=RuntimeError("503 UNAVAILABLE"),
    retryable=True,
    attempts=2,
)
Notes

Raised by the mutation proposer. ReflectionTimeoutError and other EvolutionError subclasses from the reflection function pass through unwrapped.

Source code in src/gepa_adk/domain/exceptions.py
class ReflectionError(EvolutionError):
    """Raised when the reflection function raises an unexpected exception.

    The proposer wraps any non-``EvolutionError`` exception from the
    reflection function in this error and classifies it. A retryable error
    (quota, availability or connection failure) is retried once; when the
    retry fails too, the engine records the iteration with
    ``skip_reason="reflection_error"``, counts it toward patience and
    continues the loop. A non-retryable error aborts the run.

    Attributes:
        component (str): Name of the component the reflection was proposing
            text for.
        cause (BaseException): The exception the reflection function raised
            on the last attempt.
        retryable (bool): Whether the cause is a transient provider failure.
        attempts (int): Number of reflection calls made before giving up.

    Examples:
        ```python
        raise ReflectionError(
            "instruction",
            cause=RuntimeError("503 UNAVAILABLE"),
            retryable=True,
            attempts=2,
        )
        ```

    Notes:
        Raised by the mutation proposer. ``ReflectionTimeoutError`` and
        other ``EvolutionError`` subclasses from the reflection function
        pass through unwrapped.
    """

    def __init__(
        self,
        component: str,
        *,
        cause: BaseException,
        retryable: bool,
        attempts: int,
    ) -> None:
        """Initialize ReflectionError with the component and its cause.

        Args:
            component: Name of the component whose reflection failed.
            cause: The exception the reflection function raised.
            retryable: Whether the cause is a transient provider failure.
            attempts: Number of reflection calls made before giving up.
        """
        noun = "attempt" if attempts == 1 else "attempts"
        super().__init__(
            f"Reflection agent raised {type(cause).__name__}: {cause} for "
            f"component {component!r} after {attempts} {noun}."
        )
        self.component = component
        self.cause = cause
        self.retryable = retryable
        self.attempts = attempts

__init__

__init__(
    component: str,
    *,
    cause: BaseException,
    retryable: bool,
    attempts: int,
) -> None

Initialize ReflectionError with the component and its cause.

PARAMETER DESCRIPTION
component

Name of the component whose reflection failed.

TYPE: str

cause

The exception the reflection function raised.

TYPE: BaseException

retryable

Whether the cause is a transient provider failure.

TYPE: bool

attempts

Number of reflection calls made before giving up.

TYPE: int

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    component: str,
    *,
    cause: BaseException,
    retryable: bool,
    attempts: int,
) -> None:
    """Initialize ReflectionError with the component and its cause.

    Args:
        component: Name of the component whose reflection failed.
        cause: The exception the reflection function raised.
        retryable: Whether the cause is a transient provider failure.
        attempts: Number of reflection calls made before giving up.
    """
    noun = "attempt" if attempts == 1 else "attempts"
    super().__init__(
        f"Reflection agent raised {type(cause).__name__}: {cause} for "
        f"component {component!r} after {attempts} {noun}."
    )
    self.component = component
    self.cause = cause
    self.retryable = retryable
    self.attempts = attempts

ReflectionTimeoutError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.ReflectionTimeoutError[ReflectionTimeoutError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.ReflectionTimeoutError
                


              click gepa_adk.domain.ReflectionTimeoutError href "" "gepa_adk.domain.ReflectionTimeoutError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when the reflection agent exceeds its timeout.

The engine treats this error as a skipped iteration rather than a fatal one: it records the iteration with skip_reason="reflection_timeout", counts it toward patience and continues the loop. The proposer does not retry a timed-out reflection.

ATTRIBUTE DESCRIPTION
component

Name of the component the reflection was proposing text for.

TYPE: str

timeout_seconds

Timeout the reflection ran under, or None when the executor's own default applied.

TYPE: int | None

Examples:

raise ReflectionTimeoutError("instruction", timeout_seconds=120)
Notes

Raised by the ADK reflection function when the executor returns ExecutionStatus.TIMEOUT, as distinct from a failed run or an empty response.

Source code in src/gepa_adk/domain/exceptions.py
class ReflectionTimeoutError(EvolutionError):
    """Raised when the reflection agent exceeds its timeout.

    The engine treats this error as a skipped iteration rather than a fatal
    one: it records the iteration with ``skip_reason="reflection_timeout"``,
    counts it toward patience and continues the loop. The proposer does not
    retry a timed-out reflection.

    Attributes:
        component (str): Name of the component the reflection was proposing
            text for.
        timeout_seconds (int | None): Timeout the reflection ran under, or
            ``None`` when the executor's own default applied.

    Examples:
        ```python
        raise ReflectionTimeoutError("instruction", timeout_seconds=120)
        ```

    Notes:
        Raised by the ADK reflection function when the executor returns
        ``ExecutionStatus.TIMEOUT``, as distinct from a failed run or an
        empty response.
    """

    def __init__(self, component: str, timeout_seconds: int | None = None) -> None:
        """Initialize ReflectionTimeoutError with the component and timeout.

        Args:
            component: Name of the component whose reflection timed out.
            timeout_seconds: Timeout the reflection ran under, or ``None``
                when the executor's own default applied.
        """
        limit = (
            f"after {timeout_seconds} seconds"
            if timeout_seconds is not None
            else "under the executor's default timeout"
        )
        super().__init__(
            f"Reflection agent timed out for component {component!r} {limit}."
        )
        self.component = component
        self.timeout_seconds = timeout_seconds

__init__

__init__(
    component: str, timeout_seconds: int | None = None
) -> None

Initialize ReflectionTimeoutError with the component and timeout.

PARAMETER DESCRIPTION
component

Name of the component whose reflection timed out.

TYPE: str

timeout_seconds

Timeout the reflection ran under, or None when the executor's own default applied.

TYPE: int | None DEFAULT: None

Source code in src/gepa_adk/domain/exceptions.py
def __init__(self, component: str, timeout_seconds: int | None = None) -> None:
    """Initialize ReflectionTimeoutError with the component and timeout.

    Args:
        component: Name of the component whose reflection timed out.
        timeout_seconds: Timeout the reflection ran under, or ``None``
            when the executor's own default applied.
    """
    limit = (
        f"after {timeout_seconds} seconds"
        if timeout_seconds is not None
        else "under the executor's default timeout"
    )
    super().__init__(
        f"Reflection agent timed out for component {component!r} {limit}."
    )
    self.component = component
    self.timeout_seconds = timeout_seconds

ScoringError

Bases: EvolutionError


              flowchart TD
              gepa_adk.domain.ScoringError[ScoringError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.ScoringError
                


              click gepa_adk.domain.ScoringError href "" "gepa_adk.domain.ScoringError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Base exception for all scoring-related errors.

All scoring exceptions inherit from this class to allow for unified exception handling in scoring operations.

ATTRIBUTE DESCRIPTION
cause

Original exception that caused this error.

TYPE: Exception | None

Examples:

Catching scoring errors:

from gepa_adk.domain.exceptions import ScoringError

try:
    score, metadata = await scorer.async_score(...)
except ScoringError as e:
    print(f"Scoring failed: {e}")
Notes

All scoring exceptions inherit from this base class. ScoringError extends EvolutionError, following ADR-009 exception hierarchy guidelines.

Source code in src/gepa_adk/domain/exceptions.py
class ScoringError(EvolutionError):
    """Base exception for all scoring-related errors.

    All scoring exceptions inherit from this class to allow for unified
    exception handling in scoring operations.

    Attributes:
        cause (Exception | None): Original exception that caused this error.

    Examples:
        Catching scoring errors:

        ```python
        from gepa_adk.domain.exceptions import ScoringError

        try:
            score, metadata = await scorer.async_score(...)
        except ScoringError as e:
            print(f"Scoring failed: {e}")
        ```

    Notes:
        All scoring exceptions inherit from this base class.
        ScoringError extends EvolutionError, following ADR-009 exception
        hierarchy guidelines.
    """

    def __init__(
        self,
        message: str,
        *,
        cause: Exception | None = None,
    ) -> None:
        """Initialize ScoringError with message and optional cause.

        Args:
            message: Human-readable error description.
            cause: Original exception that caused this error.

        """
        super().__init__(message)
        self.cause = cause

    def __str__(self) -> str:
        """Return string with cause chain if present.

        Returns:
            Formatted message with cause information.

        Notes:
            Outputs formatted error message with cause chain when available,
            preserving base message structure.
        """
        base = super().__str__()
        if self.cause:
            base = f"{base} (caused by: {self.cause})"
        return base

__init__

__init__(
    message: str, *, cause: Exception | None = None
) -> None

Initialize ScoringError with message and optional cause.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

cause

Original exception that caused this error.

TYPE: Exception | None DEFAULT: None

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    cause: Exception | None = None,
) -> None:
    """Initialize ScoringError with message and optional cause.

    Args:
        message: Human-readable error description.
        cause: Original exception that caused this error.

    """
    super().__init__(message)
    self.cause = cause

__str__

__str__() -> str

Return string with cause chain if present.

RETURNS DESCRIPTION
str

Formatted message with cause information.

Notes

Outputs formatted error message with cause chain when available, preserving base message structure.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string with cause chain if present.

    Returns:
        Formatted message with cause information.

    Notes:
        Outputs formatted error message with cause chain when available,
        preserving base message structure.
    """
    base = super().__str__()
    if self.cause:
        base = f"{base} (caused by: {self.cause})"
    return base

VideoValidationError

Bases: ConfigurationError


              flowchart TD
              gepa_adk.domain.VideoValidationError[VideoValidationError]
              gepa_adk.domain.exceptions.ConfigurationError[ConfigurationError]
              gepa_adk.domain.exceptions.EvolutionError[EvolutionError]

                              gepa_adk.domain.exceptions.ConfigurationError --> gepa_adk.domain.VideoValidationError
                                gepa_adk.domain.exceptions.EvolutionError --> gepa_adk.domain.exceptions.ConfigurationError
                



              click gepa_adk.domain.VideoValidationError href "" "gepa_adk.domain.VideoValidationError"
              click gepa_adk.domain.exceptions.ConfigurationError href "" "gepa_adk.domain.exceptions.ConfigurationError"
              click gepa_adk.domain.exceptions.EvolutionError href "" "gepa_adk.domain.exceptions.EvolutionError"
            

Raised when video file validation fails.

This exception is raised during video file processing when a video file does not exist, exceeds size limits, or has an invalid MIME type.

ATTRIBUTE DESCRIPTION
video_path

The path to the video file that failed validation.

TYPE: str

field

The configuration field name (default "video").

TYPE: str

constraint

Description of the validation constraint that was violated.

TYPE: str

Examples:

Raising a video validation error:

from gepa_adk.domain.exceptions import VideoValidationError

raise VideoValidationError(
    "Video file not found",
    video_path="/path/to/missing.mp4",
    constraint="file must exist",
)

Handling video validation errors:

from gepa_adk.domain.exceptions import VideoValidationError

try:
    await video_service.prepare_video_parts(["/bad/path.mp4"])
except VideoValidationError as e:
    print(f"Invalid video: {e.video_path}")
    print(f"Constraint violated: {e.constraint}")
Notes

Arises from video file validation failures during multimodal input processing. File existence, size limits (2GB), and MIME type (video/*) are validated before loading video content.

Source code in src/gepa_adk/domain/exceptions.py
class VideoValidationError(ConfigurationError):
    """Raised when video file validation fails.

    This exception is raised during video file processing when a video
    file does not exist, exceeds size limits, or has an invalid MIME type.

    Attributes:
        video_path (str): The path to the video file that failed validation.
        field (str): The configuration field name (default "video").
        constraint (str): Description of the validation constraint that was violated.

    Examples:
        Raising a video validation error:

        ```python
        from gepa_adk.domain.exceptions import VideoValidationError

        raise VideoValidationError(
            "Video file not found",
            video_path="/path/to/missing.mp4",
            constraint="file must exist",
        )
        ```

        Handling video validation errors:

        ```python
        from gepa_adk.domain.exceptions import VideoValidationError

        try:
            await video_service.prepare_video_parts(["/bad/path.mp4"])
        except VideoValidationError as e:
            print(f"Invalid video: {e.video_path}")
            print(f"Constraint violated: {e.constraint}")
        ```

    Notes:
        Arises from video file validation failures during multimodal input
        processing. File existence, size limits (2GB), and MIME type
        (video/*) are validated before loading video content.
    """

    def __init__(
        self,
        message: str,
        *,
        video_path: str,
        field: str = "video",
        constraint: str,
    ) -> None:
        """Initialize VideoValidationError with video file context.

        Args:
            message: Human-readable error description.
            video_path: The path to the video file that failed validation.
            field: Name of the configuration field (default "video").
            constraint: Description of the validation constraint violated.

        """
        super().__init__(message, field=field, value=video_path, constraint=constraint)
        self.video_path = video_path

    def __str__(self) -> str:
        """Return string representation with video path context.

        Returns:
            Formatted error message including video_path and constraint.

        Notes:
            Outputs formatted error message with video_path for easy
            identification of the problematic file in error logs.
        """
        base = Exception.__str__(self)
        return (
            f"{base} [video_path={self.video_path!r}, constraint={self.constraint!r}]"
        )

__init__

__init__(
    message: str,
    *,
    video_path: str,
    field: str = "video",
    constraint: str,
) -> None

Initialize VideoValidationError with video file context.

PARAMETER DESCRIPTION
message

Human-readable error description.

TYPE: str

video_path

The path to the video file that failed validation.

TYPE: str

field

Name of the configuration field (default "video").

TYPE: str DEFAULT: 'video'

constraint

Description of the validation constraint violated.

TYPE: str

Source code in src/gepa_adk/domain/exceptions.py
def __init__(
    self,
    message: str,
    *,
    video_path: str,
    field: str = "video",
    constraint: str,
) -> None:
    """Initialize VideoValidationError with video file context.

    Args:
        message: Human-readable error description.
        video_path: The path to the video file that failed validation.
        field: Name of the configuration field (default "video").
        constraint: Description of the validation constraint violated.

    """
    super().__init__(message, field=field, value=video_path, constraint=constraint)
    self.video_path = video_path

__str__

__str__() -> str

Return string representation with video path context.

RETURNS DESCRIPTION
str

Formatted error message including video_path and constraint.

Notes

Outputs formatted error message with video_path for easy identification of the problematic file in error logs.

Source code in src/gepa_adk/domain/exceptions.py
def __str__(self) -> str:
    """Return string representation with video path context.

    Returns:
        Formatted error message including video_path and constraint.

    Notes:
        Outputs formatted error message with video_path for easy
        identification of the problematic file in error logs.
    """
    base = Exception.__str__(self)
    return (
        f"{base} [video_path={self.video_path!r}, constraint={self.constraint!r}]"
    )

Candidate dataclass

Represents an instruction candidate being evolved.

Unlike GEPA's simple dict[str, str] type alias, this class provides richer state tracking for async scenarios including lineage and metadata.

ATTRIBUTE DESCRIPTION
components

Component name to text value mapping. Common keys include 'instruction' (main agent prompt) and 'output_schema'.

TYPE: dict[str, str]

generation

Generation number in the evolution lineage (0 = initial). A mutation proposal gets its parent's generation plus one when the engine creates it.

TYPE: int

parent_id

Candidate.id of the candidate this one was mutated from, set when the engine creates the proposal. None for seed candidates and merge candidates.

TYPE: str | None

parent_ids

Pareto indices of both parents of a merge candidate, set by the merge proposer. None for seed candidates and for mutation proposals, whose lineage is parent_id plus the genealogy edge the engine records.

TYPE: list[int] | None

metadata

Extensible metadata dict for async tracking and debugging.

TYPE: dict[str, Any]

id

Read-only short content hash of components: the first 12 hex characters of the SHA-256 of their canonical JSON. Equal components give equal ids across processes and runs. The engine logs it as candidate_id.

TYPE: str

Examples:

Creating a candidate:

from gepa_adk.domain.models import Candidate

candidate = Candidate(
    components={"instruction": "Be helpful"},
    generation=0,
)
print(candidate.components["instruction"])  # Be helpful
print(candidate.generation)  # 0
Notes

to_dict() and from_dict() round-trip every field; the engine uses them to checkpoint the best candidate. A mutable candidate representation with richer state tracking than GEPA's simple dict. Components and metadata can be modified during the evolution process. Use generation and parent_id to track lineage.

Source code in src/gepa_adk/domain/models.py
@dataclass(slots=True, kw_only=True)
class Candidate:
    """Represents an instruction candidate being evolved.

    Unlike GEPA's simple `dict[str, str]` type alias, this class provides
    richer state tracking for async scenarios including lineage and metadata.

    Attributes:
        components (dict[str, str]): Component name to text value mapping.
            Common keys include 'instruction' (main agent prompt) and
            'output_schema'.
        generation (int): Generation number in the evolution lineage
            (0 = initial). A mutation proposal gets its parent's generation
            plus one when the engine creates it.
        parent_id (str | None): ``Candidate.id`` of the candidate this one
            was mutated from, set when the engine creates the proposal.
            None for seed candidates and merge candidates.
        parent_ids (list[int] | None): Pareto indices of both parents of a
            merge candidate, set by the merge proposer. None for seed
            candidates and for mutation proposals, whose lineage is
            ``parent_id`` plus the genealogy edge the engine records.
        metadata (dict[str, Any]): Extensible metadata dict for async tracking
            and debugging.
        id (str): Read-only short content hash of ``components``: the first
            12 hex characters of the SHA-256 of their canonical JSON. Equal
            components give equal ids across processes and runs. The engine
            logs it as ``candidate_id``.

    Examples:
        Creating a candidate:

        ```python
        from gepa_adk.domain.models import Candidate

        candidate = Candidate(
            components={"instruction": "Be helpful"},
            generation=0,
        )
        print(candidate.components["instruction"])  # Be helpful
        print(candidate.generation)  # 0
        ```

    Notes:
        ``to_dict()`` and ``from_dict()`` round-trip every field; the
        engine uses them to checkpoint the best candidate.
        A mutable candidate representation with richer state tracking than
        GEPA's simple dict. Components and metadata can be modified during
        the evolution process. Use generation and parent_id to track lineage.
    """

    components: dict[str, str] = field(default_factory=dict)
    generation: int = 0
    parent_id: str | None = None
    parent_ids: list[int] | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    @property
    def id(self) -> str:
        """Return a short, stable hash of this candidate's components.

        Returns:
            The first 12 hex characters of the SHA-256 digest of the
            components serialised as JSON with sorted keys.

        Examples:
            ```python
            a = Candidate(components={"instruction": "seed"})
            b = Candidate(components={"instruction": "seed"}, generation=2)
            assert a.id == b.id
            ```
        """
        canonical = json.dumps(self.components, sort_keys=True, ensure_ascii=False)
        return hashlib.sha256(canonical.encode()).hexdigest()[:12]

    def to_dict(self) -> dict[str, Any]:
        """Serialise this candidate to a stdlib-only dict.

        Returns:
            Dict with ``components``, ``generation``, ``parent_id``,
            ``parent_ids`` and ``metadata``. ``id`` is derived from
            ``components`` and is not stored. The output is
            ``json.dumps()``-compatible when ``metadata`` is.

        Examples:
            ```python
            data = Candidate(components={"instruction": "seed"}).to_dict()
            assert Candidate.from_dict(data).components == {"instruction": "seed"}
            ```
        """
        return {
            "components": dict(self.components),
            "generation": self.generation,
            "parent_id": self.parent_id,
            "parent_ids": None if self.parent_ids is None else list(self.parent_ids),
            "metadata": dict(self.metadata),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Candidate":
        """Rebuild a candidate from ``to_dict()`` output.

        Args:
            data: Dict produced by ``to_dict()``. Only ``components`` is
                required; the other fields take their defaults when absent.

        Returns:
            The reconstructed candidate.

        Raises:
            KeyError: If ``components`` is missing from ``data``.
        """
        parent_ids = data.get("parent_ids")
        return cls(
            components=dict(data["components"]),
            generation=data.get("generation", 0),
            parent_id=data.get("parent_id"),
            parent_ids=None if parent_ids is None else list(parent_ids),
            metadata=dict(data.get("metadata") or {}),
        )

id property

id: str

Return a short, stable hash of this candidate's components.

RETURNS DESCRIPTION
str

The first 12 hex characters of the SHA-256 digest of the

str

components serialised as JSON with sorted keys.

Examples:

a = Candidate(components={"instruction": "seed"})
b = Candidate(components={"instruction": "seed"}, generation=2)
assert a.id == b.id

to_dict

to_dict() -> dict[str, Any]

Serialise this candidate to a stdlib-only dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with components, generation, parent_id,

dict[str, Any]

parent_ids and metadata. id is derived from

dict[str, Any]

components and is not stored. The output is

dict[str, Any]

json.dumps()-compatible when metadata is.

Examples:

data = Candidate(components={"instruction": "seed"}).to_dict()
assert Candidate.from_dict(data).components == {"instruction": "seed"}
Source code in src/gepa_adk/domain/models.py
def to_dict(self) -> dict[str, Any]:
    """Serialise this candidate to a stdlib-only dict.

    Returns:
        Dict with ``components``, ``generation``, ``parent_id``,
        ``parent_ids`` and ``metadata``. ``id`` is derived from
        ``components`` and is not stored. The output is
        ``json.dumps()``-compatible when ``metadata`` is.

    Examples:
        ```python
        data = Candidate(components={"instruction": "seed"}).to_dict()
        assert Candidate.from_dict(data).components == {"instruction": "seed"}
        ```
    """
    return {
        "components": dict(self.components),
        "generation": self.generation,
        "parent_id": self.parent_id,
        "parent_ids": None if self.parent_ids is None else list(self.parent_ids),
        "metadata": dict(self.metadata),
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> Candidate

Rebuild a candidate from to_dict() output.

PARAMETER DESCRIPTION
data

Dict produced by to_dict(). Only components is required; the other fields take their defaults when absent.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
Candidate

The reconstructed candidate.

RAISES DESCRIPTION
KeyError

If components is missing from data.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Candidate":
    """Rebuild a candidate from ``to_dict()`` output.

    Args:
        data: Dict produced by ``to_dict()``. Only ``components`` is
            required; the other fields take their defaults when absent.

    Returns:
        The reconstructed candidate.

    Raises:
        KeyError: If ``components`` is missing from ``data``.
    """
    parent_ids = data.get("parent_ids")
    return cls(
        components=dict(data["components"]),
        generation=data.get("generation", 0),
        parent_id=data.get("parent_id"),
        parent_ids=None if parent_ids is None else list(parent_ids),
        metadata=dict(data.get("metadata") or {}),
    )

EvolutionConfig dataclass

Configuration parameters for an evolution run.

Defines the parameters that control how evolution proceeds, including iteration limits, concurrency settings, and stopping criteria.

ATTRIBUTE DESCRIPTION
max_iterations

Maximum number of evolution iterations. 0 means just evaluate baseline without evolving.

TYPE: int

max_concurrent_evals

Maximum concurrent evaluation rows per batch in evolve(). Does not apply to evolve_group() or evolve_workflow(). Must be at least 1.

TYPE: int

min_improvement_threshold

Minimum score improvement to accept a new candidate. Set to 0.0 to accept any improvement.

TYPE: float

patience

Number of iterations without improvement before stopping early. Set to 0 to disable early stopping.

TYPE: int

reflection_model

Model for reflection/mutation operations: a non-empty model identifier string or a BaseLlm instance (e.g. a LiteLlm with a custom api_base or temperature), which is passed to the reflection agent unchanged.

TYPE: str | BaseLlm

frontier_type

Frontier tracking strategy for Pareto selection (default: INSTANCE). OBJECTIVE, HYBRID and CARTESIAN require per-example objective scores. The single-agent adapter forwards non-empty dimension_scores dictionaries from scorer metadata automatically when every example provides them; mixed batches raise ConfigurationError naming the example index.

TYPE: FrontierType

acceptance_metric

Aggregation method for acceptance decisions on iteration evaluation batches. "sum" uses sum of scores (default, aligns with upstream GEPA). "mean" uses mean of scores (legacy behavior).

TYPE: Literal['sum', 'mean']

use_merge

Enable merge proposals for genetic crossover. Defaults to False.

TYPE: bool

max_merge_invocations

Maximum number of merge attempts per run. Defaults to 10. Must be non-negative.

TYPE: int

reflection_prompt

Custom reflection/mutation prompt template. If provided, this template is used instead of the default when the reflection model proposes improved text. Required placeholders: - {component_text}: The current component text being evolved - {trials}: Trial data with feedback and trajectory for each test case If None or empty string, the default prompt template is used.

TYPE: str | None

stop_callbacks

List of stopper callbacks for custom stop conditions. Each callback receives a StopperState and returns True to signal stop. Defaults to an empty list.

TYPE: list[StopperProtocol]

seed

Random seed for deterministic engine decisions. When set, a seeded random.Random is created and shared across all stochastic components (candidate selector, merge proposer). None (default) preserves current random behavior.

TYPE: int | None

reflection_max_trials

Maximum number of trials per component sent to the reflection prompt. When a component has more, failing trials (score below 1.0) fill up to half the slots (rounded up) and passing trials fill the rest, each in batch order. None (default) sends every trial. Must be at least 1.

TYPE: int | None

reflection_max_trial_chars

Maximum length of any string value inside a trial sent to the reflection prompt, at any depth. Longer strings are cut and end with a …[truncated, N chars omitted] marker. None (default) leaves strings intact. Must be at least 1.

TYPE: int | None

reflection_timeout_seconds

Seconds the reflection agent may run for one proposal before the executor stops it. None (default) keeps the executor's own default (300 seconds in AgentExecutor). A timed-out reflection skips the iteration with skip_reason="reflection_timeout" and counts toward patience; a reflection that keeps raising a retryable provider error after one retry is skipped the same way with skip_reason="reflection_error", and one cut off at the output-token limit (or opening a reasoning tag it never closes) with skip_reason="incomplete_proposal". Must be an int (not bool) of at least 1.

TYPE: int | None

max_consecutive_reflection_errors

Number of consecutive reflection_error skips whose causes share the same exception type and message after which the engine stops retrying and raises a non-retryable ReflectionError carrying the partial result (StopReason.ERROR). A different error, or any other iteration in between, resets the count. Defaults to 3. Must be at least 1.

TYPE: int

reflection_minibatch_size

Number of trainset rows a proposal runs on before it earns its full evaluation. Each iteration the engine draws a fresh seeded sample of this many rows, evaluates the proposal on them and compares its mean score with the parent's cached scores on the same rows. Only a strictly higher mean earns the full trainset pass and the scoring pass; a proposal that loses or ties is recorded with skip_reason="minibatch_rejected". None (default), or a size at least the trainset size, evaluates every proposal on the full trainset. Must be an int of at least 1.

TYPE: int | None

on_iteration

Called once per iteration, after its IterationRecord is appended to the history and before stop conditions are checked, with the record and the id of the candidate it concerns. Skipped iterations are reported too: an empty_proposal skip passes None as the id and a duplicate skip passes the duplicate proposal's id. The baseline evaluation triggers no call. An awaitable return value is awaited before the loop continues, and an exception raised by the callback propagates out of run(). None (default) disables it.

TYPE: OnIterationCallback | None

proposal_validator

Called with each evolved component's name and proposed text after reflection and before schema validation or evaluation. The first non-None return rejects the proposal: the iteration is recorded with skip_reason="proposal_rejected", score=0.0 and the reason in rejection_reason, nothing is evaluated, and the rejection counts toward patience. An exception raised by the validator propagates out of run(). None (default) accepts every proposal.

TYPE: ProposalValidator | None

checkpoint_path

JSON file the engine writes its state to after the baseline and after every recorded iteration, atomically (a temporary file beside it, then os.replace). A str is converted to Path in __post_init__, so the stored value is a Path or None. None (default) writes no checkpoint. The engine refuses it together with a candidate_selector, because Pareto state is not checkpointed.

TYPE: Path | str | None

resume

When True, run() restores the state in checkpoint_path instead of evaluating the baseline, and continues the iteration count, the stagnation counter and the evaluation counter without re-evaluating a scored candidate. Requires checkpoint_path. Defaults to False, which starts fresh and overwrites an existing checkpoint on its first write.

TYPE: bool

Examples:

Creating a configuration with defaults:

from gepa_adk.domain.models import EvolutionConfig

config = EvolutionConfig(max_iterations=100, patience=10)
print(config.max_iterations)  # 100
print(config.reflection_model)  # ollama_chat/gpt-oss:20b
print(config.max_consecutive_reflection_errors)  # 3

Aborting after the first repeat of an identical reflection error:

config = EvolutionConfig(max_consecutive_reflection_errors=2)
Notes

All numeric parameters are validated in post_init to ensure they meet their constraints. Cross-field consistency is also checked (e.g., use_merge requires max_merge_invocations > 0, stop_callbacks, on_iteration and proposal_validator must be callable). Invalid values raise ConfigurationError.

Determinism applies to engine decisions only (candidate selection, component selection, merge proposals). LLM inference is inherently stochastic and not covered by the seed guarantee.

Source code in src/gepa_adk/domain/models.py
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
@dataclass(slots=True, kw_only=True)
class EvolutionConfig:
    """Configuration parameters for an evolution run.

    Defines the parameters that control how evolution proceeds, including
    iteration limits, concurrency settings, and stopping criteria.

    Attributes:
        max_iterations (int): Maximum number of evolution iterations. 0 means
            just evaluate baseline without evolving.
        max_concurrent_evals (int): Maximum concurrent evaluation rows per batch
            in evolve(). Does not apply to evolve_group() or evolve_workflow().
            Must be at least 1.
        min_improvement_threshold (float): Minimum score improvement to accept
            a new candidate. Set to 0.0 to accept any improvement.
        patience (int): Number of iterations without improvement before stopping
            early. Set to 0 to disable early stopping.
        reflection_model (str | BaseLlm): Model for reflection/mutation
            operations: a non-empty model identifier string or a ``BaseLlm``
            instance (e.g. a ``LiteLlm`` with a custom ``api_base`` or
            temperature), which is passed to the reflection agent unchanged.
        frontier_type (FrontierType): Frontier tracking strategy for Pareto
            selection (default: INSTANCE). OBJECTIVE, HYBRID and CARTESIAN
            require per-example objective scores. The single-agent adapter
            forwards non-empty ``dimension_scores`` dictionaries from scorer
            metadata automatically when every example provides them; mixed
            batches raise ``ConfigurationError`` naming the example index.
        acceptance_metric (Literal["sum", "mean"]): Aggregation method for
            acceptance decisions on iteration evaluation batches. "sum" uses
            sum of scores (default, aligns with upstream GEPA). "mean" uses
            mean of scores (legacy behavior).
        use_merge (bool): Enable merge proposals for genetic crossover.
            Defaults to False.
        max_merge_invocations (int): Maximum number of merge attempts per run.
            Defaults to 10. Must be non-negative.
        reflection_prompt (str | None): Custom reflection/mutation prompt template.
            If provided, this template is used instead of the default when the
            reflection model proposes improved text. Required placeholders:
            - {component_text}: The current component text being evolved
            - {trials}: Trial data with feedback and trajectory for each test case
            If None or empty string, the default prompt template is used.
        stop_callbacks (list[StopperProtocol]): List of stopper callbacks for
            custom stop conditions. Each callback receives a StopperState and
            returns True to signal stop. Defaults to an empty list.
        seed (int | None): Random seed for deterministic engine decisions.
            When set, a seeded ``random.Random`` is created and shared across
            all stochastic components (candidate selector, merge proposer).
            ``None`` (default) preserves current random behavior.
        reflection_max_trials (int | None): Maximum number of trials per
            component sent to the reflection prompt. When a component has
            more, failing trials (score below 1.0) fill up to half the slots
            (rounded up) and passing trials fill the rest, each in batch
            order. ``None`` (default) sends every trial. Must be at least 1.
        reflection_max_trial_chars (int | None): Maximum length of any string
            value inside a trial sent to the reflection prompt, at any depth.
            Longer strings are cut and end with a
            ``…[truncated, N chars omitted]`` marker. ``None`` (default)
            leaves strings intact. Must be at least 1.
        reflection_timeout_seconds (int | None): Seconds the reflection agent
            may run for one proposal before the executor stops it. ``None``
            (default) keeps the executor's own default (300 seconds in
            ``AgentExecutor``). A timed-out reflection skips the iteration
            with ``skip_reason="reflection_timeout"`` and counts toward
            ``patience``; a reflection that keeps raising a retryable
            provider error after one retry is skipped the same way with
            ``skip_reason="reflection_error"``, and one cut off at the
            output-token limit (or opening a reasoning tag it never closes)
            with ``skip_reason="incomplete_proposal"``. Must be an ``int`` (not
            ``bool``) of at least 1.
        max_consecutive_reflection_errors (int): Number of consecutive
            ``reflection_error`` skips whose causes share the same exception
            type and message after which the engine stops retrying and raises
            a non-retryable ``ReflectionError`` carrying the partial result
            (``StopReason.ERROR``). A different error, or any other
            iteration in between, resets the count. Defaults to 3. Must be at least 1.
        reflection_minibatch_size (int | None): Number of trainset rows a
            proposal runs on before it earns its full evaluation. Each
            iteration the engine draws a fresh seeded sample of this many
            rows, evaluates the proposal on them and compares its mean score
            with the parent's cached scores on the same rows. Only a strictly
            higher mean earns the full trainset pass and the scoring pass; a
            proposal that loses or ties is recorded with
            ``skip_reason="minibatch_rejected"``. ``None`` (default), or a
            size at least the trainset size, evaluates every proposal on the
            full trainset. Must be an ``int`` of at least 1.
        on_iteration (OnIterationCallback | None): Called once per iteration,
            after its IterationRecord is appended to the history and before
            stop conditions are checked, with the record and the id of the
            candidate it concerns. Skipped iterations are reported too: an
            ``empty_proposal`` skip passes ``None`` as the id and a
            ``duplicate`` skip passes the duplicate proposal's id. The baseline
            evaluation triggers no call. An awaitable return value is awaited
            before the loop continues, and an exception raised by the callback
            propagates out of ``run()``. ``None`` (default) disables it.
        proposal_validator (ProposalValidator | None): Called with each
            evolved component's name and proposed text after reflection and
            before schema validation or evaluation. The first non-None
            return rejects the proposal: the iteration is recorded with
            ``skip_reason="proposal_rejected"``, ``score=0.0`` and the reason
            in ``rejection_reason``, nothing is evaluated, and the rejection
            counts toward ``patience``. An exception raised by the validator
            propagates out of ``run()``. ``None`` (default) accepts every
            proposal.
        checkpoint_path (Path | str | None): JSON file the engine writes its state
            to after the baseline and after every recorded iteration,
            atomically (a temporary file beside it, then ``os.replace``).
            A ``str`` is converted to ``Path`` in ``__post_init__``, so the
            stored value is a ``Path`` or ``None``. ``None`` (default) writes no
            checkpoint. The engine refuses it together with a
            ``candidate_selector``, because Pareto state is not checkpointed.
        resume (bool): When True, ``run()`` restores the state in
            ``checkpoint_path`` instead of evaluating the baseline, and
            continues the iteration count, the stagnation counter and the
            evaluation counter without re-evaluating a scored candidate.
            Requires ``checkpoint_path``. Defaults to False, which starts
            fresh and overwrites an existing checkpoint on its first write.

    Examples:
        Creating a configuration with defaults:

        ```python
        from gepa_adk.domain.models import EvolutionConfig

        config = EvolutionConfig(max_iterations=100, patience=10)
        print(config.max_iterations)  # 100
        print(config.reflection_model)  # ollama_chat/gpt-oss:20b
        print(config.max_consecutive_reflection_errors)  # 3
        ```

        Aborting after the first repeat of an identical reflection error:

        ```python
        config = EvolutionConfig(max_consecutive_reflection_errors=2)
        ```

    Notes:
        All numeric parameters are validated in __post_init__ to ensure
        they meet their constraints. Cross-field consistency is also checked
        (e.g., use_merge requires max_merge_invocations > 0, stop_callbacks,
        on_iteration and proposal_validator must be callable). Invalid values
        raise ConfigurationError.

        Determinism applies to engine decisions only (candidate selection,
        component selection, merge proposals). LLM inference is inherently
        stochastic and not covered by the seed guarantee.
    """

    max_iterations: int = 50
    max_concurrent_evals: int = 5
    min_improvement_threshold: float = 0.01
    patience: int = 5
    reflection_model: "str | BaseLlm" = "ollama_chat/gpt-oss:20b"
    frontier_type: FrontierType = FrontierType.INSTANCE
    acceptance_metric: Literal["sum", "mean"] = "sum"
    use_merge: bool = False
    max_merge_invocations: int = 10
    reflection_prompt: str | None = None
    stop_callbacks: list["StopperProtocol"] = field(default_factory=list)
    seed: int | None = None
    reflection_max_trials: int | None = None
    reflection_max_trial_chars: int | None = None
    reflection_timeout_seconds: int | None = None
    max_consecutive_reflection_errors: int = 3
    reflection_minibatch_size: int | None = None
    on_iteration: OnIterationCallback | None = None
    proposal_validator: ProposalValidator | None = None
    checkpoint_path: Path | str | None = None
    resume: bool = False

    def __post_init__(self) -> None:
        """Validate configuration parameters after initialization.

        Raises:
            ConfigurationError: If any parameter violates its constraints,
                including non-finite floats (NaN, Inf), cross-field consistency
                rules (e.g., use_merge requires max_merge_invocations > 0,
                stop_callbacks, on_iteration and proposal_validator must be
                callable), a reflection cap (reflection_max_trials,
                reflection_max_trial_chars) below 1, a
                ``max_consecutive_reflection_errors`` below 1, a
                ``reflection_timeout_seconds`` or ``reflection_minibatch_size``
                that is not ``None`` or an
                ``int`` of at least 1, or a ``reflection_model`` that is
                ``None`` or an empty string, or ``resume=True`` without a
                ``checkpoint_path``.

        Notes:
            Operates automatically after dataclass __init__ completes. Validates
            all fields including finite-float checks, cross-field consistency,
            and raises ConfigurationError with context on failure. A non-string
            ``reflection_model`` passes this check; the resolver in
            ``gepa_adk.api`` rejects values that are not a ``BaseLlm``.
            A ``str`` ``checkpoint_path`` is converted to ``Path``.
        """
        if self.max_iterations < 0:
            raise ConfigurationError(
                "max_iterations must be non-negative",
                field="max_iterations",
                value=self.max_iterations,
                constraint=">= 0",
            )

        if self.max_concurrent_evals < 1:
            raise ConfigurationError(
                "max_concurrent_evals must be at least 1",
                field="max_concurrent_evals",
                value=self.max_concurrent_evals,
                constraint=">= 1",
            )

        if self.max_consecutive_reflection_errors < 1:
            raise ConfigurationError(
                "max_consecutive_reflection_errors must be at least 1",
                field="max_consecutive_reflection_errors",
                value=self.max_consecutive_reflection_errors,
                constraint=">= 1",
            )

        if not math.isfinite(self.min_improvement_threshold):
            raise ConfigurationError(
                "min_improvement_threshold must be a finite number",
                field="min_improvement_threshold",
                value=self.min_improvement_threshold,
                constraint="finite float",
            )

        if self.min_improvement_threshold < 0.0:
            raise ConfigurationError(
                "min_improvement_threshold must be non-negative",
                field="min_improvement_threshold",
                value=self.min_improvement_threshold,
                constraint=">= 0.0",
            )

        if self.patience < 0:
            raise ConfigurationError(
                "patience must be non-negative",
                field="patience",
                value=self.patience,
                constraint=">= 0",
            )

        if not self.reflection_model:
            raise ConfigurationError(
                "reflection_model must be a non-empty string or BaseLlm instance",
                field="reflection_model",
                value=self.reflection_model,
                constraint="non-empty string",
            )

        if not isinstance(self.frontier_type, FrontierType):
            try:
                self.frontier_type = FrontierType(self.frontier_type)
            except ValueError as exc:
                raise ConfigurationError(
                    "frontier_type must be a supported FrontierType value",
                    field="frontier_type",
                    value=self.frontier_type,
                    constraint=", ".join(t.value for t in FrontierType),
                ) from exc

        if self.acceptance_metric not in ("sum", "mean"):
            raise ConfigurationError(
                "acceptance_metric must be 'sum' or 'mean'",
                field="acceptance_metric",
                value=self.acceptance_metric,
                constraint="sum|mean",
            )

        if self.max_merge_invocations < 0:
            raise ConfigurationError(
                "max_merge_invocations must be non-negative",
                field="max_merge_invocations",
                value=self.max_merge_invocations,
                constraint=">= 0",
            )

        self._validate_reflection_caps()
        self._validate_reflection_timeout()
        self._validate_minibatch_size()
        self._validate_checkpoint()

        # Cross-field consistency checks
        self._validate_consistency()

        # Validate reflection_prompt if provided
        self._validate_reflection_prompt()

    def _validate_reflection_caps(self) -> None:
        """Validate the optional caps on what the reflector sees.

        Raises:
            ConfigurationError: If ``reflection_max_trials`` or
                ``reflection_max_trial_chars`` is set to a value below 1.
        """
        for name, value in (
            ("reflection_max_trials", self.reflection_max_trials),
            ("reflection_max_trial_chars", self.reflection_max_trial_chars),
        ):
            if value is not None and value < 1:
                raise ConfigurationError(
                    f"{name} must be at least 1 or None",
                    field=name,
                    value=value,
                    constraint=">= 1 or None",
                )

    def _validate_reflection_timeout(self) -> None:
        """Validate the optional reflection timeout.

        Raises:
            ConfigurationError: If ``reflection_timeout_seconds`` is set and
                is not an ``int`` (a ``bool`` is rejected) of at least 1.
        """
        value = self.reflection_timeout_seconds
        if value is None:
            return
        if isinstance(value, bool) or not isinstance(value, int) or value < 1:
            raise ConfigurationError(
                "reflection_timeout_seconds must be an int of at least 1 or None",
                field="reflection_timeout_seconds",
                value=value,
                constraint="int >= 1 or None",
            )

    def _validate_minibatch_size(self) -> None:
        """Validate the optional reflection minibatch size.

        Raises:
            ConfigurationError: If ``reflection_minibatch_size`` is neither
                ``None`` nor an ``int`` (a ``bool`` does not count) of at
                least 1.
        """
        value = self.reflection_minibatch_size
        if value is None:
            return
        if isinstance(value, bool) or not isinstance(value, int) or value < 1:
            raise ConfigurationError(
                "reflection_minibatch_size must be an int of at least 1 or None",
                field="reflection_minibatch_size",
                value=value,
                constraint="int >= 1 or None",
            )

    def _validate_checkpoint(self) -> None:
        """Normalise ``checkpoint_path`` and check ``resume`` against it.

        Raises:
            ConfigurationError: If ``checkpoint_path`` is neither None, a
                ``str`` nor a ``Path``, or if ``resume`` is True and
                ``checkpoint_path`` is None.
        """
        if isinstance(self.checkpoint_path, str):
            self.checkpoint_path = Path(self.checkpoint_path)
        elif self.checkpoint_path is not None and not isinstance(
            self.checkpoint_path, Path
        ):
            raise ConfigurationError(
                "checkpoint_path must be a str, a Path or None",
                field="checkpoint_path",
                value=self.checkpoint_path,
                constraint="str, Path or None",
            )
        if self.resume and self.checkpoint_path is None:
            raise ConfigurationError(
                "resume=True requires checkpoint_path",
                field="checkpoint_path",
                value=None,
                constraint="not None when resume=True",
            )

    def _validate_consistency(self) -> None:
        """Validate cross-field consistency rules.

        Raises:
            ConfigurationError: If use_merge is True but max_merge_invocations
                is zero, if stop_callbacks contains non-callable items, or if
                on_iteration or proposal_validator is set and not callable.

        Notes:
            Hard errors raise ConfigurationError; soft issues log warnings.
            Called from __post_init__ after individual field validation.
        """
        if self.use_merge and self.max_merge_invocations == 0:
            raise ConfigurationError(
                "use_merge=True requires max_merge_invocations > 0",
                field="max_merge_invocations",
                value=self.max_merge_invocations,
                constraint="> 0 when use_merge=True",
            )

        if (
            self.patience > 0
            and self.max_iterations > 0
            and self.patience > self.max_iterations
        ):
            logger.warning(
                "config.patience.exceeds_max_iterations",
                patience=self.patience,
                max_iterations=self.max_iterations,
                message="patience exceeds max_iterations; early stopping will never trigger",
            )

        for i, callback in enumerate(self.stop_callbacks):
            if not callable(callback):
                raise ConfigurationError(
                    f"stop_callbacks[{i}] is not callable",
                    field=f"stop_callbacks[{i}]",
                    value=type(callback).__name__,
                    constraint="must be callable",
                )

        if self.on_iteration is not None and not callable(self.on_iteration):
            raise ConfigurationError(
                "on_iteration is not callable",
                field="on_iteration",
                value=type(self.on_iteration).__name__,
                constraint="must be callable or None",
            )

        if self.proposal_validator is not None and not callable(
            self.proposal_validator
        ):
            raise ConfigurationError(
                "proposal_validator is not callable",
                field="proposal_validator",
                value=type(self.proposal_validator).__name__,
                constraint="must be callable or None",
            )

    def _validate_reflection_prompt(self) -> None:
        """Validate reflection_prompt and handle empty string.

        Converts empty string to None with info log. Warns if required
        placeholders are missing but allows the config to be created.

        Notes:
            Soft validation approach - missing placeholders trigger warnings
            but don't prevent config creation for maximum flexibility.
        """
        # Handle empty string as "use default"
        if self.reflection_prompt == "":
            logger.info(
                "config.reflection_prompt.empty",
                message="Empty reflection_prompt provided, using default template",
            )
            # Use object.__setattr__ because slots=True prevents direct assignment
            object.__setattr__(self, "reflection_prompt", None)
            return

        # Skip validation if None
        if self.reflection_prompt is None:
            return

        # Warn about missing placeholders
        if "{component_text}" not in self.reflection_prompt:
            logger.warning(
                "config.reflection_prompt.missing_placeholder",
                placeholder="component_text",
                message="reflection_prompt is missing {component_text} placeholder",
            )

        if "{trials}" not in self.reflection_prompt:
            logger.warning(
                "config.reflection_prompt.missing_placeholder",
                placeholder="trials",
                message="reflection_prompt is missing {trials} placeholder",
            )

__post_init__

__post_init__() -> None

Validate configuration parameters after initialization.

RAISES DESCRIPTION
ConfigurationError

If any parameter violates its constraints, including non-finite floats (NaN, Inf), cross-field consistency rules (e.g., use_merge requires max_merge_invocations > 0, stop_callbacks, on_iteration and proposal_validator must be callable), a reflection cap (reflection_max_trials, reflection_max_trial_chars) below 1, a max_consecutive_reflection_errors below 1, a reflection_timeout_seconds or reflection_minibatch_size that is not None or an int of at least 1, or a reflection_model that is None or an empty string, or resume=True without a checkpoint_path.

Notes

Operates automatically after dataclass init completes. Validates all fields including finite-float checks, cross-field consistency, and raises ConfigurationError with context on failure. A non-string reflection_model passes this check; the resolver in gepa_adk.api rejects values that are not a BaseLlm. A str checkpoint_path is converted to Path.

Source code in src/gepa_adk/domain/models.py
def __post_init__(self) -> None:
    """Validate configuration parameters after initialization.

    Raises:
        ConfigurationError: If any parameter violates its constraints,
            including non-finite floats (NaN, Inf), cross-field consistency
            rules (e.g., use_merge requires max_merge_invocations > 0,
            stop_callbacks, on_iteration and proposal_validator must be
            callable), a reflection cap (reflection_max_trials,
            reflection_max_trial_chars) below 1, a
            ``max_consecutive_reflection_errors`` below 1, a
            ``reflection_timeout_seconds`` or ``reflection_minibatch_size``
            that is not ``None`` or an
            ``int`` of at least 1, or a ``reflection_model`` that is
            ``None`` or an empty string, or ``resume=True`` without a
            ``checkpoint_path``.

    Notes:
        Operates automatically after dataclass __init__ completes. Validates
        all fields including finite-float checks, cross-field consistency,
        and raises ConfigurationError with context on failure. A non-string
        ``reflection_model`` passes this check; the resolver in
        ``gepa_adk.api`` rejects values that are not a ``BaseLlm``.
        A ``str`` ``checkpoint_path`` is converted to ``Path``.
    """
    if self.max_iterations < 0:
        raise ConfigurationError(
            "max_iterations must be non-negative",
            field="max_iterations",
            value=self.max_iterations,
            constraint=">= 0",
        )

    if self.max_concurrent_evals < 1:
        raise ConfigurationError(
            "max_concurrent_evals must be at least 1",
            field="max_concurrent_evals",
            value=self.max_concurrent_evals,
            constraint=">= 1",
        )

    if self.max_consecutive_reflection_errors < 1:
        raise ConfigurationError(
            "max_consecutive_reflection_errors must be at least 1",
            field="max_consecutive_reflection_errors",
            value=self.max_consecutive_reflection_errors,
            constraint=">= 1",
        )

    if not math.isfinite(self.min_improvement_threshold):
        raise ConfigurationError(
            "min_improvement_threshold must be a finite number",
            field="min_improvement_threshold",
            value=self.min_improvement_threshold,
            constraint="finite float",
        )

    if self.min_improvement_threshold < 0.0:
        raise ConfigurationError(
            "min_improvement_threshold must be non-negative",
            field="min_improvement_threshold",
            value=self.min_improvement_threshold,
            constraint=">= 0.0",
        )

    if self.patience < 0:
        raise ConfigurationError(
            "patience must be non-negative",
            field="patience",
            value=self.patience,
            constraint=">= 0",
        )

    if not self.reflection_model:
        raise ConfigurationError(
            "reflection_model must be a non-empty string or BaseLlm instance",
            field="reflection_model",
            value=self.reflection_model,
            constraint="non-empty string",
        )

    if not isinstance(self.frontier_type, FrontierType):
        try:
            self.frontier_type = FrontierType(self.frontier_type)
        except ValueError as exc:
            raise ConfigurationError(
                "frontier_type must be a supported FrontierType value",
                field="frontier_type",
                value=self.frontier_type,
                constraint=", ".join(t.value for t in FrontierType),
            ) from exc

    if self.acceptance_metric not in ("sum", "mean"):
        raise ConfigurationError(
            "acceptance_metric must be 'sum' or 'mean'",
            field="acceptance_metric",
            value=self.acceptance_metric,
            constraint="sum|mean",
        )

    if self.max_merge_invocations < 0:
        raise ConfigurationError(
            "max_merge_invocations must be non-negative",
            field="max_merge_invocations",
            value=self.max_merge_invocations,
            constraint=">= 0",
        )

    self._validate_reflection_caps()
    self._validate_reflection_timeout()
    self._validate_minibatch_size()
    self._validate_checkpoint()

    # Cross-field consistency checks
    self._validate_consistency()

    # Validate reflection_prompt if provided
    self._validate_reflection_prompt()

EvolutionResult dataclass

Outcome of a completed evolution run.

Contains the final results after evolution completes, including all evolved component values, performance metrics, and full history.

ATTRIBUTE DESCRIPTION
schema_version

Schema version for forward-compatible serialization. Always CURRENT_SCHEMA_VERSION for newly created results.

TYPE: int

stop_reason

Why the evolution run terminated. Defaults to StopReason.COMPLETED. StopReason.ERROR marks the partial result attached to an EvolutionError in partial_result.

TYPE: StopReason

original_score

Starting performance score (baseline).

TYPE: float

final_score

Ending performance score (best achieved).

TYPE: float

evolved_components

Dictionary mapping component names to their final evolved text values. Keys include "instruction" and optionally "output_schema" or other components. Access individual components via result.evolved_components["instruction"].

TYPE: dict[str, str]

iteration_history

Chronological list of iteration records. A skipped iteration's record names why in skip_reason: "empty_proposal", "duplicate", "reflection_timeout", "reflection_error", "incomplete_proposal", "minibatch_rejected", "schema_validation_failed" or "proposal_rejected".

TYPE: list[IterationRecord]

total_iterations

Number of iterations performed.

TYPE: int

valset_score

Score on validation set used for acceptance decisions. None if no validation set was used.

TYPE: float | None

trainset_score

Score on trainset used for reflection diagnostics. None if not computed.

TYPE: float | None

objective_scores

Optional per-example multi-objective scores from the best candidate's final evaluation. None when no objective scores were tracked. Each dict maps objective name to score value. Index-aligned with evaluation batch examples.

TYPE: list[dict[str, float]] | None

original_components

Optional snapshot of pre-evolution component values. When present, enables zero-arg show_diff() calls. None for results created before this field was added or when originals were not captured.

TYPE: dict[str, str] | None

baseline_failed_evaluations

Number of rows whose agent run or scorer raised, or whose run returned a failed execution, during the baseline evaluation of the initial candidate. Defaults to 0.

TYPE: int

total_failed_evaluations

baseline_failed_evaluations plus the sum of failed_evaluations over iteration_history. A failed row scores 0.0 like a wrong answer; this count tells the two apart. Defaults to 0.

TYPE: int

token_usage

Token usage observed on the rows evaluated over the run, baseline included, and on its reflection calls. None when not recorded (results saved before schema version 3). The rollup carries an evaluation split, set on every result the engine wrote at version 6 and on migrated results, and a reflection split that is None when no reflection call was observed.

TYPE: TokenRollup | None

reflection_reasoning

Read-only property returning the reflection reasoning from the last iteration. Convenience accessor; None if no iterations or last iteration has no reasoning.

TYPE: str | None

Examples:

Creating and analyzing a result:

from gepa_adk.domain.models import EvolutionResult, IterationRecord

result = EvolutionResult(
    original_score=0.60,
    final_score=0.85,
    evolved_components={"instruction": "Be helpful and concise"},
    original_components={"instruction": "Be helpful"},
    iteration_history=[],
    total_iterations=10,
)
print(result.improvement)  # 0.25
print(result.show_diff())  # unified diff of instruction changes

Serialization round-trip:

import json

d = result.to_dict()
json_str = json.dumps(d)
restored = EvolutionResult.from_dict(json.loads(json_str))
Notes

As a frozen dataclass, EvolutionResult instances cannot be modified.

Source code in src/gepa_adk/domain/models.py
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
@dataclass(slots=True, frozen=True, kw_only=True)
class EvolutionResult:
    """Outcome of a completed evolution run.

    Contains the final results after evolution completes, including
    all evolved component values, performance metrics, and full history.

    Attributes:
        schema_version (int): Schema version for forward-compatible serialization.
            Always ``CURRENT_SCHEMA_VERSION`` for newly created results.
        stop_reason (StopReason): Why the evolution run terminated. Defaults to
            ``StopReason.COMPLETED``. ``StopReason.ERROR`` marks the partial
            result attached to an ``EvolutionError`` in ``partial_result``.
        original_score (float): Starting performance score (baseline).
        final_score (float): Ending performance score (best achieved).
        evolved_components (dict[str, str]): Dictionary mapping component names
            to their final evolved text values. Keys include "instruction" and
            optionally "output_schema" or other components. Access individual
            components via ``result.evolved_components["instruction"]``.
        iteration_history (list[IterationRecord]): Chronological list of
            iteration records. A skipped iteration's record names why in
            ``skip_reason``: ``"empty_proposal"``, ``"duplicate"``,
            ``"reflection_timeout"``, ``"reflection_error"``,
            ``"incomplete_proposal"``, ``"minibatch_rejected"``,
            ``"schema_validation_failed"`` or ``"proposal_rejected"``.
        total_iterations (int): Number of iterations performed.
        valset_score (float | None): Score on validation set used for
            acceptance decisions. None if no validation set was used.
        trainset_score (float | None): Score on trainset used for reflection
            diagnostics. None if not computed.
        objective_scores (list[dict[str, float]] | None): Optional per-example
            multi-objective scores from the best candidate's final evaluation.
            None when no objective scores were tracked. Each dict maps objective
            name to score value. Index-aligned with evaluation batch examples.
        original_components (dict[str, str] | None): Optional snapshot of
            pre-evolution component values. When present, enables zero-arg
            ``show_diff()`` calls. None for results created before this field
            was added or when originals were not captured.
        baseline_failed_evaluations (int): Number of rows whose agent run or
            scorer raised, or whose run returned a failed execution, during
            the baseline evaluation of the initial candidate. Defaults to 0.
        total_failed_evaluations (int): ``baseline_failed_evaluations`` plus
            the sum of ``failed_evaluations`` over ``iteration_history``.
            A failed row scores 0.0 like a wrong answer; this count tells
            the two apart. Defaults to 0.
        token_usage (TokenRollup | None): Token usage observed on the rows
            evaluated over the run, baseline included, and on its
            reflection calls. None when not recorded (results saved before
            schema version 3). The rollup carries an ``evaluation`` split,
            set on every result the engine wrote at version 6 and on
            migrated results, and a ``reflection`` split that is None when
            no reflection call was observed.
        reflection_reasoning (str | None): Read-only property returning the
            reflection reasoning from the last iteration. Convenience
            accessor; None if no iterations or last iteration has no reasoning.

    Examples:
        Creating and analyzing a result:

        ```python
        from gepa_adk.domain.models import EvolutionResult, IterationRecord

        result = EvolutionResult(
            original_score=0.60,
            final_score=0.85,
            evolved_components={"instruction": "Be helpful and concise"},
            original_components={"instruction": "Be helpful"},
            iteration_history=[],
            total_iterations=10,
        )
        print(result.improvement)  # 0.25
        print(result.show_diff())  # unified diff of instruction changes
        ```

        Serialization round-trip:

        ```python
        import json

        d = result.to_dict()
        json_str = json.dumps(d)
        restored = EvolutionResult.from_dict(json.loads(json_str))
        ```

    Notes:
        As a frozen dataclass, EvolutionResult instances cannot be modified.
    """

    schema_version: int = CURRENT_SCHEMA_VERSION
    stop_reason: StopReason = StopReason.COMPLETED
    original_score: float
    final_score: float
    evolved_components: dict[str, str]
    iteration_history: list[IterationRecord]
    total_iterations: int
    valset_score: float | None = None
    trainset_score: float | None = None
    objective_scores: list[dict[str, float]] | None = None
    original_components: dict[str, str] | None = None
    baseline_failed_evaluations: int = 0
    total_failed_evaluations: int = 0
    token_usage: TokenRollup | None = None

    @property
    def reflection_reasoning(self) -> str | None:
        """Return the reflection reasoning from the last iteration.

        Convenience accessor for the most recent iteration's reasoning
        explaining why the reflection agent proposed its mutation.

        Returns:
            The reasoning string from the last iteration record, or None
            if no iterations exist or the last iteration has no reasoning.
        """
        if not self.iteration_history:
            return None
        return self.iteration_history[-1].reflection_reasoning

    def to_dict(self) -> dict[str, Any]:
        """Serialize this result to a stdlib-only dict.

        Returns:
            Dict containing all fields. ``stop_reason`` is serialized
            as its string value. ``iteration_history`` is serialized as a
            list of dicts. Includes ``baseline_failed_evaluations``,
            ``total_failed_evaluations`` and ``token_usage`` (None when
            unset). Output is directly ``json.dumps()``-compatible.
        """
        return {
            "schema_version": self.schema_version,
            "stop_reason": self.stop_reason.value,
            "original_score": self.original_score,
            "final_score": self.final_score,
            "evolved_components": self.evolved_components,
            "iteration_history": [r.to_dict() for r in self.iteration_history],
            "total_iterations": self.total_iterations,
            "valset_score": self.valset_score,
            "trainset_score": self.trainset_score,
            "objective_scores": self.objective_scores,
            "original_components": self.original_components,
            "baseline_failed_evaluations": self.baseline_failed_evaluations,
            "total_failed_evaluations": self.total_failed_evaluations,
            "token_usage": _rollup_to_dict(self.token_usage),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EvolutionResult":
        """Reconstruct an EvolutionResult from a dict.

        Validates schema version, applies migration if needed, and
        reconstructs all nested objects including optional
        original_components. Version 1 dicts migrate through
        ``_migrate_v1_to_v2()``, so their failure counts read as 0, and
        version 1 and 2 dicts through ``_migrate_v2_to_v3()``, so their
        ``token_usage`` reads as None, and version 1 to 3 dicts through
        ``_migrate_v3_to_v4()``, so each record's ``candidate_id`` and
        ``parent_ids`` read as None, and version 1 to 4 dicts through
        ``_migrate_v4_to_v5()``, so each record's ``rejection_reason`` reads
        as None, and version 1 to 5 dicts through ``_migrate_v5_to_v6()``,
        so each ``token_usage`` loads as its ``evaluation`` split with
        ``reflection`` None, and version 1 to 6 dicts through
        ``_migrate_v6_to_v7()``, so each record's ``rows_evaluated`` reads
        as None.

        Args:
            data: Dict containing evolution result fields.

        Returns:
            Reconstructed EvolutionResult instance.

        Raises:
            ConfigurationError: If ``schema_version`` exceeds the current
                version or ``stop_reason`` is not a valid enum value.
            KeyError: If a required field is missing from the dict.
        """
        version = data.get("schema_version", 1)
        if version > CURRENT_SCHEMA_VERSION:
            raise ConfigurationError(
                f"Cannot deserialize result with schema_version {version} "
                f"(current version is {CURRENT_SCHEMA_VERSION}). "
                f"Upgrade gepa-adk to load this result.",
                field="schema_version",
                value=version,
                constraint=f"<= {CURRENT_SCHEMA_VERSION}",
            )
        migrated = _migrate_result_dict(data, from_version=version)
        try:
            stop_reason = StopReason(migrated.get("stop_reason", "completed"))
        except ValueError:
            raw = migrated.get("stop_reason")
            raise ConfigurationError(
                f"Invalid stop_reason value: {raw!r}",
                field="stop_reason",
                value=raw,
                constraint=("one of: " + ", ".join(sr.value for sr in StopReason)),
            ) from None
        return cls(
            schema_version=migrated["schema_version"],
            stop_reason=stop_reason,
            original_score=migrated["original_score"],
            final_score=migrated["final_score"],
            evolved_components=migrated["evolved_components"],
            iteration_history=[
                IterationRecord.from_dict(r)
                for r in migrated.get("iteration_history", [])
            ],
            total_iterations=migrated["total_iterations"],
            valset_score=migrated.get("valset_score"),
            trainset_score=migrated.get("trainset_score"),
            objective_scores=migrated.get("objective_scores"),
            original_components=migrated.get("original_components"),
            baseline_failed_evaluations=migrated.get("baseline_failed_evaluations", 0),
            total_failed_evaluations=migrated.get("total_failed_evaluations", 0),
            token_usage=_rollup_from_dict(migrated.get("token_usage")),
        )

    @property
    def improvement(self) -> float:
        """Calculate the score improvement from original to final.

        Returns:
            The difference between final_score and original_score.
            Positive values indicate improvement, negative indicates degradation.

        Notes:
            Override is not needed since frozen dataclasses support properties.
        """
        return self.final_score - self.original_score

    @property
    def improved(self) -> bool:
        """Check if the final score is better than the original.

        Returns:
            True if final_score > original_score, False otherwise.

        Notes:
            Only returns True for strict improvement, not equal scores.
        """
        return self.final_score > self.original_score

    def __repr__(self) -> str:
        """Narrative summary of the evolution result.

        Returns:
            Human-readable multi-line summary with improvement percentage,
            iterations, stop reason, component names, and acceptance rate.
            Uses 2-space indent, no box-drawing characters, every line
            greppable. When the original score is zero the improvement is
            reported as an absolute value instead of a percentage.
        """
        if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
            imp_str = f"{self.improvement:+.4f} improvement"
        else:
            pct = self.improvement * 100 / abs(self.original_score)
            sign = "+" if pct > 0 else ""
            imp_str = f"{sign}{pct:.1f}% improvement"
        lines = [
            f"EvolutionResult: {imp_str} "
            f"({self.original_score:.2f} \u2192 {self.final_score:.2f})",
            f"  iterations: {self.total_iterations}, "
            f"stop_reason: {self.stop_reason.value}",
            f"  components: {', '.join(sorted(self.evolved_components))}",
        ]
        if self.total_iterations > 0:
            accepted = sum(1 for r in self.iteration_history if r.accepted)
            lines.append(f"  acceptance_rate: {accepted}/{self.total_iterations}")
        return "\n".join(lines)

    def show_diff(self, original_components: dict[str, str] | None = None) -> str:
        """Show unified diff between original and evolved components.

        Uses stored ``original_components`` if no explicit argument is
        provided. Produces git-diff-style output (``---``/``+++``/``@@``)
        for each component that changed.

        Args:
            original_components: Pre-evolution component values. If None,
                falls back to ``self.original_components``.

        Returns:
            Unified diff string, or ``"No changes detected."`` if all
            components are identical.

        Raises:
            ValueError: If both the argument and ``self.original_components``
                are None.
        """
        originals = (
            original_components
            if original_components is not None
            else self.original_components
        )
        if originals is None:
            raise ValueError(
                "No original components available. "
                "Pass original_components or use a result that stores them."
            )
        return _build_diff(self.evolved_components, originals)

    def _repr_html_(self) -> str:
        """Render an HTML summary for Jupyter notebooks.

        Returns:
            HTML string with summary and components tables. Uses semantic
            ``<th>`` header elements and inline CSS for portability across
            JupyterLab, Colab, and VS Code. Iteration history is wrapped
            in a collapsible ``<details>``/``<summary>`` block. The improvement
            is shown as a signed absolute value when the original score is
            zero, since a percentage is undefined there.
        """
        if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
            imp_html = f"{self.improvement:+.4f}"
        else:
            pct = self.improvement * 100 / abs(self.original_score)
            sign = "+" if pct > 0 else ""
            imp_html = f"{sign}{pct:.1f}%"
        style = (
            'style="border-collapse:collapse;'
            "font-family:monospace;font-size:13px;"
            'margin:8px 0"'
        )
        td = 'style="padding:4px 12px;border:1px solid #ddd"'
        th = 'style="padding:4px 12px;border:1px solid #ddd;background:#f5f5f5"'

        rows = [
            f"<tr><th {th}>Improvement</th><td {td}>{imp_html}</td></tr>",
            f"<tr><th {th}>Original Score</th>"
            f"<td {td}>{self.original_score:.4f}</td></tr>",
            f"<tr><th {th}>Final Score</th><td {td}>{self.final_score:.4f}</td></tr>",
            f"<tr><th {th}>Iterations</th><td {td}>{self.total_iterations}</td></tr>",
            f"<tr><th {th}>Stop Reason</th>"
            f"<td {td}>{html_mod.escape(self.stop_reason.value)}</td></tr>",
        ]
        summary_table = f"<table {style}>{''.join(rows)}</table>"

        comp_rows = []
        for name in sorted(self.evolved_components):
            val = html_mod.escape(_truncate(self.evolved_components[name], 200))
            comp_rows.append(
                f"<tr><td {td}>{html_mod.escape(name)}</td><td {td}>{val}</td></tr>"
            )
        comp_header = f"<tr><th {th}>Component</th><th {th}>Evolved Value</th></tr>"
        comp_table = f"<table {style}>{comp_header}{''.join(comp_rows)}</table>"

        history_rows = []
        for rec in self.iteration_history:
            accepted_mark = "\u2713" if rec.accepted else ""
            history_rows.append(
                f"<tr><td {td}>{rec.iteration_number}</td>"
                f"<td {td}>{rec.score:.4f}</td>"
                f"<td {td}>{html_mod.escape(rec.evolved_component)}</td>"
                f"<td {td}>{accepted_mark}</td></tr>"
            )
        history_header = (
            f"<tr><th {th}>#</th><th {th}>Score</th>"
            f"<th {th}>Component</th><th {th}>Accepted</th></tr>"
        )
        history_table = (
            f"<table {style}>{history_header}{''.join(history_rows)}</table>"
        )
        history_section = (
            "<details><summary>Iteration History "
            f"({len(self.iteration_history)} records)</summary>"
            f"{history_table}</details>"
        )

        return (
            "<div>"
            "<strong>EvolutionResult</strong>"
            f"{summary_table}{comp_table}{history_section}"
            "</div>"
        )

reflection_reasoning property

reflection_reasoning: str | None

Return the reflection reasoning from the last iteration.

Convenience accessor for the most recent iteration's reasoning explaining why the reflection agent proposed its mutation.

RETURNS DESCRIPTION
str | None

The reasoning string from the last iteration record, or None

str | None

if no iterations exist or the last iteration has no reasoning.

improvement property

improvement: float

Calculate the score improvement from original to final.

RETURNS DESCRIPTION
float

The difference between final_score and original_score.

float

Positive values indicate improvement, negative indicates degradation.

Notes

Override is not needed since frozen dataclasses support properties.

improved property

improved: bool

Check if the final score is better than the original.

RETURNS DESCRIPTION
bool

True if final_score > original_score, False otherwise.

Notes

Only returns True for strict improvement, not equal scores.

to_dict

to_dict() -> dict[str, Any]

Serialize this result to a stdlib-only dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict containing all fields. stop_reason is serialized

dict[str, Any]

as its string value. iteration_history is serialized as a

dict[str, Any]

list of dicts. Includes baseline_failed_evaluations,

dict[str, Any]

total_failed_evaluations and token_usage (None when

dict[str, Any]

unset). Output is directly json.dumps()-compatible.

Source code in src/gepa_adk/domain/models.py
def to_dict(self) -> dict[str, Any]:
    """Serialize this result to a stdlib-only dict.

    Returns:
        Dict containing all fields. ``stop_reason`` is serialized
        as its string value. ``iteration_history`` is serialized as a
        list of dicts. Includes ``baseline_failed_evaluations``,
        ``total_failed_evaluations`` and ``token_usage`` (None when
        unset). Output is directly ``json.dumps()``-compatible.
    """
    return {
        "schema_version": self.schema_version,
        "stop_reason": self.stop_reason.value,
        "original_score": self.original_score,
        "final_score": self.final_score,
        "evolved_components": self.evolved_components,
        "iteration_history": [r.to_dict() for r in self.iteration_history],
        "total_iterations": self.total_iterations,
        "valset_score": self.valset_score,
        "trainset_score": self.trainset_score,
        "objective_scores": self.objective_scores,
        "original_components": self.original_components,
        "baseline_failed_evaluations": self.baseline_failed_evaluations,
        "total_failed_evaluations": self.total_failed_evaluations,
        "token_usage": _rollup_to_dict(self.token_usage),
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> EvolutionResult

Reconstruct an EvolutionResult from a dict.

Validates schema version, applies migration if needed, and reconstructs all nested objects including optional original_components. Version 1 dicts migrate through _migrate_v1_to_v2(), so their failure counts read as 0, and version 1 and 2 dicts through _migrate_v2_to_v3(), so their token_usage reads as None, and version 1 to 3 dicts through _migrate_v3_to_v4(), so each record's candidate_id and parent_ids read as None, and version 1 to 4 dicts through _migrate_v4_to_v5(), so each record's rejection_reason reads as None, and version 1 to 5 dicts through _migrate_v5_to_v6(), so each token_usage loads as its evaluation split with reflection None, and version 1 to 6 dicts through _migrate_v6_to_v7(), so each record's rows_evaluated reads as None.

PARAMETER DESCRIPTION
data

Dict containing evolution result fields.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
EvolutionResult

Reconstructed EvolutionResult instance.

RAISES DESCRIPTION
ConfigurationError

If schema_version exceeds the current version or stop_reason is not a valid enum value.

KeyError

If a required field is missing from the dict.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "EvolutionResult":
    """Reconstruct an EvolutionResult from a dict.

    Validates schema version, applies migration if needed, and
    reconstructs all nested objects including optional
    original_components. Version 1 dicts migrate through
    ``_migrate_v1_to_v2()``, so their failure counts read as 0, and
    version 1 and 2 dicts through ``_migrate_v2_to_v3()``, so their
    ``token_usage`` reads as None, and version 1 to 3 dicts through
    ``_migrate_v3_to_v4()``, so each record's ``candidate_id`` and
    ``parent_ids`` read as None, and version 1 to 4 dicts through
    ``_migrate_v4_to_v5()``, so each record's ``rejection_reason`` reads
    as None, and version 1 to 5 dicts through ``_migrate_v5_to_v6()``,
    so each ``token_usage`` loads as its ``evaluation`` split with
    ``reflection`` None, and version 1 to 6 dicts through
    ``_migrate_v6_to_v7()``, so each record's ``rows_evaluated`` reads
    as None.

    Args:
        data: Dict containing evolution result fields.

    Returns:
        Reconstructed EvolutionResult instance.

    Raises:
        ConfigurationError: If ``schema_version`` exceeds the current
            version or ``stop_reason`` is not a valid enum value.
        KeyError: If a required field is missing from the dict.
    """
    version = data.get("schema_version", 1)
    if version > CURRENT_SCHEMA_VERSION:
        raise ConfigurationError(
            f"Cannot deserialize result with schema_version {version} "
            f"(current version is {CURRENT_SCHEMA_VERSION}). "
            f"Upgrade gepa-adk to load this result.",
            field="schema_version",
            value=version,
            constraint=f"<= {CURRENT_SCHEMA_VERSION}",
        )
    migrated = _migrate_result_dict(data, from_version=version)
    try:
        stop_reason = StopReason(migrated.get("stop_reason", "completed"))
    except ValueError:
        raw = migrated.get("stop_reason")
        raise ConfigurationError(
            f"Invalid stop_reason value: {raw!r}",
            field="stop_reason",
            value=raw,
            constraint=("one of: " + ", ".join(sr.value for sr in StopReason)),
        ) from None
    return cls(
        schema_version=migrated["schema_version"],
        stop_reason=stop_reason,
        original_score=migrated["original_score"],
        final_score=migrated["final_score"],
        evolved_components=migrated["evolved_components"],
        iteration_history=[
            IterationRecord.from_dict(r)
            for r in migrated.get("iteration_history", [])
        ],
        total_iterations=migrated["total_iterations"],
        valset_score=migrated.get("valset_score"),
        trainset_score=migrated.get("trainset_score"),
        objective_scores=migrated.get("objective_scores"),
        original_components=migrated.get("original_components"),
        baseline_failed_evaluations=migrated.get("baseline_failed_evaluations", 0),
        total_failed_evaluations=migrated.get("total_failed_evaluations", 0),
        token_usage=_rollup_from_dict(migrated.get("token_usage")),
    )

__repr__

__repr__() -> str

Narrative summary of the evolution result.

RETURNS DESCRIPTION
str

Human-readable multi-line summary with improvement percentage,

str

iterations, stop reason, component names, and acceptance rate.

str

Uses 2-space indent, no box-drawing characters, every line

str

greppable. When the original score is zero the improvement is

str

reported as an absolute value instead of a percentage.

Source code in src/gepa_adk/domain/models.py
def __repr__(self) -> str:
    """Narrative summary of the evolution result.

    Returns:
        Human-readable multi-line summary with improvement percentage,
        iterations, stop reason, component names, and acceptance rate.
        Uses 2-space indent, no box-drawing characters, every line
        greppable. When the original score is zero the improvement is
        reported as an absolute value instead of a percentage.
    """
    if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
        imp_str = f"{self.improvement:+.4f} improvement"
    else:
        pct = self.improvement * 100 / abs(self.original_score)
        sign = "+" if pct > 0 else ""
        imp_str = f"{sign}{pct:.1f}% improvement"
    lines = [
        f"EvolutionResult: {imp_str} "
        f"({self.original_score:.2f} \u2192 {self.final_score:.2f})",
        f"  iterations: {self.total_iterations}, "
        f"stop_reason: {self.stop_reason.value}",
        f"  components: {', '.join(sorted(self.evolved_components))}",
    ]
    if self.total_iterations > 0:
        accepted = sum(1 for r in self.iteration_history if r.accepted)
        lines.append(f"  acceptance_rate: {accepted}/{self.total_iterations}")
    return "\n".join(lines)

show_diff

show_diff(
    original_components: dict[str, str] | None = None,
) -> str

Show unified diff between original and evolved components.

Uses stored original_components if no explicit argument is provided. Produces git-diff-style output (---/+++/@@) for each component that changed.

PARAMETER DESCRIPTION
original_components

Pre-evolution component values. If None, falls back to self.original_components.

TYPE: dict[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
str

Unified diff string, or "No changes detected." if all

str

components are identical.

RAISES DESCRIPTION
ValueError

If both the argument and self.original_components are None.

Source code in src/gepa_adk/domain/models.py
def show_diff(self, original_components: dict[str, str] | None = None) -> str:
    """Show unified diff between original and evolved components.

    Uses stored ``original_components`` if no explicit argument is
    provided. Produces git-diff-style output (``---``/``+++``/``@@``)
    for each component that changed.

    Args:
        original_components: Pre-evolution component values. If None,
            falls back to ``self.original_components``.

    Returns:
        Unified diff string, or ``"No changes detected."`` if all
        components are identical.

    Raises:
        ValueError: If both the argument and ``self.original_components``
            are None.
    """
    originals = (
        original_components
        if original_components is not None
        else self.original_components
    )
    if originals is None:
        raise ValueError(
            "No original components available. "
            "Pass original_components or use a result that stores them."
        )
    return _build_diff(self.evolved_components, originals)

IterationRecord dataclass

Captures metrics for a single evolution iteration.

This is an immutable record of what happened during one iteration of the evolution process. Records are created by the engine and stored in EvolutionResult.iteration_history.

ATTRIBUTE DESCRIPTION
iteration_number

1-indexed iteration number for human readability.

TYPE: int

score

Score achieved in this iteration (typically in [0.0, 1.0]).

TYPE: float

component_text

The component_text that was evaluated in this iteration (e.g., the instruction text for the "instruction" component).

TYPE: str

evolved_component

The name of the component that was evolved in this iteration (e.g., "instruction", "output_schema"). Used for tracking which component changed in round-robin evolution strategies.

TYPE: str

accepted

Whether this proposal was accepted as the new best.

TYPE: bool

objective_scores

Optional per-example multi-objective scores from the valset evaluation. None when adapter does not provide objective scores. Each dict maps objective name to score value. Index-aligned with evaluation batch examples.

TYPE: list[dict[str, float]] | None

reflection_reasoning

Optional natural language reasoning from the reflection agent explaining why the mutation was proposed. None when reasoning is not available (e.g., model without thinking support or older data without this field).

TYPE: str | None

skip_reason

Why the iteration produced no evaluated proposal, or None for an ordinary iteration. "empty_proposal" marks an iteration whose reflection returned an empty response twice; such a record has score=0.0, component_text="" and accepted=False because nothing was proposed or evaluated. "duplicate" marks a proposal whose components equal a candidate already scored in the run; it is not evaluated again, and the record carries that candidate's acceptance score, the proposal's component_text and accepted=False. "reflection_timeout" marks an iteration whose reflection agent exceeded its timeout; like an empty proposal, the record has score=0.0, component_text="" and accepted=False. "reflection_error" marks an iteration whose reflection function raised a retryable provider error (quota, availability or connection) on both attempts; the record has the same shape. "incomplete_proposal" marks an iteration whose reflection output was cut off at the output-token limit or opened a reasoning tag it never closed; it is not evaluated, and the record has score=0.0, the truncated text as component_text and accepted=False. "minibatch_rejected" marks a proposal that did not beat its parent on the iteration's reflection minibatch (see EvolutionConfig.reflection_minibatch_size); it is evaluated on those rows only, so score is the acceptance aggregate over the minibatch rows only, and accepted=False. "schema_validation_failed" marks a proposal whose output_schema text failed validation; it is not evaluated, and the record has score=0.0, the invalid schema text as component_text, evolved_component="output_schema" and accepted=False. "proposal_rejected" marks a proposal that EvolutionConfig.proposal_validator rejected; it is not evaluated, and the record has score=0.0, the rejected text as component_text, the validator's reason in rejection_reason and accepted=False.

TYPE: str | None

failed_evaluations

Number of evaluated rows in this iteration whose agent run or scorer raised, or whose run returned a failed execution, summed over every evaluation the iteration made (trainset, valset and merge). Such a row still scores 0.0, so this count separates a crash from a wrong answer. Defaults to 0; an "empty_proposal" or "duplicate" skip evaluates nothing and reports 0, and a "minibatch_rejected" skip counts failures in the minibatch rows.

TYPE: int

token_usage

Token usage observed on the rows this iteration evaluated and its reflection calls, or None when not recorded (results saved before schema version 3). The rollup carries an evaluation and a reflection split; reflection is None when no reflection call was observed (baseline, merge, an adapter without the proposer, or results saved before schema version 6). evaluation is set on every rollup the engine wrote at version 6 and on migrated results. A skip that evaluates nothing reports zero evaluation usage; rows evaluated without traces are unknown.

TYPE: TokenRollup | None

candidate_id

Candidate.id of the proposal this iteration produced. Set on evaluated, "duplicate", "minibatch_rejected", "schema_validation_failed" and "proposal_rejected" records; None for skips without a proposal (empty proposal, reflection timeout or error) and for results saved before schema version 4.

TYPE: str | None

parent_ids

Candidate.id of each candidate the proposal was made from: one for a mutation (the candidate the reflector rewrote); merges write no record. None when the iteration has no proposal or the result was saved before schema version 4.

TYPE: list[str] | None

rejection_reason

The reason EvolutionConfig.proposal_validator returned when skip_reason == "proposal_rejected"; None for every other record and for results saved before schema version 5.

TYPE: str | None

rows_evaluated

Number of rows score aggregates over (summed or averaged per EvolutionConfig.acceptance_metric). On an evaluated record (accepted or rejected after its full evaluation) it is the number of rows in the scoring batch; on a "minibatch_rejected" record it is the number of minibatch rows scored. 0 means nothing was evaluated: the "empty_proposal", "reflection_timeout", "reflection_error", "incomplete_proposal", "schema_validation_failed" and "proposal_rejected" skips. None means not recorded: a "duplicate" skip, whose score was computed in an earlier iteration, and results saved before schema version 7. When acceptance_metric="sum", score / rows_evaluated is the per-row mean; guard the division, since the count may be 0 or None.

TYPE: int | None

Examples:

Creating an iteration record:

from gepa_adk.domain.models import IterationRecord

record = IterationRecord(
    iteration_number=1,
    score=0.85,
    component_text="Be helpful",
    evolved_component="instruction",
    accepted=True,
)
print(record.score)  # 0.85
print(record.evolved_component)  # "instruction"
print(record.accepted)  # True

Serialization round-trip:

d = record.to_dict()
restored = IterationRecord.from_dict(d)
assert restored.score == record.score
Notes

An immutable record that captures iteration metrics. Once created, IterationRecord instances cannot be modified, ensuring historical accuracy of the evolution trace.

Source code in src/gepa_adk/domain/models.py
@dataclass(slots=True, frozen=True, kw_only=True)
class IterationRecord:
    """Captures metrics for a single evolution iteration.

    This is an immutable record of what happened during one iteration
    of the evolution process. Records are created by the engine and
    stored in EvolutionResult.iteration_history.

    Attributes:
        iteration_number (int): 1-indexed iteration number for human
            readability.
        score (float): Score achieved in this iteration (typically in
            [0.0, 1.0]).
        component_text (str): The component_text that was evaluated in this
            iteration (e.g., the instruction text for the "instruction" component).
        evolved_component (str): The name of the component that was evolved
            in this iteration (e.g., "instruction", "output_schema"). Used for
            tracking which component changed in round-robin evolution strategies.
        accepted (bool): Whether this proposal was accepted as the new best.
        objective_scores (list[dict[str, float]] | None): Optional per-example
            multi-objective scores from the valset evaluation. None when adapter
            does not provide objective scores. Each dict maps objective name to
            score value. Index-aligned with evaluation batch examples.
        reflection_reasoning (str | None): Optional natural language reasoning
            from the reflection agent explaining why the mutation was proposed.
            None when reasoning is not available (e.g., model without thinking
            support or older data without this field).
        skip_reason (str | None): Why the iteration produced no evaluated
            proposal, or None for an ordinary iteration. ``"empty_proposal"``
            marks an iteration whose reflection returned an empty response
            twice; such a record has ``score=0.0``, ``component_text=""`` and
            ``accepted=False`` because nothing was proposed or evaluated.
            ``"duplicate"`` marks a proposal whose components equal a
            candidate already scored in the run; it is not evaluated again,
            and the record carries that candidate's acceptance score, the
            proposal's ``component_text`` and ``accepted=False``.
            ``"reflection_timeout"`` marks an iteration whose reflection
            agent exceeded its timeout; like an empty proposal, the record
            has ``score=0.0``, ``component_text=""`` and ``accepted=False``.
            ``"reflection_error"`` marks an iteration whose reflection
            function raised a retryable provider error (quota, availability
            or connection) on both attempts; the record has the same shape.
            ``"incomplete_proposal"`` marks an iteration whose reflection
            output was cut off at the output-token limit or opened a
            reasoning tag it never closed; it is not evaluated, and the
            record has ``score=0.0``, the truncated text as
            ``component_text`` and ``accepted=False``.
            ``"minibatch_rejected"`` marks a proposal that did not beat its
            parent on the iteration's reflection minibatch (see
            ``EvolutionConfig.reflection_minibatch_size``); it is evaluated
            on those rows only, so ``score`` is the acceptance aggregate over
            the minibatch rows only, and ``accepted=False``.
            ``"schema_validation_failed"`` marks a proposal whose
            ``output_schema`` text failed validation; it is not evaluated, and
            the record has ``score=0.0``, the invalid schema text as
            ``component_text``, ``evolved_component="output_schema"`` and
            ``accepted=False``.
            ``"proposal_rejected"`` marks a proposal that
            ``EvolutionConfig.proposal_validator`` rejected; it is not
            evaluated, and the record has ``score=0.0``, the rejected text
            as ``component_text``, the validator's reason in
            ``rejection_reason`` and ``accepted=False``.
        failed_evaluations (int): Number of evaluated rows in this iteration
            whose agent run or scorer raised, or whose run returned a failed
            execution, summed over
            every evaluation the iteration made (trainset, valset and merge).
            Such a row still scores 0.0, so this count separates a crash from
            a wrong answer. Defaults to 0; an ``"empty_proposal"`` or
            ``"duplicate"`` skip evaluates nothing and reports 0, and a
            ``"minibatch_rejected"`` skip counts failures in the minibatch
            rows.
        token_usage (TokenRollup | None): Token usage observed on the rows
            this iteration evaluated and its reflection calls, or None when
            not recorded (results saved before schema version 3). The
            rollup carries an ``evaluation`` and a ``reflection`` split;
            ``reflection`` is None when no reflection call was observed
            (baseline, merge, an adapter without the proposer, or results
            saved before schema version 6). ``evaluation`` is set on every
            rollup the engine wrote at version 6 and on migrated results. A
            skip that evaluates nothing
            reports zero evaluation usage; rows evaluated without traces
            are unknown.
        candidate_id (str | None): ``Candidate.id`` of the proposal this
            iteration produced. Set on evaluated, ``"duplicate"``,
            ``"minibatch_rejected"``, ``"schema_validation_failed"`` and
            ``"proposal_rejected"`` records; None for skips without a proposal (empty proposal,
            reflection timeout or error) and for results saved before
            schema version 4.
        parent_ids (list[str] | None): ``Candidate.id`` of each candidate
            the proposal was made from: one for a mutation (the candidate
            the reflector rewrote); merges write no record. None when the
            iteration has no proposal or the result was saved before
            schema version 4.
        rejection_reason (str | None): The reason
            ``EvolutionConfig.proposal_validator`` returned when
            ``skip_reason == "proposal_rejected"``; None for every other
            record and for results saved before schema version 5.
        rows_evaluated (int | None): Number of rows ``score`` aggregates
            over (summed or averaged per ``EvolutionConfig.acceptance_metric``).
            On an evaluated record (accepted or rejected after its full
            evaluation) it is the number of rows in the scoring batch; on a
            ``"minibatch_rejected"`` record it is the number of minibatch
            rows scored. ``0`` means nothing was evaluated: the
            ``"empty_proposal"``, ``"reflection_timeout"``,
            ``"reflection_error"``, ``"incomplete_proposal"``,
            ``"schema_validation_failed"`` and ``"proposal_rejected"``
            skips. ``None`` means not recorded: a ``"duplicate"`` skip,
            whose ``score`` was computed in an earlier iteration, and
            results saved before schema version 7. When
            ``acceptance_metric="sum"``, ``score / rows_evaluated`` is the
            per-row mean; guard the division, since the count may be ``0``
            or ``None``.

    Examples:
        Creating an iteration record:

        ```python
        from gepa_adk.domain.models import IterationRecord

        record = IterationRecord(
            iteration_number=1,
            score=0.85,
            component_text="Be helpful",
            evolved_component="instruction",
            accepted=True,
        )
        print(record.score)  # 0.85
        print(record.evolved_component)  # "instruction"
        print(record.accepted)  # True
        ```

        Serialization round-trip:

        ```python
        d = record.to_dict()
        restored = IterationRecord.from_dict(d)
        assert restored.score == record.score
        ```

    Notes:
        An immutable record that captures iteration metrics. Once created,
        IterationRecord instances cannot be modified, ensuring historical
        accuracy of the evolution trace.
    """

    iteration_number: int
    score: float
    component_text: str
    evolved_component: str
    accepted: bool
    objective_scores: list[dict[str, float]] | None = None
    reflection_reasoning: str | None = None
    skip_reason: str | None = None
    failed_evaluations: int = 0
    token_usage: TokenRollup | None = None
    candidate_id: str | None = None
    parent_ids: list[str] | None = None
    rejection_reason: str | None = None
    rows_evaluated: int | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialize this record to a stdlib-only dict.

        Returns:
            Dict containing all 14 fields; ``token_usage``, ``candidate_id``,
            ``parent_ids``, ``rejection_reason`` and ``rows_evaluated`` are
            always present and None when unset.
            ``parent_ids`` is copied to a new list. Output is directly
            ``json.dumps()``-compatible.
        """
        return {
            "iteration_number": self.iteration_number,
            "score": self.score,
            "component_text": self.component_text,
            "evolved_component": self.evolved_component,
            "accepted": self.accepted,
            "objective_scores": self.objective_scores,
            "reflection_reasoning": self.reflection_reasoning,
            "skip_reason": self.skip_reason,
            "failed_evaluations": self.failed_evaluations,
            "token_usage": _rollup_to_dict(self.token_usage),
            "candidate_id": self.candidate_id,
            "parent_ids": (
                list(self.parent_ids) if self.parent_ids is not None else None
            ),
            "rejection_reason": self.rejection_reason,
            "rows_evaluated": self.rows_evaluated,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "IterationRecord":
        """Reconstruct an IterationRecord from a dict.

        Unknown keys are silently ignored for forward compatibility,
        allowing older code to load records produced by newer versions.
        Optional fields (``objective_scores``, ``reflection_reasoning``,
        ``skip_reason``, ``token_usage``, ``candidate_id``, ``parent_ids``,
        ``rejection_reason``, ``rows_evaluated``) default to None and
        ``failed_evaluations`` to 0 when missing from
        the input dict. ``parent_ids`` is copied to a new list so the frozen
        record does not alias the input.

        Args:
            data: Dict containing iteration record fields.

        Returns:
            Reconstructed IterationRecord instance.

        Raises:
            KeyError: If a required field is missing from the dict.
        """
        return cls(
            iteration_number=data["iteration_number"],
            score=data["score"],
            component_text=data["component_text"],
            evolved_component=data["evolved_component"],
            accepted=data["accepted"],
            objective_scores=data.get("objective_scores"),
            reflection_reasoning=data.get("reflection_reasoning"),
            skip_reason=data.get("skip_reason"),
            failed_evaluations=data.get("failed_evaluations", 0),
            token_usage=_rollup_from_dict(data.get("token_usage")),
            candidate_id=data.get("candidate_id"),
            parent_ids=(
                list(parent_ids)
                if (parent_ids := data.get("parent_ids")) is not None
                else None
            ),
            rejection_reason=data.get("rejection_reason"),
            rows_evaluated=data.get("rows_evaluated"),
        )

to_dict

to_dict() -> dict[str, Any]

Serialize this record to a stdlib-only dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict containing all 14 fields; token_usage, candidate_id,

dict[str, Any]

parent_ids, rejection_reason and rows_evaluated are

dict[str, Any]

always present and None when unset.

dict[str, Any]

parent_ids is copied to a new list. Output is directly

dict[str, Any]

json.dumps()-compatible.

Source code in src/gepa_adk/domain/models.py
def to_dict(self) -> dict[str, Any]:
    """Serialize this record to a stdlib-only dict.

    Returns:
        Dict containing all 14 fields; ``token_usage``, ``candidate_id``,
        ``parent_ids``, ``rejection_reason`` and ``rows_evaluated`` are
        always present and None when unset.
        ``parent_ids`` is copied to a new list. Output is directly
        ``json.dumps()``-compatible.
    """
    return {
        "iteration_number": self.iteration_number,
        "score": self.score,
        "component_text": self.component_text,
        "evolved_component": self.evolved_component,
        "accepted": self.accepted,
        "objective_scores": self.objective_scores,
        "reflection_reasoning": self.reflection_reasoning,
        "skip_reason": self.skip_reason,
        "failed_evaluations": self.failed_evaluations,
        "token_usage": _rollup_to_dict(self.token_usage),
        "candidate_id": self.candidate_id,
        "parent_ids": (
            list(self.parent_ids) if self.parent_ids is not None else None
        ),
        "rejection_reason": self.rejection_reason,
        "rows_evaluated": self.rows_evaluated,
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> IterationRecord

Reconstruct an IterationRecord from a dict.

Unknown keys are silently ignored for forward compatibility, allowing older code to load records produced by newer versions. Optional fields (objective_scores, reflection_reasoning, skip_reason, token_usage, candidate_id, parent_ids, rejection_reason, rows_evaluated) default to None and failed_evaluations to 0 when missing from the input dict. parent_ids is copied to a new list so the frozen record does not alias the input.

PARAMETER DESCRIPTION
data

Dict containing iteration record fields.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
IterationRecord

Reconstructed IterationRecord instance.

RAISES DESCRIPTION
KeyError

If a required field is missing from the dict.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "IterationRecord":
    """Reconstruct an IterationRecord from a dict.

    Unknown keys are silently ignored for forward compatibility,
    allowing older code to load records produced by newer versions.
    Optional fields (``objective_scores``, ``reflection_reasoning``,
    ``skip_reason``, ``token_usage``, ``candidate_id``, ``parent_ids``,
    ``rejection_reason``, ``rows_evaluated``) default to None and
    ``failed_evaluations`` to 0 when missing from
    the input dict. ``parent_ids`` is copied to a new list so the frozen
    record does not alias the input.

    Args:
        data: Dict containing iteration record fields.

    Returns:
        Reconstructed IterationRecord instance.

    Raises:
        KeyError: If a required field is missing from the dict.
    """
    return cls(
        iteration_number=data["iteration_number"],
        score=data["score"],
        component_text=data["component_text"],
        evolved_component=data["evolved_component"],
        accepted=data["accepted"],
        objective_scores=data.get("objective_scores"),
        reflection_reasoning=data.get("reflection_reasoning"),
        skip_reason=data.get("skip_reason"),
        failed_evaluations=data.get("failed_evaluations", 0),
        token_usage=_rollup_from_dict(data.get("token_usage")),
        candidate_id=data.get("candidate_id"),
        parent_ids=(
            list(parent_ids)
            if (parent_ids := data.get("parent_ids")) is not None
            else None
        ),
        rejection_reason=data.get("rejection_reason"),
        rows_evaluated=data.get("rows_evaluated"),
    )

MultiAgentEvolutionResult dataclass

Outcome of a completed multi-agent evolution run.

Contains evolved component_text for all agents in the group, along with performance metrics and evolution history.

ATTRIBUTE DESCRIPTION
schema_version

Schema version for forward-compatible serialization.

TYPE: int

stop_reason

Why the evolution run terminated.

TYPE: StopReason

evolved_components

Mapping of agent name to evolved component_text.

TYPE: dict[str, str]

original_score

Starting performance score (baseline).

TYPE: float

final_score

Ending performance score (best achieved).

TYPE: float

primary_agent

Name of the agent whose output was used for scoring.

TYPE: str

iteration_history

Chronological list of iteration records.

TYPE: list[IterationRecord]

total_iterations

Number of iterations performed.

TYPE: int

original_components

Optional snapshot of pre-evolution component values. When present, enables zero-arg show_diff() calls. None for results created before this field was added or when originals were not captured.

TYPE: dict[str, str] | None

baseline_failed_evaluations

Number of rows whose pipeline run or scorer raised, or whose run returned a failed execution, during the baseline evaluation. Defaults to 0.

TYPE: int

total_failed_evaluations

baseline_failed_evaluations plus the sum of failed_evaluations over iteration_history. Defaults to 0.

TYPE: int

token_usage

Token usage observed on the rows evaluated over the run, baseline included, and on its reflection calls. None when not recorded (results saved before schema version 3). The rollup carries an evaluation split, set on every result the engine wrote at version 6 and on migrated results, and a reflection split that is None when no reflection call was observed.

TYPE: TokenRollup | None

Examples:

Creating and analyzing a multi-agent result:

from gepa_adk.domain.models import MultiAgentEvolutionResult, IterationRecord

result = MultiAgentEvolutionResult(
    evolved_components={
        "generator": "Generate high-quality code",
        "critic": "Review code thoroughly",
    },
    original_components={
        "generator": "Generate code",
        "critic": "Review code",
    },
    original_score=0.60,
    final_score=0.85,
    primary_agent="generator",
    iteration_history=[],
    total_iterations=10,
)
print(result.improvement)  # 0.25
print(result.show_diff())  # unified diff of component changes
print(result.agent_names)  # ["critic", "generator"]
assert result.schema_version == 7

Serialization round-trip:

import json

d = result.to_dict()
restored = MultiAgentEvolutionResult.from_dict(json.loads(json.dumps(d)))
Notes

An immutable result container for multi-agent evolution. Once created, MultiAgentEvolutionResult instances cannot be modified. Use computed properties like improvement, improved, and agent_names to analyze results without modifying the underlying data.

Source code in src/gepa_adk/domain/models.py
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
@dataclass(slots=True, frozen=True, kw_only=True)
class MultiAgentEvolutionResult:
    """Outcome of a completed multi-agent evolution run.

    Contains evolved component_text for all agents in the group,
    along with performance metrics and evolution history.

    Attributes:
        schema_version (int): Schema version for forward-compatible serialization.
        stop_reason (StopReason): Why the evolution run terminated.
        evolved_components (dict[str, str]): Mapping of agent name to evolved
            component_text.
        original_score (float): Starting performance score (baseline).
        final_score (float): Ending performance score (best achieved).
        primary_agent (str): Name of the agent whose output was used for scoring.
        iteration_history (list[IterationRecord]): Chronological list of iteration records.
        total_iterations (int): Number of iterations performed.
        original_components (dict[str, str] | None): Optional snapshot of
            pre-evolution component values. When present, enables zero-arg
            ``show_diff()`` calls. None for results created before this field
            was added or when originals were not captured.
        baseline_failed_evaluations (int): Number of rows whose pipeline run or
            scorer raised, or whose run returned a failed execution, during
            the baseline evaluation. Defaults to 0.
        total_failed_evaluations (int): ``baseline_failed_evaluations`` plus
            the sum of ``failed_evaluations`` over ``iteration_history``.
            Defaults to 0.
        token_usage (TokenRollup | None): Token usage observed on the rows
            evaluated over the run, baseline included, and on its
            reflection calls. None when not recorded (results saved before
            schema version 3). The rollup carries an ``evaluation`` split,
            set on every result the engine wrote at version 6 and on
            migrated results, and a ``reflection`` split that is None when
            no reflection call was observed.

    Examples:
        Creating and analyzing a multi-agent result:

        ```python
        from gepa_adk.domain.models import MultiAgentEvolutionResult, IterationRecord

        result = MultiAgentEvolutionResult(
            evolved_components={
                "generator": "Generate high-quality code",
                "critic": "Review code thoroughly",
            },
            original_components={
                "generator": "Generate code",
                "critic": "Review code",
            },
            original_score=0.60,
            final_score=0.85,
            primary_agent="generator",
            iteration_history=[],
            total_iterations=10,
        )
        print(result.improvement)  # 0.25
        print(result.show_diff())  # unified diff of component changes
        print(result.agent_names)  # ["critic", "generator"]
        assert result.schema_version == 7
        ```

        Serialization round-trip:

        ```python
        import json

        d = result.to_dict()
        restored = MultiAgentEvolutionResult.from_dict(json.loads(json.dumps(d)))
        ```

    Notes:
        An immutable result container for multi-agent evolution. Once created,
        MultiAgentEvolutionResult instances cannot be modified. Use computed
        properties like `improvement`, `improved`, and `agent_names` to analyze
        results without modifying the underlying data.
    """

    schema_version: int = CURRENT_SCHEMA_VERSION
    stop_reason: StopReason = StopReason.COMPLETED
    evolved_components: dict[str, str]
    original_score: float
    final_score: float
    primary_agent: str
    iteration_history: list[IterationRecord]
    total_iterations: int
    original_components: dict[str, str] | None = None
    baseline_failed_evaluations: int = 0
    total_failed_evaluations: int = 0
    token_usage: TokenRollup | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialize this result to a stdlib-only dict.

        Returns:
            Dict containing all 12 fields. ``stop_reason`` is serialized
            as its string value. ``iteration_history`` is serialized as a
            list of dicts. Output is directly ``json.dumps()``-compatible.
        """
        return {
            "schema_version": self.schema_version,
            "stop_reason": self.stop_reason.value,
            "evolved_components": self.evolved_components,
            "original_score": self.original_score,
            "final_score": self.final_score,
            "primary_agent": self.primary_agent,
            "iteration_history": [r.to_dict() for r in self.iteration_history],
            "total_iterations": self.total_iterations,
            "original_components": self.original_components,
            "baseline_failed_evaluations": self.baseline_failed_evaluations,
            "total_failed_evaluations": self.total_failed_evaluations,
            "token_usage": _rollup_to_dict(self.token_usage),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "MultiAgentEvolutionResult":
        """Reconstruct a MultiAgentEvolutionResult from a dict.

        Validates schema version, applies migration if needed, and
        reconstructs all nested objects including optional
        original_components. Version 1 dicts migrate through
        ``_migrate_v1_to_v2()``, so their failure counts read as 0, and
        version 1 and 2 dicts through ``_migrate_v2_to_v3()``, so their
        ``token_usage`` reads as None, and version 1 to 3 dicts through
        ``_migrate_v3_to_v4()``, so each record's ``candidate_id`` and
        ``parent_ids`` read as None, and version 1 to 4 dicts through
        ``_migrate_v4_to_v5()``, so each record's ``rejection_reason`` reads
        as None, and version 1 to 5 dicts through ``_migrate_v5_to_v6()``,
        so each ``token_usage`` loads as its ``evaluation`` split with
        ``reflection`` None, and version 1 to 6 dicts through
        ``_migrate_v6_to_v7()``, so each record's ``rows_evaluated`` reads
        as None.

        Args:
            data: Dict containing multi-agent evolution result fields.

        Returns:
            Reconstructed MultiAgentEvolutionResult instance.

        Raises:
            ConfigurationError: If ``schema_version`` exceeds the current
                version or ``stop_reason`` is not a valid enum value.
            KeyError: If a required field is missing from the dict.
        """
        version = data.get("schema_version", 1)
        if version > CURRENT_SCHEMA_VERSION:
            raise ConfigurationError(
                f"Cannot deserialize result with schema_version {version} "
                f"(current version is {CURRENT_SCHEMA_VERSION}). "
                f"Upgrade gepa-adk to load this result.",
                field="schema_version",
                value=version,
                constraint=f"<= {CURRENT_SCHEMA_VERSION}",
            )
        migrated = _migrate_result_dict(data, from_version=version)
        try:
            stop_reason = StopReason(migrated.get("stop_reason", "completed"))
        except ValueError:
            raw = migrated.get("stop_reason")
            raise ConfigurationError(
                f"Invalid stop_reason value: {raw!r}",
                field="stop_reason",
                value=raw,
                constraint=("one of: " + ", ".join(sr.value for sr in StopReason)),
            ) from None
        return cls(
            schema_version=migrated["schema_version"],
            stop_reason=stop_reason,
            evolved_components=migrated["evolved_components"],
            original_score=migrated["original_score"],
            final_score=migrated["final_score"],
            primary_agent=migrated["primary_agent"],
            iteration_history=[
                IterationRecord.from_dict(r)
                for r in migrated.get("iteration_history", [])
            ],
            total_iterations=migrated["total_iterations"],
            original_components=migrated.get("original_components"),
            baseline_failed_evaluations=migrated.get("baseline_failed_evaluations", 0),
            total_failed_evaluations=migrated.get("total_failed_evaluations", 0),
            token_usage=_rollup_from_dict(migrated.get("token_usage")),
        )

    @property
    def improvement(self) -> float:
        """Calculate the score improvement from original to final.

        Returns:
            The difference between final_score and original_score.
            Positive values indicate improvement, negative indicates degradation.

        Notes:
            Override is not needed since frozen dataclasses support properties.
        """
        return self.final_score - self.original_score

    @property
    def improved(self) -> bool:
        """Check if the final score is better than the original.

        Returns:
            True if final_score > original_score, False otherwise.

        Notes:
            Only returns True for strict improvement, not equal scores.
        """
        return self.final_score > self.original_score

    @property
    def agent_names(self) -> list[str]:
        """Get sorted list of evolved agent names.

        Returns:
            Sorted list of agent names from evolved_components keys.

        Notes:
            Outputs a new list each time, sorted alphabetically for
            consistent ordering regardless of insertion order.
        """
        return sorted(self.evolved_components.keys())

    def __repr__(self) -> str:
        """Narrative summary of the multi-agent evolution result.

        Returns:
            Human-readable multi-line summary with improvement percentage,
            iterations, stop reason, primary agent, agent names, and
            acceptance rate. Uses 2-space indent, no box-drawing characters,
            every line greppable. When the original score is zero the
            improvement is reported as an absolute value instead of a
            percentage.
        """
        if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
            imp_str = f"{self.improvement:+.4f} improvement"
        else:
            pct = self.improvement * 100 / abs(self.original_score)
            sign = "+" if pct > 0 else ""
            imp_str = f"{sign}{pct:.1f}% improvement"
        lines = [
            f"MultiAgentEvolutionResult: {imp_str} "
            f"({self.original_score:.2f} \u2192 {self.final_score:.2f})",
            f"  iterations: {self.total_iterations}, "
            f"stop_reason: {self.stop_reason.value}",
            f"  primary_agent: {self.primary_agent}",
            f"  agents: {', '.join(sorted(self.evolved_components))}",
        ]
        if self.total_iterations > 0:
            accepted = sum(1 for r in self.iteration_history if r.accepted)
            lines.append(f"  acceptance_rate: {accepted}/{self.total_iterations}")
        return "\n".join(lines)

    def show_diff(self, original_components: dict[str, str] | None = None) -> str:
        """Show unified diff between original and evolved components.

        Uses stored ``original_components`` if no explicit argument is
        provided. Produces git-diff-style output (``---``/``+++``/``@@``)
        for each component that changed.

        Args:
            original_components: Pre-evolution component values. If None,
                falls back to ``self.original_components``.

        Returns:
            Unified diff string, or ``"No changes detected."`` if all
            components are identical.

        Raises:
            ValueError: If both the argument and ``self.original_components``
                are None.
        """
        originals = (
            original_components
            if original_components is not None
            else self.original_components
        )
        if originals is None:
            raise ValueError(
                "No original components available. "
                "Pass original_components or use a result that stores them."
            )
        return _build_diff(self.evolved_components, originals)

    def _repr_html_(self) -> str:
        """Render an HTML summary for Jupyter notebooks.

        Returns:
            HTML string with summary and components tables. Uses semantic
            ``<th>`` header elements and inline CSS for portability across
            JupyterLab, Colab, and VS Code. Iteration history is wrapped
            in a collapsible ``<details>``/``<summary>`` block. The improvement
            is shown as a signed absolute value when the original score is
            zero, since a percentage is undefined there.
        """
        if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
            imp_html = f"{self.improvement:+.4f}"
        else:
            pct = self.improvement * 100 / abs(self.original_score)
            sign = "+" if pct > 0 else ""
            imp_html = f"{sign}{pct:.1f}%"
        style = (
            'style="border-collapse:collapse;'
            "font-family:monospace;font-size:13px;"
            'margin:8px 0"'
        )
        td = 'style="padding:4px 12px;border:1px solid #ddd"'
        th = 'style="padding:4px 12px;border:1px solid #ddd;background:#f5f5f5"'

        rows = [
            f"<tr><th {th}>Improvement</th><td {td}>{imp_html}</td></tr>",
            f"<tr><th {th}>Original Score</th>"
            f"<td {td}>{self.original_score:.4f}</td></tr>",
            f"<tr><th {th}>Final Score</th><td {td}>{self.final_score:.4f}</td></tr>",
            f"<tr><th {th}>Iterations</th><td {td}>{self.total_iterations}</td></tr>",
            f"<tr><th {th}>Stop Reason</th>"
            f"<td {td}>{html_mod.escape(self.stop_reason.value)}</td></tr>",
            f"<tr><th {th}>Primary Agent</th>"
            f"<td {td}>{html_mod.escape(self.primary_agent)}</td></tr>",
        ]
        summary_table = f"<table {style}>{''.join(rows)}</table>"

        comp_rows = []
        for name in sorted(self.evolved_components):
            val = html_mod.escape(_truncate(self.evolved_components[name], 200))
            comp_rows.append(
                f"<tr><td {td}>{html_mod.escape(name)}</td><td {td}>{val}</td></tr>"
            )
        comp_header = f"<tr><th {th}>Agent</th><th {th}>Evolved Value</th></tr>"
        comp_table = f"<table {style}>{comp_header}{''.join(comp_rows)}</table>"

        history_rows = []
        for rec in self.iteration_history:
            accepted_mark = "\u2713" if rec.accepted else ""
            history_rows.append(
                f"<tr><td {td}>{rec.iteration_number}</td>"
                f"<td {td}>{rec.score:.4f}</td>"
                f"<td {td}>{html_mod.escape(rec.evolved_component)}</td>"
                f"<td {td}>{accepted_mark}</td></tr>"
            )
        history_header = (
            f"<tr><th {th}>#</th><th {th}>Score</th>"
            f"<th {th}>Component</th><th {th}>Accepted</th></tr>"
        )
        history_table = (
            f"<table {style}>{history_header}{''.join(history_rows)}</table>"
        )
        history_section = (
            "<details><summary>Iteration History "
            f"({len(self.iteration_history)} records)</summary>"
            f"{history_table}</details>"
        )

        return (
            "<div>"
            "<strong>MultiAgentEvolutionResult</strong>"
            f"{summary_table}{comp_table}{history_section}"
            "</div>"
        )

improvement property

improvement: float

Calculate the score improvement from original to final.

RETURNS DESCRIPTION
float

The difference between final_score and original_score.

float

Positive values indicate improvement, negative indicates degradation.

Notes

Override is not needed since frozen dataclasses support properties.

improved property

improved: bool

Check if the final score is better than the original.

RETURNS DESCRIPTION
bool

True if final_score > original_score, False otherwise.

Notes

Only returns True for strict improvement, not equal scores.

agent_names property

agent_names: list[str]

Get sorted list of evolved agent names.

RETURNS DESCRIPTION
list[str]

Sorted list of agent names from evolved_components keys.

Notes

Outputs a new list each time, sorted alphabetically for consistent ordering regardless of insertion order.

to_dict

to_dict() -> dict[str, Any]

Serialize this result to a stdlib-only dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict containing all 12 fields. stop_reason is serialized

dict[str, Any]

as its string value. iteration_history is serialized as a

dict[str, Any]

list of dicts. Output is directly json.dumps()-compatible.

Source code in src/gepa_adk/domain/models.py
def to_dict(self) -> dict[str, Any]:
    """Serialize this result to a stdlib-only dict.

    Returns:
        Dict containing all 12 fields. ``stop_reason`` is serialized
        as its string value. ``iteration_history`` is serialized as a
        list of dicts. Output is directly ``json.dumps()``-compatible.
    """
    return {
        "schema_version": self.schema_version,
        "stop_reason": self.stop_reason.value,
        "evolved_components": self.evolved_components,
        "original_score": self.original_score,
        "final_score": self.final_score,
        "primary_agent": self.primary_agent,
        "iteration_history": [r.to_dict() for r in self.iteration_history],
        "total_iterations": self.total_iterations,
        "original_components": self.original_components,
        "baseline_failed_evaluations": self.baseline_failed_evaluations,
        "total_failed_evaluations": self.total_failed_evaluations,
        "token_usage": _rollup_to_dict(self.token_usage),
    }

from_dict classmethod

from_dict(
    data: dict[str, Any],
) -> MultiAgentEvolutionResult

Reconstruct a MultiAgentEvolutionResult from a dict.

Validates schema version, applies migration if needed, and reconstructs all nested objects including optional original_components. Version 1 dicts migrate through _migrate_v1_to_v2(), so their failure counts read as 0, and version 1 and 2 dicts through _migrate_v2_to_v3(), so their token_usage reads as None, and version 1 to 3 dicts through _migrate_v3_to_v4(), so each record's candidate_id and parent_ids read as None, and version 1 to 4 dicts through _migrate_v4_to_v5(), so each record's rejection_reason reads as None, and version 1 to 5 dicts through _migrate_v5_to_v6(), so each token_usage loads as its evaluation split with reflection None, and version 1 to 6 dicts through _migrate_v6_to_v7(), so each record's rows_evaluated reads as None.

PARAMETER DESCRIPTION
data

Dict containing multi-agent evolution result fields.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
MultiAgentEvolutionResult

Reconstructed MultiAgentEvolutionResult instance.

RAISES DESCRIPTION
ConfigurationError

If schema_version exceeds the current version or stop_reason is not a valid enum value.

KeyError

If a required field is missing from the dict.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "MultiAgentEvolutionResult":
    """Reconstruct a MultiAgentEvolutionResult from a dict.

    Validates schema version, applies migration if needed, and
    reconstructs all nested objects including optional
    original_components. Version 1 dicts migrate through
    ``_migrate_v1_to_v2()``, so their failure counts read as 0, and
    version 1 and 2 dicts through ``_migrate_v2_to_v3()``, so their
    ``token_usage`` reads as None, and version 1 to 3 dicts through
    ``_migrate_v3_to_v4()``, so each record's ``candidate_id`` and
    ``parent_ids`` read as None, and version 1 to 4 dicts through
    ``_migrate_v4_to_v5()``, so each record's ``rejection_reason`` reads
    as None, and version 1 to 5 dicts through ``_migrate_v5_to_v6()``,
    so each ``token_usage`` loads as its ``evaluation`` split with
    ``reflection`` None, and version 1 to 6 dicts through
    ``_migrate_v6_to_v7()``, so each record's ``rows_evaluated`` reads
    as None.

    Args:
        data: Dict containing multi-agent evolution result fields.

    Returns:
        Reconstructed MultiAgentEvolutionResult instance.

    Raises:
        ConfigurationError: If ``schema_version`` exceeds the current
            version or ``stop_reason`` is not a valid enum value.
        KeyError: If a required field is missing from the dict.
    """
    version = data.get("schema_version", 1)
    if version > CURRENT_SCHEMA_VERSION:
        raise ConfigurationError(
            f"Cannot deserialize result with schema_version {version} "
            f"(current version is {CURRENT_SCHEMA_VERSION}). "
            f"Upgrade gepa-adk to load this result.",
            field="schema_version",
            value=version,
            constraint=f"<= {CURRENT_SCHEMA_VERSION}",
        )
    migrated = _migrate_result_dict(data, from_version=version)
    try:
        stop_reason = StopReason(migrated.get("stop_reason", "completed"))
    except ValueError:
        raw = migrated.get("stop_reason")
        raise ConfigurationError(
            f"Invalid stop_reason value: {raw!r}",
            field="stop_reason",
            value=raw,
            constraint=("one of: " + ", ".join(sr.value for sr in StopReason)),
        ) from None
    return cls(
        schema_version=migrated["schema_version"],
        stop_reason=stop_reason,
        evolved_components=migrated["evolved_components"],
        original_score=migrated["original_score"],
        final_score=migrated["final_score"],
        primary_agent=migrated["primary_agent"],
        iteration_history=[
            IterationRecord.from_dict(r)
            for r in migrated.get("iteration_history", [])
        ],
        total_iterations=migrated["total_iterations"],
        original_components=migrated.get("original_components"),
        baseline_failed_evaluations=migrated.get("baseline_failed_evaluations", 0),
        total_failed_evaluations=migrated.get("total_failed_evaluations", 0),
        token_usage=_rollup_from_dict(migrated.get("token_usage")),
    )

__repr__

__repr__() -> str

Narrative summary of the multi-agent evolution result.

RETURNS DESCRIPTION
str

Human-readable multi-line summary with improvement percentage,

str

iterations, stop reason, primary agent, agent names, and

str

acceptance rate. Uses 2-space indent, no box-drawing characters,

str

every line greppable. When the original score is zero the

str

improvement is reported as an absolute value instead of a

str

percentage.

Source code in src/gepa_adk/domain/models.py
def __repr__(self) -> str:
    """Narrative summary of the multi-agent evolution result.

    Returns:
        Human-readable multi-line summary with improvement percentage,
        iterations, stop reason, primary agent, agent names, and
        acceptance rate. Uses 2-space indent, no box-drawing characters,
        every line greppable. When the original score is zero the
        improvement is reported as an absolute value instead of a
        percentage.
    """
    if abs(self.original_score) < 1e-9:  # noqa: PLR2004  # zero guard before a percentage division
        imp_str = f"{self.improvement:+.4f} improvement"
    else:
        pct = self.improvement * 100 / abs(self.original_score)
        sign = "+" if pct > 0 else ""
        imp_str = f"{sign}{pct:.1f}% improvement"
    lines = [
        f"MultiAgentEvolutionResult: {imp_str} "
        f"({self.original_score:.2f} \u2192 {self.final_score:.2f})",
        f"  iterations: {self.total_iterations}, "
        f"stop_reason: {self.stop_reason.value}",
        f"  primary_agent: {self.primary_agent}",
        f"  agents: {', '.join(sorted(self.evolved_components))}",
    ]
    if self.total_iterations > 0:
        accepted = sum(1 for r in self.iteration_history if r.accepted)
        lines.append(f"  acceptance_rate: {accepted}/{self.total_iterations}")
    return "\n".join(lines)

show_diff

show_diff(
    original_components: dict[str, str] | None = None,
) -> str

Show unified diff between original and evolved components.

Uses stored original_components if no explicit argument is provided. Produces git-diff-style output (---/+++/@@) for each component that changed.

PARAMETER DESCRIPTION
original_components

Pre-evolution component values. If None, falls back to self.original_components.

TYPE: dict[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
str

Unified diff string, or "No changes detected." if all

str

components are identical.

RAISES DESCRIPTION
ValueError

If both the argument and self.original_components are None.

Source code in src/gepa_adk/domain/models.py
def show_diff(self, original_components: dict[str, str] | None = None) -> str:
    """Show unified diff between original and evolved components.

    Uses stored ``original_components`` if no explicit argument is
    provided. Produces git-diff-style output (``---``/``+++``/``@@``)
    for each component that changed.

    Args:
        original_components: Pre-evolution component values. If None,
            falls back to ``self.original_components``.

    Returns:
        Unified diff string, or ``"No changes detected."`` if all
        components are identical.

    Raises:
        ValueError: If both the argument and ``self.original_components``
            are None.
    """
    originals = (
        original_components
        if original_components is not None
        else self.original_components
    )
    if originals is None:
        raise ValueError(
            "No original components available. "
            "Pass original_components or use a result that stores them."
        )
    return _build_diff(self.evolved_components, originals)

TokenRollup dataclass

Token usage summed over evaluated rows and reflection calls.

Each counter is the sum over the rows that reported usage. A row that reported none is counted in rows_unknown, never as zero tokens. The counters are None only when no row reported usage and at least one row was unknown; nothing evaluated gives zeros. A rollup built with split() also carries its evaluation and reflection parts, and its counters cover both; a rollup built by from_batch() is a plain rollup without splits.

ATTRIBUTE DESCRIPTION
input_tokens

Prompt tokens over the counted rows, or None when unknown.

TYPE: int | None

output_tokens

Generated tokens over the counted rows, or None when unknown.

TYPE: int | None

total_tokens

Total tokens over the counted rows, or None when unknown.

TYPE: int | None

rows_counted

Rows that reported usage.

TYPE: int

rows_unknown

Rows that reported no usage.

TYPE: int

evaluation

The evaluated rows' part of a split rollup, or None for a rollup without splits.

TYPE: TokenRollup | None

reflection

The reflection calls' part of a split rollup, one row per call, or None when no reflection call was observed.

TYPE: TokenRollup | None

Examples:

Summing an evaluation batch and serializing the result:

from gepa_adk.domain.models import TokenRollup

rollup = TokenRollup.from_batch(batch)
print(rollup.total_tokens, rollup.rows_unknown)
data = rollup.to_dict()  # None counters become "unknown"
assert TokenRollup.from_dict(data) == rollup
Notes

The evaluation part covers the rows the adapters evaluated with traces; rows evaluated without traces, such as a separate valset pass, are not observed. The reflection part counts each reflection call that returned; a call whose executor captured no events is unknown. The parts are rollups without splits of their own.

Source code in src/gepa_adk/domain/models.py
@dataclass(slots=True, frozen=True, kw_only=True)
class TokenRollup:
    """Token usage summed over evaluated rows and reflection calls.

    Each counter is the sum over the rows that reported usage. A row that
    reported none is counted in ``rows_unknown``, never as zero tokens. The
    counters are None only when no row reported usage and at least one row
    was unknown; nothing evaluated gives zeros. A rollup built with
    ``split()`` also carries its ``evaluation`` and ``reflection`` parts,
    and its counters cover both; a rollup built by ``from_batch()`` is a
    plain rollup without splits.

    Attributes:
        input_tokens (int | None): Prompt tokens over the counted rows, or
            None when unknown.
        output_tokens (int | None): Generated tokens over the counted rows,
            or None when unknown.
        total_tokens (int | None): Total tokens over the counted rows, or
            None when unknown.
        rows_counted (int): Rows that reported usage.
        rows_unknown (int): Rows that reported no usage.
        evaluation (TokenRollup | None): The evaluated rows' part of a split
            rollup, or None for a rollup without splits.
        reflection (TokenRollup | None): The reflection calls' part of a
            split rollup, one row per call, or None when no reflection call
            was observed.

    Examples:
        Summing an evaluation batch and serializing the result:

        ```python
        from gepa_adk.domain.models import TokenRollup

        rollup = TokenRollup.from_batch(batch)
        print(rollup.total_tokens, rollup.rows_unknown)
        data = rollup.to_dict()  # None counters become "unknown"
        assert TokenRollup.from_dict(data) == rollup
        ```

    Notes:
        The evaluation part covers the rows the adapters evaluated with
        traces; rows evaluated without traces, such as a separate valset
        pass, are not observed. The reflection part counts each reflection
        call that returned; a call whose executor captured no events is
        unknown. The parts are rollups without splits of their own.
    """

    input_tokens: int | None
    output_tokens: int | None
    total_tokens: int | None
    rows_counted: int
    rows_unknown: int
    evaluation: "TokenRollup | None" = None
    reflection: "TokenRollup | None" = None

    @classmethod
    def _build(
        cls,
        sums: tuple[int, int, int],
        rows_counted: int,
        rows_unknown: int,
    ) -> "TokenRollup":
        """Build a rollup, making counters unknown only under the unknown rule.

        Args:
            sums: Input, output and total token sums over counted rows.
            rows_counted: Rows that reported usage.
            rows_unknown: Rows that reported no usage.

        Returns:
            The rollup, with None counters when no row was counted and at
            least one row was unknown.
        """
        if rows_counted == 0 and rows_unknown > 0:
            return cls(
                input_tokens=None,
                output_tokens=None,
                total_tokens=None,
                rows_counted=0,
                rows_unknown=rows_unknown,
            )
        return cls(
            input_tokens=sums[0],
            output_tokens=sums[1],
            total_tokens=sums[2],
            rows_counted=rows_counted,
            rows_unknown=rows_unknown,
        )

    @classmethod
    def from_batch(cls, batch: Any) -> "TokenRollup":
        """Sum the token usage reported by each row of an evaluation batch.

        Reads ``batch.trajectories[i]`` for each row of ``batch.scores``. A
        trajectory whose ``token_usage`` or ``total_token_usage`` is a
        ``TokenUsage`` counts its three numbers; any other row (no
        trajectories, a trajectory without usage, a dict) is unknown.

        Args:
            batch: An ``EvaluationBatch`` or any object with ``scores`` and
                an optional ``trajectories`` sequence.

        Returns:
            The rollup of the batch. An empty batch gives zeros.
        """
        trajectories = getattr(batch, "trajectories", None) or []
        sums = [0, 0, 0]
        counted = 0
        unknown = 0
        for i in range(len(batch.scores)):
            usage = _row_usage(trajectories[i]) if i < len(trajectories) else None
            if usage is None:
                unknown += 1
                continue
            counted += 1
            sums[0] += usage.input_tokens
            sums[1] += usage.output_tokens
            sums[2] += usage.total_tokens
        return cls._build((sums[0], sums[1], sums[2]), counted, unknown)

    @classmethod
    def split(
        cls,
        *,
        evaluation: "TokenRollup | None",
        reflection: "TokenRollup | None",
    ) -> "TokenRollup":
        """Build a rollup from its evaluation and reflection parts.

        Args:
            evaluation: The evaluated rows' rollup, or None when absent.
            reflection: The reflection calls' rollup, or None when no
                reflection call was observed.

        Returns:
            A rollup whose counters combine both parts (a None part adds
            nothing; counters are None only when nothing was counted and
            something was unknown) and whose ``evaluation`` and
            ``reflection`` are the parts as given.

        Raises:
            ValueError: If a part has splits of its own; parts are rollups
                without splits, and a nested split would be flattened on
                save and load.
        """
        for name, part in (("evaluation", evaluation), ("reflection", reflection)):
            if part is not None and part._has_split():
                raise ValueError(
                    f"TokenRollup.split() {name} part must not have splits of its own"
                )
        total = cls._leaf_sum(evaluation, reflection)
        return cls(
            input_tokens=total.input_tokens,
            output_tokens=total.output_tokens,
            total_tokens=total.total_tokens,
            rows_counted=total.rows_counted,
            rows_unknown=total.rows_unknown,
            evaluation=evaluation,
            reflection=reflection,
        )

    @classmethod
    def _leaf_sum(
        cls, first: "TokenRollup | None", second: "TokenRollup | None"
    ) -> "TokenRollup":
        """Add the counters of two optional rollups, ignoring their splits.

        Args:
            first: A rollup, or None to add nothing.
            second: A rollup, or None to add nothing.

        Returns:
            A rollup without splits; zeros when both are None.
        """
        parts = [part for part in (first, second) if part is not None]
        sums = (
            sum(part.input_tokens or 0 for part in parts),
            sum(part.output_tokens or 0 for part in parts),
            sum(part.total_tokens or 0 for part in parts),
        )
        return cls._build(
            sums,
            sum(part.rows_counted for part in parts),
            sum(part.rows_unknown for part in parts),
        )

    def combine(self, other: "TokenRollup") -> "TokenRollup":
        """Add another rollup to this one.

        Counted rows and both row counts add up. A None counter on either
        side is treated as absent. When either side has splits, the result
        combines split-wise: a side without splits counts as evaluation,
        and a split absent on both sides stays None.

        Args:
            other: The rollup to add.

        Returns:
            A new rollup; its counters are None only when no row was counted
            and at least one row was unknown.
        """
        if not (self._has_split() or other._has_split()):
            return self._leaf_sum(self, other)
        return TokenRollup.split(
            evaluation=_combine_optional(
                self._evaluation_part(), other._evaluation_part()
            ),
            reflection=_combine_optional(self.reflection, other.reflection),
        )

    def _has_split(self) -> bool:
        """Tell whether this rollup carries an evaluation or reflection part.

        Returns:
            True when either split is set.
        """
        return self.evaluation is not None or self.reflection is not None

    def _evaluation_part(self) -> "TokenRollup | None":
        """Return the part of this rollup that counts as evaluation.

        Returns:
            The ``evaluation`` split of a split rollup, or this whole rollup
            when it has no splits.
        """
        if self._has_split():
            return self.evaluation
        return self

    def to_dict(self) -> dict[str, Any]:
        """Serialize this rollup to a stdlib-only dict.

        Returns:
            Dict with the five counters, where a None counter is written as
            ``"unknown"``, plus ``"evaluation"`` and ``"reflection"``, each
            the part's dict or None when absent. Output is directly
            ``json.dumps()``-compatible.
        """
        data = self._counters_dict()
        data["evaluation"] = _rollup_to_dict(self.evaluation)
        data["reflection"] = _rollup_to_dict(self.reflection)
        return data

    def _counters_dict(self) -> dict[str, Any]:
        """Serialize the five counters of this rollup.

        Returns:
            Dict with the five counters; a None counter is ``"unknown"``.
        """
        return {
            "input_tokens": (
                "unknown" if self.input_tokens is None else self.input_tokens
            ),
            "output_tokens": (
                "unknown" if self.output_tokens is None else self.output_tokens
            ),
            "total_tokens": (
                "unknown" if self.total_tokens is None else self.total_tokens
            ),
            "rows_counted": self.rows_counted,
            "rows_unknown": self.rows_unknown,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "TokenRollup":
        """Reconstruct a TokenRollup from a dict.

        Args:
            data: Dict produced by ``to_dict()``. A counter may be an int,
                None or ``"unknown"``. ``"evaluation"`` and ``"reflection"``
                are read when present; absent or None means no split. The
                parts load as rollups without splits of their own.

        Returns:
            Reconstructed TokenRollup; ``"unknown"`` and None load as None.

        Raises:
            KeyError: If a row count is missing from the dict or a part.
        """
        evaluation = data.get("evaluation")
        reflection = data.get("reflection")
        return cls(
            **cls._counters_from_dict(data),
            evaluation=(
                None
                if evaluation is None
                else cls(**cls._counters_from_dict(evaluation))
            ),
            reflection=(
                None
                if reflection is None
                else cls(**cls._counters_from_dict(reflection))
            ),
        )

    @staticmethod
    def _counters_from_dict(data: dict[str, Any]) -> dict[str, Any]:
        """Read the five counters of a serialized rollup.

        Args:
            data: A serialized rollup.

        Returns:
            Keyword arguments for the five counters.

        Raises:
            KeyError: If a row count is missing from the dict.
        """
        return {
            "input_tokens": _unknown_or_int(data.get("input_tokens")),
            "output_tokens": _unknown_or_int(data.get("output_tokens")),
            "total_tokens": _unknown_or_int(data.get("total_tokens")),
            "rows_counted": data["rows_counted"],
            "rows_unknown": data["rows_unknown"],
        }

from_batch classmethod

from_batch(batch: Any) -> TokenRollup

Sum the token usage reported by each row of an evaluation batch.

Reads batch.trajectories[i] for each row of batch.scores. A trajectory whose token_usage or total_token_usage is a TokenUsage counts its three numbers; any other row (no trajectories, a trajectory without usage, a dict) is unknown.

PARAMETER DESCRIPTION
batch

An EvaluationBatch or any object with scores and an optional trajectories sequence.

TYPE: Any

RETURNS DESCRIPTION
TokenRollup

The rollup of the batch. An empty batch gives zeros.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_batch(cls, batch: Any) -> "TokenRollup":
    """Sum the token usage reported by each row of an evaluation batch.

    Reads ``batch.trajectories[i]`` for each row of ``batch.scores``. A
    trajectory whose ``token_usage`` or ``total_token_usage`` is a
    ``TokenUsage`` counts its three numbers; any other row (no
    trajectories, a trajectory without usage, a dict) is unknown.

    Args:
        batch: An ``EvaluationBatch`` or any object with ``scores`` and
            an optional ``trajectories`` sequence.

    Returns:
        The rollup of the batch. An empty batch gives zeros.
    """
    trajectories = getattr(batch, "trajectories", None) or []
    sums = [0, 0, 0]
    counted = 0
    unknown = 0
    for i in range(len(batch.scores)):
        usage = _row_usage(trajectories[i]) if i < len(trajectories) else None
        if usage is None:
            unknown += 1
            continue
        counted += 1
        sums[0] += usage.input_tokens
        sums[1] += usage.output_tokens
        sums[2] += usage.total_tokens
    return cls._build((sums[0], sums[1], sums[2]), counted, unknown)

split classmethod

split(
    *,
    evaluation: TokenRollup | None,
    reflection: TokenRollup | None,
) -> TokenRollup

Build a rollup from its evaluation and reflection parts.

PARAMETER DESCRIPTION
evaluation

The evaluated rows' rollup, or None when absent.

TYPE: TokenRollup | None

reflection

The reflection calls' rollup, or None when no reflection call was observed.

TYPE: TokenRollup | None

RETURNS DESCRIPTION
TokenRollup

A rollup whose counters combine both parts (a None part adds

TokenRollup

nothing; counters are None only when nothing was counted and

TokenRollup

something was unknown) and whose evaluation and

TokenRollup

reflection are the parts as given.

RAISES DESCRIPTION
ValueError

If a part has splits of its own; parts are rollups without splits, and a nested split would be flattened on save and load.

Source code in src/gepa_adk/domain/models.py
@classmethod
def split(
    cls,
    *,
    evaluation: "TokenRollup | None",
    reflection: "TokenRollup | None",
) -> "TokenRollup":
    """Build a rollup from its evaluation and reflection parts.

    Args:
        evaluation: The evaluated rows' rollup, or None when absent.
        reflection: The reflection calls' rollup, or None when no
            reflection call was observed.

    Returns:
        A rollup whose counters combine both parts (a None part adds
        nothing; counters are None only when nothing was counted and
        something was unknown) and whose ``evaluation`` and
        ``reflection`` are the parts as given.

    Raises:
        ValueError: If a part has splits of its own; parts are rollups
            without splits, and a nested split would be flattened on
            save and load.
    """
    for name, part in (("evaluation", evaluation), ("reflection", reflection)):
        if part is not None and part._has_split():
            raise ValueError(
                f"TokenRollup.split() {name} part must not have splits of its own"
            )
    total = cls._leaf_sum(evaluation, reflection)
    return cls(
        input_tokens=total.input_tokens,
        output_tokens=total.output_tokens,
        total_tokens=total.total_tokens,
        rows_counted=total.rows_counted,
        rows_unknown=total.rows_unknown,
        evaluation=evaluation,
        reflection=reflection,
    )

combine

combine(other: TokenRollup) -> TokenRollup

Add another rollup to this one.

Counted rows and both row counts add up. A None counter on either side is treated as absent. When either side has splits, the result combines split-wise: a side without splits counts as evaluation, and a split absent on both sides stays None.

PARAMETER DESCRIPTION
other

The rollup to add.

TYPE: TokenRollup

RETURNS DESCRIPTION
TokenRollup

A new rollup; its counters are None only when no row was counted

TokenRollup

and at least one row was unknown.

Source code in src/gepa_adk/domain/models.py
def combine(self, other: "TokenRollup") -> "TokenRollup":
    """Add another rollup to this one.

    Counted rows and both row counts add up. A None counter on either
    side is treated as absent. When either side has splits, the result
    combines split-wise: a side without splits counts as evaluation,
    and a split absent on both sides stays None.

    Args:
        other: The rollup to add.

    Returns:
        A new rollup; its counters are None only when no row was counted
        and at least one row was unknown.
    """
    if not (self._has_split() or other._has_split()):
        return self._leaf_sum(self, other)
    return TokenRollup.split(
        evaluation=_combine_optional(
            self._evaluation_part(), other._evaluation_part()
        ),
        reflection=_combine_optional(self.reflection, other.reflection),
    )

to_dict

to_dict() -> dict[str, Any]

Serialize this rollup to a stdlib-only dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with the five counters, where a None counter is written as

dict[str, Any]

"unknown", plus "evaluation" and "reflection", each

dict[str, Any]

the part's dict or None when absent. Output is directly

dict[str, Any]

json.dumps()-compatible.

Source code in src/gepa_adk/domain/models.py
def to_dict(self) -> dict[str, Any]:
    """Serialize this rollup to a stdlib-only dict.

    Returns:
        Dict with the five counters, where a None counter is written as
        ``"unknown"``, plus ``"evaluation"`` and ``"reflection"``, each
        the part's dict or None when absent. Output is directly
        ``json.dumps()``-compatible.
    """
    data = self._counters_dict()
    data["evaluation"] = _rollup_to_dict(self.evaluation)
    data["reflection"] = _rollup_to_dict(self.reflection)
    return data

from_dict classmethod

from_dict(data: dict[str, Any]) -> TokenRollup

Reconstruct a TokenRollup from a dict.

PARAMETER DESCRIPTION
data

Dict produced by to_dict(). A counter may be an int, None or "unknown". "evaluation" and "reflection" are read when present; absent or None means no split. The parts load as rollups without splits of their own.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
TokenRollup

Reconstructed TokenRollup; "unknown" and None load as None.

RAISES DESCRIPTION
KeyError

If a row count is missing from the dict or a part.

Source code in src/gepa_adk/domain/models.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "TokenRollup":
    """Reconstruct a TokenRollup from a dict.

    Args:
        data: Dict produced by ``to_dict()``. A counter may be an int,
            None or ``"unknown"``. ``"evaluation"`` and ``"reflection"``
            are read when present; absent or None means no split. The
            parts load as rollups without splits of their own.

    Returns:
        Reconstructed TokenRollup; ``"unknown"`` and None load as None.

    Raises:
        KeyError: If a row count is missing from the dict or a part.
    """
    evaluation = data.get("evaluation")
    reflection = data.get("reflection")
    return cls(
        **cls._counters_from_dict(data),
        evaluation=(
            None
            if evaluation is None
            else cls(**cls._counters_from_dict(evaluation))
        ),
        reflection=(
            None
            if reflection is None
            else cls(**cls._counters_from_dict(reflection))
        ),
    )

ParetoFrontier dataclass

Tracks non-dominated candidates across multiple frontier dimensions.

ATTRIBUTE DESCRIPTION
example_leaders

Instance-level mapping of example_idx to leader candidate indices.

TYPE: dict[int, set[int]]

best_scores

Instance-level mapping of example_idx to best score.

TYPE: dict[int, float]

objective_leaders

Objective-level mapping of objective_name to leader candidate indices.

TYPE: dict[str, set[int]]

objective_best_scores

Objective-level mapping of objective_name to best score.

TYPE: dict[str, float]

cartesian_leaders

Cartesian mapping of (example_idx, objective) to leader candidate indices.

TYPE: dict[tuple[int, str], set[int]]

cartesian_best_scores

Cartesian mapping of (example_idx, objective) to best score.

TYPE: dict[tuple[int, str], float]

Examples:

frontier = ParetoFrontier()
frontier.update(0, {0: 0.8, 1: 0.6})
Notes

A frontier stores the best candidate indices per dimension for sampling. The active dimension depends on frontier_type.

Source code in src/gepa_adk/domain/state.py
@dataclass(slots=True)
class ParetoFrontier:
    """Tracks non-dominated candidates across multiple frontier dimensions.

    Attributes:
        example_leaders (dict[int, set[int]]): Instance-level mapping of
            example_idx to leader candidate indices.
        best_scores (dict[int, float]): Instance-level mapping of
            example_idx to best score.
        objective_leaders (dict[str, set[int]]): Objective-level mapping of
            objective_name to leader candidate indices.
        objective_best_scores (dict[str, float]): Objective-level mapping of
            objective_name to best score.
        cartesian_leaders (dict[tuple[int, str], set[int]]): Cartesian mapping of
            (example_idx, objective) to leader candidate indices.
        cartesian_best_scores (dict[tuple[int, str], float]): Cartesian mapping of
            (example_idx, objective) to best score.

    Examples:
        ```python
        frontier = ParetoFrontier()
        frontier.update(0, {0: 0.8, 1: 0.6})
        ```

    Notes:
        A frontier stores the best candidate indices per dimension for sampling.
        The active dimension depends on frontier_type.
    """

    example_leaders: dict[int, set[int]] = field(default_factory=dict)
    best_scores: dict[int, float] = field(default_factory=dict)
    objective_leaders: dict[str, set[int]] = field(default_factory=dict)
    objective_best_scores: dict[str, float] = field(default_factory=dict)
    cartesian_leaders: dict[tuple[int, str], set[int]] = field(default_factory=dict)
    cartesian_best_scores: dict[tuple[int, str], float] = field(default_factory=dict)

    def update(
        self,
        candidate_idx: int,
        scores: dict[int, float],
        *,
        logger: FrontierLogger | None = None,
    ) -> None:
        """Update frontier leadership with a candidate's scores.

        Args:
            candidate_idx: Index of the candidate being added.
            scores: Mapping of example index to score.
            logger: Optional structured logger for leader updates.

        Notes:
            Mutates ``example_leaders`` and ``best_scores`` for instance-level
            frontier tracking.

        Examples:
            ```python
            frontier.update(2, {0: 0.7, 1: 0.9})
            ```
        """
        for example_idx, score in scores.items():
            best_score = self.best_scores.get(example_idx)
            if best_score is None or score > best_score:
                previous_leaders = self.example_leaders.get(example_idx, set())
                self.best_scores[example_idx] = score
                self.example_leaders[example_idx] = {candidate_idx}
                if logger is not None:
                    logger.info(
                        "pareto_frontier.leader_updated",
                        example_idx=example_idx,
                        candidate_idx=candidate_idx,
                        score=score,
                        previous_leaders=sorted(previous_leaders),
                    )
            elif score == best_score:
                leaders = self.example_leaders.setdefault(example_idx, set())
                if candidate_idx not in leaders:
                    leaders.add(candidate_idx)
                    if logger is not None:
                        logger.info(
                            "pareto_frontier.leader_tied",
                            example_idx=example_idx,
                            candidate_idx=candidate_idx,
                            score=score,
                        )

    def get_non_dominated(self) -> set[int]:
        """Return candidate indices that lead any example.

        Returns:
            Set of candidate indices that are non-dominated (lead at
            least one example).

        Notes:
            Outputs the union of all leader sets across example indices.
        """
        candidates: set[int] = set()
        for leaders in self.example_leaders.values():
            candidates.update(leaders)
        return candidates

    def get_selection_weights(self) -> dict[int, int]:
        """Return selection weights based on leadership frequency.

        Returns:
            Mapping of candidate index to leadership count, usable as
            weights for weighted sampling.

        Notes:
            Outputs weights proportional to how many examples each candidate
            leads, enabling weighted sampling.
        """
        weights: dict[int, int] = {}
        for leaders in self.example_leaders.values():
            for candidate_idx in leaders:
                weights[candidate_idx] = weights.get(candidate_idx, 0) + 1
        return weights

    def update_objective(
        self,
        candidate_idx: int,
        objective_scores: dict[str, float],
        *,
        logger: FrontierLogger | None = None,
    ) -> None:
        """Update objective-level frontier with a candidate's objective scores.

        Args:
            candidate_idx: Index of the candidate being added.
            objective_scores: Mapping of objective name to score.
            logger: Optional structured logger for leader updates.

        Notes:
            Mutates ``objective_leaders`` and ``objective_best_scores`` for
            objective-level frontier tracking.

        Examples:
            ```python
            frontier.update_objective(0, {"accuracy": 0.9, "latency": 0.7})
            ```
        """
        for objective_name, score in objective_scores.items():
            best_score = self.objective_best_scores.get(objective_name)
            if best_score is None or score > best_score:
                previous_leaders = self.objective_leaders.get(objective_name, set())
                self.objective_best_scores[objective_name] = score
                self.objective_leaders[objective_name] = {candidate_idx}
                if logger is not None:
                    logger.info(
                        "pareto_frontier.objective_leader_updated",
                        objective_name=objective_name,
                        candidate_idx=candidate_idx,
                        score=score,
                        previous_leaders=sorted(previous_leaders),
                    )
            elif score == best_score:
                leaders = self.objective_leaders.setdefault(objective_name, set())
                if candidate_idx not in leaders:
                    leaders.add(candidate_idx)
                    if logger is not None:
                        logger.info(
                            "pareto_frontier.objective_leader_tied",
                            objective_name=objective_name,
                            candidate_idx=candidate_idx,
                            score=score,
                        )

    def update_cartesian(
        self,
        candidate_idx: int,
        scores: dict[int, float],
        objective_scores: dict[int, dict[str, float]],
        *,
        logger: FrontierLogger | None = None,
    ) -> None:
        """Update cartesian frontier per (example, objective) pair.

        Args:
            candidate_idx: Index of the candidate being added.
            scores: Mapping of example index to score.
            objective_scores: Mapping of example index to objective scores dict.
            logger: Optional structured logger for leader updates.

        Notes:
            Mutates ``cartesian_leaders`` and ``cartesian_best_scores`` for
            per (example, objective) pair frontier tracking.

        Examples:
            ```python
            frontier.update_cartesian(
                0, {0: 0.8, 1: 0.6}, {0: {"accuracy": 0.9}, 1: {"accuracy": 0.7}}
            )
            ```
        """
        for example_idx, example_objectives in objective_scores.items():
            for objective_name, score in example_objectives.items():
                key = (example_idx, objective_name)
                best_score = self.cartesian_best_scores.get(key)
                if best_score is None or score > best_score:
                    previous_leaders = self.cartesian_leaders.get(key, set())
                    self.cartesian_best_scores[key] = score
                    self.cartesian_leaders[key] = {candidate_idx}
                    if logger is not None:
                        logger.info(
                            "pareto_frontier.cartesian_leader_updated",
                            example_idx=example_idx,
                            objective_name=objective_name,
                            candidate_idx=candidate_idx,
                            score=score,
                            previous_leaders=sorted(previous_leaders),
                        )
                elif score == best_score:
                    leaders = self.cartesian_leaders.setdefault(key, set())
                    if candidate_idx not in leaders:
                        leaders.add(candidate_idx)
                        if logger is not None:
                            logger.info(
                                "pareto_frontier.cartesian_leader_tied",
                                example_idx=example_idx,
                                objective_name=objective_name,
                                candidate_idx=candidate_idx,
                                score=score,
                            )

    def get_pareto_front_mapping(
        self, frontier_type: FrontierType
    ) -> dict[FrontierKey, set[int]]:
        """Return frontier mapping for specified frontier type.

        Args:
            frontier_type (FrontierType): Type of frontier to return mapping for.

        Returns:
            dict[FrontierKey, set[int]]: Mapping from frontier key to set of
                candidate indices.

        Raises:
            ValueError: If frontier_type is not a supported value.

        Notes:
            Outputs a mapping with keys appropriate for the frontier type
            (int for INSTANCE, str for OBJECTIVE, tuples for HYBRID/CARTESIAN).

        Examples:
            ```python
            mapping = frontier.get_pareto_front_mapping(FrontierType.INSTANCE)
            # Returns: {0: {1, 2}, 1: {2, 3}}
            ```
        """
        if frontier_type == FrontierType.INSTANCE:
            return {
                key: leaders.copy() for key, leaders in self.example_leaders.items()
            }
        elif frontier_type == FrontierType.OBJECTIVE:
            return {
                key: leaders.copy() for key, leaders in self.objective_leaders.items()
            }
        elif frontier_type == FrontierType.HYBRID:
            mapping: dict[FrontierKey, set[int]] = {}
            # Add instance-level with type tag
            for example_idx, leaders in self.example_leaders.items():
                mapping[("val_id", example_idx)] = leaders.copy()
            # Add objective-level with type tag
            for objective_name, leaders in self.objective_leaders.items():
                mapping[("objective", objective_name)] = leaders.copy()
            return mapping
        elif frontier_type == FrontierType.CARTESIAN:
            mapping: dict[FrontierKey, set[int]] = {}
            for (
                example_idx,
                objective_name,
            ), leaders in self.cartesian_leaders.items():
                mapping[("cartesian", example_idx, objective_name)] = leaders.copy()
            return mapping
        else:
            raise ValueError(f"Unknown frontier type: {frontier_type}")

update

update(
    candidate_idx: int,
    scores: dict[int, float],
    *,
    logger: FrontierLogger | None = None,
) -> None

Update frontier leadership with a candidate's scores.

PARAMETER DESCRIPTION
candidate_idx

Index of the candidate being added.

TYPE: int

scores

Mapping of example index to score.

TYPE: dict[int, float]

logger

Optional structured logger for leader updates.

TYPE: FrontierLogger | None DEFAULT: None

Notes

Mutates example_leaders and best_scores for instance-level frontier tracking.

Examples:

frontier.update(2, {0: 0.7, 1: 0.9})
Source code in src/gepa_adk/domain/state.py
def update(
    self,
    candidate_idx: int,
    scores: dict[int, float],
    *,
    logger: FrontierLogger | None = None,
) -> None:
    """Update frontier leadership with a candidate's scores.

    Args:
        candidate_idx: Index of the candidate being added.
        scores: Mapping of example index to score.
        logger: Optional structured logger for leader updates.

    Notes:
        Mutates ``example_leaders`` and ``best_scores`` for instance-level
        frontier tracking.

    Examples:
        ```python
        frontier.update(2, {0: 0.7, 1: 0.9})
        ```
    """
    for example_idx, score in scores.items():
        best_score = self.best_scores.get(example_idx)
        if best_score is None or score > best_score:
            previous_leaders = self.example_leaders.get(example_idx, set())
            self.best_scores[example_idx] = score
            self.example_leaders[example_idx] = {candidate_idx}
            if logger is not None:
                logger.info(
                    "pareto_frontier.leader_updated",
                    example_idx=example_idx,
                    candidate_idx=candidate_idx,
                    score=score,
                    previous_leaders=sorted(previous_leaders),
                )
        elif score == best_score:
            leaders = self.example_leaders.setdefault(example_idx, set())
            if candidate_idx not in leaders:
                leaders.add(candidate_idx)
                if logger is not None:
                    logger.info(
                        "pareto_frontier.leader_tied",
                        example_idx=example_idx,
                        candidate_idx=candidate_idx,
                        score=score,
                    )

get_non_dominated

get_non_dominated() -> set[int]

Return candidate indices that lead any example.

RETURNS DESCRIPTION
set[int]

Set of candidate indices that are non-dominated (lead at

set[int]

least one example).

Notes

Outputs the union of all leader sets across example indices.

Source code in src/gepa_adk/domain/state.py
def get_non_dominated(self) -> set[int]:
    """Return candidate indices that lead any example.

    Returns:
        Set of candidate indices that are non-dominated (lead at
        least one example).

    Notes:
        Outputs the union of all leader sets across example indices.
    """
    candidates: set[int] = set()
    for leaders in self.example_leaders.values():
        candidates.update(leaders)
    return candidates

get_selection_weights

get_selection_weights() -> dict[int, int]

Return selection weights based on leadership frequency.

RETURNS DESCRIPTION
dict[int, int]

Mapping of candidate index to leadership count, usable as

dict[int, int]

weights for weighted sampling.

Notes

Outputs weights proportional to how many examples each candidate leads, enabling weighted sampling.

Source code in src/gepa_adk/domain/state.py
def get_selection_weights(self) -> dict[int, int]:
    """Return selection weights based on leadership frequency.

    Returns:
        Mapping of candidate index to leadership count, usable as
        weights for weighted sampling.

    Notes:
        Outputs weights proportional to how many examples each candidate
        leads, enabling weighted sampling.
    """
    weights: dict[int, int] = {}
    for leaders in self.example_leaders.values():
        for candidate_idx in leaders:
            weights[candidate_idx] = weights.get(candidate_idx, 0) + 1
    return weights

update_objective

update_objective(
    candidate_idx: int,
    objective_scores: dict[str, float],
    *,
    logger: FrontierLogger | None = None,
) -> None

Update objective-level frontier with a candidate's objective scores.

PARAMETER DESCRIPTION
candidate_idx

Index of the candidate being added.

TYPE: int

objective_scores

Mapping of objective name to score.

TYPE: dict[str, float]

logger

Optional structured logger for leader updates.

TYPE: FrontierLogger | None DEFAULT: None

Notes

Mutates objective_leaders and objective_best_scores for objective-level frontier tracking.

Examples:

frontier.update_objective(0, {"accuracy": 0.9, "latency": 0.7})
Source code in src/gepa_adk/domain/state.py
def update_objective(
    self,
    candidate_idx: int,
    objective_scores: dict[str, float],
    *,
    logger: FrontierLogger | None = None,
) -> None:
    """Update objective-level frontier with a candidate's objective scores.

    Args:
        candidate_idx: Index of the candidate being added.
        objective_scores: Mapping of objective name to score.
        logger: Optional structured logger for leader updates.

    Notes:
        Mutates ``objective_leaders`` and ``objective_best_scores`` for
        objective-level frontier tracking.

    Examples:
        ```python
        frontier.update_objective(0, {"accuracy": 0.9, "latency": 0.7})
        ```
    """
    for objective_name, score in objective_scores.items():
        best_score = self.objective_best_scores.get(objective_name)
        if best_score is None or score > best_score:
            previous_leaders = self.objective_leaders.get(objective_name, set())
            self.objective_best_scores[objective_name] = score
            self.objective_leaders[objective_name] = {candidate_idx}
            if logger is not None:
                logger.info(
                    "pareto_frontier.objective_leader_updated",
                    objective_name=objective_name,
                    candidate_idx=candidate_idx,
                    score=score,
                    previous_leaders=sorted(previous_leaders),
                )
        elif score == best_score:
            leaders = self.objective_leaders.setdefault(objective_name, set())
            if candidate_idx not in leaders:
                leaders.add(candidate_idx)
                if logger is not None:
                    logger.info(
                        "pareto_frontier.objective_leader_tied",
                        objective_name=objective_name,
                        candidate_idx=candidate_idx,
                        score=score,
                    )

update_cartesian

update_cartesian(
    candidate_idx: int,
    scores: dict[int, float],
    objective_scores: dict[int, dict[str, float]],
    *,
    logger: FrontierLogger | None = None,
) -> None

Update cartesian frontier per (example, objective) pair.

PARAMETER DESCRIPTION
candidate_idx

Index of the candidate being added.

TYPE: int

scores

Mapping of example index to score.

TYPE: dict[int, float]

objective_scores

Mapping of example index to objective scores dict.

TYPE: dict[int, dict[str, float]]

logger

Optional structured logger for leader updates.

TYPE: FrontierLogger | None DEFAULT: None

Notes

Mutates cartesian_leaders and cartesian_best_scores for per (example, objective) pair frontier tracking.

Examples:

frontier.update_cartesian(
    0, {0: 0.8, 1: 0.6}, {0: {"accuracy": 0.9}, 1: {"accuracy": 0.7}}
)
Source code in src/gepa_adk/domain/state.py
def update_cartesian(
    self,
    candidate_idx: int,
    scores: dict[int, float],
    objective_scores: dict[int, dict[str, float]],
    *,
    logger: FrontierLogger | None = None,
) -> None:
    """Update cartesian frontier per (example, objective) pair.

    Args:
        candidate_idx: Index of the candidate being added.
        scores: Mapping of example index to score.
        objective_scores: Mapping of example index to objective scores dict.
        logger: Optional structured logger for leader updates.

    Notes:
        Mutates ``cartesian_leaders`` and ``cartesian_best_scores`` for
        per (example, objective) pair frontier tracking.

    Examples:
        ```python
        frontier.update_cartesian(
            0, {0: 0.8, 1: 0.6}, {0: {"accuracy": 0.9}, 1: {"accuracy": 0.7}}
        )
        ```
    """
    for example_idx, example_objectives in objective_scores.items():
        for objective_name, score in example_objectives.items():
            key = (example_idx, objective_name)
            best_score = self.cartesian_best_scores.get(key)
            if best_score is None or score > best_score:
                previous_leaders = self.cartesian_leaders.get(key, set())
                self.cartesian_best_scores[key] = score
                self.cartesian_leaders[key] = {candidate_idx}
                if logger is not None:
                    logger.info(
                        "pareto_frontier.cartesian_leader_updated",
                        example_idx=example_idx,
                        objective_name=objective_name,
                        candidate_idx=candidate_idx,
                        score=score,
                        previous_leaders=sorted(previous_leaders),
                    )
            elif score == best_score:
                leaders = self.cartesian_leaders.setdefault(key, set())
                if candidate_idx not in leaders:
                    leaders.add(candidate_idx)
                    if logger is not None:
                        logger.info(
                            "pareto_frontier.cartesian_leader_tied",
                            example_idx=example_idx,
                            objective_name=objective_name,
                            candidate_idx=candidate_idx,
                            score=score,
                        )

get_pareto_front_mapping

get_pareto_front_mapping(
    frontier_type: FrontierType,
) -> dict[FrontierKey, set[int]]

Return frontier mapping for specified frontier type.

PARAMETER DESCRIPTION
frontier_type

Type of frontier to return mapping for.

TYPE: FrontierType

RETURNS DESCRIPTION
dict[FrontierKey, set[int]]

dict[FrontierKey, set[int]]: Mapping from frontier key to set of candidate indices.

RAISES DESCRIPTION
ValueError

If frontier_type is not a supported value.

Notes

Outputs a mapping with keys appropriate for the frontier type (int for INSTANCE, str for OBJECTIVE, tuples for HYBRID/CARTESIAN).

Examples:

mapping = frontier.get_pareto_front_mapping(FrontierType.INSTANCE)
# Returns: {0: {1, 2}, 1: {2, 3}}
Source code in src/gepa_adk/domain/state.py
def get_pareto_front_mapping(
    self, frontier_type: FrontierType
) -> dict[FrontierKey, set[int]]:
    """Return frontier mapping for specified frontier type.

    Args:
        frontier_type (FrontierType): Type of frontier to return mapping for.

    Returns:
        dict[FrontierKey, set[int]]: Mapping from frontier key to set of
            candidate indices.

    Raises:
        ValueError: If frontier_type is not a supported value.

    Notes:
        Outputs a mapping with keys appropriate for the frontier type
        (int for INSTANCE, str for OBJECTIVE, tuples for HYBRID/CARTESIAN).

    Examples:
        ```python
        mapping = frontier.get_pareto_front_mapping(FrontierType.INSTANCE)
        # Returns: {0: {1, 2}, 1: {2, 3}}
        ```
    """
    if frontier_type == FrontierType.INSTANCE:
        return {
            key: leaders.copy() for key, leaders in self.example_leaders.items()
        }
    elif frontier_type == FrontierType.OBJECTIVE:
        return {
            key: leaders.copy() for key, leaders in self.objective_leaders.items()
        }
    elif frontier_type == FrontierType.HYBRID:
        mapping: dict[FrontierKey, set[int]] = {}
        # Add instance-level with type tag
        for example_idx, leaders in self.example_leaders.items():
            mapping[("val_id", example_idx)] = leaders.copy()
        # Add objective-level with type tag
        for objective_name, leaders in self.objective_leaders.items():
            mapping[("objective", objective_name)] = leaders.copy()
        return mapping
    elif frontier_type == FrontierType.CARTESIAN:
        mapping: dict[FrontierKey, set[int]] = {}
        for (
            example_idx,
            objective_name,
        ), leaders in self.cartesian_leaders.items():
            mapping[("cartesian", example_idx, objective_name)] = leaders.copy()
        return mapping
    else:
        raise ValueError(f"Unknown frontier type: {frontier_type}")

ParetoState dataclass

Tracks evolution state for Pareto-aware selection.

ATTRIBUTE DESCRIPTION
candidates

Candidates discovered during evolution.

TYPE: list[Candidate]

candidate_scores

Per-example scores mapping candidate index to example-score mappings.

TYPE: dict[int, dict[int, float]]

frontier

Current frontier leader sets.

TYPE: ParetoFrontier

frontier_type

Frontier tracking strategy.

TYPE: FrontierType

iteration

Current iteration number.

TYPE: int

best_average_idx

Index of best-average candidate.

TYPE: int | None

parent_indices

Genealogy map tracking parent relationships, mapping candidate_idx to parent index list or [None] for seeds. For a mutation the engine records the index of the candidate the reflector rewrote (the selector's single draw for that iteration); for a merge, the two merged parents.

TYPE: dict[int, list[int | None]]

Examples:

state = ParetoState()
state.add_candidate(Candidate(components={"instruction": "seed"}), [0.5])
Notes

A single state object keeps frontier and candidate metrics aligned.

Source code in src/gepa_adk/domain/state.py
@dataclass(slots=True)
class ParetoState:
    """Tracks evolution state for Pareto-aware selection.

    Attributes:
        candidates (list[Candidate]): Candidates discovered during evolution.
        candidate_scores (dict[int, dict[int, float]]): Per-example scores
            mapping candidate index to example-score mappings.
        frontier (ParetoFrontier): Current frontier leader sets.
        frontier_type (FrontierType): Frontier tracking strategy.
        iteration (int): Current iteration number.
        best_average_idx (int | None): Index of best-average candidate.
        parent_indices (dict[int, list[int | None]]): Genealogy map tracking
            parent relationships, mapping candidate_idx to parent index list
            or ``[None]`` for seeds. For a mutation the engine records the
            index of the candidate the reflector rewrote (the selector's
            single draw for that iteration); for a merge, the two merged
            parents.

    Examples:
        ```python
        state = ParetoState()
        state.add_candidate(Candidate(components={"instruction": "seed"}), [0.5])
        ```

    Notes:
        A single state object keeps frontier and candidate metrics aligned.
    """

    candidates: list[Candidate] = field(default_factory=list)
    candidate_scores: dict[int, dict[int, float]] = field(default_factory=dict)
    candidate_objective_scores: dict[int, dict[str, float]] = field(
        default_factory=dict
    )
    frontier: ParetoFrontier = field(default_factory=ParetoFrontier)
    frontier_type: FrontierType = FrontierType.INSTANCE
    iteration: int = 0
    best_average_idx: int | None = None
    parent_indices: dict[int, list[int | None]] = field(default_factory=dict)
    _frontier_type_initialized: bool = field(default=False, init=False)

    def __post_init__(self) -> None:
        """Validate state configuration and initialize averages.

        Raises:
            ConfigurationError: If any candidate_scores index exceeds the
                candidates list length.

        Notes:
            Checks candidate_scores indices are valid and marks frontier_type
            as initialized for immutability enforcement.
        """
        if self.candidate_scores:
            max_index = len(self.candidates) - 1
            for candidate_idx in self.candidate_scores:
                if candidate_idx > max_index:
                    raise ConfigurationError(
                        "candidate_scores index out of range",
                        field="candidate_scores",
                        value=candidate_idx,
                        constraint="<= len(candidates) - 1",
                    )
        self._frontier_type_initialized = True
        self.update_best_average()

    def __setattr__(self, name: str, value: object) -> None:
        """Enforce frontier_type immutability after initialization (T069).

        Args:
            name (str): Attribute being set.
            value (object): New value. Re-assigning the current frontier_type
                is allowed.

        Raises:
            ConfigurationError: If frontier_type is changed after
                ParetoState initialization.

        Notes:
            Only frontier_type is protected because it determines the frontier
            update routing logic in add_candidate(). Other fields (candidates,
            frontier, candidate_scores) are intentionally mutable to support
            evolution state updates. Using frozen=True would prevent all
            mutations, which is too restrictive for evolution state management.
        """
        # Allow setting during __init__ and __post_init__; once initialized,
        # reject an attempt to change frontier_type
        if (
            name == "frontier_type"
            and hasattr(self, "_frontier_type_initialized")
            and self._frontier_type_initialized
            and getattr(self, "frontier_type", None) != value
        ):
            raise ConfigurationError(
                "frontier_type cannot be changed after ParetoState initialization",
                field="frontier_type",
                value=value,
                constraint="immutable after initialization",
            )
        # Use object.__setattr__ to avoid recursion during initialization
        object.__setattr__(self, name, value)

    @staticmethod
    def _validate_parent_indices(
        indices: Sequence[int | None], label: str
    ) -> list[int | None]:
        """Validate parent indices for genealogy tracking.

        Returns:
            Validated list of parent indices (int or None).

        Raises:
            TypeError: If any element in indices is not int or None.
        """
        validated: list[int | None] = []
        for idx, parent_idx in enumerate(indices):
            if not (isinstance(parent_idx, int) or parent_idx is None):
                raise TypeError(
                    f"{label} elements must be int or None; "
                    f"got {type(parent_idx).__name__} at position {idx}"
                )
            validated.append(parent_idx)
        return validated

    def add_candidate(  # noqa: C901, PLR0912  # validation then one branch per frontier type
        self,
        candidate: Candidate,
        scores: Sequence[Score],
        *,
        score_indices: Sequence[int] | None = None,
        objective_scores: dict[str, float] | None = None,
        per_example_objective_scores: dict[int, dict[str, float]] | None = None,
        parent_indices: list[int] | None = None,
        logger: FrontierLogger | None = None,
    ) -> int:
        """Add a candidate and update frontier tracking.

        Args:
            candidate: Candidate to add.
            scores: Per-example scores for the candidate.
            score_indices: Optional sequence mapping scores to example indices.
                If None, scores are assumed to be indexed 0, 1, 2, ... (full valset).
                If provided, scores[i] corresponds to example index score_indices[i].
            objective_scores: Optional aggregated objective scores
                (required for OBJECTIVE, HYBRID, CARTESIAN).
            per_example_objective_scores: Optional per-example objective scores
                (required for CARTESIAN).
            parent_indices: Optional parent candidate indices for genealogy tracking.
                The engine passes the index of the candidate the proposal was
                mutated from, as returned with the proposal. If None, uses
                candidate.parent_ids if available, otherwise [None] for seed.
            logger: Optional structured logger for frontier updates.

        Returns:
            Index of the newly added candidate.

        Raises:
            ConfigurationError: If objective_scores are required but not provided.
            ValueError: If score_indices and scores differ in length.
            TypeError: If a parent index is neither an int nor None.

        Notes:
            Outputs the new candidate index after routing to the appropriate
            frontier update method based on frontier_type.

        Examples:
            ```python
            candidate_idx = state.add_candidate(candidate, [0.7, 0.8])
            candidate_idx = state.add_candidate(
                candidate, [0.7, 0.8], objective_scores={"accuracy": 0.9}
            )
            ```
        """
        # Validate objective_scores requirement for objective-based frontier types
        if self.frontier_type in (
            FrontierType.OBJECTIVE,
            FrontierType.HYBRID,
            FrontierType.CARTESIAN,
        ):
            if objective_scores is None:
                raise ConfigurationError(
                    "objective_scores required for frontier_type",
                    field="objective_scores",
                    value=None,
                    constraint=f"required for {self.frontier_type}",
                )
            if (
                self.frontier_type == FrontierType.CARTESIAN
                and per_example_objective_scores is None
            ):
                raise ConfigurationError(
                    "per_example_objective_scores required for CARTESIAN frontier_type",
                    field="per_example_objective_scores",
                    value=None,
                    constraint="required for CARTESIAN",
                )

        candidate_idx = len(self.candidates)
        self.candidates.append(candidate)

        # Track parent indices for genealogy
        if parent_indices is not None:
            self.parent_indices[candidate_idx] = self._validate_parent_indices(
                parent_indices, "parent_indices"
            )
        elif candidate.parent_ids is not None:
            self.parent_indices[candidate_idx] = self._validate_parent_indices(
                candidate.parent_ids, "candidate.parent_ids"
            )
        else:
            # Seed candidate with no parents
            self.parent_indices[candidate_idx] = [None]

        # Map scores to example indices
        if score_indices is not None:
            if len(score_indices) != len(scores):
                raise ValueError(
                    f"score_indices length ({len(score_indices)}) must match "
                    f"scores length ({len(scores)})"
                )
            score_map = dict(zip(score_indices, scores, strict=True))
        else:
            # Default: scores are indexed 0, 1, 2, ... (full valset)
            score_map = dict(enumerate(scores))
        self.candidate_scores[candidate_idx] = score_map

        # Store objective scores if provided
        if objective_scores is not None:
            self.candidate_objective_scores[candidate_idx] = objective_scores

        # Route to appropriate frontier update based on frontier_type
        if self.frontier_type == FrontierType.INSTANCE:
            self.frontier.update(candidate_idx, score_map, logger=logger)
        elif self.frontier_type == FrontierType.OBJECTIVE:
            assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
            self.frontier.update_objective(
                candidate_idx, objective_scores, logger=logger
            )
        elif self.frontier_type == FrontierType.HYBRID:
            assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
            self.frontier.update(candidate_idx, score_map, logger=logger)
            self.frontier.update_objective(
                candidate_idx, objective_scores, logger=logger
            )
        elif self.frontier_type == FrontierType.CARTESIAN:
            assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
            assert per_example_objective_scores is not None  # noqa: S101  # validated above; narrows for ty
            self.frontier.update_cartesian(
                candidate_idx, score_map, per_example_objective_scores, logger=logger
            )
        else:
            raise ValueError(f"Unknown frontier type: {self.frontier_type}")

        self.update_best_average()
        return candidate_idx

    def get_average_score(self, candidate_idx: int) -> float:
        """Return average score for a candidate.

        Args:
            candidate_idx: Index of the candidate.

        Returns:
            Mean score across examples.

        Raises:
            NoCandidateAvailableError: If candidate scores are missing.

        Notes:
            Outputs the arithmetic mean of all scores for the candidate
            across evaluated examples.

        Examples:
            ```python
            average = state.get_average_score(candidate_idx)
            ```
        """
        scores = self.candidate_scores.get(candidate_idx)
        if not scores:
            raise NoCandidateAvailableError(
                "No scores available for candidate",
                candidate_idx=candidate_idx,
            )
        return fmean(scores.values())

    def update_best_average(self) -> None:
        """Update best_average_idx based on current scores.

        Notes:
            Outputs the candidate index with the highest mean score, or None
            if no candidates have scores.
        """
        if not self.candidate_scores:
            self.best_average_idx = None
            return
        best_idx = None
        best_score = float("-inf")
        for candidate_idx, scores in self.candidate_scores.items():
            average = fmean(scores.values())
            if average > best_score:
                best_score = average
                best_idx = candidate_idx
        self.best_average_idx = best_idx

__post_init__

__post_init__() -> None

Validate state configuration and initialize averages.

RAISES DESCRIPTION
ConfigurationError

If any candidate_scores index exceeds the candidates list length.

Notes

Checks candidate_scores indices are valid and marks frontier_type as initialized for immutability enforcement.

Source code in src/gepa_adk/domain/state.py
def __post_init__(self) -> None:
    """Validate state configuration and initialize averages.

    Raises:
        ConfigurationError: If any candidate_scores index exceeds the
            candidates list length.

    Notes:
        Checks candidate_scores indices are valid and marks frontier_type
        as initialized for immutability enforcement.
    """
    if self.candidate_scores:
        max_index = len(self.candidates) - 1
        for candidate_idx in self.candidate_scores:
            if candidate_idx > max_index:
                raise ConfigurationError(
                    "candidate_scores index out of range",
                    field="candidate_scores",
                    value=candidate_idx,
                    constraint="<= len(candidates) - 1",
                )
    self._frontier_type_initialized = True
    self.update_best_average()

__setattr__

__setattr__(name: str, value: object) -> None

Enforce frontier_type immutability after initialization (T069).

PARAMETER DESCRIPTION
name

Attribute being set.

TYPE: str

value

New value. Re-assigning the current frontier_type is allowed.

TYPE: object

RAISES DESCRIPTION
ConfigurationError

If frontier_type is changed after ParetoState initialization.

Notes

Only frontier_type is protected because it determines the frontier update routing logic in add_candidate(). Other fields (candidates, frontier, candidate_scores) are intentionally mutable to support evolution state updates. Using frozen=True would prevent all mutations, which is too restrictive for evolution state management.

Source code in src/gepa_adk/domain/state.py
def __setattr__(self, name: str, value: object) -> None:
    """Enforce frontier_type immutability after initialization (T069).

    Args:
        name (str): Attribute being set.
        value (object): New value. Re-assigning the current frontier_type
            is allowed.

    Raises:
        ConfigurationError: If frontier_type is changed after
            ParetoState initialization.

    Notes:
        Only frontier_type is protected because it determines the frontier
        update routing logic in add_candidate(). Other fields (candidates,
        frontier, candidate_scores) are intentionally mutable to support
        evolution state updates. Using frozen=True would prevent all
        mutations, which is too restrictive for evolution state management.
    """
    # Allow setting during __init__ and __post_init__; once initialized,
    # reject an attempt to change frontier_type
    if (
        name == "frontier_type"
        and hasattr(self, "_frontier_type_initialized")
        and self._frontier_type_initialized
        and getattr(self, "frontier_type", None) != value
    ):
        raise ConfigurationError(
            "frontier_type cannot be changed after ParetoState initialization",
            field="frontier_type",
            value=value,
            constraint="immutable after initialization",
        )
    # Use object.__setattr__ to avoid recursion during initialization
    object.__setattr__(self, name, value)

add_candidate

add_candidate(
    candidate: Candidate,
    scores: Sequence[Score],
    *,
    score_indices: Sequence[int] | None = None,
    objective_scores: dict[str, float] | None = None,
    per_example_objective_scores: dict[
        int, dict[str, float]
    ]
    | None = None,
    parent_indices: list[int] | None = None,
    logger: FrontierLogger | None = None,
) -> int

Add a candidate and update frontier tracking.

PARAMETER DESCRIPTION
candidate

Candidate to add.

TYPE: Candidate

scores

Per-example scores for the candidate.

TYPE: Sequence[Score]

score_indices

Optional sequence mapping scores to example indices. If None, scores are assumed to be indexed 0, 1, 2, ... (full valset). If provided, scores[i] corresponds to example index score_indices[i].

TYPE: Sequence[int] | None DEFAULT: None

objective_scores

Optional aggregated objective scores (required for OBJECTIVE, HYBRID, CARTESIAN).

TYPE: dict[str, float] | None DEFAULT: None

per_example_objective_scores

Optional per-example objective scores (required for CARTESIAN).

TYPE: dict[int, dict[str, float]] | None DEFAULT: None

parent_indices

Optional parent candidate indices for genealogy tracking. The engine passes the index of the candidate the proposal was mutated from, as returned with the proposal. If None, uses candidate.parent_ids if available, otherwise [None] for seed.

TYPE: list[int] | None DEFAULT: None

logger

Optional structured logger for frontier updates.

TYPE: FrontierLogger | None DEFAULT: None

RETURNS DESCRIPTION
int

Index of the newly added candidate.

RAISES DESCRIPTION
ConfigurationError

If objective_scores are required but not provided.

ValueError

If score_indices and scores differ in length.

TypeError

If a parent index is neither an int nor None.

Notes

Outputs the new candidate index after routing to the appropriate frontier update method based on frontier_type.

Examples:

candidate_idx = state.add_candidate(candidate, [0.7, 0.8])
candidate_idx = state.add_candidate(
    candidate, [0.7, 0.8], objective_scores={"accuracy": 0.9}
)
Source code in src/gepa_adk/domain/state.py
def add_candidate(  # noqa: C901, PLR0912  # validation then one branch per frontier type
    self,
    candidate: Candidate,
    scores: Sequence[Score],
    *,
    score_indices: Sequence[int] | None = None,
    objective_scores: dict[str, float] | None = None,
    per_example_objective_scores: dict[int, dict[str, float]] | None = None,
    parent_indices: list[int] | None = None,
    logger: FrontierLogger | None = None,
) -> int:
    """Add a candidate and update frontier tracking.

    Args:
        candidate: Candidate to add.
        scores: Per-example scores for the candidate.
        score_indices: Optional sequence mapping scores to example indices.
            If None, scores are assumed to be indexed 0, 1, 2, ... (full valset).
            If provided, scores[i] corresponds to example index score_indices[i].
        objective_scores: Optional aggregated objective scores
            (required for OBJECTIVE, HYBRID, CARTESIAN).
        per_example_objective_scores: Optional per-example objective scores
            (required for CARTESIAN).
        parent_indices: Optional parent candidate indices for genealogy tracking.
            The engine passes the index of the candidate the proposal was
            mutated from, as returned with the proposal. If None, uses
            candidate.parent_ids if available, otherwise [None] for seed.
        logger: Optional structured logger for frontier updates.

    Returns:
        Index of the newly added candidate.

    Raises:
        ConfigurationError: If objective_scores are required but not provided.
        ValueError: If score_indices and scores differ in length.
        TypeError: If a parent index is neither an int nor None.

    Notes:
        Outputs the new candidate index after routing to the appropriate
        frontier update method based on frontier_type.

    Examples:
        ```python
        candidate_idx = state.add_candidate(candidate, [0.7, 0.8])
        candidate_idx = state.add_candidate(
            candidate, [0.7, 0.8], objective_scores={"accuracy": 0.9}
        )
        ```
    """
    # Validate objective_scores requirement for objective-based frontier types
    if self.frontier_type in (
        FrontierType.OBJECTIVE,
        FrontierType.HYBRID,
        FrontierType.CARTESIAN,
    ):
        if objective_scores is None:
            raise ConfigurationError(
                "objective_scores required for frontier_type",
                field="objective_scores",
                value=None,
                constraint=f"required for {self.frontier_type}",
            )
        if (
            self.frontier_type == FrontierType.CARTESIAN
            and per_example_objective_scores is None
        ):
            raise ConfigurationError(
                "per_example_objective_scores required for CARTESIAN frontier_type",
                field="per_example_objective_scores",
                value=None,
                constraint="required for CARTESIAN",
            )

    candidate_idx = len(self.candidates)
    self.candidates.append(candidate)

    # Track parent indices for genealogy
    if parent_indices is not None:
        self.parent_indices[candidate_idx] = self._validate_parent_indices(
            parent_indices, "parent_indices"
        )
    elif candidate.parent_ids is not None:
        self.parent_indices[candidate_idx] = self._validate_parent_indices(
            candidate.parent_ids, "candidate.parent_ids"
        )
    else:
        # Seed candidate with no parents
        self.parent_indices[candidate_idx] = [None]

    # Map scores to example indices
    if score_indices is not None:
        if len(score_indices) != len(scores):
            raise ValueError(
                f"score_indices length ({len(score_indices)}) must match "
                f"scores length ({len(scores)})"
            )
        score_map = dict(zip(score_indices, scores, strict=True))
    else:
        # Default: scores are indexed 0, 1, 2, ... (full valset)
        score_map = dict(enumerate(scores))
    self.candidate_scores[candidate_idx] = score_map

    # Store objective scores if provided
    if objective_scores is not None:
        self.candidate_objective_scores[candidate_idx] = objective_scores

    # Route to appropriate frontier update based on frontier_type
    if self.frontier_type == FrontierType.INSTANCE:
        self.frontier.update(candidate_idx, score_map, logger=logger)
    elif self.frontier_type == FrontierType.OBJECTIVE:
        assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
        self.frontier.update_objective(
            candidate_idx, objective_scores, logger=logger
        )
    elif self.frontier_type == FrontierType.HYBRID:
        assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
        self.frontier.update(candidate_idx, score_map, logger=logger)
        self.frontier.update_objective(
            candidate_idx, objective_scores, logger=logger
        )
    elif self.frontier_type == FrontierType.CARTESIAN:
        assert objective_scores is not None  # noqa: S101  # validated above; narrows for ty
        assert per_example_objective_scores is not None  # noqa: S101  # validated above; narrows for ty
        self.frontier.update_cartesian(
            candidate_idx, score_map, per_example_objective_scores, logger=logger
        )
    else:
        raise ValueError(f"Unknown frontier type: {self.frontier_type}")

    self.update_best_average()
    return candidate_idx

get_average_score

get_average_score(candidate_idx: int) -> float

Return average score for a candidate.

PARAMETER DESCRIPTION
candidate_idx

Index of the candidate.

TYPE: int

RETURNS DESCRIPTION
float

Mean score across examples.

RAISES DESCRIPTION
NoCandidateAvailableError

If candidate scores are missing.

Notes

Outputs the arithmetic mean of all scores for the candidate across evaluated examples.

Examples:

average = state.get_average_score(candidate_idx)
Source code in src/gepa_adk/domain/state.py
def get_average_score(self, candidate_idx: int) -> float:
    """Return average score for a candidate.

    Args:
        candidate_idx: Index of the candidate.

    Returns:
        Mean score across examples.

    Raises:
        NoCandidateAvailableError: If candidate scores are missing.

    Notes:
        Outputs the arithmetic mean of all scores for the candidate
        across evaluated examples.

    Examples:
        ```python
        average = state.get_average_score(candidate_idx)
        ```
    """
    scores = self.candidate_scores.get(candidate_idx)
    if not scores:
        raise NoCandidateAvailableError(
            "No scores available for candidate",
            candidate_idx=candidate_idx,
        )
    return fmean(scores.values())

update_best_average

update_best_average() -> None

Update best_average_idx based on current scores.

Notes

Outputs the candidate index with the highest mean score, or None if no candidates have scores.

Source code in src/gepa_adk/domain/state.py
def update_best_average(self) -> None:
    """Update best_average_idx based on current scores.

    Notes:
        Outputs the candidate index with the highest mean score, or None
        if no candidates have scores.
    """
    if not self.candidate_scores:
        self.best_average_idx = None
        return
    best_idx = None
    best_score = float("-inf")
    for candidate_idx, scores in self.candidate_scores.items():
        average = fmean(scores.values())
        if average > best_score:
            best_score = average
            best_idx = candidate_idx
    self.best_average_idx = best_idx

StopperState dataclass

Immutable snapshot of evolution state for stopper decisions.

Provides stoppers with read-only access to evolution metrics without exposing internal engine state.

ATTRIBUTE DESCRIPTION
iteration

Current iteration number (0-indexed).

TYPE: int

best_score

Best score achieved so far.

TYPE: float

stagnation_counter

Number of iterations without improvement.

TYPE: int

total_evaluations

Total examples evaluated (sum of batch sizes).

TYPE: int

candidates_count

Number of candidates in the frontier.

TYPE: int

elapsed_seconds

Wall-clock time since evolution started.

TYPE: float

latest_score

Score recorded for the most recent iteration, accepted or not. 0.0 for an iteration that produced no proposal. None before the first iteration completes; stoppers that need a current score fall back to best_score then.

TYPE: float | None

Examples:

Creating a state snapshot:

from gepa_adk.domain.stopper import StopperState

state = StopperState(
    iteration=10,
    best_score=0.92,
    stagnation_counter=3,
    total_evaluations=100,
    candidates_count=5,
    elapsed_seconds=300.0,
    latest_score=0.88,
)
print(state.best_score)  # 0.92
print(state.latest_score)  # 0.88
print(state.stagnation_counter)  # 3

Attempting to modify raises an error:

state.iteration = 11  # Raises FrozenInstanceError
Notes

A frozen dataclass, all fields are immutable after creation. Using slots=True for memory efficiency. latest_score is the only field with a default, so it stays last.

Source code in src/gepa_adk/domain/stopper.py
@dataclass(frozen=True, slots=True)
class StopperState:
    """Immutable snapshot of evolution state for stopper decisions.

    Provides stoppers with read-only access to evolution metrics
    without exposing internal engine state.

    Attributes:
        iteration (int): Current iteration number (0-indexed).
        best_score (float): Best score achieved so far.
        stagnation_counter (int): Number of iterations without improvement.
        total_evaluations (int): Total examples evaluated (sum of batch sizes).
        candidates_count (int): Number of candidates in the frontier.
        elapsed_seconds (float): Wall-clock time since evolution started.
        latest_score (float | None): Score recorded for the most recent
            iteration, accepted or not. ``0.0`` for an iteration that produced
            no proposal. ``None`` before the first iteration completes; stoppers
            that need a current score fall back to ``best_score`` then.

    Examples:
        Creating a state snapshot:

        ```python
        from gepa_adk.domain.stopper import StopperState

        state = StopperState(
            iteration=10,
            best_score=0.92,
            stagnation_counter=3,
            total_evaluations=100,
            candidates_count=5,
            elapsed_seconds=300.0,
            latest_score=0.88,
        )
        print(state.best_score)  # 0.92
        print(state.latest_score)  # 0.88
        print(state.stagnation_counter)  # 3
        ```

        Attempting to modify raises an error:

        ```python
        state.iteration = 11  # Raises FrozenInstanceError
        ```

    Notes:
        A frozen dataclass, all fields are immutable after creation.
        Using slots=True for memory efficiency. ``latest_score`` is the only
        field with a default, so it stays last.
    """

    iteration: int
    best_score: float
    stagnation_counter: int
    total_evaluations: int
    candidates_count: int
    elapsed_seconds: float
    latest_score: float | None = None

ADKTrajectory dataclass

Execution trace from ADK agent evaluation.

Captures complete execution details from a single agent evaluation run, including all tool calls, state changes, token usage, and final output. This data enables debugging, optimization, and reflection-based learning. to_dict() and from_dict() convert it to and from a JSON-safe dict, turning tuples into lists and back.

ATTRIBUTE DESCRIPTION
tool_calls

Immutable sequence of tool invocations during execution.

TYPE: tuple[ToolCallRecord, ...]

state_deltas

Sequence of state changes (session state updates).

TYPE: tuple[dict[str, Any], ...]

token_usage

Optional token consumption metrics from LLM calls.

TYPE: TokenUsage | None

final_output

Final text response from the agent.

TYPE: str

error

Error message if execution failed, None otherwise.

TYPE: str | None

Examples:

trajectory = ADKTrajectory(
    tool_calls=(ToolCallRecord("search", {"query": "AI"}, ["result1"], 0.1),),
    state_deltas=({"search_count": 1},),
    token_usage=TokenUsage(100, 50, 150),
    final_output="Based on the search...",
    error=None,
)
Notes

All fields use immutable types (tuples, not lists) to prevent accidental modification of captured trace data.

Source code in src/gepa_adk/domain/trajectory.py
@dataclass(frozen=True, slots=True)
class ADKTrajectory:
    """Execution trace from ADK agent evaluation.

    Captures complete execution details from a single agent evaluation run,
    including all tool calls, state changes, token usage, and final output.
    This data enables debugging, optimization, and reflection-based learning.
    ``to_dict()`` and ``from_dict()`` convert it to and from a JSON-safe dict,
    turning tuples into lists and back.

    Attributes:
        tool_calls (tuple[ToolCallRecord, ...]): Immutable sequence of tool
            invocations during execution.
        state_deltas (tuple[dict[str, Any], ...]): Sequence of state changes
            (session state updates).
        token_usage (TokenUsage | None): Optional token consumption metrics
            from LLM calls.
        final_output (str): Final text response from the agent.
        error (str | None): Error message if execution failed, None otherwise.

    Examples:
        ```python
        trajectory = ADKTrajectory(
            tool_calls=(ToolCallRecord("search", {"query": "AI"}, ["result1"], 0.1),),
            state_deltas=({"search_count": 1},),
            token_usage=TokenUsage(100, 50, 150),
            final_output="Based on the search...",
            error=None,
        )
        ```

    Notes:
        All fields use immutable types (tuples, not lists) to prevent
        accidental modification of captured trace data.
    """

    tool_calls: tuple[ToolCallRecord, ...]
    state_deltas: tuple[dict[str, Any], ...]
    token_usage: TokenUsage | None
    final_output: str
    error: str | None

    def to_dict(self) -> dict[str, Any]:
        """Serialise this trace to a JSON-safe dict.

        Returns:
            Dict with ``tool_calls`` and ``state_deltas`` as lists,
            ``token_usage`` as a dict or None, ``final_output`` and
            ``error``. A state-delta value that JSON cannot encode is stored
            as its ``str()``.
        """
        return {
            "tool_calls": [call.to_dict() for call in self.tool_calls],
            "state_deltas": [_json_safe_dict(delta) for delta in self.state_deltas],
            "token_usage": (
                None if self.token_usage is None else self.token_usage.to_dict()
            ),
            "final_output": self.final_output,
            "error": self.error,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ADKTrajectory:
        """Rebuild a trace from ``to_dict()`` output.

        Args:
            data: Dict produced by ``to_dict()``.

        Returns:
            The reconstructed trace, with lists turned back into tuples.

        Raises:
            KeyError: If a field is missing from ``data``.
        """
        return cls(
            tool_calls=tuple(ToolCallRecord.from_dict(c) for c in data["tool_calls"]),
            state_deltas=tuple(dict(delta) for delta in data["state_deltas"]),
            token_usage=_usage_from_dict(data["token_usage"]),
            final_output=data["final_output"],
            error=data["error"],
        )

to_dict

to_dict() -> dict[str, Any]

Serialise this trace to a JSON-safe dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with tool_calls and state_deltas as lists,

dict[str, Any]

token_usage as a dict or None, final_output and

dict[str, Any]

error. A state-delta value that JSON cannot encode is stored

dict[str, Any]

as its str().

Source code in src/gepa_adk/domain/trajectory.py
def to_dict(self) -> dict[str, Any]:
    """Serialise this trace to a JSON-safe dict.

    Returns:
        Dict with ``tool_calls`` and ``state_deltas`` as lists,
        ``token_usage`` as a dict or None, ``final_output`` and
        ``error``. A state-delta value that JSON cannot encode is stored
        as its ``str()``.
    """
    return {
        "tool_calls": [call.to_dict() for call in self.tool_calls],
        "state_deltas": [_json_safe_dict(delta) for delta in self.state_deltas],
        "token_usage": (
            None if self.token_usage is None else self.token_usage.to_dict()
        ),
        "final_output": self.final_output,
        "error": self.error,
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> ADKTrajectory

Rebuild a trace from to_dict() output.

PARAMETER DESCRIPTION
data

Dict produced by to_dict().

TYPE: dict[str, Any]

RETURNS DESCRIPTION
ADKTrajectory

The reconstructed trace, with lists turned back into tuples.

RAISES DESCRIPTION
KeyError

If a field is missing from data.

Source code in src/gepa_adk/domain/trajectory.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ADKTrajectory:
    """Rebuild a trace from ``to_dict()`` output.

    Args:
        data: Dict produced by ``to_dict()``.

    Returns:
        The reconstructed trace, with lists turned back into tuples.

    Raises:
        KeyError: If a field is missing from ``data``.
    """
    return cls(
        tool_calls=tuple(ToolCallRecord.from_dict(c) for c in data["tool_calls"]),
        state_deltas=tuple(dict(delta) for delta in data["state_deltas"]),
        token_usage=_usage_from_dict(data["token_usage"]),
        final_output=data["final_output"],
        error=data["error"],
    )

MultiAgentTrajectory dataclass

Execution trace from multi-agent pipeline evaluation.

Captures individual agent trajectories and overall pipeline metrics. to_dict() and from_dict() convert it to and from a JSON-safe dict, nesting each agent's ADKTrajectory dict.

ATTRIBUTE DESCRIPTION
agent_trajectories

Mapping of agent name to trajectory.

TYPE: dict[str, ADKTrajectory]

pipeline_output

Final output from the primary agent.

TYPE: str

total_token_usage

Aggregated token usage across all agents.

TYPE: TokenUsage | None

error

Error message if pipeline execution failed.

TYPE: str | None

Examples:

Creating a multi-agent trajectory:

from gepa_adk.domain.trajectory import (
    MultiAgentTrajectory,
    ADKTrajectory,
    TokenUsage,
)

trajectory = MultiAgentTrajectory(
    agent_trajectories={
        "generator": ADKTrajectory(...),
        "critic": ADKTrajectory(...),
    },
    pipeline_output="Generated code output",
    total_token_usage=TokenUsage(200, 100, 300),
    error=None,
)
Notes

All fields use immutable types to prevent accidental modification of captured trace data. agent_trajectories maps agent names to their individual ADKTrajectory records.

Source code in src/gepa_adk/domain/trajectory.py
@dataclass(frozen=True, slots=True)
class MultiAgentTrajectory:
    """Execution trace from multi-agent pipeline evaluation.

    Captures individual agent trajectories and overall pipeline metrics.
    ``to_dict()`` and ``from_dict()`` convert it to and from a JSON-safe dict,
    nesting each agent's ``ADKTrajectory`` dict.

    Attributes:
        agent_trajectories (dict[str, ADKTrajectory]): Mapping of agent name to trajectory.
        pipeline_output (str): Final output from the primary agent.
        total_token_usage (TokenUsage | None): Aggregated token usage across all agents.
        error (str | None): Error message if pipeline execution failed.

    Examples:
        Creating a multi-agent trajectory:

        ```python
        from gepa_adk.domain.trajectory import (
            MultiAgentTrajectory,
            ADKTrajectory,
            TokenUsage,
        )

        trajectory = MultiAgentTrajectory(
            agent_trajectories={
                "generator": ADKTrajectory(...),
                "critic": ADKTrajectory(...),
            },
            pipeline_output="Generated code output",
            total_token_usage=TokenUsage(200, 100, 300),
            error=None,
        )
        ```

    Notes:
        All fields use immutable types to prevent accidental modification
        of captured trace data. agent_trajectories maps agent names to
        their individual ADKTrajectory records.
    """

    agent_trajectories: dict[str, ADKTrajectory]
    pipeline_output: str
    total_token_usage: TokenUsage | None
    error: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialise this pipeline trace to a JSON-safe dict.

        Returns:
            Dict with ``agent_trajectories`` (agent name to
            ``ADKTrajectory.to_dict()``), ``pipeline_output``,
            ``total_token_usage`` as a dict or None, and ``error``.
        """
        return {
            "agent_trajectories": {
                name: trajectory.to_dict()
                for name, trajectory in self.agent_trajectories.items()
            },
            "pipeline_output": self.pipeline_output,
            "total_token_usage": (
                None
                if self.total_token_usage is None
                else self.total_token_usage.to_dict()
            ),
            "error": self.error,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MultiAgentTrajectory:
        """Rebuild a pipeline trace from ``to_dict()`` output.

        Args:
            data: Dict produced by ``to_dict()``. ``error`` may be absent.

        Returns:
            The reconstructed pipeline trace.

        Raises:
            KeyError: If a required field is missing from ``data``.
        """
        return cls(
            agent_trajectories={
                name: ADKTrajectory.from_dict(trajectory)
                for name, trajectory in data["agent_trajectories"].items()
            },
            pipeline_output=data["pipeline_output"],
            total_token_usage=_usage_from_dict(data["total_token_usage"]),
            error=data.get("error"),
        )

to_dict

to_dict() -> dict[str, Any]

Serialise this pipeline trace to a JSON-safe dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with agent_trajectories (agent name to

dict[str, Any]

ADKTrajectory.to_dict()), pipeline_output,

dict[str, Any]

total_token_usage as a dict or None, and error.

Source code in src/gepa_adk/domain/trajectory.py
def to_dict(self) -> dict[str, Any]:
    """Serialise this pipeline trace to a JSON-safe dict.

    Returns:
        Dict with ``agent_trajectories`` (agent name to
        ``ADKTrajectory.to_dict()``), ``pipeline_output``,
        ``total_token_usage`` as a dict or None, and ``error``.
    """
    return {
        "agent_trajectories": {
            name: trajectory.to_dict()
            for name, trajectory in self.agent_trajectories.items()
        },
        "pipeline_output": self.pipeline_output,
        "total_token_usage": (
            None
            if self.total_token_usage is None
            else self.total_token_usage.to_dict()
        ),
        "error": self.error,
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> MultiAgentTrajectory

Rebuild a pipeline trace from to_dict() output.

PARAMETER DESCRIPTION
data

Dict produced by to_dict(). error may be absent.

TYPE: dict[str, Any]

RETURNS DESCRIPTION
MultiAgentTrajectory

The reconstructed pipeline trace.

RAISES DESCRIPTION
KeyError

If a required field is missing from data.

Source code in src/gepa_adk/domain/trajectory.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MultiAgentTrajectory:
    """Rebuild a pipeline trace from ``to_dict()`` output.

    Args:
        data: Dict produced by ``to_dict()``. ``error`` may be absent.

    Returns:
        The reconstructed pipeline trace.

    Raises:
        KeyError: If a required field is missing from ``data``.
    """
    return cls(
        agent_trajectories={
            name: ADKTrajectory.from_dict(trajectory)
            for name, trajectory in data["agent_trajectories"].items()
        },
        pipeline_output=data["pipeline_output"],
        total_token_usage=_usage_from_dict(data["total_token_usage"]),
        error=data.get("error"),
    )

TokenUsage dataclass

Token usage statistics from LLM calls.

Tracks token consumption for monitoring costs and performance of language model interactions during agent execution. to_dict() and from_dict() convert it to and from a JSON-safe dict.

ATTRIBUTE DESCRIPTION
input_tokens

Number of tokens in the prompt/context.

TYPE: int

output_tokens

Number of tokens generated in the response.

TYPE: int

total_tokens

Sum of input_tokens and output_tokens.

TYPE: int

Examples:

usage = TokenUsage(input_tokens=150, output_tokens=50, total_tokens=200)
Source code in src/gepa_adk/domain/trajectory.py
@dataclass(frozen=True, slots=True)
class TokenUsage:
    """Token usage statistics from LLM calls.

    Tracks token consumption for monitoring costs and performance of
    language model interactions during agent execution.
    ``to_dict()`` and ``from_dict()`` convert it to and from a JSON-safe dict.

    Attributes:
        input_tokens (int): Number of tokens in the prompt/context.
        output_tokens (int): Number of tokens generated in the response.
        total_tokens (int): Sum of input_tokens and output_tokens.

    Examples:
        ```python
        usage = TokenUsage(input_tokens=150, output_tokens=50, total_tokens=200)
        ```
    """

    input_tokens: int
    output_tokens: int
    total_tokens: int

    def to_dict(self) -> dict[str, Any]:
        """Serialise these counts to a JSON-safe dict.

        Returns:
            Dict with ``input_tokens``, ``output_tokens`` and
            ``total_tokens``.
        """
        return {
            "input_tokens": self.input_tokens,
            "output_tokens": self.output_tokens,
            "total_tokens": self.total_tokens,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> TokenUsage:
        """Rebuild token counts from ``to_dict()`` output.

        Args:
            data: Dict produced by ``to_dict()``.

        Returns:
            The reconstructed counts.

        Raises:
            KeyError: If a field is missing from ``data``.
        """
        return cls(
            input_tokens=data["input_tokens"],
            output_tokens=data["output_tokens"],
            total_tokens=data["total_tokens"],
        )

to_dict

to_dict() -> dict[str, Any]

Serialise these counts to a JSON-safe dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with input_tokens, output_tokens and

dict[str, Any]

total_tokens.

Source code in src/gepa_adk/domain/trajectory.py
def to_dict(self) -> dict[str, Any]:
    """Serialise these counts to a JSON-safe dict.

    Returns:
        Dict with ``input_tokens``, ``output_tokens`` and
        ``total_tokens``.
    """
    return {
        "input_tokens": self.input_tokens,
        "output_tokens": self.output_tokens,
        "total_tokens": self.total_tokens,
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> TokenUsage

Rebuild token counts from to_dict() output.

PARAMETER DESCRIPTION
data

Dict produced by to_dict().

TYPE: dict[str, Any]

RETURNS DESCRIPTION
TokenUsage

The reconstructed counts.

RAISES DESCRIPTION
KeyError

If a field is missing from data.

Source code in src/gepa_adk/domain/trajectory.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> TokenUsage:
    """Rebuild token counts from ``to_dict()`` output.

    Args:
        data: Dict produced by ``to_dict()``.

    Returns:
        The reconstructed counts.

    Raises:
        KeyError: If a field is missing from ``data``.
    """
    return cls(
        input_tokens=data["input_tokens"],
        output_tokens=data["output_tokens"],
        total_tokens=data["total_tokens"],
    )

ToolCallRecord dataclass

Record of a single tool call during agent execution.

Captures the invocation details of a tool/function call made by an agent during evaluation, including arguments, results, and timing information. to_dict() and from_dict() convert it to and from a JSON-safe dict.

ATTRIBUTE DESCRIPTION
name

Tool or function name that was called.

TYPE: str

arguments

Dictionary of arguments passed to the tool.

TYPE: dict[str, Any]

result

Return value from the tool execution.

TYPE: Any

timestamp

Relative time in seconds from evaluation start.

TYPE: float

Examples:

record = ToolCallRecord(
    name="get_weather",
    arguments={"city": "Paris"},
    result={"temp": 22, "condition": "sunny"},
    timestamp=0.123,
)
Source code in src/gepa_adk/domain/trajectory.py
@dataclass(frozen=True, slots=True)
class ToolCallRecord:
    """Record of a single tool call during agent execution.

    Captures the invocation details of a tool/function call made by an agent
    during evaluation, including arguments, results, and timing information.
    ``to_dict()`` and ``from_dict()`` convert it to and from a JSON-safe dict.

    Attributes:
        name (str): Tool or function name that was called.
        arguments (dict[str, Any]): Dictionary of arguments passed to the tool.
        result (Any): Return value from the tool execution.
        timestamp (float): Relative time in seconds from evaluation start.

    Examples:
        ```python
        record = ToolCallRecord(
            name="get_weather",
            arguments={"city": "Paris"},
            result={"temp": 22, "condition": "sunny"},
            timestamp=0.123,
        )
        ```
    """

    name: str
    arguments: dict[str, Any]
    result: Any
    timestamp: float

    def to_dict(self) -> dict[str, Any]:
        """Serialise this record to a JSON-safe dict.

        Returns:
            Dict with ``name``, ``arguments``, ``result`` and ``timestamp``.
            An argument value or result that JSON cannot encode is stored as
            its ``str()``.
        """
        return {
            "name": self.name,
            "arguments": _json_safe_dict(self.arguments),
            "result": _json_safe(self.result),
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ToolCallRecord:
        """Rebuild a record from ``to_dict()`` output.

        Args:
            data: Dict produced by ``to_dict()``.

        Returns:
            The reconstructed record.

        Raises:
            KeyError: If a field is missing from ``data``.
        """
        return cls(
            name=data["name"],
            arguments=dict(data["arguments"]),
            result=data["result"],
            timestamp=data["timestamp"],
        )

to_dict

to_dict() -> dict[str, Any]

Serialise this record to a JSON-safe dict.

RETURNS DESCRIPTION
dict[str, Any]

Dict with name, arguments, result and timestamp.

dict[str, Any]

An argument value or result that JSON cannot encode is stored as

dict[str, Any]

its str().

Source code in src/gepa_adk/domain/trajectory.py
def to_dict(self) -> dict[str, Any]:
    """Serialise this record to a JSON-safe dict.

    Returns:
        Dict with ``name``, ``arguments``, ``result`` and ``timestamp``.
        An argument value or result that JSON cannot encode is stored as
        its ``str()``.
    """
    return {
        "name": self.name,
        "arguments": _json_safe_dict(self.arguments),
        "result": _json_safe(self.result),
        "timestamp": self.timestamp,
    }

from_dict classmethod

from_dict(data: dict[str, Any]) -> ToolCallRecord

Rebuild a record from to_dict() output.

PARAMETER DESCRIPTION
data

Dict produced by to_dict().

TYPE: dict[str, Any]

RETURNS DESCRIPTION
ToolCallRecord

The reconstructed record.

RAISES DESCRIPTION
KeyError

If a field is missing from data.

Source code in src/gepa_adk/domain/trajectory.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ToolCallRecord:
    """Rebuild a record from ``to_dict()`` output.

    Args:
        data: Dict produced by ``to_dict()``.

    Returns:
        The reconstructed record.

    Raises:
        KeyError: If a field is missing from ``data``.
    """
    return cls(
        name=data["name"],
        arguments=dict(data["arguments"]),
        result=data["result"],
        timestamp=data["timestamp"],
    )

FrontierType

Bases: str, Enum


              flowchart TD
              gepa_adk.domain.FrontierType[FrontierType]

              

              click gepa_adk.domain.FrontierType href "" "gepa_adk.domain.FrontierType"
            

Supported frontier tracking strategies for Pareto selection.

Notes

INSTANCE tracks per-example scores, OBJECTIVE aggregated objective scores, HYBRID both, and CARTESIAN per-example objective scores. Members are str values, so they compare equal to and serialize as their plain strings ("instance" and so on).

Examples:

frontier_type = FrontierType.INSTANCE
Source code in src/gepa_adk/domain/types.py
class FrontierType(str, Enum):  # noqa: UP042  # StrEnum would change str() of public members
    """Supported frontier tracking strategies for Pareto selection.

    Notes:
        ``INSTANCE`` tracks per-example scores, ``OBJECTIVE`` aggregated
        objective scores, ``HYBRID`` both, and ``CARTESIAN`` per-example
        objective scores. Members are ``str`` values, so they compare equal
        to and serialize as their plain strings (``"instance"`` and so on).

    Examples:
        ```python
        frontier_type = FrontierType.INSTANCE
        ```
    """

    INSTANCE = "instance"
    OBJECTIVE = "objective"
    HYBRID = "hybrid"
    CARTESIAN = "cartesian"

SchemaConstraints dataclass

Constraints for output schema evolution.

Controls which fields must be preserved during schema evolution, including field existence and type constraints. Used by the OutputSchemaHandler to validate proposed schema mutations.

ATTRIBUTE DESCRIPTION
required_fields

Field names that must exist in evolved schemas. Mutations removing these fields are rejected.

TYPE: tuple[str, ...]

preserve_types

Mapping of field names to allowed type(s). Mutations changing a field's type to an incompatible type are rejected.

TYPE: dict[str, type | tuple[type, ...]]

Examples:

Preserve required fields only:

from gepa_adk.domain.types import SchemaConstraints

constraints = SchemaConstraints(
    required_fields=("score", "feedback"),
)

Preserve required fields with type constraints:

constraints = SchemaConstraints(
    required_fields=("score",),
    preserve_types={
        "score": (float, int),  # Allow numeric types
        "order_id": str,  # Must stay string
    },
)
Notes

A frozen dataclass ensures immutability during evolution runs. Configuration is validated at evolution start.

Source code in src/gepa_adk/domain/types.py
@dataclass(frozen=True, slots=True)
class SchemaConstraints:
    """Constraints for output schema evolution.

    Controls which fields must be preserved during schema evolution,
    including field existence and type constraints. Used by the
    OutputSchemaHandler to validate proposed schema mutations.

    Attributes:
        required_fields (tuple[str, ...]): Field names that must exist
            in evolved schemas. Mutations removing these fields are rejected.
        preserve_types (dict[str, type | tuple[type, ...]]): Mapping of
            field names to allowed type(s). Mutations changing a field's
            type to an incompatible type are rejected.

    Examples:
        Preserve required fields only:

        ```python
        from gepa_adk.domain.types import SchemaConstraints

        constraints = SchemaConstraints(
            required_fields=("score", "feedback"),
        )
        ```

        Preserve required fields with type constraints:

        ```python
        constraints = SchemaConstraints(
            required_fields=("score",),
            preserve_types={
                "score": (float, int),  # Allow numeric types
                "order_id": str,  # Must stay string
            },
        )
        ```

    Notes:
        A frozen dataclass ensures immutability during evolution runs.
        Configuration is validated at evolution start.
    """

    required_fields: tuple[str, ...] = ()
    preserve_types: dict[str, type | tuple[type, ...]] = field(default_factory=dict)

StopReason

Bases: str, Enum


              flowchart TD
              gepa_adk.domain.StopReason[StopReason]

              

              click gepa_adk.domain.StopReason href "" "gepa_adk.domain.StopReason"
            

Why an evolution run terminated.

Each value represents a distinct termination condition. The engine sets the appropriate value when building the final EvolutionResult. Members are str values, so a serialized result stores them as plain strings.

The engine sets KEYBOARD_INTERRUPT or CANCELLED when a run is interrupted or cancelled after the baseline is scored. TIMEOUT is reserved and not set by the engine yet.

MAX_ITERATIONS means the iteration cap was reached. PATIENCE means early stopping fired because stagnation_counter reached patience.

ERROR marks the partial result the engine attaches to an EvolutionError in partial_result when it aborts a run after the baseline was scored.

Examples:

reason = StopReason.MAX_ITERATIONS
assert reason == "max_iterations"
Source code in src/gepa_adk/domain/types.py
class StopReason(str, Enum):  # noqa: UP042  # StrEnum would change str() of public members
    """Why an evolution run terminated.

    Each value represents a distinct termination condition. The engine sets the
    appropriate value when building the final ``EvolutionResult``. Members are
    ``str`` values, so a serialized result stores them as plain strings.

    The engine sets ``KEYBOARD_INTERRUPT`` or ``CANCELLED`` when a run is
    interrupted or cancelled after the baseline is scored. ``TIMEOUT`` is
    reserved and not set by the engine yet.

    ``MAX_ITERATIONS`` means the iteration cap was reached. ``PATIENCE`` means
    early stopping fired because ``stagnation_counter`` reached ``patience``.

    ``ERROR`` marks the partial result the engine attaches to an
    ``EvolutionError`` in ``partial_result`` when it aborts a run after the
    baseline was scored.

    Examples:
        ```python
        reason = StopReason.MAX_ITERATIONS
        assert reason == "max_iterations"
        ```
    """

    COMPLETED = "completed"
    MAX_ITERATIONS = "max_iterations"
    PATIENCE = "patience"
    STOPPER_TRIGGERED = "stopper_triggered"
    KEYBOARD_INTERRUPT = "keyboard_interrupt"
    TIMEOUT = "timeout"
    CANCELLED = "cancelled"
    ERROR = "error"

TrajectoryConfig dataclass

Configuration for trajectory extraction behavior.

Controls which components are extracted from ADK event streams, whether sensitive data should be redacted, and whether large values should be truncated.

Notes

All configuration fields are immutable after instantiation, ensuring consistent extraction behavior throughout evolution.

ATTRIBUTE DESCRIPTION
include_tool_calls

Extract tool/function call records. Defaults to True.

TYPE: bool

include_state_deltas

Extract session state changes. Defaults to True.

TYPE: bool

include_token_usage

Extract LLM token consumption metrics. Defaults to True.

TYPE: bool

redact_sensitive

Apply sensitive data redaction. When True, fields matching sensitive_keys will be replaced with "[REDACTED]". Defaults to True for secure-by-default behavior.

TYPE: bool

sensitive_keys

Field names to redact via exact match. Case-sensitive. Defaults to DEFAULT_SENSITIVE_KEYS.

TYPE: tuple[str, ...]

max_string_length

Truncate strings longer than this with a marker indicating truncation. None disables truncation. Defaults to 10000 characters.

TYPE: int | None

Examples:

Default configuration (secure, with truncation):

config = TrajectoryConfig()  # All features enabled, redaction ON

Minimal configuration (tool calls only):

config = TrajectoryConfig(
    include_tool_calls=True,
    include_state_deltas=False,
    include_token_usage=False,
    redact_sensitive=False,
)

Custom sensitive keys (extends defaults):

config = TrajectoryConfig(
    sensitive_keys=DEFAULT_SENSITIVE_KEYS + ("ssn",),
    max_string_length=5000,  # Truncate DOM/screenshots earlier
)

Disable truncation (keep full values):

config = TrajectoryConfig(max_string_length=None)
Notes

Redaction takes precedence over truncation. Sensitive values are replaced with "[REDACTED]" first, then truncation applies to the remaining (non-sensitive) strings.

Source code in src/gepa_adk/domain/types.py
@dataclass(frozen=True, slots=True)
class TrajectoryConfig:
    """Configuration for trajectory extraction behavior.

    Controls which components are extracted from ADK event streams,
    whether sensitive data should be redacted, and whether large
    values should be truncated.

    Notes:
        All configuration fields are immutable after instantiation,
        ensuring consistent extraction behavior throughout evolution.

    Attributes:
        include_tool_calls (bool): Extract tool/function call records.
            Defaults to True.
        include_state_deltas (bool): Extract session state changes.
            Defaults to True.
        include_token_usage (bool): Extract LLM token consumption metrics.
            Defaults to True.
        redact_sensitive (bool): Apply sensitive data redaction. When True,
            fields matching sensitive_keys will be replaced with "[REDACTED]".
            Defaults to True for secure-by-default behavior.
        sensitive_keys (tuple[str, ...]): Field names to redact via exact
            match. Case-sensitive. Defaults to ``DEFAULT_SENSITIVE_KEYS``.
        max_string_length (int | None): Truncate strings longer than this
            with a marker indicating truncation. None disables truncation.
            Defaults to 10000 characters.

    Examples:
        Default configuration (secure, with truncation):

        ```python
        config = TrajectoryConfig()  # All features enabled, redaction ON
        ```

        Minimal configuration (tool calls only):

        ```python
        config = TrajectoryConfig(
            include_tool_calls=True,
            include_state_deltas=False,
            include_token_usage=False,
            redact_sensitive=False,
        )
        ```

        Custom sensitive keys (extends defaults):

        ```python
        config = TrajectoryConfig(
            sensitive_keys=DEFAULT_SENSITIVE_KEYS + ("ssn",),
            max_string_length=5000,  # Truncate DOM/screenshots earlier
        )
        ```

        Disable truncation (keep full values):

        ```python
        config = TrajectoryConfig(max_string_length=None)
        ```

    Notes:
        Redaction takes precedence over truncation. Sensitive values are
        replaced with "[REDACTED]" first, then truncation applies to the
        remaining (non-sensitive) strings.
    """

    include_tool_calls: bool = True
    include_state_deltas: bool = True
    include_token_usage: bool = True
    redact_sensitive: bool = True
    sensitive_keys: tuple[str, ...] = DEFAULT_SENSITIVE_KEYS
    max_string_length: int | None = 10000