feat(optimization): enforce run-context usage, budgets, and cancellation in the prompt optimizers (RFC #6357, PR 2/2)#6359
Conversation
…ntracts (OptimizationRunContext, OptimizerCapabilities, terminal_status) RFC: google#6357. PR 1 of 2 (contracts only; no optimizer behavior change). Adds the one-shot OptimizationRunContext ledger with truthful usage coverage (verified/partial/unreported, never zero-coerced), call/token budgets with commit-then-terminate overshoot semantics and configurable on_budget_exceeded (raise | return_partial), cooperative cancellation, conservative OptimizerCapabilities defaults on AgentOptimizer, an optional keyword-only run_context parameter on optimize(), and an experimental terminal_status field on OptimizerResult. Enforcement in the two prompt optimizers lands in the stacked PR 2.
Code Review ResultsScope: google/adk-python PR #6359. Enforcement delta Scope CheckREQUIREMENTS MISSING. The Simple happy-path accounting is present and the root/skill optimizer rejects early, but the GEPA sentinel/stopper/post-check contract, governed provider-error terminal path, cancellation boundaries, partial-score semantics, and no-context cancellation parity are incomplete. Triage Groups
P1 -- High
P2 -- Moderate
Inherited blockers from PR #6358PR #6359 is currently based on Verification
Testing gaps
Actionable Findings
|
…e token compliance, granular capabilities, OptimizerResult unchanged Per the precision pass on RFC google#6357: logical-call limits are the only hard enforcement (atomic slot admission); reported-token ceilings terminate reactively over authoritative totals with within_limit/exceeded/indeterminate compliance (missing totals are never proof of compliance); call status is split from run status; capabilities gain accepts_run_context and split logical-call vs reported-token enforceability; terminal_status is removed from OptimizerResult -- the caller-owned snapshot is authoritative; adds OptimizationProviderError for governed in-band error termination.
d30a0ad to
c1f214e
Compare
Fresh review of the enforcement PRReviewed the complete stack at Verdict: keep this PR in draft / request changes. The budget sentinel fix is real and important, but provider-error termination, cancellation boundaries, and no-context compatibility are still incomplete. The current 57 green tests do not cover these paths. What is now right
[P1] GEPA provider errors can still become successful optimization results
The reflection generator never checks The raised-exception path also loses usage accumulated inside Required bridge:
[P1] SimplePromptOptimizer does not honor its advertised cancellation contract
Add the RFC-pinned boundaries before and after every sampler/model operation, finalize native cancellation, and re-raise it. Tests must assert model and sampler call counts, not only [P1] GEPA misses the post-sampler boundary and changes ungoverned cancellation behavior
Keep the legacy direct-await path when no context is supplied. Use the shield/request/drain behavior only for governed runs, add the post-sampler check, and commit the cancellation terminal state before re-raising. The caller watchdog remains the bounded hard backstop. [P1] Successful and failed optimizer exits do not finalize the caller-owned snapshotNeither optimizer marks a normal run Once #6358 exposes lock-protected terminal transitions, wrap both implementations so every return or exception path produces exactly one final status. Do not infer partial success from “any non-None status,” as Simple currently does; branch specifically on [P2] The test and merge gates give false confidenceVerification on the reviewed head:
The critical GEPA tests currently use a fake optimizer with only the budget exception behavior. Please add real tests against both the minimum supported GEPA and latest GEPA, as #6357 requires, plus provider-error and cancellation tests using faithful exception-swallowing loop behavior. The substantive CI matrix is not running on these fork PRs, so local green tests are not a substitute for those gates. The budget-abort path is now a solid foundation. The next pass needs to apply the same rigor to provider failures, every cancellation boundary, and observational compatibility when the context is absent. |
…en ledger, provider-failure precedence, independent type-cleanliness - Explicit first-terminal-wins state machine (completed/budget_exceeded/ cancelled/failed) with finalize_success/finalize_cancelled/finalize_failed; begin_model_call rejects any terminal context; a late cancellation cannot overwrite an earlier terminal state. - Provider failure is a governed terminal: end_model_call with sanitized error_code/error_type commits usage, transitions the run to failed, and raises OptimizationProviderError; provider failure takes precedence over a simultaneous token overshoot while preserving usage and token-compliance evidence. Raw exception text never enters the ledger. - Ledger integrity: handles bound to their context (foreign commit is a typed error), atomic close under the context lock (concurrent double-close commits once), private unattached sentinel (attach(None) is one-shot), frozen OptimizationBudgets (ge=0 validated), frozen snapshots/events (tuples), snapshot carries budgets + terminal sequence + terminal error metadata; finalize_cancelled closes open call events. - Stage is an extensible string with documented constants. - Built-in optimizers accept the keyword and reject a supplied context with UnsupportedOptimizationContextError in this PR, so the abstract-signature change is independently type-clean; real support lands in the stacked enforcement PR. - isort + pyink applied.
c1f214e to
0542043
Compare
Review disposition — all four P1s and the P2 gates addressed (head
|
Fresh adversarial review of the hardened enforcement headReviewed at Verdict: keep draft / request changes. The sentinel/stopper/post-check bridge is now sound on the tested budget and provider paths. Three P1 enforcement gaps remain, including a concrete regression for callers that do not use the new context. Verified improvements
[P1] No-context GEPA behavior is not observationally equivalent
Before this PR, a no-context reflection continued consuming the stream because it did not inspect Fix: branch on in-band errors only when [P1] GEPA failures before the executor boundary do not finalize the attached contextThe context is consumed at Missing optional dependency, adapter construction, model construction, and train/validation ID retrieval all escape without a terminal snapshot. I reproduced Fix: finish fallible setup before attachment where possible, then wrap every post-attachment path in one outer finalization boundary. The final snapshot guarantee must apply to setup failure as well as executor failure. [P1] Simple native cancellation loses usage already reported
This breaks the stated goal of reconciling provider-reported usage whenever it was available. Fix: add/use a cancellation-aware call commit that records usage/model metadata and closes the specific handle as [P1] Raised provider codes are not normalized before the contract boundaryBoth The context should enforce normalization, but adapters should also convert provider-specific values into the normalized experimental contract explicitly. [P2] Cancellation during final validation can return normal successSimple checks before final validation but not after it. If cancellation is requested while validation runs, If late cancellation is intentionally allowed to lose to completed validation, pin that outcome in the RFC and a test. Otherwise add a post-validation boundary and make success finalization atomically cancellation-aware. I recommend the latter because it gives one unambiguous final outcome. Test and description driftThe implementation behaves correctly in several places that have no first-class regression test. The repository currently has no GEPA tests for:
My independent versions of all four pass, but the no-context stream-content parity probe above fails. These should join the suite because they are load-bearing claims in the RFC/disposition. The PR body also still says 57/57 and describes the earlier boundary set. Update it to the current 81-test stack and the real current behavior. GEPA matrix statusThe code currently passes the complete optimization suite on both ends of the admitted range that exist today: GEPA 0.1.0 and 0.1.1. The remaining RFC gate is automation: CI still does not install and test both explicitly. That is an honest maintainer/configuration dependency, not a current compatibility failure. The provider sentinel work is now strong. The next pass should preserve legacy behavior when the seam is absent and make the final snapshot truthful for setup failure and mid-stream native cancellation. |
Fresh review round — enforcement PR (
|
…pes (round-2 adversarial findings) - Validate-before-commit: usage counters must be finite non-negative numbers (NaN/inf/negative -> not reported, never a crash on a half-closed record); error metadata is normalized to bounded single-token identifiers at the context boundary (numeric provider codes become strings; multiline/path text is scrubbed); the event, counters, and terminal transition then commit atomically under one lock acquisition. - Every caller observes the committed terminal: an already-admitted call settles for truthful accounting and then receives the existing terminal error; finalize_success observes pending cancellation and commits/raises CANCELLED instead of recording a false success. - OptimizationFailedError added for non-provider failures; OptimizationProviderError is reserved for governed provider-call terminals (terminal provenance tracked). - end_model_call(cancelled=True) settles a call as cancelled while preserving usage evidence; completed_calls documented and counted as settled calls regardless of call status (finalize_cancelled counts its closures). - Event timestamps are wall-clock epoch seconds for durable, cross-worker attempt records. - Built-in optimize() signatures fully typed (Optional[OptimizationRunContext]). - 10 new regression tests reproduce every round-2 finding. Suite: 70/70.
0542043 to
b9293de
Compare
Round-2 disposition — all four P1s implemented (head
|
Round-3 fresh review — enforcement head
|
…zation, local-abort settlement, private module packaging, typed internals - finalize_success is an invariant boundary: idempotent only on COMPLETED; any other existing terminal re-raises its typed outcome; a pending cancellation commits CANCELLED and raises; an open (unsettled) call commits FAILED (OPEN_MODEL_CALLS) and raises OptimizationFailedError so a run can never escape nonterminal with an open event. - returned_model_version is normalized (bounded optional string) before mutation -- a malformed value from a custom BaseLlm can no longer make every subsequent snapshot raise ValidationError. - Fractional token counters are unreported, not silently truncated (ints excluding bool; floats only when integral); zero and very large integers accepted -- tested. - abort_model_call settles an admitted call that failed locally as call-status ABORTED with run-status FAILED via OptimizationFailedError: truthfully distinct from provider failures and cancellation (needed by the enforcement PR's post-admission exception boundary). - ADK visibility rule: module renamed to _run_context.py; the supported experimental surface is exported from google.adk.optimization with explicit __all__; internal imports and tests use the package surface. - Typed _CallRecord/_CallHandle constructors and attributes; optional .value access narrowed (the five repo-config mypy errors named in review; exact gate reproduction is blocked in this environment by a numpy-stub / python_version=3.11 syntax failure, noted honestly). - New tests: success-with-open-call, success-after-budget/provider/cancel, malformed model version, fractional/integral/zero/large usage, local abort, concurrent attach (1 of 16 wins), concurrent admission (exactly k of 16, unique sequences, shared terminal), success-vs-settlement race. Suite: 54 contract tests, full optimization directory green.
Lets consumers distinguish a provider-call FAILED terminal from a local/ optimizer FAILED terminal without private state -- required by the stacked enforcement PR's post-check mapping (OptimizationProviderError vs OptimizationFailedError).
b9293de to
ef7016b
Compare
Round-3 disposition — every finding implemented (head
|
Round-4 fresh review — enforcement head
|
…he ledger; duplicate terminal settlement re-raises; race test is warning-clean - _extract_usage is defensive around attribute ACCESS as well as conversion: a throwing usage property or hostile numeric degrades that counter to unreported and the call settles atomically (no more stranded open events). All str() normalization paths survive hostile __str__ (error metadata degrades to UNSTRINGABLE; optional fields to None). - Duplicate settlement of an already-closed call on a TERMINAL run re-raises the committed terminal (governance callers cannot continue past it); the no-op return remains only for a closed call on a nonterminal run. abort_model_call follows the same rule. - Admission and cancellation metadata are normalized at the boundary: stage must be a non-empty bounded string (ValueError otherwise, no slot consumed), requested_model and cancel reason are bounded normalized strings -- hostile values can no longer make every snapshot raise. - The remaining strict-mypy optional-.value read is narrowed via a local. - The success/settlement race test uses a Barrier, captures both threads' outcomes, asserts the two legal resolutions explicitly and completed_calls == started_calls -- clean under -W error::pytest.PytestUnhandledThreadExceptionWarning. - Seven new regression tests reproduce every round-4 contract finding.
…settlement, pre-cancel precedence, typed internals, GEPA behavior pin - GEPA reflection builds and validates LlmRequest BEFORE reserving the model-call slot (a construction failure is a GEPA-native no-proposal with a clean ledger, never an open call); from admission onward every operation -- including run_coroutine_threadsafe scheduling -- lives inside one settlement boundary; local scheduling failures settle via the new abort_model_call as call-status ABORTED / run-status FAILED (OptimizationFailedError, truthfully distinct from provider failures and cancellation) and convert to the sentinel so GEPA cannot swallow them into success. - Snapshot terminal provenance (terminal_from_provider_call) distinguishes provider FAILED from local FAILED in the post-check and sentinel-escape mappings. - Pre-cancellation wins over discovery: both optimizers observe raise_if_cancelled immediately after attach, before imports, model/ adapter construction, and any sampler method; cancellation also wins over a competing discovery exception (tested with sampler-method assert_not_called for both optimizers). - finalize_success is gated to genuinely non-terminal runs on the governed GEPA partial path (the authoritative BUDGET_EXCEEDED terminal survives). - Typed _ReflectionCapture replaces the heterogeneous holder dict; adapter run_context parameter typed; gepa.optimize is two fully typed branches -- the legacy call shape preserved exactly for absent-context parity, the governed branch pinning raise_on_exception=True so generic evaluator/ sampler failures stay fail-closed under the unbounded gepa>=0.1 range; sentinel docstring attribution corrected (reflective_mutation.py converts reflection errors to no-proposal independent of the engine flag). - New real-GEPA durable coverage: in-band provider error, post-sampler cancellation, reflection-request failure. Full suite: 106/106.
ef7016b to
50acc88
Compare
…D, coroutine hygiene, terminal evidence parity - A governed LlmRequest construction failure keeps the call ledger correctly empty (no provider call began) but finalizes the run FAILED (REQUEST_CONSTRUCTION_FAILURE) and aborts via the sentinel -- a malformed optimizer request can never be recorded as a successful governed optimization. Legacy no-context behavior preserved. The two round-3 tests pinning the superseded clean-ledger-success shape are updated to the corrected semantics (stub + real GEPA). - Scheduling failures close the never-awaited coroutine before settling the abort (reflection and the adapter's sampler coroutine) -- the regression now runs with RuntimeWarning promoted to error. - Terminal settlements carry the same evidence as successes: in-band and raised provider errors and Simple native cancellation all preserve returned_model_version alongside usage (regressions per optimizer). - Full suite: 116/116.
50acc88 to
8a19693
Compare
Round-4 disposition — all findings implemented (head
|
RFC: #6357 — stacked on #6358 (contracts). Review #6358 first; this PR contains its commits plus one enforcement commit.
Updated to match the RFC's precision pass — including the load-bearing fix it demanded: GEPA's engine catches iteration exceptions and continues (verified at
gepa/core/engine.py:588), so a typed budget error raised from the reflection callable would be swallowed and sampler calls would keep scheduling. This PR implements the sentinel/stopper/post-check bridge instead.What this adds
SimplePromptOptimizer: capability opt-in; each candidate-generation call is one recorded logical invocation (streamed usage + model version; provider exceptions preserve usage-so-far); in-bandLlmResponse.error_codeterminates a governed run withOptimizationProviderError(usage preserved) instead of silently succeeding; cancellation observed before every iteration;return_partialkeeps the best-so-far agent, schedules nothing further (final validation included, sooverall_score=None), and leaves run status to the authoritative snapshot — the result schema is unchanged.GEPARootAgentPromptOptimizer— the sentinel bridge:completed; run statusbudget_exceededon overshoot);_GovernedRunAbortsentinel — GEPA converts the interrupted reflection to "no proposal" and keeps looping;stop_callbacksstopper observes any terminal run status (not just cancellation) at the next loop boundary;gepa.optimizereturns maps the committed state toraiseorreturn_partial— never a false success — with GEPA's best-so-far candidates available in the partial path.Cancellation is additionally observed before each sampler evaluation is scheduled onto the main loop; native task cancellation requests the cooperative stop, drains the executor worker once the active operation reaches a boundary, then re-raises.
GEPARootAgentOptimizer(root/skill surface): rejects a supplied context withUnsupportedOptimizationContextErrorbefore any sampler or model work; conservative capabilities. Instrumenting this surface is deliberate fast-follow per the RFC.Verification
106/106 passing across
tests/unittests/optimization/(all pre-existing optimizer tests + the contracts suite + enforcement tests). New in this PR, including the RFC-mandated regression: no sampler call is scheduled after the overshoot commits, proven against a gepa stub with faithful engine semantics (iteration exceptions swallowed, stoppers checked at loop boundaries);raise-mode post-check mapping; in-band provider-error termination; snapshot-authoritativereturn_partialwithoverall_score=None; capability opt-ins; typed rejection before work on the root/skill optimizer.Downstream consumer: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#318.
Round 3: reflection requests build before slot admission (construction failures are GEPA-native no-proposals with a clean ledger); post-admission operations live in one settlement boundary with local scheduling failures settling as
aborted→OptimizationFailedError(never provider-typed, never success); pre-cancellation wins over sampler discovery in both optimizers (asserted on every sampler method); the governedgepa.optimizebranch pinsraise_on_exception=Truewith the legacy call shape preserved exactly when no context is supplied; typed_ReflectionCapture/adapter internals; real-GEPA durable coverage extended to in-band provider errors, post-sampler cancellation, and reflection-request failure.