Skip to content

Core API

api

Public API functions for gepa-adk evolution engine.

This module provides high-level async functions for evolving agent instructions using the GEPA (Generalized Evolutionary Prompt-programming Architecture) approach. Pre-flight validation runs synchronously before any LLM calls to give developers immediate feedback on invalid configurations. Each entry point has a dedicated pre-flight validator (_pre_flight_validate_evolve, _pre_flight_validate_group, _pre_flight_validate_workflow). Each entry point accepts either a critic agent or a caller-supplied scorer (mutually exclusive); an explicit scorer takes precedence over schema-based scoring. When evolve() falls back to SchemaBasedScorer over trainset rows that carry expected, it logs a warning that points to LabelAgreementScorer via scorer=. The default reflection agent takes EvolutionConfig.reflection_model as either a model string or a BaseLlm instance, which is passed through unchanged so a custom api_base or temperature reaches the reflector. evolve() seeds any component other than instruction and output_schema from its handler in the component handler registry passed as registry (the default registry when omitted), so names added by register_handler() or register_mapping_components() can be evolved. evolve(), evolve_group() and evolve_workflow() resolve every component name through that one registry and raise ConfigurationError naming a component it does not hold. evolve() returns the engine's EvolutionResult via dataclasses.replace, so its failure counters, stop reason and token usage reach the caller. Its ADKAdapter limits concurrent evaluation rows to the configured max_concurrent_evals; group evolution does not apply this setting.

Notes

The public API exposes evolve(), evolve_group(), evolve_workflow(), and run_sync() as primary entry points. All async functions should be awaited. For synchronous usage in scripts, use run_sync(evolve(...)) which handles event loop management internally; inside a running loop it needs the optional nest_asyncio package. evolve_sync() is deprecated in favor of run_sync() and emits a DeprecationWarning. For reproducible evolution, pass a seeded config: config=EvolutionConfig(seed=42).

Examples:

Single-agent evolution (synchronous):

from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk.api import evolve, run_sync

agent = LlmAgent(
    name="helper",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="Be helpful.",
)
result = run_sync(evolve(agent, trainset=[{"input": "hi"}]))

Workflow evolution across a multi-agent graph:

from gepa_adk.api import evolve_workflow

result = await evolve_workflow(workflow_root, trainset=trainset)
See Also

gepa_adk.engine: Core evolution engine and mutation proposers. gepa_adk.ports: Protocol definitions consumed by this module. gepa_adk.domain.models: Candidate, EvolutionConfig, and EvolutionResult.

SchemaBasedScorer

Scorer that extracts scores from agent's structured output_schema.

When an agent has an output_schema, its output is structured JSON. This scorer parses that JSON and extracts a "score" field.

ATTRIBUTE DESCRIPTION
output_schema

The Pydantic BaseModel schema class from agent.output_schema. Must contain a "score" field.

TYPE: type[BaseModel]

Examples:

Basic usage:

from pydantic import BaseModel, Field
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk.api import SchemaBasedScorer


class OutputSchema(BaseModel):
    score: float = Field(ge=0.0, le=1.0)
    result: str


agent = LlmAgent(
    name="agent",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    output_schema=OutputSchema,
)

scorer = SchemaBasedScorer(output_schema=OutputSchema)
score, metadata = await scorer.async_score(
    input_text="test",
    output='{"score": 0.8, "result": "good"}',
)
Notes

Adheres to Scorer protocol. Requires output_schema to have a "score" field. If score field is missing, raises MissingScoreFieldError. The expected argument is never read: the score is the agent's self-reported score field. For a labelled trainset, pass LabelAgreementScorer through the scorer= keyword instead.

Source code in src/gepa_adk/api.py
class SchemaBasedScorer:
    """Scorer that extracts scores from agent's structured output_schema.

    When an agent has an output_schema, its output is structured JSON.
    This scorer parses that JSON and extracts a "score" field.

    Attributes:
        output_schema (type[BaseModel]): The Pydantic BaseModel schema class
            from agent.output_schema. Must contain a "score" field.

    Examples:
        Basic usage:

        ```python
        from pydantic import BaseModel, Field
        from google.adk.agents import LlmAgent
        from google.adk.models.lite_llm import LiteLlm
        from gepa_adk.api import SchemaBasedScorer


        class OutputSchema(BaseModel):
            score: float = Field(ge=0.0, le=1.0)
            result: str


        agent = LlmAgent(
            name="agent",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            output_schema=OutputSchema,
        )

        scorer = SchemaBasedScorer(output_schema=OutputSchema)
        score, metadata = await scorer.async_score(
            input_text="test",
            output='{"score": 0.8, "result": "good"}',
        )
        ```

    Notes:
        Adheres to Scorer protocol. Requires output_schema to have a "score"
        field. If score field is missing, raises MissingScoreFieldError.
        The ``expected`` argument is never read: the score is the agent's
        self-reported ``score`` field. For a labelled trainset, pass
        ``LabelAgreementScorer`` through the ``scorer=`` keyword instead.
    """

    def __init__(self, output_schema: type[BaseModel]) -> None:
        """Initialize schema-based scorer.

        Args:
            output_schema: Pydantic BaseModel class from agent.output_schema.

        Raises:
            ConfigurationError: If output_schema doesn't have a "score" field.

        Notes:
            Checks that the schema contains a "score" field during initialization.
        """
        self.output_schema = output_schema

        # Verify schema has score field
        if (
            not hasattr(output_schema, "model_fields")
            or "score" not in output_schema.model_fields
        ):
            raise ConfigurationError(
                f"output_schema {output_schema.__name__} must have a 'score' field",
                field="output_schema",
                value=output_schema.__name__,
                constraint="must have 'score' field",
            )

    def score(
        self,
        input_text: str,
        output: str,
        expected: str | None = None,
    ) -> tuple[float, dict[str, Any]]:
        """Score an agent output synchronously.

        Args:
            input_text (str): The input provided to the agent.
            output (str): The agent's structured JSON output.
            expected (str | None): Optional expected output (not used for schema-based scoring).

        Returns:
            Tuple of (score, metadata) where score is extracted from output JSON
            and metadata contains all other fields from the schema.

        Raises:
            OutputParseError: If output cannot be parsed as JSON.
            SchemaValidationError: If output doesn't match the schema.
            MissingScoreFieldError: If score field is null in parsed output.

        Examples:
            Basic scoring with JSON output:

            ```python
            scorer = SchemaBasedScorer(output_schema=MySchema)
            score, metadata = scorer.score(
                input_text="What is 2+2?",
                output='{"score": 0.9, "result": "4"}',
            )
            # score == 0.9, metadata == {"result": "4"}
            ```

        Notes:
            Operates synchronously by parsing JSON and extracting the score field.
            The expected parameter is ignored for schema-based scoring.
        """
        try:
            # Parse JSON output
            parsed = json.loads(output)
            # Parse with Pydantic schema for validation
            schema_instance = self.output_schema.model_validate(parsed)

            # Extract score - schema validated in __init__ has "score" field,
            # and model_validate succeeded, so score attribute exists.
            # The value could still be None if schema allows nullable scores.
            score_value = cast(_ScoreSchema, schema_instance).score
            if score_value is None:
                raise MissingScoreFieldError(
                    f"output_schema {self.output_schema.__name__} has score=None; "
                    "score must be a numeric value",
                    parsed_output=parsed,
                )

            # Explicit cast since we've validated it's not None
            score = float(score_value)

            # Build metadata from all other fields
            metadata = schema_instance.model_dump(exclude={"score"})

        except json.JSONDecodeError as e:
            raise OutputParseError(
                f"Failed to parse output as JSON: {e}",
                raw_output=output,
                parse_error=str(e),
                cause=e,
            ) from e
        except ValidationError as e:
            raise SchemaValidationError(
                f"Output does not match schema {self.output_schema.__name__}: {e}",
                raw_output=output,
                validation_error=str(e),
                cause=e,
            ) from e
        else:
            return score, metadata

    async def async_score(
        self,
        input_text: str,
        output: str,
        expected: str | None = None,
    ) -> tuple[float, dict[str, Any]]:
        """Score an agent output asynchronously.

        Args:
            input_text: The input provided to the agent.
            output: The agent's structured JSON output.
            expected: Optional expected output (not used for schema-based scoring).

        Returns:
            Tuple of (score, metadata) where score is extracted from output JSON
            and metadata contains all other fields from the schema.

        Raises:
            OutputParseError: If output cannot be parsed as JSON.
            SchemaValidationError: If output doesn't match the schema.
            MissingScoreFieldError: If score field is null in parsed output.

        Examples:
            Async scoring with JSON output:

            ```python
            scorer = SchemaBasedScorer(output_schema=MySchema)
            score, metadata = await scorer.async_score(
                input_text="What is 2+2?",
                output='{"score": 0.9, "result": "4"}',
            )
            # score == 0.9, metadata == {"result": "4"}
            ```

        Notes:
            Operates by delegating to synchronous score() since JSON parsing
            does not require async I/O operations.
        """
        # Schema-based scoring is synchronous (just JSON parsing)
        return self.score(input_text, output, expected)

__init__

__init__(output_schema: type[BaseModel]) -> None

Initialize schema-based scorer.

PARAMETER DESCRIPTION
output_schema

Pydantic BaseModel class from agent.output_schema.

TYPE: type[BaseModel]

RAISES DESCRIPTION
ConfigurationError

If output_schema doesn't have a "score" field.

Notes

Checks that the schema contains a "score" field during initialization.

Source code in src/gepa_adk/api.py
def __init__(self, output_schema: type[BaseModel]) -> None:
    """Initialize schema-based scorer.

    Args:
        output_schema: Pydantic BaseModel class from agent.output_schema.

    Raises:
        ConfigurationError: If output_schema doesn't have a "score" field.

    Notes:
        Checks that the schema contains a "score" field during initialization.
    """
    self.output_schema = output_schema

    # Verify schema has score field
    if (
        not hasattr(output_schema, "model_fields")
        or "score" not in output_schema.model_fields
    ):
        raise ConfigurationError(
            f"output_schema {output_schema.__name__} must have a 'score' field",
            field="output_schema",
            value=output_schema.__name__,
            constraint="must have 'score' field",
        )

score

score(
    input_text: str,
    output: str,
    expected: str | None = None,
) -> tuple[float, dict[str, Any]]

Score an agent output synchronously.

PARAMETER DESCRIPTION
input_text

The input provided to the agent.

TYPE: str

output

The agent's structured JSON output.

TYPE: str

expected

Optional expected output (not used for schema-based scoring).

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
float

Tuple of (score, metadata) where score is extracted from output JSON

dict[str, Any]

and metadata contains all other fields from the schema.

RAISES DESCRIPTION
OutputParseError

If output cannot be parsed as JSON.

SchemaValidationError

If output doesn't match the schema.

MissingScoreFieldError

If score field is null in parsed output.

Examples:

Basic scoring with JSON output:

scorer = SchemaBasedScorer(output_schema=MySchema)
score, metadata = scorer.score(
    input_text="What is 2+2?",
    output='{"score": 0.9, "result": "4"}',
)
# score == 0.9, metadata == {"result": "4"}
Notes

Operates synchronously by parsing JSON and extracting the score field. The expected parameter is ignored for schema-based scoring.

Source code in src/gepa_adk/api.py
def score(
    self,
    input_text: str,
    output: str,
    expected: str | None = None,
) -> tuple[float, dict[str, Any]]:
    """Score an agent output synchronously.

    Args:
        input_text (str): The input provided to the agent.
        output (str): The agent's structured JSON output.
        expected (str | None): Optional expected output (not used for schema-based scoring).

    Returns:
        Tuple of (score, metadata) where score is extracted from output JSON
        and metadata contains all other fields from the schema.

    Raises:
        OutputParseError: If output cannot be parsed as JSON.
        SchemaValidationError: If output doesn't match the schema.
        MissingScoreFieldError: If score field is null in parsed output.

    Examples:
        Basic scoring with JSON output:

        ```python
        scorer = SchemaBasedScorer(output_schema=MySchema)
        score, metadata = scorer.score(
            input_text="What is 2+2?",
            output='{"score": 0.9, "result": "4"}',
        )
        # score == 0.9, metadata == {"result": "4"}
        ```

    Notes:
        Operates synchronously by parsing JSON and extracting the score field.
        The expected parameter is ignored for schema-based scoring.
    """
    try:
        # Parse JSON output
        parsed = json.loads(output)
        # Parse with Pydantic schema for validation
        schema_instance = self.output_schema.model_validate(parsed)

        # Extract score - schema validated in __init__ has "score" field,
        # and model_validate succeeded, so score attribute exists.
        # The value could still be None if schema allows nullable scores.
        score_value = cast(_ScoreSchema, schema_instance).score
        if score_value is None:
            raise MissingScoreFieldError(
                f"output_schema {self.output_schema.__name__} has score=None; "
                "score must be a numeric value",
                parsed_output=parsed,
            )

        # Explicit cast since we've validated it's not None
        score = float(score_value)

        # Build metadata from all other fields
        metadata = schema_instance.model_dump(exclude={"score"})

    except json.JSONDecodeError as e:
        raise OutputParseError(
            f"Failed to parse output as JSON: {e}",
            raw_output=output,
            parse_error=str(e),
            cause=e,
        ) from e
    except ValidationError as e:
        raise SchemaValidationError(
            f"Output does not match schema {self.output_schema.__name__}: {e}",
            raw_output=output,
            validation_error=str(e),
            cause=e,
        ) from e
    else:
        return score, metadata

async_score async

async_score(
    input_text: str,
    output: str,
    expected: str | None = None,
) -> tuple[float, dict[str, Any]]

Score an agent output asynchronously.

PARAMETER DESCRIPTION
input_text

The input provided to the agent.

TYPE: str

output

The agent's structured JSON output.

TYPE: str

expected

Optional expected output (not used for schema-based scoring).

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
float

Tuple of (score, metadata) where score is extracted from output JSON

dict[str, Any]

and metadata contains all other fields from the schema.

RAISES DESCRIPTION
OutputParseError

If output cannot be parsed as JSON.

SchemaValidationError

If output doesn't match the schema.

MissingScoreFieldError

If score field is null in parsed output.

Examples:

Async scoring with JSON output:

scorer = SchemaBasedScorer(output_schema=MySchema)
score, metadata = await scorer.async_score(
    input_text="What is 2+2?",
    output='{"score": 0.9, "result": "4"}',
)
# score == 0.9, metadata == {"result": "4"}
Notes

Operates by delegating to synchronous score() since JSON parsing does not require async I/O operations.

Source code in src/gepa_adk/api.py
async def async_score(
    self,
    input_text: str,
    output: str,
    expected: str | None = None,
) -> tuple[float, dict[str, Any]]:
    """Score an agent output asynchronously.

    Args:
        input_text: The input provided to the agent.
        output: The agent's structured JSON output.
        expected: Optional expected output (not used for schema-based scoring).

    Returns:
        Tuple of (score, metadata) where score is extracted from output JSON
        and metadata contains all other fields from the schema.

    Raises:
        OutputParseError: If output cannot be parsed as JSON.
        SchemaValidationError: If output doesn't match the schema.
        MissingScoreFieldError: If score field is null in parsed output.

    Examples:
        Async scoring with JSON output:

        ```python
        scorer = SchemaBasedScorer(output_schema=MySchema)
        score, metadata = await scorer.async_score(
            input_text="What is 2+2?",
            output='{"score": 0.9, "result": "4"}',
        )
        # score == 0.9, metadata == {"result": "4"}
        ```

    Notes:
        Operates by delegating to synchronous score() since JSON parsing
        does not require async I/O operations.
    """
    # Schema-based scoring is synchronous (just JSON parsing)
    return self.score(input_text, output, expected)

evolve_group async

evolve_group(
    agents: dict[str, LlmAgent],
    primary: str,
    trainset: list[dict[str, Any]],
    *,
    components: dict[str, list[str]] | None = None,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    share_session: bool = True,
    config: EvolutionConfig | None = None,
    state_guard: StateGuard | None = None,
    component_selector: ComponentSelectorProtocol
    | str
    | None = None,
    reflection_agent: LlmAgent | None = None,
    trajectory_config: TrajectoryConfig | None = None,
    workflow: SequentialAgent
    | LoopAgent
    | ParallelAgent
    | None = None,
    session_service: BaseSessionService | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> MultiAgentEvolutionResult

Evolve multiple agents together with per-agent component configuration.

Optimizes specified components for each agent by targeting the primary agent's output score. When share_session=True, agents execute sequentially with shared session state, enabling later agents to access earlier agents' outputs via template strings.

PARAMETER DESCRIPTION
agents

Named ADK agents to evolve together as dict mapping agent names to LlmAgent instances. Must have at least one agent.

TYPE: dict[str, LlmAgent]

primary

Name of the agent whose output is used for scoring. Must match one of the agent names in the dict.

TYPE: str

trainset

Training examples for evaluation. Each example should have an "input" key and optionally an "expected" key.

TYPE: list[dict[str, Any]]

PARAMETER DESCRIPTION
components

Per-agent component configuration mapping agent names to lists of component names to evolve. If None, defaults to evolving "instruction" for all agents. Use empty list to exclude an agent from evolution. Available component names: "instruction", "output_schema", "generate_content_config".

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

critic

Optional critic agent for scoring. If None and no scorer is given, the primary agent must have an output_schema for schema-based scoring. Mutually exclusive with scorer.

TYPE: LlmAgent | None

scorer

Optional object implementing the Scorer protocol (score and async_score). When given, it scores the primary agent's output directly and takes precedence over schema-based scoring. Mutually exclusive with critic.

TYPE: Scorer | None

share_session

Whether agents share session state during execution. When True (default), uses SequentialAgent. When False, only the primary agent runs in its own session; other agents are not executed.

TYPE: bool

config

Evolution configuration. If None, uses EvolutionConfig defaults. Its reflection_max_trials and reflection_max_trial_chars bound the trials each reflection call sees, and its reflection_timeout_seconds bounds each reflection call's run time. max_concurrent_evals does not apply here: MultiAgentAdapter has no corresponding limit.

TYPE: EvolutionConfig | None

state_guard

Optional StateGuard instance for validating and repairing state injection tokens in evolved instructions.

TYPE: StateGuard | None

component_selector

Optional selector instance or selector name for choosing which components to update.

TYPE: ComponentSelectorProtocol | str | None

reflection_agent

Optional ADK agent for proposals. If None, creates a default reflection agent using config.reflection_model (a model string, or a BaseLlm instance passed through unchanged).

TYPE: LlmAgent | None

trajectory_config

Trajectory capture settings (uses defaults if None).

TYPE: TrajectoryConfig | None

workflow

Optional original workflow structure to preserve during evaluation. When provided, LoopAgent iterations and ParallelAgent concurrency are preserved instead of flattening to SequentialAgent. Used internally by evolve_workflow(); not typically set directly.

TYPE: SequentialAgent | LoopAgent | ParallelAgent | None

session_service

Optional ADK session service for state management. If None (default), creates an InMemorySessionService internally. Pass a custom service (e.g., SqliteSessionService, DatabaseSessionService; on ADK 2.x SQL-backed services need the gepa-adk[db] extra, which installs google-adk[db]) to persist sessions alongside other agent executions in a shared database. SqliteSessionService sets no busy timeout or WAL of its own, so on a loaded disk a write can wait past the five-second default of Python's sqlite3 connection (which aiosqlite uses) and raise database is locked. The internal executor retries that error under its default RetryPolicy (three attempts, 0.5 s backoff, doubling) before the row counts as a failed evaluation.

TYPE: BaseSessionService | None

app

Optional ADK App instance. When provided, evolution uses the app's configuration. Note that App does not hold services directly; pass a Runner for service extraction, or combine with session_service param.

TYPE: App | None

runner

Optional ADK Runner instance. When provided, evolution extracts and uses the runner's session_service for all agent executions (evolved agents, critic, and reflection agent). Takes precedence over both app and session_service parameters. This enables seamless integration with existing ADK infrastructure.

TYPE: Runner | None

registry

Optional ComponentHandlerRegistry that resolves every component name; defaults to the default registry that register_handler() and register_mapping_components() fill.

TYPE: ComponentHandlerRegistry | None

RETURNS DESCRIPTION
MultiAgentEvolutionResult

MultiAgentEvolutionResult containing evolved_components dict

MultiAgentEvolutionResult

mapping qualified component names (agent.component format) to their

MultiAgentEvolutionResult

optimized values, along with score metrics, iteration history

MultiAgentEvolutionResult

(each record names its candidate_id and parent_ids),

MultiAgentEvolutionResult

original_components (filtered to the qualified keyspace),

MultiAgentEvolutionResult

schema_version (4), and stop_reason propagated from the engine

MultiAgentEvolutionResult

result.

RAISES DESCRIPTION
ConfigurationError

If pre-flight validation fails: invalid agent names, non-LlmAgent critic, both critic and scorer given, a scorer that does not implement Scorer, empty trainset, duplicate or empty component names per agent, EvolutionConfig consistency errors, or a component name the resolved registry holds no handler for.

MultiAgentValidationError

If agents dict is empty, primary agent not found, or no scorer and primary lacks output_schema.

ValueError

If components mapping contains unknown agents or is missing entries for agents.

EvolutionError

If evolution fails during execution. A reflection call that keeps raising a retryable provider error is skipped with skip_reason="reflection_error"; a non-retryable one raises ReflectionError. A reflection cut off at the output-token limit, or one that opens a reasoning tag it never closes, is skipped with skip_reason="incomplete_proposal". An error raised after the baseline was scored carries the engine's EvolutionResult for the recorded iterations in partial_result.

Examples:

Basic usage with per-agent components (API v0.3.x):

from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk import evolve_group

generator = LlmAgent(
    name="generator",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="Generate code based on the requirement.",
)
critic = LlmAgent(
    name="critic",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="Review the code in {generator_output}.",
)
validator = LlmAgent(
    name="validator",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="Validate the reviewed code.",
    output_schema=ValidationResult,
)

result = await evolve_group(
    agents={
        "generator": generator,
        "critic": critic,
        "validator": validator,
    },
    primary="validator",
    trainset=training_data,
    components={
        "generator": ["instruction", "output_schema"],
        "critic": ["instruction"],
        "validator": ["instruction"],
    },
)

# Access evolved components using qualified names
print(result.evolved_components["generator.instruction"])
print(result.evolved_components["critic.instruction"])
print(result.evolved_components["validator.instruction"])

Exclude an agent from evolution:

result = await evolve_group(
    agents={"generator": gen, "static_validator": val},
    primary="generator",
    trainset=training_data,
    components={
        "generator": ["instruction"],
        "static_validator": [],  # Excluded from evolution
    },
)

Using custom session service for persistence:

# Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
from google.adk.sessions import SqliteSessionService

# Use SQLite for session persistence
session_service = SqliteSessionService(db_path="evolution_sessions.db")

result = await evolve_group(
    agents={"generator": gen, "critic": critic},
    primary="critic",
    trainset=training_data,
    session_service=session_service,  # Sessions persisted to SQLite
)

Using App/Runner for existing infrastructure integration:

from google.adk.runners import Runner

# Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
from google.adk.sessions import DatabaseSessionService

# Configure Runner with your production session service
runner = Runner(
    app_name="my_app",
    agent=generator,  # Any agent from the group
    session_service=DatabaseSessionService(connection_string="..."),
)

# Evolution uses Runner's session_service for all operations
result = await evolve_group(
    agents={"generator": gen, "refiner": ref},
    primary="refiner",
    trainset=training_data,
    runner=runner,  # Services extracted from runner
)
Notes

Breaking change in v0.3.x: The agents parameter changed from list[LlmAgent] to dict[str, LlmAgent]. Candidate keys now use qualified names (agent.component) instead of {agent_name}_instruction.

For reproducible evolution, pass a seeded config: config=EvolutionConfig(seed=42).

The returned result copies baseline_failed_evaluations, total_failed_evaluations and token_usage from the engine result.

Source code in src/gepa_adk/api.py
 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
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
async def evolve_group(  # noqa: C901, PLR0912, PLR0913  # decomposition tracked in #506; public API keyword arguments
    agents: dict[str, LlmAgent],
    primary: str,
    trainset: list[dict[str, Any]],
    *,
    components: dict[str, list[str]] | None = None,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    share_session: bool = True,
    config: EvolutionConfig | None = None,
    state_guard: StateGuard | None = None,
    component_selector: ComponentSelectorProtocol | str | None = None,
    reflection_agent: LlmAgent | None = None,
    trajectory_config: TrajectoryConfig | None = None,
    workflow: SequentialAgent | LoopAgent | ParallelAgent | None = None,
    session_service: BaseSessionService | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> MultiAgentEvolutionResult:
    """Evolve multiple agents together with per-agent component configuration.

    Optimizes specified components for each agent by targeting the primary
    agent's output score. When share_session=True, agents execute sequentially
    with shared session state, enabling later agents to access earlier
    agents' outputs via template strings.

    Args:
        agents (dict[str, LlmAgent]): Named ADK agents to evolve together as dict mapping agent
            names to LlmAgent instances. Must have at least one agent.
        primary (str): Name of the agent whose output is used for scoring.
            Must match one of the agent names in the dict.
        trainset (list[dict[str, Any]]): Training examples for evaluation. Each example should
            have an "input" key and optionally an "expected" key.

    Keyword Args:
        components (dict[str, list[str]] | None): Per-agent component configuration
            mapping agent names to lists of component names to evolve. If None, defaults
            to evolving "instruction" for all agents. Use empty list to exclude an agent
            from evolution. Available component names: "instruction", "output_schema",
            "generate_content_config".
        critic (LlmAgent | None): Optional critic agent for scoring. If None and no scorer is
            given, the primary agent must have an output_schema for
            schema-based scoring. Mutually exclusive with scorer.
        scorer (Scorer | None): Optional object implementing the Scorer protocol
            (``score`` and ``async_score``). When given, it scores the
            primary agent's output directly and takes precedence over
            schema-based scoring. Mutually exclusive with critic.
        share_session (bool): Whether agents share session state during
            execution. When True (default), uses SequentialAgent.
            When False, only the primary agent runs in its own session; other
            agents are not executed.
        config (EvolutionConfig | None): Evolution configuration. If None, uses EvolutionConfig
            defaults. Its ``reflection_max_trials`` and
            ``reflection_max_trial_chars`` bound the trials each
            reflection call sees, and its ``reflection_timeout_seconds``
            bounds each reflection call's run time. ``max_concurrent_evals``
            does not apply here: MultiAgentAdapter has no corresponding limit.
        state_guard (StateGuard | None): Optional StateGuard instance for validating and
            repairing state injection tokens in evolved instructions.
        component_selector (ComponentSelectorProtocol | str | None): Optional selector
            instance or selector name for choosing which components to update.
        reflection_agent (LlmAgent | None): Optional ADK agent for proposals. If None, creates a
            default reflection agent using config.reflection_model (a model
            string, or a ``BaseLlm`` instance passed through unchanged).
        trajectory_config (TrajectoryConfig | None): Trajectory capture settings (uses
            defaults if None).
        workflow (SequentialAgent | LoopAgent | ParallelAgent | None): Optional original
            workflow structure to preserve during evaluation. When provided, LoopAgent
            iterations and ParallelAgent concurrency are preserved instead of flattening
            to SequentialAgent. Used internally by evolve_workflow(); not typically set
            directly.
        session_service (BaseSessionService | None): Optional ADK session service for
            state management. If None (default), creates an InMemorySessionService
            internally. Pass a custom service (e.g., SqliteSessionService,
            DatabaseSessionService; on ADK 2.x SQL-backed services need the
            ``gepa-adk[db]`` extra, which installs ``google-adk[db]``) to persist
            sessions alongside other agent executions in a shared database.
            ``SqliteSessionService`` sets no busy timeout or WAL of its own, so on a
            loaded disk a write can wait past the five-second default of Python's
            sqlite3 connection (which aiosqlite uses) and raise ``database is
            locked``. The internal executor retries that error under its default
            ``RetryPolicy`` (three attempts, 0.5 s backoff, doubling) before the row
            counts as a failed evaluation.
        app (App | None): Optional ADK App instance. When provided, evolution uses the app's
            configuration. Note that App does not hold services directly; pass
            a Runner for service extraction, or combine with session_service param.
        runner (Runner | None): Optional ADK Runner instance. When provided, evolution extracts
            and uses the runner's session_service for all agent executions
            (evolved agents, critic, and reflection agent). Takes precedence
            over both app and session_service parameters. This enables seamless
            integration with existing ADK infrastructure.
        registry (ComponentHandlerRegistry | None): Optional ComponentHandlerRegistry
            that resolves every component name; defaults to the default registry that
            ``register_handler()`` and ``register_mapping_components()`` fill.

    Returns:
        MultiAgentEvolutionResult containing evolved_components dict
        mapping qualified component names (agent.component format) to their
        optimized values, along with score metrics, iteration history
        (each record names its ``candidate_id`` and ``parent_ids``),
        original_components (filtered to the qualified keyspace),
        schema_version (4), and stop_reason propagated from the engine
        result.

    Raises:
        ConfigurationError: If pre-flight validation fails: invalid agent
            names, non-LlmAgent critic, both critic and scorer given, a
            scorer that does not implement Scorer, empty trainset, duplicate
            or empty component names per agent, EvolutionConfig
            consistency errors, or a component name the resolved registry
            holds no handler for.
        MultiAgentValidationError: If agents dict is empty, primary agent
            not found, or no scorer and primary lacks output_schema.
        ValueError: If components mapping contains unknown agents or is
            missing entries for agents.
        EvolutionError: If evolution fails during execution. A reflection
            call that keeps raising a retryable provider error is skipped
            with ``skip_reason="reflection_error"``; a non-retryable one
            raises ``ReflectionError``. A reflection cut off at the
            output-token limit, or one that opens a reasoning tag it never
            closes, is skipped with ``skip_reason="incomplete_proposal"``.
            An error raised after the baseline
            was scored carries the engine's ``EvolutionResult`` for the
            recorded iterations in ``partial_result``.

    Examples:
        Basic usage with per-agent components (API v0.3.x):

        ```python
        from google.adk.agents import LlmAgent
        from google.adk.models.lite_llm import LiteLlm
        from gepa_adk import evolve_group

        generator = LlmAgent(
            name="generator",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="Generate code based on the requirement.",
        )
        critic = LlmAgent(
            name="critic",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="Review the code in {generator_output}.",
        )
        validator = LlmAgent(
            name="validator",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="Validate the reviewed code.",
            output_schema=ValidationResult,
        )

        result = await evolve_group(
            agents={
                "generator": generator,
                "critic": critic,
                "validator": validator,
            },
            primary="validator",
            trainset=training_data,
            components={
                "generator": ["instruction", "output_schema"],
                "critic": ["instruction"],
                "validator": ["instruction"],
            },
        )

        # Access evolved components using qualified names
        print(result.evolved_components["generator.instruction"])
        print(result.evolved_components["critic.instruction"])
        print(result.evolved_components["validator.instruction"])
        ```

        Exclude an agent from evolution:

        ```python
        result = await evolve_group(
            agents={"generator": gen, "static_validator": val},
            primary="generator",
            trainset=training_data,
            components={
                "generator": ["instruction"],
                "static_validator": [],  # Excluded from evolution
            },
        )
        ```

        Using custom session service for persistence:

        ```python
        # Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
        from google.adk.sessions import SqliteSessionService

        # Use SQLite for session persistence
        session_service = SqliteSessionService(db_path="evolution_sessions.db")

        result = await evolve_group(
            agents={"generator": gen, "critic": critic},
            primary="critic",
            trainset=training_data,
            session_service=session_service,  # Sessions persisted to SQLite
        )
        ```

        Using App/Runner for existing infrastructure integration:

        ```python
        from google.adk.runners import Runner

        # Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
        from google.adk.sessions import DatabaseSessionService

        # Configure Runner with your production session service
        runner = Runner(
            app_name="my_app",
            agent=generator,  # Any agent from the group
            session_service=DatabaseSessionService(connection_string="..."),
        )

        # Evolution uses Runner's session_service for all operations
        result = await evolve_group(
            agents={"generator": gen, "refiner": ref},
            primary="refiner",
            trainset=training_data,
            runner=runner,  # Services extracted from runner
        )
        ```

    Notes:
        Breaking change in v0.3.x: The `agents` parameter changed from
        `list[LlmAgent]` to `dict[str, LlmAgent]`. Candidate keys now use
        qualified names (agent.component) instead of {agent_name}_instruction.

        For reproducible evolution, pass a seeded config:
        ``config=EvolutionConfig(seed=42)``.

        The returned result copies ``baseline_failed_evaluations``,
        ``total_failed_evaluations`` and ``token_usage`` from the engine
        result.
    """
    # Pre-flight validation (T012a + Story 2.5)
    _pre_flight_validate_group(agents, trainset, critic, components, scorer=scorer)

    # Default components: evolve "instruction" for all agents
    if components is None:
        components = {name: ["instruction"] for name in agents}

    # Resolve every component name through one registry for this round (#451)
    resolved_registry = registry if registry is not None else component_handlers
    for comp_list in components.values():
        for comp_name in comp_list:
            _resolve_handler(comp_name, resolved_registry)

    # Capture original instructions for StateGuard validation
    original_instructions = {
        name: str(agent.instruction) for name, agent in agents.items()
    }

    # Log precedence warnings if multiple config sources provided (#227 T009)
    if runner is not None and app is not None:
        logger.warning(
            "evolve_group.precedence.runner_over_app",
            message="Both runner and app provided; using runner (runner takes precedence)",
            runner_app_name=runner.app_name,
            app_name=app.name,
        )

    # Resolve services using precedence rules: runner > app > session_service > default (#227)
    resolved_session_service, _artifact_service = _resolve_evolution_services(
        runner=runner,
        app=app,
        session_service=session_service,
    )

    # Resolve app_name for session isolation (#239)
    resolved_app_name = _resolve_app_name(runner=runner, app=app)

    # Create unified executor for consistent session management (FR-003)
    executor = AgentExecutor(
        session_service=resolved_session_service,
        app_name=resolved_app_name,
    )

    # Build scorer with executor (FR-005); an explicit scorer wins
    if scorer is None and critic:
        scorer = CriticScorer(critic_agent=critic, executor=executor)

    # Resolve config for reflection_model
    resolved_config = config or EvolutionConfig()
    rng = (
        random.Random(resolved_config.seed)  # noqa: S311  # evolution sampling, not security
        if resolved_config.seed is not None
        else None
    )

    # Create reflection-based proposer with executor (FR-006)
    # Use provided reflection_agent or create a default one
    if reflection_agent is None:
        reflection_agent = LlmAgent(
            name="reflection_agent",
            model=_resolve_model_for_agent(resolved_config.reflection_model),
            instruction=resolved_config.reflection_prompt or REFLECTION_INSTRUCTION,
        )
    adk_reflection_fn = create_adk_reflection_fn(
        reflection_agent,
        executor=executor,
        timeout_seconds=resolved_config.reflection_timeout_seconds,
    )
    proposer = AsyncReflectiveMutationProposer(
        adk_reflection_fn=adk_reflection_fn,
        max_trials=resolved_config.reflection_max_trials,
        max_trial_chars=resolved_config.reflection_max_trial_chars,
    )

    # Create adapter with executor (FR-004)
    adapter = MultiAgentAdapter(
        agents=agents,
        primary=primary,
        components=components,
        scorer=scorer,
        share_session=share_session,
        session_service=resolved_session_service,
        trajectory_config=trajectory_config,
        proposer=proposer,
        executor=executor,
        workflow=workflow,  # Preserve workflow structure (#215)
        registry=resolved_registry,
    )

    # Build seed candidate using qualified names (agent.component format per ADR-012)
    primary_agent = agents[primary]
    # Extract all configured components using their handlers
    seed_candidate_components: dict[str, str] = {}
    for agent_name, comp_list in components.items():
        agent = agents[agent_name]
        for comp_name in comp_list:
            qualified_name = f"{agent_name}.{comp_name}"
            handler = _resolve_handler(comp_name, resolved_registry)
            seed_candidate_components[qualified_name] = handler.serialize(agent)
    # Add required "instruction" key for engine compatibility
    seed_candidate_components["instruction"] = str(primary_agent.instruction)
    initial_candidate = Candidate(components=seed_candidate_components)

    # Create engine
    resolved_component_selector: ComponentSelectorProtocol | None = None
    if component_selector is not None:
        if isinstance(component_selector, str):
            resolved_component_selector = create_component_selector(component_selector)
        else:
            resolved_component_selector = component_selector

    engine = AsyncGEPAEngine(
        adapter=adapter,
        config=resolved_config,
        initial_candidate=initial_candidate,
        batch=trainset,
        component_selector=resolved_component_selector,
        rng=rng,
    )

    # Run evolution
    evolution_result = await engine.run()

    # Extract best candidate components from engine state using qualified names
    # The engine stores evolved_components from the candidate, which now uses
    # qualified names (agent.component format per ADR-012)
    evolved_components = _extract_evolved_components(
        evolution_result=evolution_result,
        seed_components=seed_candidate_components,
        agents=agents,
        components=components,
        primary=primary,
    )

    # Apply StateGuard validation to instruction components only
    if state_guard is not None:
        validated_components = {}
        for qualified_name, evolved_value in evolved_components.items():
            # Only apply StateGuard to instruction components
            if qualified_name.endswith(".instruction"):
                agent_name = qualified_name.rsplit(".", 1)[0]
                original_instruction = original_instructions.get(agent_name, "")
                validated_components[qualified_name] = _apply_state_guard_validation(
                    state_guard=state_guard,
                    original_component_text=original_instruction,
                    evolved_component_text=evolved_value,
                    agent_name=agent_name,
                )
            else:
                # Non-instruction components pass through unchanged
                validated_components[qualified_name] = evolved_value
        evolved_components = validated_components

    # Filter original_components to the qualified keyspace used by
    # evolved_components, stripping engine-internal keys (e.g. bare
    # "instruction" added for engine compatibility).
    orig = evolution_result.original_components
    if orig is not None:
        orig = {k: v for k, v in orig.items() if k in evolved_components}

    # Convert EvolutionResult to MultiAgentEvolutionResult
    return MultiAgentEvolutionResult(
        schema_version=evolution_result.schema_version,
        stop_reason=evolution_result.stop_reason,
        evolved_components=evolved_components,
        original_score=evolution_result.original_score,
        final_score=evolution_result.final_score,
        primary_agent=primary,
        iteration_history=evolution_result.iteration_history,
        total_iterations=evolution_result.total_iterations,
        original_components=orig,
        baseline_failed_evaluations=evolution_result.baseline_failed_evaluations,
        total_failed_evaluations=evolution_result.total_failed_evaluations,
        token_usage=evolution_result.token_usage,
    )

evolve_workflow async

evolve_workflow(
    workflow: SequentialAgent | LoopAgent | ParallelAgent,
    trainset: list[dict[str, Any]],
    *,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    primary: str | None = None,
    max_depth: int = 5,
    config: EvolutionConfig | None = None,
    state_guard: StateGuard | None = None,
    component_selector: ComponentSelectorProtocol
    | str
    | None = None,
    round_robin: bool = False,
    components: dict[str, list[str]] | None = None,
    session_service: BaseSessionService | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> MultiAgentEvolutionResult

Evolve LlmAgents within a workflow agent structure.

Discovers all LlmAgent instances within a workflow (SequentialAgent, LoopAgent, or ParallelAgent) and evolves them while preserving the workflow structure. Uses shared session state to maintain workflow context during evaluation.

PARAMETER DESCRIPTION
workflow

Workflow agent containing LlmAgents to evolve. Must be SequentialAgent, LoopAgent, or ParallelAgent.

TYPE: SequentialAgent | LoopAgent | ParallelAgent

trainset

Training examples for evaluation. Each example should have an "input" key and optionally an "expected" key.

TYPE: list[dict[str, Any]]

PARAMETER DESCRIPTION
critic

Optional critic agent for scoring. If None and no scorer is given, the primary agent must have an output_schema for schema-based scoring. Mutually exclusive with scorer.

TYPE: LlmAgent | None

scorer

Optional object implementing the Scorer protocol (score and async_score). Forwarded to evolve_group() and takes precedence over schema-based scoring. Mutually exclusive with critic.

TYPE: Scorer | None

primary

Name of the agent to score. Defaults to the last LlmAgent found in the workflow (for sequential workflows, this is typically the final output producer).

TYPE: str | None

max_depth

Maximum recursion depth for nested workflows (default: 5). Limits how deeply nested workflow structures are traversed.

TYPE: int

config

Evolution configuration. If None, uses EvolutionConfig defaults.

TYPE: EvolutionConfig | None

state_guard

Optional StateGuard instance for validating and repairing state injection tokens in evolved component_text.

TYPE: StateGuard | None

component_selector

Optional selector instance or selector name for choosing which components to update.

TYPE: ComponentSelectorProtocol | str | None

round_robin

If False (default), only the first discovered agent's instruction is evolved across all iterations. If True, all agents' instructions are evolved in round-robin fashion (the engine cycles through agents each iteration). Ignored when components is provided.

TYPE: bool

components

Optional per-agent component configuration mapping agent names to lists of component names to evolve. When provided, takes precedence over round_robin. Use empty list to exclude an agent.

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

session_service

Optional ADK session service for state management. If None (default), creates an InMemorySessionService internally. Pass a custom service (e.g., SqliteSessionService, DatabaseSessionService; on ADK 2.x SQL-backed services need the gepa-adk[db] extra, which installs google-adk[db]) to persist sessions alongside other agent executions in a shared database. SqliteSessionService sets no busy timeout or WAL of its own, so on a loaded disk a write can wait past the five-second default of Python's sqlite3 connection (which aiosqlite uses) and raise database is locked. The internal executor retries that error under its default RetryPolicy (three attempts, 0.5 s backoff, doubling) before the row counts as a failed evaluation.

TYPE: BaseSessionService | None

app

Optional ADK App instance supplying the session app name when runner is absent. Services come from runner when provided; otherwise evolution uses session_service or InMemorySessionService().

TYPE: App | None

runner

Optional ADK Runner instance. When provided, evolution extracts and uses the runner's session_service for all agent executions (evolved agents, critic, and reflection agent). Takes precedence over both app and session_service parameters. This enables seamless integration with existing ADK infrastructure.

TYPE: Runner | None

registry

Optional ComponentHandlerRegistry that resolves every component name; defaults to the default registry that register_handler() and register_mapping_components() fill.

TYPE: ComponentHandlerRegistry | None

RETURNS DESCRIPTION
MultiAgentEvolutionResult

MultiAgentEvolutionResult containing evolved_components dict mapping

MultiAgentEvolutionResult

qualified agent.component names (e.g., "writer.instruction") to their

MultiAgentEvolutionResult

optimized component_text, along with score

MultiAgentEvolutionResult

metrics and iteration history (each record names its

MultiAgentEvolutionResult

candidate_id and parent_ids).

RAISES DESCRIPTION
ConfigurationError

If pre-flight validation fails: non-LlmAgent critic, both critic and scorer given, a scorer that does not implement Scorer, empty trainset, duplicate or empty component names, EvolutionConfig consistency errors, or a component name the resolved registry holds no handler for.

WorkflowEvolutionError

If workflow contains no LlmAgents.

MultiAgentValidationError

If primary agent not found or no scorer available.

EvolutionError

If evolution fails during execution. A reflection call that keeps raising a retryable provider error is skipped with skip_reason="reflection_error"; a non-retryable one raises ReflectionError. A reflection cut off at the output-token limit, or one that opens a reasoning tag it never closes, is skipped with skip_reason="incomplete_proposal". An error raised after the baseline was scored carries the engine's EvolutionResult for the recorded iterations in partial_result.

Examples:

Default behavior (evolve first agent only):

from google.adk.agents import LlmAgent, SequentialAgent
from gepa_adk import evolve_workflow

generator = LlmAgent(name="generator", instruction="Generate code")
refiner = LlmAgent(name="refiner", instruction="Refine code")
writer = LlmAgent(name="writer", instruction="Write docs")
pipeline = SequentialAgent(
    name="Pipeline", sub_agents=[generator, refiner, writer]
)

# Only generator.instruction is evolved across all iterations
result = await evolve_workflow(workflow=pipeline, trainset=trainset)

Round-robin evolution (evolve all agents):

# All agents are evolved in round-robin: generator -> refiner -> writer -> ...
result = await evolve_workflow(
    workflow=pipeline,
    trainset=trainset,
    round_robin=True,
)

Explicit components override (takes precedence over round_robin):

# Only generator and writer are evolved; refiner is excluded
result = await evolve_workflow(
    workflow=pipeline,
    trainset=trainset,
    components={
        "generator": ["instruction"],
        "writer": ["instruction"],
        "refiner": [],  # Excluded
    },
)

Using custom session service for persistence:

# Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
from google.adk.sessions import SqliteSessionService

# Persist workflow evolution sessions to SQLite
session_service = SqliteSessionService(db_path="workflow_sessions.db")

result = await evolve_workflow(
    workflow=pipeline,
    trainset=trainset,
    session_service=session_service,
)

Using App/Runner for existing infrastructure integration:

from google.adk.runners import Runner

# Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
from google.adk.sessions import DatabaseSessionService

# Configure Runner with your production session service
runner = Runner(
    app_name="my_workflow_app",
    agent=pipeline,  # The workflow agent
    session_service=DatabaseSessionService(connection_string="..."),
)

# Evolution uses Runner's session_service for all operations
result = await evolve_workflow(
    workflow=pipeline,
    trainset=trainset,
    runner=runner,  # Services extracted from runner
)
Notes

Pre-flight validation runs synchronously before any LLM calls. Supports workflow agents (SequentialAgent, LoopAgent, ParallelAgent) with recursive traversal and depth limiting via max_depth parameter. Handles nested structures. LoopAgent and ParallelAgent configurations (max_iterations, etc.) are preserved during evolution. Always uses share_session=True to maintain workflow context (FR-010).

For reproducible evolution, pass a seeded config: config=EvolutionConfig(seed=42).

Source code in src/gepa_adk/api.py
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
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
async def evolve_workflow(  # noqa: PLR0913  # public API; each option is a keyword argument
    workflow: SequentialAgent | LoopAgent | ParallelAgent,
    trainset: list[dict[str, Any]],
    *,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    primary: str | None = None,
    max_depth: int = 5,
    config: EvolutionConfig | None = None,
    state_guard: StateGuard | None = None,
    component_selector: ComponentSelectorProtocol | str | None = None,
    round_robin: bool = False,
    components: dict[str, list[str]] | None = None,
    session_service: BaseSessionService | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> MultiAgentEvolutionResult:
    """Evolve LlmAgents within a workflow agent structure.

    Discovers all LlmAgent instances within a workflow (SequentialAgent,
    LoopAgent, or ParallelAgent) and evolves them while preserving the
    workflow structure. Uses shared session state to maintain workflow
    context during evaluation.

    Args:
        workflow (SequentialAgent | LoopAgent | ParallelAgent): Workflow agent
            containing LlmAgents to evolve. Must be SequentialAgent, LoopAgent, or
            ParallelAgent.
        trainset (list[dict[str, Any]]): Training examples for evaluation. Each example should have
            an "input" key and optionally an "expected" key.

    Keyword Args:
        critic (LlmAgent | None): Optional critic agent for scoring. If None and no scorer is
            given, the primary agent must have an output_schema for
            schema-based scoring. Mutually exclusive with scorer.
        scorer (Scorer | None): Optional object implementing the Scorer protocol
            (``score`` and ``async_score``). Forwarded to evolve_group() and
            takes precedence over schema-based scoring. Mutually exclusive
            with critic.
        primary (str | None): Name of the agent to score. Defaults to the last LlmAgent
            found in the workflow (for sequential workflows, this is typically
            the final output producer).
        max_depth (int): Maximum recursion depth for nested workflows (default: 5).
            Limits how deeply nested workflow structures are traversed.
        config (EvolutionConfig | None): Evolution configuration. If None, uses
            EvolutionConfig defaults.
        state_guard (StateGuard | None): Optional StateGuard instance for validating and
            repairing state injection tokens in evolved component_text.
        component_selector (ComponentSelectorProtocol | str | None): Optional selector
            instance or selector name for choosing which components to update.
        round_robin (bool): If False (default), only the first discovered agent's
            instruction is evolved across all iterations. If True, all agents'
            instructions are evolved in round-robin fashion (the engine cycles
            through agents each iteration). Ignored when components is provided.
        components (dict[str, list[str]] | None): Optional per-agent component
            configuration mapping agent names to lists of component names to evolve.
            When provided, takes precedence over round_robin. Use empty list to exclude
            an agent.
        session_service (BaseSessionService | None): Optional ADK session service for
            state management. If None (default), creates an InMemorySessionService
            internally. Pass a custom service (e.g., SqliteSessionService,
            DatabaseSessionService; on ADK 2.x SQL-backed services need the
            ``gepa-adk[db]`` extra, which installs ``google-adk[db]``) to persist
            sessions alongside other agent executions in a shared database.
            ``SqliteSessionService`` sets no busy timeout or WAL of its own, so on a
            loaded disk a write can wait past the five-second default of Python's
            sqlite3 connection (which aiosqlite uses) and raise ``database is
            locked``. The internal executor retries that error under its default
            ``RetryPolicy`` (three attempts, 0.5 s backoff, doubling) before the row
            counts as a failed evaluation.
        app (App | None): Optional ADK App instance supplying the session app name
            when runner is absent. Services come from runner when provided;
            otherwise evolution uses session_service or InMemorySessionService().
        runner (Runner | None): Optional ADK Runner instance. When provided, evolution extracts
            and uses the runner's session_service for all agent executions
            (evolved agents, critic, and reflection agent). Takes precedence
            over both app and session_service parameters. This enables seamless
            integration with existing ADK infrastructure.
        registry (ComponentHandlerRegistry | None): Optional ComponentHandlerRegistry
            that resolves every component name; defaults to the default registry that
            ``register_handler()`` and ``register_mapping_components()`` fill.

    Returns:
        MultiAgentEvolutionResult containing evolved_components dict mapping
        qualified agent.component names (e.g., "writer.instruction") to their
        optimized component_text, along with score
        metrics and iteration history (each record names its
        ``candidate_id`` and ``parent_ids``).

    Raises:
        ConfigurationError: If pre-flight validation fails: non-LlmAgent
            critic, both critic and scorer given, a scorer that does not
            implement Scorer, empty trainset, duplicate or empty component
            names, EvolutionConfig consistency errors, or a component name
            the resolved registry holds no handler for.
        WorkflowEvolutionError: If workflow contains no LlmAgents.
        MultiAgentValidationError: If primary agent not found or no scorer
            available.
        EvolutionError: If evolution fails during execution. A reflection
            call that keeps raising a retryable provider error is skipped
            with ``skip_reason="reflection_error"``; a non-retryable one
            raises ``ReflectionError``. A reflection cut off at the
            output-token limit, or one that opens a reasoning tag it never
            closes, is skipped with ``skip_reason="incomplete_proposal"``.
            An error raised after the baseline
            was scored carries the engine's ``EvolutionResult`` for the
            recorded iterations in ``partial_result``.

    Examples:
        Default behavior (evolve first agent only):

        ```python
        from google.adk.agents import LlmAgent, SequentialAgent
        from gepa_adk import evolve_workflow

        generator = LlmAgent(name="generator", instruction="Generate code")
        refiner = LlmAgent(name="refiner", instruction="Refine code")
        writer = LlmAgent(name="writer", instruction="Write docs")
        pipeline = SequentialAgent(
            name="Pipeline", sub_agents=[generator, refiner, writer]
        )

        # Only generator.instruction is evolved across all iterations
        result = await evolve_workflow(workflow=pipeline, trainset=trainset)
        ```

        Round-robin evolution (evolve all agents):

        ```python
        # All agents are evolved in round-robin: generator -> refiner -> writer -> ...
        result = await evolve_workflow(
            workflow=pipeline,
            trainset=trainset,
            round_robin=True,
        )
        ```

        Explicit components override (takes precedence over round_robin):

        ```python
        # Only generator and writer are evolved; refiner is excluded
        result = await evolve_workflow(
            workflow=pipeline,
            trainset=trainset,
            components={
                "generator": ["instruction"],
                "writer": ["instruction"],
                "refiner": [],  # Excluded
            },
        )
        ```

        Using custom session service for persistence:

        ```python
        # Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
        from google.adk.sessions import SqliteSessionService

        # Persist workflow evolution sessions to SQLite
        session_service = SqliteSessionService(db_path="workflow_sessions.db")

        result = await evolve_workflow(
            workflow=pipeline,
            trainset=trainset,
            session_service=session_service,
        )
        ```

        Using App/Runner for existing infrastructure integration:

        ```python
        from google.adk.runners import Runner

        # Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
        from google.adk.sessions import DatabaseSessionService

        # Configure Runner with your production session service
        runner = Runner(
            app_name="my_workflow_app",
            agent=pipeline,  # The workflow agent
            session_service=DatabaseSessionService(connection_string="..."),
        )

        # Evolution uses Runner's session_service for all operations
        result = await evolve_workflow(
            workflow=pipeline,
            trainset=trainset,
            runner=runner,  # Services extracted from runner
        )
        ```

    Notes:
        Pre-flight validation runs synchronously before any LLM calls.
        Supports workflow agents (SequentialAgent, LoopAgent, ParallelAgent)
        with recursive traversal and depth limiting via max_depth parameter.
        Handles nested structures. LoopAgent and ParallelAgent configurations
        (max_iterations, etc.) are preserved during evolution. Always uses
        share_session=True to maintain workflow context (FR-010).

        For reproducible evolution, pass a seeded config:
        ``config=EvolutionConfig(seed=42)``.
    """
    # Pre-flight validation (Story 2.5)
    _pre_flight_validate_workflow(trainset, critic, components, scorer=scorer)

    logger.info(
        "Starting workflow evolution",
        workflow_name=workflow.name,
        workflow_type=type(workflow).__name__,
    )

    # Find all LlmAgents in the workflow recursively up to max_depth (US3)
    llm_agents = find_llm_agents(workflow, max_depth=max_depth)

    # Validate that at least one LlmAgent was found
    if not llm_agents:
        error_msg = (
            f"No LlmAgents found in workflow '{workflow.name}'. "
            "Workflow must contain at least one LlmAgent to evolve."
        )
        logger.error(
            "Workflow evolution failed", workflow_name=workflow.name, error=error_msg
        )
        raise WorkflowEvolutionError(
            error_msg,
            workflow_name=workflow.name,
        )

    logger.info(
        "Found LlmAgents in workflow",
        workflow_name=workflow.name,
        agent_count=len(llm_agents),
        agent_names=[agent.name for agent in llm_agents],
    )

    # Determine primary agent (default to last agent for sequential workflows)
    if primary is None:
        primary = llm_agents[-1].name
        logger.debug(
            "Using default primary agent",
            workflow_name=workflow.name,
            primary=primary,
        )

    # Convert list to dict for evolve_group
    agents_dict = {agent.name: agent for agent in llm_agents}

    # Build components dict based on round_robin flag
    # Explicit components parameter takes precedence over round_robin
    resolved_components: dict[str, list[str]] | None = None
    if components is not None:
        # Explicit components provided - use as-is
        resolved_components = components
        logger.debug(
            "Using explicit components",
            workflow_name=workflow.name,
            components=list(components.keys()),
        )
    elif round_robin:
        # round_robin=True: evolve all agents
        resolved_components = {agent.name: ["instruction"] for agent in llm_agents}
        logger.debug(
            "Using round_robin mode - evolving all agents",
            workflow_name=workflow.name,
            agents=[agent.name for agent in llm_agents],
        )
    else:
        # Default: evolve only the first agent
        first_agent = llm_agents[0]
        resolved_components = {first_agent.name: ["instruction"]}
        # Add empty lists for other agents (excluded from evolution)
        for agent in llm_agents[1:]:
            resolved_components[agent.name] = []
        logger.debug(
            "Using default mode - evolving first agent only",
            workflow_name=workflow.name,
            first_agent=first_agent.name,
        )

    # Delegate to evolve_group with share_session=True (FR-010)
    logger.debug(
        "Delegating to evolve_group",
        workflow_name=workflow.name,
        agent_count=len(llm_agents),
        primary=primary,
        share_session=True,
        round_robin=round_robin,
    )

    return await evolve_group(
        agents=agents_dict,
        primary=primary,
        trainset=trainset,
        components=resolved_components,
        critic=critic,
        scorer=scorer,
        share_session=True,  # FR-010: Always use shared session for workflow context
        config=config,
        state_guard=state_guard,
        component_selector=component_selector,
        workflow=workflow,  # Preserve workflow structure (#215)
        session_service=session_service,  # Pass through for persistence (#226)
        app=app,  # Pass through for App/Runner pattern (#227)
        runner=runner,  # Pass through for App/Runner pattern (#227)
        registry=registry,  # Resolve component handlers per round (#451)
    )

evolve async

evolve(
    agent: LlmAgent,
    trainset: list[dict[str, Any]],
    *,
    valset: list[dict[str, Any]] | None = None,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    reflection_agent: LlmAgent | None = None,
    config: EvolutionConfig | None = None,
    trajectory_config: TrajectoryConfig | None = None,
    state_guard: StateGuard | None = None,
    candidate_selector: CandidateSelectorProtocol
    | str
    | None = None,
    component_selector: ComponentSelectorProtocol
    | str
    | None = None,
    executor: AgentExecutorProtocol | None = None,
    components: list[str] | None = None,
    schema_constraints: SchemaConstraints | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> EvolutionResult

Evolve an ADK agent's instruction.

Optimizes the instruction for a single ADK agent using evolutionary optimization. The agent's instruction is iteratively improved based on performance on the training set.

PARAMETER DESCRIPTION
agent

The ADK LlmAgent to evolve. When critic is not provided, agent.output_schema must be a pydantic BaseModel subclass (other schema types accepted by ADK 1.34+ are not supported here).

TYPE: LlmAgent

trainset

Training examples [{"input": "...", "expected": "..."}].

TYPE: list[dict[str, Any]]

PARAMETER DESCRIPTION
valset

Optional validation examples used for scoring and acceptance. Defaults to the trainset when omitted.

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

critic

Optional ADK agent for scoring (uses schema scoring if None and no scorer is given). Mutually exclusive with scorer.

TYPE: LlmAgent | None

scorer

Optional object implementing the Scorer protocol (score and async_score). When given, it is used as-is and takes precedence over the agent's output_schema. Mutually exclusive with critic.

TYPE: Scorer | None

reflection_agent

Optional ADK agent for proposals. If None, creates a default reflection agent using config.reflection_model (a model string, or a BaseLlm instance passed through unchanged).

TYPE: LlmAgent | None

config

Evolution configuration (uses defaults if None). Its reflection_max_trials and reflection_max_trial_chars bound the trials each reflection call sees, and reflection_timeout_seconds bounds each reflection call's run time; a timed-out reflection skips the iteration. max_concurrent_evals is passed to ADKAdapter to limit concurrent evaluation rows within each batch.

TYPE: EvolutionConfig | None

trajectory_config

Trajectory capture settings (uses defaults if None).

TYPE: TrajectoryConfig | None

state_guard

Optional state token preservation settings.

TYPE: StateGuard | None

candidate_selector

Optional selector instance or selector name.

TYPE: CandidateSelectorProtocol | str | None

component_selector

Optional selector instance or selector name for choosing which components to update.

TYPE: ComponentSelectorProtocol | str | None

executor

Optional AgentExecutorProtocol implementation for unified agent execution. When provided, both the ADKAdapter and CriticScorer use this executor for consistent session management and execution. If None, creates an AgentExecutor automatically. An AgentExecutor retries a transient database is locked session error under its RetryPolicy (three attempts by default). SqliteSessionService sets no busy timeout or WAL of its own, so on a loaded disk a write can wait past the five-second default of Python's sqlite3 connection (which aiosqlite uses) and raise that error; to change the policy, pass AgentExecutor(session_service=..., retry_policy=RetryPolicy(...)).

TYPE: AgentExecutorProtocol | None

components

List of component names to include in evolution. Supported: - "instruction": The agent's instruction text (default if None). - "output_schema": The agent's Pydantic output_schema (serialized). - Any other name with a handler in the default component handler registry, such as "generate_content_config" or the keys added by register_mapping_components(); the candidate is seeded with that handler's serialize(agent). When None, defaults to ["instruction"]. Use ["output_schema"] with a schema reflection agent to evolve the output schema.

TYPE: list[str] | None

schema_constraints

Optional SchemaConstraints for output_schema evolution. When provided, proposed schema mutations are validated against these constraints. Mutations that violate constraints (e.g., remove required fields) are rejected and the original schema is preserved.

TYPE: SchemaConstraints | None

app

Optional ADK App instance. When provided, evolution uses the app's configuration. Note that App does not hold services directly; pass a Runner for service extraction, or combine with session_service param. See the App/Runner integration guide for details.

TYPE: App | None

runner

Optional ADK Runner instance. When provided, evolution extracts and uses the runner's session_service for all agent executions (evolved agents, critic, and reflection agent). Takes precedence over both app and executor parameters. This enables seamless integration with existing ADK infrastructure.

TYPE: Runner | None

registry

Optional ComponentHandlerRegistry that resolves every component name; defaults to the default registry that register_handler() and register_mapping_components() fill.

TYPE: ComponentHandlerRegistry | None

RETURNS DESCRIPTION
EvolutionResult

The engine's EvolutionResult with evolved_components (after state

EvolutionResult

guard validation), valset_score, trainset_score and

EvolutionResult

original_components (each component's seeded text) replaced. Every

EvolutionResult

other field, including the failed-evaluation counters, stop_reason,

EvolutionResult

token_usage and objective_scores, comes from the engine unchanged.

RAISES DESCRIPTION
ConfigurationError

If invalid parameters provided, including pre-flight validation failures: non-LlmAgent agent or critic, both critic and scorer given, a scorer that does not implement Scorer, empty trainset, duplicate or empty component names, missing critic, scorer and output_schema, EvolutionConfig consistency errors, or a component name the resolved registry holds no handler for.

EvolutionError

If evolution fails during execution. A reflection call that keeps raising a retryable provider error is skipped with skip_reason="reflection_error"; a non-retryable one raises ReflectionError. A reflection cut off at the output-token limit, or one that opens a reasoning tag it never closes, is skipped with skip_reason="incomplete_proposal". An error raised after the baseline was scored carries the engine's EvolutionResult for the recorded iterations in partial_result.

Notes

Pre-flight validation runs synchronously before any LLM calls. When SchemaBasedScorer is selected and trainset rows carry expected, a scorer.schema_based_over_labelled_trainset warning is logged; the selected scorer does not change. Single-agent evolution with trainset reflection and valset scoring.

For reproducible evolution, pass a seeded config: config=EvolutionConfig(seed=42).

Examples:

Basic usage with output_schema:

from pydantic import BaseModel, Field
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk import evolve


class OutputSchema(BaseModel):
    answer: str
    score: float = Field(ge=0.0, le=1.0)


agent = LlmAgent(
    name="assistant",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="You are a helpful assistant.",
    output_schema=OutputSchema,
)

trainset = [
    {"input": "What is 2+2?", "expected": "4"},
    {"input": "What is the capital of France?", "expected": "Paris"},
]

result = await evolve(agent, trainset)
print(f"Evolved: {result.evolved_components['instruction']}")

With critic agent:

from pydantic import BaseModel, Field
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk import evolve


class CriticOutput(BaseModel):
    score: float = Field(ge=0.0, le=1.0)


critic = LlmAgent(
    name="critic",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="Score the response quality.",
    output_schema=CriticOutput,
)

result = await evolve(agent, trainset, critic=critic)

Evolving output_schema with schema reflection:

from gepa_adk.adapters.agents.reflection_agents import (
    create_schema_reflection_agent,
)

# Create schema reflection agent with validation tool
schema_reflector = create_schema_reflection_agent("gemini-3.8-flash")

# Evolve output_schema component
result = await evolve(
    agent,
    trainset,
    critic=critic,
    reflection_agent=schema_reflector,
    components=["output_schema"],  # Evolve schema, not instruction
)
print(f"Evolved schema: {result.evolved_components['output_schema']}")

Using App/Runner for existing infrastructure integration:

from google.adk.apps.app import App
from google.adk.runners import Runner

# Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
from google.adk.sessions import DatabaseSessionService

# Configure Runner with your production session service
session_service = DatabaseSessionService(connection_string="...")
runner = Runner(
    app_name="my_app",
    agent=agent,
    session_service=session_service,
)

# Evolution uses your Runner's session_service for all operations
result = await evolve(
    agent,
    trainset,
    runner=runner,  # Services extracted from runner
)
Source code in src/gepa_adk/api.py
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
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
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
async def evolve(  # noqa: C901, PLR0912, PLR0913, PLR0915  # decomposition tracked in #506; public API keyword arguments
    agent: LlmAgent,
    trainset: list[dict[str, Any]],
    *,
    valset: list[dict[str, Any]] | None = None,
    critic: LlmAgent | None = None,
    scorer: Scorer | None = None,
    reflection_agent: LlmAgent | None = None,
    config: EvolutionConfig | None = None,
    trajectory_config: TrajectoryConfig | None = None,
    state_guard: StateGuard | None = None,
    candidate_selector: CandidateSelectorProtocol | str | None = None,
    component_selector: ComponentSelectorProtocol | str | None = None,
    executor: AgentExecutorProtocol | None = None,
    components: list[str] | None = None,
    schema_constraints: SchemaConstraints | None = None,
    app: App | None = None,
    runner: Runner | None = None,
    registry: ComponentHandlerRegistry | None = None,
) -> EvolutionResult:
    """Evolve an ADK agent's instruction.

    Optimizes the instruction for a single ADK agent using evolutionary
    optimization. The agent's instruction is iteratively improved based on
    performance on the training set.

    Args:
        agent (LlmAgent): The ADK LlmAgent to evolve. When ``critic`` is not provided,
            ``agent.output_schema`` must be a pydantic ``BaseModel`` subclass
            (other schema types accepted by ADK 1.34+ are not supported here).
        trainset (list[dict[str, Any]]): Training examples [{"input": "...", "expected": "..."}].

    Keyword Args:
        valset (list[dict[str, Any]] | None): Optional validation examples used for
            scoring and acceptance. Defaults to the trainset when omitted.
        critic (LlmAgent | None): Optional ADK agent for scoring (uses schema scoring if None
            and no scorer is given). Mutually exclusive with scorer.
        scorer (Scorer | None): Optional object implementing the Scorer protocol
            (``score`` and ``async_score``). When given, it is used as-is
            and takes precedence over the agent's output_schema. Mutually
            exclusive with critic.
        reflection_agent (LlmAgent | None): Optional ADK agent for proposals. If None, creates a
            default reflection agent using config.reflection_model (a model
            string, or a ``BaseLlm`` instance passed through unchanged).
        config (EvolutionConfig | None): Evolution configuration (uses defaults if None). Its
            ``reflection_max_trials`` and ``reflection_max_trial_chars``
            bound the trials each reflection call sees, and
            ``reflection_timeout_seconds`` bounds each reflection call's run
            time; a timed-out reflection skips the iteration.
            ``max_concurrent_evals`` is passed to ADKAdapter to limit
            concurrent evaluation rows within each batch.
        trajectory_config (TrajectoryConfig | None): Trajectory capture settings (uses
            defaults if None).
        state_guard (StateGuard | None): Optional state token preservation settings.
        candidate_selector (CandidateSelectorProtocol | str | None): Optional selector
            instance or selector name.
        component_selector (ComponentSelectorProtocol | str | None): Optional selector
            instance or selector name for choosing which components to update.
        executor (AgentExecutorProtocol | None): Optional AgentExecutorProtocol
            implementation for unified agent execution. When provided, both the
            ADKAdapter and CriticScorer use this executor for consistent session
            management and execution. If None, creates an AgentExecutor automatically.
            An ``AgentExecutor`` retries a transient ``database is locked`` session
            error under its ``RetryPolicy`` (three attempts by default).
            ``SqliteSessionService`` sets no busy timeout or WAL of its own, so on a
            loaded disk a write can wait past the five-second default of Python's
            sqlite3 connection (which aiosqlite uses) and raise that error; to change
            the policy, pass ``AgentExecutor(session_service=...,
            retry_policy=RetryPolicy(...))``.
        components (list[str] | None): List of component names to include in evolution. Supported:
            - "instruction": The agent's instruction text (default if None).
            - "output_schema": The agent's Pydantic output_schema (serialized).
            - Any other name with a handler in the default component handler
              registry, such as "generate_content_config" or the keys added
              by ``register_mapping_components()``; the candidate is seeded
              with that handler's ``serialize(agent)``.
            When None, defaults to ["instruction"]. Use ["output_schema"] with
            a schema reflection agent to evolve the output schema.
        schema_constraints (SchemaConstraints | None): Optional SchemaConstraints for
            output_schema evolution. When provided, proposed schema mutations are
            validated against these constraints. Mutations that violate constraints
            (e.g., remove required fields) are rejected and the original schema is
            preserved.
        app (App | None): Optional ADK App instance. When provided, evolution uses the app's
            configuration. Note that App does not hold services directly; pass
            a Runner for service extraction, or combine with session_service param.
            See the App/Runner integration guide for details.
        runner (Runner | None): Optional ADK Runner instance. When provided, evolution extracts
            and uses the runner's session_service for all agent executions
            (evolved agents, critic, and reflection agent). Takes precedence
            over both app and executor parameters. This enables seamless
            integration with existing ADK infrastructure.
        registry (ComponentHandlerRegistry | None): Optional ComponentHandlerRegistry
            that resolves every component name; defaults to the default registry that
            ``register_handler()`` and ``register_mapping_components()`` fill.

    Returns:
        The engine's EvolutionResult with evolved_components (after state
        guard validation), valset_score, trainset_score and
        original_components (each component's seeded text) replaced. Every
        other field, including the failed-evaluation counters, stop_reason,
        token_usage and objective_scores, comes from the engine unchanged.

    Raises:
        ConfigurationError: If invalid parameters provided, including
            pre-flight validation failures: non-LlmAgent agent or critic,
            both critic and scorer given, a scorer that does not implement
            Scorer, empty trainset, duplicate or empty component names,
            missing critic, scorer and output_schema, EvolutionConfig
            consistency errors, or a component name the resolved registry
            holds no handler for.
        EvolutionError: If evolution fails during execution. A reflection
            call that keeps raising a retryable provider error is skipped
            with ``skip_reason="reflection_error"``; a non-retryable one
            raises ``ReflectionError``. A reflection cut off at the
            output-token limit, or one that opens a reasoning tag it never
            closes, is skipped with ``skip_reason="incomplete_proposal"``.
            An error raised after the baseline
            was scored carries the engine's ``EvolutionResult`` for the
            recorded iterations in ``partial_result``.

    Notes:
        Pre-flight validation runs synchronously before any LLM calls.
        When ``SchemaBasedScorer`` is selected and trainset rows carry
        ``expected``, a ``scorer.schema_based_over_labelled_trainset``
        warning is logged; the selected scorer does not change.
        Single-agent evolution with trainset reflection and valset scoring.

        For reproducible evolution, pass a seeded config:
        ``config=EvolutionConfig(seed=42)``.

    Examples:
        Basic usage with output_schema:

        ```python
        from pydantic import BaseModel, Field
        from google.adk.agents import LlmAgent
        from google.adk.models.lite_llm import LiteLlm
        from gepa_adk import evolve


        class OutputSchema(BaseModel):
            answer: str
            score: float = Field(ge=0.0, le=1.0)


        agent = LlmAgent(
            name="assistant",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="You are a helpful assistant.",
            output_schema=OutputSchema,
        )

        trainset = [
            {"input": "What is 2+2?", "expected": "4"},
            {"input": "What is the capital of France?", "expected": "Paris"},
        ]

        result = await evolve(agent, trainset)
        print(f"Evolved: {result.evolved_components['instruction']}")
        ```

        With critic agent:

        ```python
        from pydantic import BaseModel, Field
        from google.adk.agents import LlmAgent
        from google.adk.models.lite_llm import LiteLlm
        from gepa_adk import evolve


        class CriticOutput(BaseModel):
            score: float = Field(ge=0.0, le=1.0)


        critic = LlmAgent(
            name="critic",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="Score the response quality.",
            output_schema=CriticOutput,
        )

        result = await evolve(agent, trainset, critic=critic)
        ```

        Evolving output_schema with schema reflection:

        ```python
        from gepa_adk.adapters.agents.reflection_agents import (
            create_schema_reflection_agent,
        )

        # Create schema reflection agent with validation tool
        schema_reflector = create_schema_reflection_agent("gemini-3.8-flash")

        # Evolve output_schema component
        result = await evolve(
            agent,
            trainset,
            critic=critic,
            reflection_agent=schema_reflector,
            components=["output_schema"],  # Evolve schema, not instruction
        )
        print(f"Evolved schema: {result.evolved_components['output_schema']}")
        ```

        Using App/Runner for existing infrastructure integration:

        ```python
        from google.adk.apps.app import App
        from google.adk.runners import Runner

        # Requires the gepa-adk[db] extra (google-adk[db]) on ADK 2.x
        from google.adk.sessions import DatabaseSessionService

        # Configure Runner with your production session service
        session_service = DatabaseSessionService(connection_string="...")
        runner = Runner(
            app_name="my_app",
            agent=agent,
            session_service=session_service,
        )

        # Evolution uses your Runner's session_service for all operations
        result = await evolve(
            agent,
            trainset,
            runner=runner,  # Services extracted from runner
        )
        ```
    """
    # Pre-flight validation (Story 2.5)
    _pre_flight_validate_evolve(agent, trainset, critic, components, scorer=scorer)
    required_keys = (
        set(trainset[0].keys()) if trainset and len(trainset) > 0 else {"input"}
    )

    resolved_valset = valset if valset is not None else trainset
    if valset is not None:
        _validate_dataset(
            valset,
            "valset",
            allow_empty=False,
            required_keys=required_keys,
        )

    # Log reflection_agent configuration if provided
    if reflection_agent is not None:
        logger.debug(
            "evolve.reflection_agent.configured",
            agent_name=agent.name,
            reflection_agent_name=reflection_agent.name,
            message="Using ADK reflection agent for instruction improvement",
        )

    # Capture original instruction for StateGuard validation
    original_instruction = str(agent.instruction)

    # Resolve every component name through one registry for this round (#451)
    resolved_registry = registry if registry is not None else component_handlers
    for comp_name in components or [DEFAULT_COMPONENT_NAME]:
        _resolve_handler(comp_name, resolved_registry)

    candidate_selector_label = (
        candidate_selector
        if isinstance(candidate_selector, str)
        else type(candidate_selector).__name__
        if candidate_selector is not None
        else None
    )

    component_selector_label = (
        component_selector
        if isinstance(component_selector, str)
        else type(component_selector).__name__
        if component_selector is not None
        else None
    )

    # Log evolution start
    logger.info(
        "evolve.start",
        agent_name=agent.name,
        trainset_size=len(trainset),
        valset_size=len(resolved_valset),
        valset_defaulted=valset is None,
        has_critic=critic is not None,
        has_reflection_agent=reflection_agent is not None,
        has_state_guard=state_guard is not None,
        candidate_selector=candidate_selector_label,
        component_selector=component_selector_label,
    )

    # Resolve services from runner/app with precedence warnings (#227)
    # Log precedence warnings if multiple config sources provided (T009)
    if runner is not None and app is not None:
        logger.warning(
            "evolve.precedence.runner_over_app",
            message="Both runner and app provided; using runner (runner takes precedence)",
            runner_app_name=runner.app_name,
            app_name=app.name,
        )
    if runner is not None and executor is not None:
        logger.warning(
            "evolve.precedence.runner_over_executor",
            message="Both runner and executor provided; using runner's session_service",
            runner_app_name=runner.app_name,
        )

    # Extract services using precedence rules (T006)
    resolved_session_service, _artifact_service = _resolve_evolution_services(
        runner=runner,
        app=app,
        session_service=None,  # evolve() doesn't have direct session_service param
    )

    # Resolve app_name for session isolation (#239)
    resolved_app_name = _resolve_app_name(runner=runner, app=app)

    # Create executor with resolved session_service (T007)
    # Runner takes precedence over user-provided executor
    if runner is not None:
        resolved_executor = AgentExecutor(
            session_service=resolved_session_service,
            app_name=resolved_app_name,
        )
    else:
        resolved_executor = executor or AgentExecutor(
            session_service=resolved_session_service,
            app_name=resolved_app_name,
        )

    # Build scorer: explicit scorer > critic > output_schema
    resolved_scorer: Scorer
    if scorer is not None:
        resolved_scorer = scorer
    elif critic:
        resolved_scorer = CriticScorer(critic_agent=critic, executor=resolved_executor)
    elif hasattr(agent, "output_schema") and agent.output_schema is not None:
        # Use schema-based scorer when agent has output_schema
        _warn_if_self_grading_labelled_rows(trainset)
        resolved_scorer = SchemaBasedScorer(
            output_schema=cast(type[BaseModel], agent.output_schema)
        )
    else:
        raise ConfigurationError(
            "Either critic must be provided or agent must have output_schema",
            field="critic",
            value=None,
            constraint="must provide critic or agent.output_schema",
        )

    # Resolve config
    resolved_config = config or EvolutionConfig()
    rng = (
        random.Random(resolved_config.seed)  # noqa: S311  # evolution sampling, not security
        if resolved_config.seed is not None
        else None
    )

    # Create reflection agent if not provided
    resolved_reflection_agent = reflection_agent
    if resolved_reflection_agent is None:
        # Create default reflection agent with config settings
        resolved_reflection_agent = LlmAgent(
            name="reflection_agent",
            model=_resolve_model_for_agent(resolved_config.reflection_model),
            instruction=resolved_config.reflection_prompt or REFLECTION_INSTRUCTION,
        )
        logger.debug(
            "evolve.reflection_agent.default",
            reflection_model=_describe_reflection_model(
                resolved_config.reflection_model
            ),
        )

    # Build proposer chain in the composition root (api.py)
    adk_reflection_fn = create_adk_reflection_fn(
        resolved_reflection_agent,
        executor=resolved_executor,
        timeout_seconds=resolved_config.reflection_timeout_seconds,
    )
    proposer = AsyncReflectiveMutationProposer(
        adk_reflection_fn=adk_reflection_fn,
        max_trials=resolved_config.reflection_max_trials,
        max_trial_chars=resolved_config.reflection_max_trial_chars,
    )

    # Create adapter with resolved session_service (T008)
    adapter = ADKAdapter(
        agent=agent,
        scorer=resolved_scorer,
        trajectory_config=trajectory_config,
        proposer=proposer,
        executor=resolved_executor,
        schema_constraints=schema_constraints,
        max_concurrent_evals=resolved_config.max_concurrent_evals,
        session_service=resolved_session_service,
        registry=resolved_registry,
    )

    # Build initial candidate components based on requested components
    resolved_components = components if components else [DEFAULT_COMPONENT_NAME]
    initial_components: dict[str, str] = {}

    for comp_name in resolved_components:
        if comp_name == DEFAULT_COMPONENT_NAME:
            initial_components[comp_name] = original_instruction
        elif comp_name == COMPONENT_OUTPUT_SCHEMA:
            if not hasattr(agent, "output_schema") or agent.output_schema is None:
                raise ConfigurationError(
                    f"Cannot evolve '{COMPONENT_OUTPUT_SCHEMA}': agent has no output_schema",
                    field="components",
                    value=comp_name,
                    constraint="agent must have output_schema to evolve it",
                )
            schema_text = serialize_pydantic_schema(
                cast(type[BaseModel], agent.output_schema)
            )
            initial_components[comp_name] = schema_text
        else:
            handler_text = _serialize_registered_component(
                agent, comp_name, resolved_registry
            )
            initial_components[comp_name] = handler_text

    logger.debug(
        "evolve.components.resolved",
        agent_name=agent.name,
        components=resolved_components,
    )

    original_component_values = initial_components.copy()
    initial_candidate = Candidate(components=initial_components)

    # Create engine
    resolved_candidate_selector: CandidateSelectorProtocol | None = None
    if candidate_selector is not None:
        if isinstance(candidate_selector, str):
            resolved_candidate_selector = create_candidate_selector(
                candidate_selector, rng=rng
            )
        else:
            resolved_candidate_selector = candidate_selector

    resolved_component_selector: ComponentSelectorProtocol | None = None
    if component_selector is not None:
        if isinstance(component_selector, str):
            resolved_component_selector = create_component_selector(component_selector)
        else:
            resolved_component_selector = component_selector

    engine = AsyncGEPAEngine(
        adapter=adapter,
        config=resolved_config,
        initial_candidate=initial_candidate,
        batch=trainset,
        valset=resolved_valset,
        candidate_selector=resolved_candidate_selector,
        component_selector=resolved_component_selector,
        rng=rng,
    )

    # Run evolution with cleanup
    try:
        result = await engine.run()

        valset_score = result.valset_score
        trainset_score = result.trainset_score

        if trainset_score is not None:
            logger.info(
                "evolve.trainset.scored",
                agent_name=agent.name,
                trainset_size=len(trainset),
                trainset_score=trainset_score,
            )
        if valset_score is not None:
            logger.info(
                "evolve.valset.scored",
                agent_name=agent.name,
                valset_size=len(resolved_valset),
                valset_score=valset_score,
                valset_defaulted=valset is None,
            )

        # Apply state guard validation if provided (for token preservation)
        # Only applies to text components (instruction), not to output_schema
        validated_components = dict(result.evolved_components)
        for comp_name in resolved_components:
            if comp_name in result.evolved_components:
                if comp_name == DEFAULT_COMPONENT_NAME and state_guard is not None:
                    validated_components[comp_name] = _apply_state_guard_validation(
                        state_guard=state_guard,
                        original_component_text=original_component_values[comp_name],
                        evolved_component_text=result.evolved_components[comp_name],
                        agent_name=agent.name,
                    )
                else:
                    validated_components[comp_name] = result.evolved_components[
                        comp_name
                    ]

        # Log evolution completion
        logger.info(
            "evolve.complete",
            agent_name=agent.name,
            original_score=result.original_score,
            final_score=result.final_score,
            improvement=result.improvement,
            total_iterations=result.total_iterations,
            valset_score=valset_score,
            trainset_score=trainset_score,
            components=resolved_components,
        )

        # replace() rather than a rebuilt EvolutionResult, so engine-owned
        # fields (failure counters, stop_reason, token_usage) are not reset
        # to their defaults.
        return replace(
            result,
            evolved_components=validated_components,
            valset_score=valset_score,
            trainset_score=trainset_score,
            original_components=original_component_values,
        )
    finally:
        # Clean up adapter resources (clears handler constraints)
        adapter.cleanup()

run_sync

run_sync(coro: Coroutine[Any, Any, T]) -> T

Run an async coroutine synchronously and return its result.

Universal sync wrapper that accepts any coroutine (e.g., evolve(), evolve_group(), evolve_workflow()) and runs it in a blocking manner. Uses asyncio.run() as the primary mechanism, with nest_asyncio as a fallback for environments with a running event loop. The fallback saves and restores the original event loop to avoid polluting the event loop policy state.

PARAMETER DESCRIPTION
coro

A coroutine object to execute (e.g., evolve(agent, trainset)). Must be a coroutine, not a function or other awaitable.

TYPE: Coroutine[Any, Any, T]

RETURNS DESCRIPTION
T

The result of the coroutine execution. The return type matches

T

the coroutine's return type (e.g., EvolutionResult for evolve(),

T

MultiAgentEvolutionResult for evolve_group()).

RAISES DESCRIPTION
TypeError

If coro is not a coroutine object.

RuntimeError

If a running event loop is detected and nest_asyncio is not installed. Any other RuntimeError from asyncio.run() propagates unchanged.

Examples:

Single-agent evolution:

from gepa_adk import run_sync, evolve

result = run_sync(evolve(agent, trainset=trainset))

Multi-agent group evolution:

from gepa_adk import run_sync, evolve_group

result = run_sync(evolve_group(agents, "primary", trainset=trainset))
Notes

In Jupyter notebooks or IPython, the event loop is already running. Use await evolve(...) directly instead of run_sync(evolve(...)). The nest_asyncio fallback may work but await is preferred.

Source code in src/gepa_adk/api.py
def run_sync[T](coro: Coroutine[Any, Any, T]) -> T:
    """Run an async coroutine synchronously and return its result.

    Universal sync wrapper that accepts any coroutine (e.g., evolve(),
    evolve_group(), evolve_workflow()) and runs it in a blocking manner.
    Uses ``asyncio.run()`` as the primary mechanism, with ``nest_asyncio``
    as a fallback for environments with a running event loop.  The fallback
    saves and restores the original event loop to avoid polluting the
    event loop policy state.

    Args:
        coro (Coroutine[Any, Any, T]): A coroutine object to execute (e.g.,
            ``evolve(agent, trainset)``). Must be a coroutine, not a function
            or other awaitable.

    Returns:
        The result of the coroutine execution. The return type matches
        the coroutine's return type (e.g., EvolutionResult for evolve(),
        MultiAgentEvolutionResult for evolve_group()).

    Raises:
        TypeError: If ``coro`` is not a coroutine object.
        RuntimeError: If a running event loop is detected and ``nest_asyncio``
            is not installed. Any other ``RuntimeError`` from ``asyncio.run()``
            propagates unchanged.

    Examples:
        Single-agent evolution:

        ```python
        from gepa_adk import run_sync, evolve

        result = run_sync(evolve(agent, trainset=trainset))
        ```

        Multi-agent group evolution:

        ```python
        from gepa_adk import run_sync, evolve_group

        result = run_sync(evolve_group(agents, "primary", trainset=trainset))
        ```

    Notes:
        In Jupyter notebooks or IPython, the event loop is already running.
        Use ``await evolve(...)`` directly instead of ``run_sync(evolve(...))``.
        The ``nest_asyncio`` fallback may work but ``await`` is preferred.
    """
    if not asyncio.iscoroutine(coro):
        raise TypeError(
            f"run_sync() requires a coroutine object, got {type(coro).__name__}. "
            "Call the async function first: run_sync(evolve(...)) not run_sync(evolve)"
        )

    try:
        return asyncio.run(coro)
    except RuntimeError as e:
        if "asyncio.run() cannot be called from a running event loop" in str(e):
            try:
                import nest_asyncio  # noqa: PLC0415  # optional dependency, needed only inside a running loop

                nest_asyncio.apply()
                try:
                    old_loop = asyncio.get_running_loop()
                except RuntimeError:
                    old_loop = None
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    return loop.run_until_complete(coro)
                finally:
                    loop.close()
                    if old_loop is not None:
                        asyncio.set_event_loop(old_loop)
            except ImportError:
                raise RuntimeError(
                    "A running event loop was detected and nest_asyncio is not "
                    "installed. Install it with: uv add nest-asyncio\n"
                    "Alternatively, use 'await evolve(...)' directly in async "
                    "contexts like Jupyter notebooks."
                ) from e
        raise

evolve_sync

evolve_sync(
    agent: LlmAgent,
    trainset: list[dict[str, Any]],
    **kwargs: Any,
) -> EvolutionResult

Synchronous wrapper for evolve().

.. deprecated:: Use run_sync(evolve(agent, trainset, ...)) instead.

Runs the async evolve() function in a blocking manner. Handles nested event loops automatically.

PARAMETER DESCRIPTION
agent

The ADK LlmAgent to evolve.

TYPE: LlmAgent

trainset

Training examples.

TYPE: list[dict[str, Any]]

**kwargs

Optional keyword arguments passed to evolve().

TYPE: Any DEFAULT: {}

PARAMETER DESCRIPTION
valset

Optional validation examples for held-out evaluation.

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

critic

Optional ADK agent for scoring.

TYPE: LlmAgent | None

reflection_agent

Optional ADK agent for proposals (not yet implemented).

TYPE: LlmAgent | None

config

EvolutionConfig for customizing evolution parameters.

TYPE: EvolutionConfig | None

trajectory_config

TrajectoryConfig for trace capture settings.

TYPE: TrajectoryConfig | None

state_guard

Optional state token preservation settings.

TYPE: StateGuard | None

candidate_selector

Optional selector instance or selector name.

TYPE: CandidateSelectorProtocol | str | None

executor

Optional unified agent executor for consistent session management across all agent types.

TYPE: AgentExecutorProtocol | None

RETURNS DESCRIPTION
EvolutionResult

EvolutionResult with evolved_components dict and metrics.

RAISES DESCRIPTION
ConfigurationError

If invalid parameters provided.

EvolutionError

If evolution fails during execution. A reflection call that keeps raising a retryable provider error is skipped with skip_reason="reflection_error"; a non-retryable one raises ReflectionError. A reflection cut off at the output-token limit, or one that opens a reasoning tag it never closes, is skipped with skip_reason="incomplete_proposal". An error raised after the baseline was scored carries the engine's EvolutionResult for the recorded iterations in partial_result.

WARNS DESCRIPTION
DeprecationWarning

Always emitted when called. Use run_sync(evolve(...)) instead.

Examples:

Basic usage in a script:

from pydantic import BaseModel, Field
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from gepa_adk import evolve_sync


class OutputSchema(BaseModel):
    answer: str
    score: float = Field(ge=0.0, le=1.0)


agent = LlmAgent(
    name="assistant",
    model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
    instruction="You are a helpful assistant.",
    output_schema=OutputSchema,
)

trainset = [
    {"input": "What is 2+2?", "expected": "4"},
]

result = evolve_sync(agent, trainset)
print(f"Evolved: {result.evolved_components['instruction']}")

With configuration:

from gepa_adk import evolve_sync, EvolutionConfig

config = EvolutionConfig(max_iterations=50)
result = evolve_sync(agent, trainset, config=config)
Notes

Deprecated. Use run_sync(evolve(agent, trainset, ...)) instead. run_sync is a universal wrapper that works with all async evolution functions.

Source code in src/gepa_adk/api.py
def evolve_sync(
    agent: LlmAgent,
    trainset: list[dict[str, Any]],
    **kwargs: Any,
) -> EvolutionResult:
    """Synchronous wrapper for evolve().

    .. deprecated::
        Use ``run_sync(evolve(agent, trainset, ...))`` instead.

    Runs the async evolve() function in a blocking manner.
    Handles nested event loops automatically.

    Args:
        agent: The ADK LlmAgent to evolve.
        trainset: Training examples.
        **kwargs: Optional keyword arguments passed to evolve().

    Other Parameters:
        valset (list[dict[str, Any]] | None): Optional validation examples for
            held-out evaluation.
        critic (LlmAgent | None): Optional ADK agent for scoring.
        reflection_agent (LlmAgent | None): Optional ADK agent for proposals
            (not yet implemented).
        config (EvolutionConfig | None): EvolutionConfig for customizing
            evolution parameters.
        trajectory_config (TrajectoryConfig | None): TrajectoryConfig for trace
            capture settings.
        state_guard (StateGuard | None): Optional state token preservation
            settings.
        candidate_selector (CandidateSelectorProtocol | str | None): Optional
            selector instance or selector name.
        executor (AgentExecutorProtocol | None): Optional unified agent executor
            for consistent session management across all agent types.

    Returns:
        EvolutionResult with evolved_components dict and metrics.

    Raises:
        ConfigurationError: If invalid parameters provided.
        EvolutionError: If evolution fails during execution. A reflection
            call that keeps raising a retryable provider error is skipped
            with ``skip_reason="reflection_error"``; a non-retryable one
            raises ``ReflectionError``. A reflection cut off at the
            output-token limit, or one that opens a reasoning tag it never
            closes, is skipped with ``skip_reason="incomplete_proposal"``.
            An error raised after the baseline
            was scored carries the engine's ``EvolutionResult`` for the
            recorded iterations in ``partial_result``.

    Warns:
        DeprecationWarning: Always emitted when called. Use
            ``run_sync(evolve(...))`` instead.

    Examples:
        Basic usage in a script:

        ```python
        from pydantic import BaseModel, Field
        from google.adk.agents import LlmAgent
        from google.adk.models.lite_llm import LiteLlm
        from gepa_adk import evolve_sync


        class OutputSchema(BaseModel):
            answer: str
            score: float = Field(ge=0.0, le=1.0)


        agent = LlmAgent(
            name="assistant",
            model=LiteLlm(model="ollama_chat/gpt-oss:20b"),
            instruction="You are a helpful assistant.",
            output_schema=OutputSchema,
        )

        trainset = [
            {"input": "What is 2+2?", "expected": "4"},
        ]

        result = evolve_sync(agent, trainset)
        print(f"Evolved: {result.evolved_components['instruction']}")
        ```

        With configuration:

        ```python
        from gepa_adk import evolve_sync, EvolutionConfig

        config = EvolutionConfig(max_iterations=50)
        result = evolve_sync(agent, trainset, config=config)
        ```

    Notes:
        Deprecated. Use ``run_sync(evolve(agent, trainset, ...))`` instead.
        ``run_sync`` is a universal wrapper that works with all async
        evolution functions.
    """
    warnings.warn(
        "evolve_sync() is deprecated, use run_sync(evolve(...)) instead",
        DeprecationWarning,
        stacklevel=2,
    )
    return run_sync(evolve(agent, trainset, **kwargs))