feat(v1): GEPA prompt optimization for native v1 environments#1952
feat(v1): GEPA prompt optimization for native v1 environments#1952Ziems wants to merge 9 commits into
Conversation
Add a `gepa` CLI command that runs GEPA (Genetic-Pareto) system-prompt optimization against native v1 taskset/harness environments, driving the v1 execution path (Environment.episode / Episode.run / Trace) through the third-party gepa adapter protocol. The v0 path (vf-gepa) is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8629d8f. Configure here.
|
|
||
| `-h`/`--help` (or no args) prints the local example tasksets/harnesses plus the full, typed | ||
| pydantic-config help — narrowed to whatever `--taskset.id` / `--harness.id` are given. | ||
| """ |
There was a problem hiding this comment.
Missing user-facing documentation
Medium Severity
This change adds a new core gepa console entrypoint for native v1 tasksets, but the user-facing docs under docs/ (for example evaluation workflow and reference material) are not updated to describe uv run gepa, how it differs from vf-gepa, or its config flags.
Triggered by project rule: BugBot Instructions
Reviewed by Cursor Bugbot for commit 8629d8f. Configure here.
There was a problem hiding this comment.
Intentionally deferred — see the "Open question: docs & skill" section in the PR description. I wasn't sure where the v1 gepa docs belong (new subsection in docs/training.md, a v0/v1 split of the existing section, or a v1-CLI doc home), so I've asked a maintainer to pick the structure and will add it here or in a follow-up.
|
|
||
| `-h`/`--help` (or no args) prints the local example tasksets/harnesses plus the full, typed | ||
| pydantic-config help — narrowed to whatever `--taskset.id` / `--harness.id` are given. | ||
| """ |
There was a problem hiding this comment.
GEPA skill not updated
Medium Severity
The new v1 gepa command and its config contract are not reflected in skills/optimize-with-environments/SKILL.md, which still centers on prime gepa run and v0-style env config rather than uv run gepa with GEPAv1Config.
Triggered by project rule: BugBot Instructions
Reviewed by Cursor Bugbot for commit 8629d8f. Configure here.
There was a problem hiding this comment.
Intentionally deferred — see the "Open question: docs & skill" section in the PR description. Whether SKILL.md should gain a parallel v1 path or be rewritten around gepa + GEPAv1Config is a maintainer call; I'll update it here or in a follow-up once placement is agreed.
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR adds a complete new GEPA prompt optimization CLI for v1 environments (~500+ lines across 7 new files). New features of this scope warrant human review. Additionally, unresolved comments identify potential resource leaks and logic issues that should be addressed. You can customize Macroscope's approvability policy. Learn more. |
- runner: configure GEPADisplay.set_valset_info so full-valset evals are classified correctly for any --num-val (not just the default 50). - runner: close the client in a finally so it runs even if optimize() raises, before the adapter's __exit__ closes the loop. - runner: split tasks first, then seed from the evaluated train+val set rather than the pre-split pool (a per-task system_prompt could otherwise seed from a task in neither split). - main: remap SIGTERM to KeyboardInterrupt (matching the sibling v1 CLIs) so a killed run still tears down serving() resources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| from gepa.core.result import GEPAResult | ||
|
|
||
| from verifiers.gepa.display import GEPADisplay | ||
| from verifiers.gepa.gepa_utils import save_gepa_results |
There was a problem hiding this comment.
same here, let's make the output format v1 native (trace)
There was a problem hiding this comment.
Done. Leaving this one open for discussion though. Right now it saves all rollouts from all prompt candidates, so theres lots of data but its not super useful. For GEPA to be properly useful you really care about:
- How it performs on the valset with the default prompt
- How it performs on the valset with the optimized prompt
So I think saving these two things and their associated rollouts is probably more useful than saving all of the rollouts done during optimization.
| """The taskset's tasks, split into disjoint `(train, val)` slices. `train` feeds reflection | ||
| minibatches; `val` scores each candidate system prompt for the pareto frontier.""" | ||
| pool = list(tasks) | ||
| if shuffle: |
There was a problem hiding this comment.
inherit these funcs from v1 vs. reimplementing them?
There was a problem hiding this comment.
I made a helper since the shuffle and split pattern is used pretty often in GEPA and elsewhere. Not sure if this is what you meant. Leaving this one open for discussion.
Matches the v1 sibling naming convention (EvalConfig, DebugConfig, ...); the `v1` suffix is redundant inside the verifiers.v1 namespace. No collision with the v0 verifiers.gepa.config.GEPAConfig — both are module-qualified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review — the current tests are coupled to the initial implementation, which is being reworked to be v1-native (v1 display/output, no v0 deps, shared v1 CLI primitives). Tests will be re-added against the new shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "load_tasks -> optional shuffle (fixed seed) -> take first N" idiom was inlined in run_eval, run_eval_server, validate, debug, and the v0 bridge. Factor it into verifiers/v1/utils/sampling.py so every entrypoint shares one reproducible task-selection helper instead of re-implementing it.
Move the GEPA implementation out of verifiers/v1/cli/gepa/ into a self-contained verifiers/v1/gepa/ package (the cli dir is for entrypoints; the optimizer logic is library code), leaving only a thin console entrypoint in cli/gepa.py. Drop all non-v1 dependencies and hand-rolled machinery: - display: remove the v0 GEPADisplay; GEPA's per-iteration progress logs through v1 logging via a small LoggerProtocol shim. - imports: replace verifiers.utils.save_utils.make_serializable with pydantic's to_jsonable_python, and verifiers.gepa.gepa_utils.save_gepa_results with a v1-native writer (system_prompt.txt + metadata.json). - execution: run_gepa is async like run_eval - it holds env.serving() open with `async with` and runs the blocking gepa.optimize() via asyncio.to_thread, the adapter marshalling rollouts back to the loop (no hand-owned event loop). - cli: parse purely with pydantic_config.cli(GEPAConfig) - no argv rewriting or narrow_config; it resolves the taskset/harness subconfigs and renders -h itself. - sampling: split_tasks reuses the shared sample_tasks helper.
Drop the near-duplicate gepa_output_path; gepa resolves its output dir via verifiers.v1.cli.output.output_path like eval, and GEPAConfig.run_dir becomes output_dir (-o) to match. Runs land in outputs/<taskset>--<model>--<harness>/<uuid> (no separate gepa/ segment); write_gepa_result still writes the prompt + metadata there.
Drop the bespoke write_gepa_result (system_prompt.txt + metadata.json) and output.py. GEPA's rollouts now stream to results.jsonl exactly as run_eval's do — each Trace records its candidate in task.system_prompt, and the best prompt is still printed to stdout (and kept in GEPA's own candidates.json). Persisting is baked into the adapter (it owns the episode loop, so it writes each trace itself via append_trace) rather than threaded through an on_complete hook from the runner.
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> None: |
There was a problem hiding this comment.
🟠 High cli/gepa.py:25
A bare command like uv run gepa reverse-text-v1 --model ... silently uses the default taskset.id instead of selecting the requested taskset, so the run targets the wrong (or empty) taskset. Unlike the other v1 CLIs, main() passes argv straight to cli(GEPAConfig, ...) without first calling with_positional_taskset(), so the leading positional token is never rewritten into --taskset.id. Apply with_positional_taskset() to argv before parsing so positional taskset ids resolve correctly.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/cli/gepa.py around line 25:
A bare command like `uv run gepa reverse-text-v1 --model ...` silently uses the default `taskset.id` instead of selecting the requested taskset, so the run targets the wrong (or empty) taskset. Unlike the other v1 CLIs, `main()` passes `argv` straight to `cli(GEPAConfig, ...)` without first calling `with_positional_taskset()`, so the leading positional token is never rewritten into `--taskset.id`. Apply `with_positional_taskset()` to `argv` before parsing so positional taskset ids resolve correctly.
| logger.info(message) | ||
|
|
||
|
|
||
| async def run_gepa(env: Environment, config: GEPAConfig) -> GEPAResult: |
There was a problem hiding this comment.
🟡 Medium gepa/runner.py:33
logger.info("gepa config:\n%s", config.model_dump_json(...)) logs the full resolved GEPAConfig, which includes client.headers and reflection_client.headers. When those headers contain authentication tokens for custom model endpoints, the secrets are written to the CLI logs/stdout, exposing credentials to anyone who can read the terminal output or collected logs. Consider redacting header values before logging the config.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/gepa/runner.py around line 33:
`logger.info("gepa config:\n%s", config.model_dump_json(...))` logs the full resolved `GEPAConfig`, which includes `client.headers` and `reflection_client.headers`. When those headers contain authentication tokens for custom model endpoints, the secrets are written to the CLI logs/stdout, exposing credentials to anyone who can read the terminal output or collected logs. Consider redacting header values before logging the config.
|
|
||
|
|
||
| def build_reflection_lm(config: GEPAConfig) -> Callable[[str], str]: | ||
| client_config = config.reflection_client or config.client |
There was a problem hiding this comment.
🟡 Medium gepa/reflection.py:16
build_reflection_lm() falls back to config.client when reflection_client is unset, but that config can be a TrainClientConfig targeting a vLLM /inference/v1/generate endpoint. The function unconditionally constructs an OpenAI client and calls chat.completions.create, so a gepa run with --client.type train (and no reflection_client override) sends chat-completions requests to an incompatible engine and the reflection step fails. Consider requiring an explicit reflection_client when the rollout client is a TrainClientConfig, or routing through TrainClient instead of a raw OpenAI client in the fallback path.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/gepa/reflection.py around line 16:
`build_reflection_lm()` falls back to `config.client` when `reflection_client` is unset, but that config can be a `TrainClientConfig` targeting a vLLM `/inference/v1/generate` endpoint. The function unconditionally constructs an `OpenAI` client and calls `chat.completions.create`, so a `gepa` run with `--client.type train` (and no `reflection_client` override) sends chat-completions requests to an incompatible engine and the reflection step fails. Consider requiring an explicit `reflection_client` when the rollout client is a `TrainClientConfig`, or routing through `TrainClient` instead of a raw `OpenAI` client in the fallback path.
| self.tasks[idx].model_copy(update={"system_prompt": system_prompt}) | ||
| for idx in batch | ||
| ] | ||
| episodes = [self.env.episode(task, self.ctx, n=1) for task in tasks] |
There was a problem hiding this comment.
🟡 Medium gepa/adapter.py:75
_run_batch always calls self.env.episode(task, self.ctx, n=1), so evaluating any taskset that uses @group_reward raises ValueError because group-reward scoring requires at least two rollouts per task. This crashes the entire gepa optimization run on valid v1 tasksets instead of optimizing them. Consider passing an n that satisfies the taskset's minimum rollout count, or validating the taskset up front and rejecting group-reward tasksets with a clear error.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/gepa/adapter.py around line 75:
`_run_batch` always calls `self.env.episode(task, self.ctx, n=1)`, so evaluating any taskset that uses `@group_reward` raises `ValueError` because group-reward scoring requires at least two rollouts per task. This crashes the entire `gepa` optimization run on valid v1 tasksets instead of optimizing them. Consider passing an `n` that satisfies the taskset's minimum rollout count, or validating the taskset up front and rejecting group-reward tasksets with a clear error.
CI runs `ruff format --check`; these three files were committed unformatted. No behavior change.


What
Adds GEPA (Genetic-Pareto) system-prompt optimization for native v1 environments as a new
gepaCLI command. The existing v0 path (vf-gepa/verifiers/gepa/) is untouched.Why
GEPA only worked against the legacy v0
Environment.generate()API. v1 composesTaskset+Harness+Environmentwith nogenerate()and no dataset split, so there was no way to optimize a native v1 taskset. This wires GEPA into the v1 execution path (Environment.episode()/Episode.run()/Trace).How
verifiers/v1/gepa/— a self-contained package (adapter,config,dataset,reflection,runner); the CLI is a thincli/gepa.pyentrypoint. Imports nothing from v0.run_gepaisasynclikerun_eval: it holdsenv.serving()open with anasync withand runs the blockinggepa.optimize()viaasyncio.to_thread, so the loop stays free.GEPAv1Adapter(GEPA'sevaluate/make_reflective_datasetprotocol) marshals each rollout batch back onto the loop withrun_coroutine_threadsafe— the one sync↔async hop the synchronous third-partygepaAPI forces. Candidates are injected viaTask.model_copy(update={"system_prompt": ...}).GEPAConfiginheritsEnvConfig(likeEvalConfig); parsing is purepydantic_config.cli(GEPAConfig)— it resolves the taskset/harness subconfigs and renders-hitself, no argv rewriting.Tracestreams toresults.jsonlvia the sameon_complete→append_tracehookrun_evaluses (each trace records its candidate intask.system_prompt); per-iteration progress logs through v1 logging; the best prompt prints to stdout.verifiers/v1/utils/sampling.py::sample_tasks(also adopted byrun_eval/run_eval_server/validate/debug/ the v0 bridge);split_taskstakes two disjoint train/val slices from it.Scope / non-goals
--idenv is rejected (usevf-gepa).EnvGrouphas no v1 equivalent); single taskset per run.--serverworker pool — GEPA runs in-process.APPENDS_SYSTEM_PROMPT=False, e.g. coding agents) warn (the candidate folds into the user prompt) rather than block. Tasksets that bake instructions intopromptinstead ofTask.system_prompt(e.g.gsm8k-v1) need--initial-prompt.Testing
echo-v1andreverse-text-v1: rollouts stream toresults.jsonl, all traces scored with no errors, and GEPA proposes/optimizes the system prompt.ruff+pre-commitclean.Files
verifiers/v1/gepa/—adapter,config,dataset,reflection,runnerverifiers/v1/cli/gepa.py— thegepaentrypointverifiers/v1/utils/sampling.py— sharedsample_taskshelperpyproject.toml— register thegepaconsole scriptOpen question: docs & skill (feedback wanted)
Bugbot flagged that this doesn't update the user-facing docs (
docs/) or the GEPA skill (skills/optimize-with-environments/SKILL.md), both of which currently describe the v0prime gepa run/vf-gepaflow. I deliberately left those out of this PR and want a maintainer's call on where they should go, because I'm not sure of the intended structure:docs/training.mdhas a "Prompt Optimization withprime gepa run" section. Should the v1gepacommand be (a) a new subsection there, (b) folded into the existing section as a v0/v1 split, or (c) documented wherever the other v1 CLIs (eval,serve,replay) are described? I couldn't find an existing v1-CLI doc home to match.skills/optimize-with-environments/SKILL.mdis v0-centric (prime gepa run, v0 env config). Should it gain a parallel v1 path, be rewritten aroundgepa+GEPAv1Config, or stay v0-only for now?Happy to do the doc/skill update in this PR or a follow-up once we agree on placement — just let me know the preferred structure.
🤖 Generated with Claude Code
Note
Medium Risk
New feature with many LLM rollout/reflection API calls and async/sync bridging in the adapter; no changes to auth or v0 GEPA path, but misconfiguration can burn large eval budgets.
Overview
Adds a
gepaconsole entrypoint (alongside existingvf-gepafor v0) that runs Genetic-Pareto optimization ofTask.system_prompton native v1 tasksets viaEnvironment.episode()/Tracerewards.GEPAv1Adapterimplements GEPA’s sync adapter protocol: candidates are injected withTask.model_copy, rollouts run under a single persistent event loop that keepsenv.serving()open for the wholeoptimize()call, andmake_reflective_datasetbuilds teacher-LM records from traces.GEPAv1ConfigextendsEnvConfig(likeeval) with model, reflection model, train/val sizes, metric budget, and output options; the CLI mirrorsevalresolution and rejects legacy v0--idenvs.Train/val splits are carved from
taskset.load_tasks()with seedable shuffle; seed prompts come from--initial-promptor the first tasksystem_prompt, with warnings when the harness does not append a real system message. The runner wiresgepa.api.optimize, sync reflection LM calls from v1 client config,GEPADisplay, and v0save_gepa_resultsunderoutputs/gepa/....tests/v1/test_gepa.pycovers splits, adapter behavior, config/CLI guards, and an optional e2e run.Reviewed by Cursor Bugbot for commit 8629d8f. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add GEPA prompt optimization CLI for native v1 environments
gepaCLI command (viaverifiers/v1/cli/gepa/) that runs GEPA prompt optimization over verifiers v1 environments, replacing the legacyvf-gepapath for v0 environments.GEPAv1Adapterwraps the v1 environment to evaluate candidate system prompts with controlled concurrency, collecting scores and traces for each candidate.GEPAv1Configprovides a Pydantic-validated config model with fields for model, client, train/val split sizes, budget, reflection model, seed prompt, and output paths.run_gepaorchestrates the full pipeline: task loading, seed prompt resolution, train/val splitting, optimization viagepa.api.optimize, result saving, and display lifecycle.--dry-run(writes config only), positional taskset selection, and rejects legacy v0 environments with a redirect tovf-gepa.Changes since #1952 opened
verifiers.v1.gepato run asynchronously withrun_gepacoroutine that manages shared environment serving, streams traces toresults.jsonlvia anon_completecallback during optimization, offloads blockinggepa.api.optimizeto a worker thread viaasyncio.to_thread, and transformedGEPAv1Adapterfrom a context-managed class to a dataclass that accepts an event loop, optional semaphore, and optionalon_completecallback instead of managing its own serving lifecycle [4b7e9be]verifiers.v1.cli.gepafrom a package with multiple modules to a single module with a newmainfunction that parsesGEPAConfigviaprime-pydantic-config, rejects legacy environments, supports--dry-runmode with config writing, installs SIGTERM handler, constructsEnvironment, and runsrun_gepaviaasyncio.run[4b7e9be]run_debug,run_eval,run_eval_server,run_validate, andrun_legacy_evalcoroutines by replacing inline manual shuffling withrandom.Random(0)and slicing logic with calls to newverifiers.v1.utils.sampling.sample_tasksutility that uses fixed seed_SHUFFLE_SEED=0[4b7e9be]GEPAv1ConfigtoGEPAConfig, replacedrun_dirfield withoutput_dir(aliased too), and updated type signatures inbuild_reflection_lmfunction to reference the renamed configuration class [4b7e9be]📊 Macroscope summarized 8629d8f. 8 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.