feat(agents): extend cooperative early stop to codex and antigravity - #64
Conversation
Widen the early-stop capability set: CodexAgent and AntigravityAgent now honor the cooperative should_stop interrupt, so armed activation suites resolve and run identically on all three harnesses. - codex: poll should_stop after each dispatched notification; interrupt the in-flight turn best-effort; make the turn-never-completed raise conditional on a stop; skip sub-agent rollout recovery on a stopped turn; finalize STOPPED_EARLY (crashed=False) - antigravity: poll after each processed step; cancel the conversation best-effort; finalize STOPPED_EARLY (crashed=False) - both: post-stop exceptions no longer escalate to a crash - a crash would retry with the watcher's decision latched and stop the retry at turn 0 (retry poisoning); timeout keeps precedence over a stop - validator: message now lists claude-code, codex, antigravity (logic unchanged; non-supporting agents are still rejected) - tests: seam suites for both agents (stop cuts the stream, clean STOPPED_EARLY, timeout precedence, post-stop-exception guards), validation acceptance, orchestrator-level wiring on a codex task, guardrail-1 tests rewritten against a dummy non-supporting agent, plus a live codex cooperative-stop test (marker-gated) - docs: early-stop notes updated across the three agent pages Claude's early-stop path is untouched; its seam tests pass unmodified.
|
Claude finished @mohsen-uipath's task in 1m 35s —— View job Code Review for PR #64: Extend cooperative early stop to codex and antigravityTodo List:
|
bai-uipath
left a comment
There was a problem hiding this comment.
Fix what you agree with, otherwise lgtm — mirrors Claude's seam, touches nothing shared, and the unarmed + Claude controls in the smoke carry the argument.
Worth fixing
- The codex interrupt is a blocking JSON-RPC round trip on the event loop. The sync interrupt path bottoms out in a response waiter with no timeout, so a slow app-server stalls every other task in a parallel batch and keeps the watchdog's cancel from landing. The same function already offloads stream iteration to a thread for exactly this reason. Fix: offload the interrupt too.
- "N turn(s) avoided" is now false on these agents. Turns-remaining derives from
max_turns, which codex ignores and antigravity effectively has one of, so an armed row claims ~29 avoided turns in run.json and run.md when only a tool-call tail was cut. Fix: suppress turns-remaining for single-turn agents and let elapsed / tool-call index carry it. - The retry-poisoning guard is at the wrong seam. Four copies across two agents, two of them unreachable (a suppressed cancel, and a break that never runs the generator's aclose in that frame), while Claude has the same exposure and no guard — so the identical-on-every-harness goal misses precisely the case the guard exists for. Fix: one "don't retry once the watcher has fired" check on the retry path, which covers all three.
Please verify
- Antigravity has no live coverage. Codex got a live test; the real cancel is exercised nowhere and the fake is a counter. Re-run the armed antigravity smoke (or add a marker-gated live test) and confirm the deciding tool call lands as UNRESOLVED in the frozen trajectory, not just that the row stopped.
Downstream, don't forget
- Cut the release as 0.9.1 (the
feat:subject would otherwise auto-bump the minor), then bump skills'.coder-eval-version(still 0.8.10) and drop the four "codex/antigravity must add-D run_limits.stop_early=false" notes in the activation docs and experiment. coder_eval_uipath's existing>=0.9.0,<0.10pin already covers 0.9.1. - Worth knowing for priority: with the activation experiment armed,
--type codex/antigravityabort at resolution and the runner has no override, so those activation slices are dead today. This fixes it; a one-linestop_early=falsein eval_runner unblocks the nightly sooner and stays useful for authoritative precision runs.
|
Re: "Please verify — Antigravity has no live coverage" Verified — this was tested before merge with a real-SDK armed vs unarmed A/B on antigravity (skill-activation suite, 24 rows each via
Agreed the durable-coverage gap stands: this is a one-off verification, so a marker-gated live test (the antigravity twin of the codex one, gated on Downstream: release cut as v0.9.1 as suggested; the skills-repo pin bump + doc cleanup PR is next. |

What
Widens early stop from Claude Code to all three built-in agents:
CodexAgentandAntigravityAgentnow honor the cooperativeshould_stopinterrupt, so armed tasks (run_limits.stop_early: true) resolve and run identically on every harness instead of hard-erroring at resolution for--type codex/--type antigravity. The watcher, stop rule, validator logic, criteria live-verdicts, event collector, and the entire Claude path are untouched — only the capability set widens; the Claude seam tests pass unmodified as the non-regression proof.How
Both agents mirror Claude's seam:
should_stop()runs after each dispatched notification/step (the emission that lets the watcher latch on the deciding tool call) and before the next item is pulled — the deciding item is kept, the next is not.handle.interrupt(), antigravityconversation.cancel()— cuts the in-flight generation so decided rows stop billing.STOPPED_EARLY, crashed=False; the interrupted tool call is force-closed asUNRESOLVEDand survives into the frozen trajectory, so the authoritativecheck_allstill sees the engagement.AgentCrashError— a crash would trigger the orchestrator retry with the watcher's decision still latched, stopping the retry at turn 0. The same exceptions without a stop still crash, unchanged.RuntimeErroris conditional on not-stopped, and sub-agent rollout recovery is skipped on a stopped turn.Testing
tests/test_early_stop.py): seam suites for both agents — stop cuts the stream (no further pulls), interrupt/cancel called exactly once, cleanSTOPPED_EARLY,should_stop=None/always-False regression guards, stop-before-completion doesn't raise while a genuine stream death still does, timeout-beats-stop, post-stop exception stays clean and still crashes without a stop, codex skips sub-agent recovery. Validation accepts codex/antigravity; guardrail-1 tests rewritten against a dummy non-supporting agent (coverage kept); orchestrator-level wiring on a codex-typed task. Claude seam tests unmodified.pytest -m live): new codex cooperative-stop test — a real multi-step turn interrupted at its first ToolStart ends promptly asSTOPPED_EARLY(the only automated check of realhandle.interrupt()behavior). All 5 live codex tests pass.Dataset smoke (skill-activation suite, 24 rows via
--sample-per-stratum 1, per harness):criterion_passedatsdk_turn=1,tool_call=1–2(~4–7s); negative correctly ran to completionsuite.jsonwritten with 23 per-skillrecall.yesaggregates; zero crash-retries in any task logtool_call=1(~3–5s)make verifygreen: format, ruff, pyright, custom lint, 3,680 passed / 3 skipped, coverage 91.2%.Notes
max_turnscaveat (documented): for codex/antigravity onecommunicate()is one SDK turn, so the early-stop saving is the truncated tool-call/token tail within that turn, not avoided turns 2–3.stop_early: falseruns — that caveat stays documented.