Skip to content

feat(agents): extend cooperative early stop to codex and antigravity - #64

Merged
mohsen-uipath merged 1 commit into
mainfrom
feat/early-stop-codex-antigravity
Jul 29, 2026
Merged

feat(agents): extend cooperative early stop to codex and antigravity#64
mohsen-uipath merged 1 commit into
mainfrom
feat/early-stop-codex-antigravity

Conversation

@mohsen-uipath

@mohsen-uipath mohsen-uipath commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

Widens early stop from Claude Code to all three built-in agents: CodexAgent and AntigravityAgent now honor the cooperative should_stop interrupt, 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:

  • Poll placement: 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.
  • Server-side abort (best-effort): codex handle.interrupt(), antigravity conversation.cancel() — cuts the in-flight generation so decided rows stop billing.
  • Clean finalize: STOPPED_EARLY, crashed=False; the interrupted tool call is force-closed as UNRESOLVED and survives into the frozen trajectory, so the authoritative check_all still sees the engagement.
  • Retry-poisoning guard: once a turn has stopped cooperatively, a post-stop exception (stream/generator cleanup, a raising interrupt/cancel) no longer escalates to 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.
  • Precedence: timeout > cooperative stop > completed, on every path.
  • Codex only: the "turn never completed" RuntimeError is conditional on not-stopped, and sub-agent rollout recovery is skipped on a stopped turn.
  • Validator: only the stale "(currently Claude Code)" message text updated; non-supporting agents are still rejected.

Testing

  • Unit (tests/test_early_stop.py): seam suites for both agents — stop cuts the stream (no further pulls), interrupt/cancel called exactly once, clean STOPPED_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.
  • Live (marker-gated, pytest -m live): new codex cooperative-stop test — a real multi-step turn interrupted at its first ToolStart ends promptly as STOPPED_EARLY (the only automated check of real handle.interrupt() behavior). All 5 live codex tests pass.
    Dataset smoke (skill-activation suite, 24 rows via --sample-per-stratum 1, per harness):
Run Early-stop behavior Cost / artifacts
codex (armed) 24/24 pass; all 23 positives stopped criterion_passed at sdk_turn=1, tool_call=1–2 (~4–7s); negative correctly ran to completion suite.json written with 23 per-skill recall.yes aggregates; zero crash-retries in any task log
codex (unarmed control) runs the tasks out — 84 recorded tool calls vs 35 armed (armed transcripts end at the deciding call) $2.19 / 521 agent-seconds vs armed $0.33 / 173s (~6.6× cheaper)
antigravity (armed) 22/23 engaging positives stopped at tool_call=1 (~3–5s) the one non-stopped row never engaged its skill (correctly inert watcher) and hit the pre-existing 120s turn-timeout path
claude-code (regression control) unchanged — positives pass-stop at turn 1; a wrong-skill misfire correctly deferred the fail-stop and ran to cap with both verdicts preserved

make verify green: format, ruff, pyright, custom lint, 3,680 passed / 3 skipped, coverage 91.2%.

Notes

  • max_turns caveat (documented): for codex/antigravity one communicate() is one SDK turn, so the early-stop saving is the truncated tool-call/token tail within that turn, not avoided turns 2–3.
  • Armed runs remain optimistic for precision (a pass-stop can hide a later distractor misfire); authoritative precision still comes from stop_early: false runs — that caveat stays documented.

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.
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @mohsen-uipath's task in 1m 35s —— View job


Code Review for PR #64: Extend cooperative early stop to codex and antigravity

Working on review...

Todo List:

  • Read code review guidelines (.github/code_review.md)
  • Read project conventions (CLAUDE.md)
  • Review the full diff (git diff origin/main...HEAD)
  • Analyze changed files:
    • docs/agents/ANTIGRAVITY.md
    • docs/agents/CLAUDE_CODE.md
    • docs/agents/CODEX.md
    • src/coder_eval/agents/antigravity_agent.py
    • src/coder_eval/agents/codex_agent.py
    • src/coder_eval/orchestration/early_stop.py
    • tests/test_codex_agent_live.py
    • tests/test_early_stop.py
  • Check cross-file consistency
  • Identify what's missing
  • Post comprehensive review

@bai-uipath bai-uipath left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.10 pin already covers 0.9.1.
  • Worth knowing for priority: with the activation experiment armed, --type codex / antigravity abort at resolution and the runner has no override, so those activation slices are dead today. This fixes it; a one-line stop_early=false in eval_runner unblocks the nightly sooner and stays useful for authoritative precision runs.

@mohsen-uipath
mohsen-uipath merged commit b849421 into main Jul 29, 2026
19 checks passed
@mohsen-uipath
mohsen-uipath deleted the feat/early-stop-codex-antigravity branch July 29, 2026 19:04
@mohsen-uipath

Copy link
Copy Markdown
Contributor Author

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 --sample-per-stratum 1, gemini-3.5-flash), and the specific claim was confirmed from the run artifacts, not just from the rows stopping:

  • Deciding call lands as UNRESOLVED in the frozen trajectory: in all 23/23 stopped rows, the deciding tool call is the last recorded command with result_status=unknown (the UNRESOLVED force-close mapping) and carries the skills/<name>/ path in its parameters (a Read of the skill files — antigravity's engagement signature). So the real conversation.cancel() preserves exactly what the authoritative check_all needs, and the 24/24 SUCCESS verdicts are grounded in real engagements.
  • Armed run: 24/24 SUCCESS; all 23 positives pass-stopped at turn 1 (most at tool_call=1); 33 tool calls total; ~$0.19; ~7.5s/row; zero crashes/retries; the negative row correctly ran to completion.
  • Unarmed control (same 24 rows): 430 tool calls, ~$8.97, and 9 rows ERRORed on the experiment's 120s turn timeout. On this harness armed mode isn't just cheaper — it's what keeps the suite inside its turn limits at all.

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 GEMINI_API_KEY, asserting STOPPED_EARLY and the UNRESOLVED deciding call) will come in the follow-up PR along with the other three items (offload the codex interrupt off the event loop, suppress turns_remaining_at_stop for single-turn agents, and the orchestrator-level "don't retry once the watcher has fired" guard).

Downstream: release cut as v0.9.1 as suggested; the skills-repo pin bump + doc cleanup PR is next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants