Uh oh!
There was an error while loading. Please reload this page.
feat(criteria): make async the primary criterion-checking surface - #60
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
…rride-contract gap Addresses the review on PR #60: - BaseCriterion.__init_subclass__ now enforces the "_check_impl or _check_impl_async" override contract at class-definition time, for every entry point (register_criterion decorator, or CriterionRegistry.register called directly) — previously only register_criterion checked, and the fallback failure mode was a silent mutual-recursion RuntimeError instead of a clear TypeError. - handle_criterion_errors / handle_criterion_errors_async no longer erase their wrapped method's signature to `(...) -> CriterionResult`; typed with PEP 695 ParamSpec + Concatenate so a caller passing the wrong criterion model or extra args is still caught statically. - check_all_async now runs the sync-criteria to_thread batch to completion BEFORE starting the native-async (judge) batch, instead of overlapping them — several first-party sync criteria mutate the sandbox (run_command, uipath_eval) while judge criteria read it, so the previous overlap could make a judge's score depend on how far a concurrent sandbox-mutating command happened to get. Judge criteria still run concurrently with EACH OTHER, which is the actual GH #55 fix. - The native-async gather now uses return_exceptions=True and re-raises only after every sibling settles, so a JudgeInfrastructureError no longer orphans sibling judge work (e.g. an agent_judge sub-agent left running against a sandbox already being torn down). - Added/repaired tests: native-async dispatch is now pinned against the *real* registered llm_judge/agent_judge checkers (not just fakes); check_all_async's reference_code/reference_dir persistence has a regression test; a two-criterion test covers the no-orphaned-siblings gather semantics; test_registry's context-kwarg conformance check now inspects whichever of _check_impl/_check_impl_async a checker actually overrides instead of vacuously passing for llm_judge/agent_judge. - Fixed stale docstrings/docs naming the old sync invoke_*_judge symbols or describing check_all() as the orchestrator's entry point; briefly documented the dual-surface contract in docs/EXTENDING.md and CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ck event loop, harden dispatch Second pass over the PR #60 review, covering the remaining nits and "what's missing" items not yet handled by the previous commit: - llm_judge / agent_judge now offload JudgeContextBuilder.build() (sandbox + reference file reads) to a worker thread via asyncio.to_thread instead of doing that synchronous file I/O directly on the event loop — the whole point of these checkers being "native async" is that they never block the loop, and this closed the one place they still did. - SubAgentRunner's finally-block cleanup no longer awaits shutil.rmtree via asyncio.to_thread — run_async is awaited directly on the orchestrator's loop (not under its own asyncio.run on a worker thread), so that await was reachable by cancellation (e.g. the task_timeout watchdog) and could skip cleanup, leaking the mkdtemp sandbox copy. It's now a plain synchronous rmtree (best-effort, bounded) that cancellation can't interrupt mid-copy, with a regression test that cancels run_async mid-communicate and asserts judge_dir is gone. - _is_native_async now classifies via the registered CLASS (CriterionRegistry.get_checker), not a constructed instance — it runs before any per-criterion error boundary in check_all_async, so a checker constructor failure no longer escapes and aborts the whole task instead of scoring that one criterion 0. - Collapsed the sync/async twin duplication in SuccessChecker: the reference/turn-record persist preamble (previously copy-pasted into check/check_all/check_all_async) is now one _resolve_refs() helper: the result-shaping + logging tail of _check_single/_check_single_async (previously a ~30-line verbatim clone apart from one line) is now shared via _finalize_result/_missing_checker_result/_error_result, so the two methods differ only in checker.check(...) vs await checker.check_async(...). - check_all_async's dead early-return-on-empty guard removed (the two dispatch batches already no-op on empty input) and the O(n^2) index-membership scan now uses a set; the [None]*len prefill is resolved with an assert + cast instead of a blanket # type: ignore. - Replaced the two wall-clock-timing concurrency assertions in test_check_all_async.py with a deterministic rendezvous barrier (_ConcurrencyProbe) / ordered event markers, and added an end-to-end regression test using the REAL registered run_command + llm_judge checkers (not fakes) proving the sequencing fix: a judge now reliably sees a file a concurrent run_command wrote, where it flakily wouldn't before. - Added check_all_async's missing Args/Returns docstring block (parity with its sync twin). Deliberately NOT done (flagged in review as "Harness & Lint Improvements", scoped as new repo-wide tooling/policy investments rather than defects in this PR's diff — better suited to a follow-up issue than bundled in): new CE0xx lint rules (CE026/032-038), dropping tests/ from the pyright exclude, reportImplicitOverride / reportUnnecessaryTypeIgnoreComment pyright config, ruff PGH lint selection, a diff-cover patch-coverage gate, a duplicate-code detector step, and a global judge-concurrency semaphore for nightly batch runs (task-level --max-parallel already caps total fan-out; the concurrency added here is only across one task's own 1-3 judge criteria, not an unbounded multiplier). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
akshaylive
commented
Jul 28, 2026
Addressed the review across two commits (7dcb8b6 → d4e6f87): Blockers
Non-blocking / nits
Deliberately not done — flagged as new repo-wide tooling/policy investments rather than defects in this diff, better suited to a follow-up issue: the ~13 new
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
…and silent misuse scoring Addresses the second review pass on PR #60 — three real scoring/correctness blockers plus the contract/API-surface follow-ups: - check_all_async now splits criteria into maximal CONTIGUOUS runs of the same kind (sync vs native-async) and executes the runs strictly in declaration order, instead of always running the whole sync batch before any async criterion. The previous scheme inverted declaration order for e.g. [llm_judge, run_command] — the judge, though declared first, always ran after run_command, silently grading post-mutation sandbox state instead of the pre-mutation state check_all's serial order would give it. Adjacent judges in the same run still gather concurrently (the actual GH #55 fix); results are built from a dict proven total over all runs, replacing the previous assert-guarded cast with a real KeyError on any future gap. Every captured sibling exception in a run is now logged (not silently dropped) before the first is re-raised. - SubAgentRunner.run_async no longer leaks a full sandbox copy when cancelled mid-copytree: judge_dir is bound with a plain synchronous tempfile.mkdtemp (one syscall, not cancellable, not worth to_thread's cancellation-window cost), and the copytree/rmtree to_thread calls are wrapped in asyncio.shield + tracked in a pending list that `finally` awaits BEFORE its own rmtree — so an orphaned worker thread can no longer recreate files after cleanup already ran. Applied the same await-in-finally fix to simulation/user_simulator.py's scratch-dir teardown (same class of leak, previously only fixed in sub_agent.py). - BaseCriterion._check_impl's derived asyncio.run bridge now detects a running event loop and raises a new CheckerMisuseError (which handle_criterion_errors(_async) escalate, like JudgeInfrastructureError) instead of letting asyncio.run's RuntimeError get silently swallowed into a scored-0.0 CriterionResult — a library/embedder calling the public sync check()/check_all() on an async-only checker from async host code now gets a loud, named error instead of a wrong score. - __init_subclass__ now enforces "exactly one", not just "at least one": a checker overriding BOTH _check_impl and _check_impl_async is also rejected (two live implementations that could drift into different scores depending on which entry point ran). Added an `abstract=True` class-kwarg escape hatch for intentional abstract intermediate bases, and a __new__ guard so BaseCriterion itself can't be instantiated directly (lost when @AbstractMethod was dropped). - Promoted the native-async capability check to a public, typed BaseCriterion.is_native_async() classmethod; SuccessChecker._is_native_async and test_registry.py now call it instead of comparing `_check_impl_async` identity across package boundaries. Decorated check()/check_async() with @typing.final so the "these are FINAL" docstring contract is pyright-enforced. Extracted the duplicated 18-line error-capture tail out of handle_criterion_errors/_async into one shared _failed_result() helper. - Tests: reversed-declaration-order regression test (real llm_judge + run_command checkers) proving the contiguous-run fix; a cancel-during-copy test on SubAgentRunner reproducing and closing the leak; a loud-CheckerMisuseError-from-a-running-loop test; both-overridden / abstract=True / direct-instantiation tests for the tightened __init_subclass__ contract; a thread-affinity assertion (not just score) for the derived async bridge; replaced two inert TestNativeAsyncDetection tests (injected into _checker_instances, which classification never reads) with registry-based ones that actually exercise the class-based dispatch rule; a checker-__init__-failure test closing the last uncovered branch in _check_single_async; one check_all_async happy-path test each for llm_judge/agent_judge (production's actual entry point — every other test in both files still drives the derived sync bridge). - Fixed stale ``check_all`` (vs check_all_async) mentions in CLAUDE.md, early_stop.py, reports.py, and orchestrator.py; documented the must-not-block-the-loop obligation and the CheckerMisuseError caveat in docs/EXTENDING.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
akshaylive
commented
Jul 28, 2026
Addressed the second review pass (commit a91cf5c → 23929f4). Blockers fixed
Contract/API-surface follow-ups
Deliberately not implemented — the "Harness & Lint Improvements" follow-up comment proposed ~8 new |
…rride-contract gap Addresses the review on PR #60: - BaseCriterion.__init_subclass__ now enforces the "_check_impl or _check_impl_async" override contract at class-definition time, for every entry point (register_criterion decorator, or CriterionRegistry.register called directly) — previously only register_criterion checked, and the fallback failure mode was a silent mutual-recursion RuntimeError instead of a clear TypeError. - handle_criterion_errors / handle_criterion_errors_async no longer erase their wrapped method's signature to `(...) -> CriterionResult`; typed with PEP 695 ParamSpec + Concatenate so a caller passing the wrong criterion model or extra args is still caught statically. - check_all_async now runs the sync-criteria to_thread batch to completion BEFORE starting the native-async (judge) batch, instead of overlapping them — several first-party sync criteria mutate the sandbox (run_command, uipath_eval) while judge criteria read it, so the previous overlap could make a judge's score depend on how far a concurrent sandbox-mutating command happened to get. Judge criteria still run concurrently with EACH OTHER, which is the actual GH #55 fix. - The native-async gather now uses return_exceptions=True and re-raises only after every sibling settles, so a JudgeInfrastructureError no longer orphans sibling judge work (e.g. an agent_judge sub-agent left running against a sandbox already being torn down). - Added/repaired tests: native-async dispatch is now pinned against the *real* registered llm_judge/agent_judge checkers (not just fakes); check_all_async's reference_code/reference_dir persistence has a regression test; a two-criterion test covers the no-orphaned-siblings gather semantics; test_registry's context-kwarg conformance check now inspects whichever of _check_impl/_check_impl_async a checker actually overrides instead of vacuously passing for llm_judge/agent_judge. - Fixed stale docstrings/docs naming the old sync invoke_*_judge symbols or describing check_all() as the orchestrator's entry point; briefly documented the dual-surface contract in docs/EXTENDING.md and CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ck event loop, harden dispatch Second pass over the PR #60 review, covering the remaining nits and "what's missing" items not yet handled by the previous commit: - llm_judge / agent_judge now offload JudgeContextBuilder.build() (sandbox + reference file reads) to a worker thread via asyncio.to_thread instead of doing that synchronous file I/O directly on the event loop — the whole point of these checkers being "native async" is that they never block the loop, and this closed the one place they still did. - SubAgentRunner's finally-block cleanup no longer awaits shutil.rmtree via asyncio.to_thread — run_async is awaited directly on the orchestrator's loop (not under its own asyncio.run on a worker thread), so that await was reachable by cancellation (e.g. the task_timeout watchdog) and could skip cleanup, leaking the mkdtemp sandbox copy. It's now a plain synchronous rmtree (best-effort, bounded) that cancellation can't interrupt mid-copy, with a regression test that cancels run_async mid-communicate and asserts judge_dir is gone. - _is_native_async now classifies via the registered CLASS (CriterionRegistry.get_checker), not a constructed instance — it runs before any per-criterion error boundary in check_all_async, so a checker constructor failure no longer escapes and aborts the whole task instead of scoring that one criterion 0. - Collapsed the sync/async twin duplication in SuccessChecker: the reference/turn-record persist preamble (previously copy-pasted into check/check_all/check_all_async) is now one _resolve_refs() helper: the result-shaping + logging tail of _check_single/_check_single_async (previously a ~30-line verbatim clone apart from one line) is now shared via _finalize_result/_missing_checker_result/_error_result, so the two methods differ only in checker.check(...) vs await checker.check_async(...). - check_all_async's dead early-return-on-empty guard removed (the two dispatch batches already no-op on empty input) and the O(n^2) index-membership scan now uses a set; the [None]*len prefill is resolved with an assert + cast instead of a blanket # type: ignore. - Replaced the two wall-clock-timing concurrency assertions in test_check_all_async.py with a deterministic rendezvous barrier (_ConcurrencyProbe) / ordered event markers, and added an end-to-end regression test using the REAL registered run_command + llm_judge checkers (not fakes) proving the sequencing fix: a judge now reliably sees a file a concurrent run_command wrote, where it flakily wouldn't before. - Added check_all_async's missing Args/Returns docstring block (parity with its sync twin). Deliberately NOT done (flagged in review as "Harness & Lint Improvements", scoped as new repo-wide tooling/policy investments rather than defects in this PR's diff — better suited to a follow-up issue than bundled in): new CE0xx lint rules (CE026/032-038), dropping tests/ from the pyright exclude, reportImplicitOverride / reportUnnecessaryTypeIgnoreComment pyright config, ruff PGH lint selection, a diff-cover patch-coverage gate, a duplicate-code detector step, and a global judge-concurrency semaphore for nightly batch runs (task-level --max-parallel already caps total fan-out; the concurrency added here is only across one task's own 1-3 judge criteria, not an unbounded multiplier). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…and silent misuse scoring Addresses the second review pass on PR #60 — three real scoring/correctness blockers plus the contract/API-surface follow-ups: - check_all_async now splits criteria into maximal CONTIGUOUS runs of the same kind (sync vs native-async) and executes the runs strictly in declaration order, instead of always running the whole sync batch before any async criterion. The previous scheme inverted declaration order for e.g. [llm_judge, run_command] — the judge, though declared first, always ran after run_command, silently grading post-mutation sandbox state instead of the pre-mutation state check_all's serial order would give it. Adjacent judges in the same run still gather concurrently (the actual GH #55 fix); results are built from a dict proven total over all runs, replacing the previous assert-guarded cast with a real KeyError on any future gap. Every captured sibling exception in a run is now logged (not silently dropped) before the first is re-raised. - SubAgentRunner.run_async no longer leaks a full sandbox copy when cancelled mid-copytree: judge_dir is bound with a plain synchronous tempfile.mkdtemp (one syscall, not cancellable, not worth to_thread's cancellation-window cost), and the copytree/rmtree to_thread calls are wrapped in asyncio.shield + tracked in a pending list that `finally` awaits BEFORE its own rmtree — so an orphaned worker thread can no longer recreate files after cleanup already ran. Applied the same await-in-finally fix to simulation/user_simulator.py's scratch-dir teardown (same class of leak, previously only fixed in sub_agent.py). - BaseCriterion._check_impl's derived asyncio.run bridge now detects a running event loop and raises a new CheckerMisuseError (which handle_criterion_errors(_async) escalate, like JudgeInfrastructureError) instead of letting asyncio.run's RuntimeError get silently swallowed into a scored-0.0 CriterionResult — a library/embedder calling the public sync check()/check_all() on an async-only checker from async host code now gets a loud, named error instead of a wrong score. - __init_subclass__ now enforces "exactly one", not just "at least one": a checker overriding BOTH _check_impl and _check_impl_async is also rejected (two live implementations that could drift into different scores depending on which entry point ran). Added an `abstract=True` class-kwarg escape hatch for intentional abstract intermediate bases, and a __new__ guard so BaseCriterion itself can't be instantiated directly (lost when @AbstractMethod was dropped). - Promoted the native-async capability check to a public, typed BaseCriterion.is_native_async() classmethod; SuccessChecker._is_native_async and test_registry.py now call it instead of comparing `_check_impl_async` identity across package boundaries. Decorated check()/check_async() with @typing.final so the "these are FINAL" docstring contract is pyright-enforced. Extracted the duplicated 18-line error-capture tail out of handle_criterion_errors/_async into one shared _failed_result() helper. - Tests: reversed-declaration-order regression test (real llm_judge + run_command checkers) proving the contiguous-run fix; a cancel-during-copy test on SubAgentRunner reproducing and closing the leak; a loud-CheckerMisuseError-from-a-running-loop test; both-overridden / abstract=True / direct-instantiation tests for the tightened __init_subclass__ contract; a thread-affinity assertion (not just score) for the derived async bridge; replaced two inert TestNativeAsyncDetection tests (injected into _checker_instances, which classification never reads) with registry-based ones that actually exercise the class-based dispatch rule; a checker-__init__-failure test closing the last uncovered branch in _check_single_async; one check_all_async happy-path test each for llm_judge/agent_judge (production's actual entry point — every other test in both files still drives the derived sync bridge). - Fixed stale ``check_all`` (vs check_all_async) mentions in CLAUDE.md, early_stop.py, reports.py, and orchestrator.py; documented the must-not-block-the-loop obligation and the CheckerMisuseError caveat in docs/EXTENDING.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
23929f4 to
d42e19dCompare
bai-uipath
left a comment
There was a problem hiding this comment.
Approve: one fix, rest optional. Low risk. Design is right, conversion is faithful, and dropping the concurrent dispatch instead of patching its ordering bug was the correct call.
Fix before merge: CheckerMisuseError gets converted back into score=0.0._check_single/_check_single_async only re-raise JudgeInfrastructureError, so it falls into except Exception and lands as a scored-0.0 result (evaluation/checker.py:363, :392). Verified at HEAD: a judge returning 0.87 scores 0.0 through SuccessChecker.check_all() from a running loop. Fix: use base._ESCALATING_EXCEPTIONS in both arms, plus a test through SuccessChecker (the existing one only covers BaseCriterion.check(), so CI can't see it).
Worth one manual run: agent_judge now shares the loop with a live main-agent SDK session in the per-turn simulation path, i.e. two anyio SDK clients on one loop, which is the hazard agents/watchdog.py documents. E2E smoke covers single-shot only.
Optional: collapse the three shielded copytree/rmtree calls into one to_thread over a staging helper, which kills the pending accumulator (sub_agent.py:150-200); and drop the guards with no in-tree consumer (abstract=True, the "not both" arm, _check_single_async's self-labelled unreachable KeyError arm).
Minor: run_async's docstring says self._pending, it's a local. base.py is 212 code lines against 245 of docstring, already drifted once. File the follow-up issue for the deferred dispatch before #55 closes.
Good: 47 tests in the changed areas including a real cancel-during-copytree case, green in 9s.
BaseCriterion previously required every checker to implement the sync _check_impl, with async only a to-thread-wrapped afterthought — so the prior fix for judge-criteria serialization (llm_judge/agent_judge) had to hand-maintain two near-duplicate implementations (a sync client + an async client) side by side. Flip the relationship: _check_impl_async is now the primary surface, with each of _check_impl / _check_impl_async deriving a default for the other (to_thread for sync-only checkers, asyncio.run for async-only ones). register_criterion enforces overriding at least one. llm_judge and agent_judge now implement ONLY _check_impl_async (AsyncAnthropic, httpx.AsyncClient, SubAgentRunner.run_async) — no sync client duplicated. SubAgentRunner.run() (sync) is removed since nothing calls it anymore; run_async() is the sole entrypoint. SuccessChecker.check_all_async detects "native async" checkers by introspecting whether _check_impl_async is overridden (no more explicit supports_native_async flag) and gathers them concurrently with each other while the remaining sync criteria still share one to_thread slot — fixing GH #55 (judge criteria serializing / pinning threads inside check_all). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rride-contract gap Addresses the review on PR #60: - BaseCriterion.__init_subclass__ now enforces the "_check_impl or _check_impl_async" override contract at class-definition time, for every entry point (register_criterion decorator, or CriterionRegistry.register called directly) — previously only register_criterion checked, and the fallback failure mode was a silent mutual-recursion RuntimeError instead of a clear TypeError. - handle_criterion_errors / handle_criterion_errors_async no longer erase their wrapped method's signature to `(...) -> CriterionResult`; typed with PEP 695 ParamSpec + Concatenate so a caller passing the wrong criterion model or extra args is still caught statically. - check_all_async now runs the sync-criteria to_thread batch to completion BEFORE starting the native-async (judge) batch, instead of overlapping them — several first-party sync criteria mutate the sandbox (run_command, uipath_eval) while judge criteria read it, so the previous overlap could make a judge's score depend on how far a concurrent sandbox-mutating command happened to get. Judge criteria still run concurrently with EACH OTHER, which is the actual GH #55 fix. - The native-async gather now uses return_exceptions=True and re-raises only after every sibling settles, so a JudgeInfrastructureError no longer orphans sibling judge work (e.g. an agent_judge sub-agent left running against a sandbox already being torn down). - Added/repaired tests: native-async dispatch is now pinned against the *real* registered llm_judge/agent_judge checkers (not just fakes); check_all_async's reference_code/reference_dir persistence has a regression test; a two-criterion test covers the no-orphaned-siblings gather semantics; test_registry's context-kwarg conformance check now inspects whichever of _check_impl/_check_impl_async a checker actually overrides instead of vacuously passing for llm_judge/agent_judge. - Fixed stale docstrings/docs naming the old sync invoke_*_judge symbols or describing check_all() as the orchestrator's entry point; briefly documented the dual-surface contract in docs/EXTENDING.md and CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ck event loop, harden dispatch Second pass over the PR #60 review, covering the remaining nits and "what's missing" items not yet handled by the previous commit: - llm_judge / agent_judge now offload JudgeContextBuilder.build() (sandbox + reference file reads) to a worker thread via asyncio.to_thread instead of doing that synchronous file I/O directly on the event loop — the whole point of these checkers being "native async" is that they never block the loop, and this closed the one place they still did. - SubAgentRunner's finally-block cleanup no longer awaits shutil.rmtree via asyncio.to_thread — run_async is awaited directly on the orchestrator's loop (not under its own asyncio.run on a worker thread), so that await was reachable by cancellation (e.g. the task_timeout watchdog) and could skip cleanup, leaking the mkdtemp sandbox copy. It's now a plain synchronous rmtree (best-effort, bounded) that cancellation can't interrupt mid-copy, with a regression test that cancels run_async mid-communicate and asserts judge_dir is gone. - _is_native_async now classifies via the registered CLASS (CriterionRegistry.get_checker), not a constructed instance — it runs before any per-criterion error boundary in check_all_async, so a checker constructor failure no longer escapes and aborts the whole task instead of scoring that one criterion 0. - Collapsed the sync/async twin duplication in SuccessChecker: the reference/turn-record persist preamble (previously copy-pasted into check/check_all/check_all_async) is now one _resolve_refs() helper: the result-shaping + logging tail of _check_single/_check_single_async (previously a ~30-line verbatim clone apart from one line) is now shared via _finalize_result/_missing_checker_result/_error_result, so the two methods differ only in checker.check(...) vs await checker.check_async(...). - check_all_async's dead early-return-on-empty guard removed (the two dispatch batches already no-op on empty input) and the O(n^2) index-membership scan now uses a set; the [None]*len prefill is resolved with an assert + cast instead of a blanket # type: ignore. - Replaced the two wall-clock-timing concurrency assertions in test_check_all_async.py with a deterministic rendezvous barrier (_ConcurrencyProbe) / ordered event markers, and added an end-to-end regression test using the REAL registered run_command + llm_judge checkers (not fakes) proving the sequencing fix: a judge now reliably sees a file a concurrent run_command wrote, where it flakily wouldn't before. - Added check_all_async's missing Args/Returns docstring block (parity with its sync twin). Deliberately NOT done (flagged in review as "Harness & Lint Improvements", scoped as new repo-wide tooling/policy investments rather than defects in this PR's diff — better suited to a follow-up issue than bundled in): new CE0xx lint rules (CE026/032-038), dropping tests/ from the pyright exclude, reportImplicitOverride / reportUnnecessaryTypeIgnoreComment pyright config, ruff PGH lint selection, a diff-cover patch-coverage gate, a duplicate-code detector step, and a global judge-concurrency semaphore for nightly batch runs (task-level --max-parallel already caps total fan-out; the concurrency added here is only across one task's own 1-3 judge criteria, not an unbounded multiplier). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- handle_criterion_errors / handle_criterion_errors_async: revert from the
PEP 695 `def f[**P](...)` type-parameter form back to a module-level
`P = ParamSpec("P")` (with a targeted `# noqa: UP047` for ruff) — CodeQL's
Python extractor doesn't parse PEP 695 type params referenced via
`P.args`/`P.kwargs` and was flagging `P` as "potentially uninitialized".
- test_check_all_async.py: build the both-defaults-unimplemented checker via
`types.new_class(...)` instead of a `class _NeitherChecker(...):` statement
inside `pytest.raises` — the name was never usable afterward anyway
(`__init_subclass__` raises before the class object exists), so CodeQL
flagged it as an unused local. `type(name, bases, ns)` doesn't support MRO
entry resolution for the generic alias `BaseCriterion[FileExistsCriterion]`,
hence `types.new_class` rather than a bare `type(...)` call.
- test_sub_agent_runner.py: `_ = await task` instead of a bare `await task`
statement inside the cancellation regression test — CodeQL flagged the
bare await as a no-op statement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>…and silent misuse scoring Addresses the second review pass on PR #60 — three real scoring/correctness blockers plus the contract/API-surface follow-ups: - check_all_async now splits criteria into maximal CONTIGUOUS runs of the same kind (sync vs native-async) and executes the runs strictly in declaration order, instead of always running the whole sync batch before any async criterion. The previous scheme inverted declaration order for e.g. [llm_judge, run_command] — the judge, though declared first, always ran after run_command, silently grading post-mutation sandbox state instead of the pre-mutation state check_all's serial order would give it. Adjacent judges in the same run still gather concurrently (the actual GH #55 fix); results are built from a dict proven total over all runs, replacing the previous assert-guarded cast with a real KeyError on any future gap. Every captured sibling exception in a run is now logged (not silently dropped) before the first is re-raised. - SubAgentRunner.run_async no longer leaks a full sandbox copy when cancelled mid-copytree: judge_dir is bound with a plain synchronous tempfile.mkdtemp (one syscall, not cancellable, not worth to_thread's cancellation-window cost), and the copytree/rmtree to_thread calls are wrapped in asyncio.shield + tracked in a pending list that `finally` awaits BEFORE its own rmtree — so an orphaned worker thread can no longer recreate files after cleanup already ran. Applied the same await-in-finally fix to simulation/user_simulator.py's scratch-dir teardown (same class of leak, previously only fixed in sub_agent.py). - BaseCriterion._check_impl's derived asyncio.run bridge now detects a running event loop and raises a new CheckerMisuseError (which handle_criterion_errors(_async) escalate, like JudgeInfrastructureError) instead of letting asyncio.run's RuntimeError get silently swallowed into a scored-0.0 CriterionResult — a library/embedder calling the public sync check()/check_all() on an async-only checker from async host code now gets a loud, named error instead of a wrong score. - __init_subclass__ now enforces "exactly one", not just "at least one": a checker overriding BOTH _check_impl and _check_impl_async is also rejected (two live implementations that could drift into different scores depending on which entry point ran). Added an `abstract=True` class-kwarg escape hatch for intentional abstract intermediate bases, and a __new__ guard so BaseCriterion itself can't be instantiated directly (lost when @AbstractMethod was dropped). - Promoted the native-async capability check to a public, typed BaseCriterion.is_native_async() classmethod; SuccessChecker._is_native_async and test_registry.py now call it instead of comparing `_check_impl_async` identity across package boundaries. Decorated check()/check_async() with @typing.final so the "these are FINAL" docstring contract is pyright-enforced. Extracted the duplicated 18-line error-capture tail out of handle_criterion_errors/_async into one shared _failed_result() helper. - Tests: reversed-declaration-order regression test (real llm_judge + run_command checkers) proving the contiguous-run fix; a cancel-during-copy test on SubAgentRunner reproducing and closing the leak; a loud-CheckerMisuseError-from-a-running-loop test; both-overridden / abstract=True / direct-instantiation tests for the tightened __init_subclass__ contract; a thread-affinity assertion (not just score) for the derived async bridge; replaced two inert TestNativeAsyncDetection tests (injected into _checker_instances, which classification never reads) with registry-based ones that actually exercise the class-based dispatch rule; a checker-__init__-failure test closing the last uncovered branch in _check_single_async; one check_all_async happy-path test each for llm_judge/agent_judge (production's actual entry point — every other test in both files still drives the derived sync bridge). - Fixed stale ``check_all`` (vs check_all_async) mentions in CLAUDE.md, early_stop.py, reports.py, and orchestrator.py; documented the must-not-block-the-loop obligation and the CheckerMisuseError caveat in docs/EXTENDING.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cy to a follow-up PR check_all_async now runs every criterion strictly sequentially, in declaration order — identical order/isolation semantics to the sync check_all — instead of gathering adjacent native-async (judge) criteria concurrently. The concurrent-dispatch scheduling change (the original GH #55 motivation) is deferred to a follow-up PR so it can be reviewed on its own. What's kept from the async-primary refactor: BaseCriterion's async-primary _check_impl/_check_impl_async derivation, is_native_async(), the __init_subclass__ override contract (exactly one, abstract=True escape hatch), CheckerMisuseError for a misused sync bridge, and native-async criteria (llm_judge/agent_judge) being awaited directly on the loop instead of pinning a thread-pool thread — none of that requires concurrent dispatch to be worthwhile on its own. check_all_async's docstring, judge_anthropic.py/judge_bedrock.py/ sub_agent.py's "concurrently with other judges" claims, and docs/EXTENDING.md's native-async guidance are updated to describe the current sequential behavior and note where the follow-up PR picks up. Tests: the concurrency-proving tests in test_check_all_async.py (rendezvous barrier, gather-return_exceptions sibling-settling) are replaced with sequential-execution tests (ordered event markers proving NO overlap, and a test that a JudgeInfrastructureError stops remaining criteria outright rather than letting siblings finish, since nothing runs concurrently anymore). Also fixes tests/test_route_seam_exhaustiveness.py, which the main-branch LiteLLM routing PR added against the pre-refactor sync invoke_bedrock_judge/ invoke_anthropic_judge names and a sync _invoke_tool_channel — rebased onto this branch's renamed *_async functions and async _invoke_tool_channel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tead of scoring 0.0 _check_single/_check_single_async only re-raised JudgeInfrastructureError, so CheckerMisuseError fell into the generic except-Exception arm and silently scored a criterion 0.0 instead of escalating like the reviewer's prior fix intended. Both now use base._ESCALATING_EXCEPTIONS, with a regression test through SuccessChecker.check (the entry point a caller actually uses, not just BaseCriterion.check() directly). Also bump the minor version to 0.9.0 (action.yml pin + CHANGELOG entry kept in sync).
d42e19d to
1791c8cCompareUh oh!
There was an error while loading. Please reload this page.
Summary
Redesign of the fix for #55, replacing PR #58. That PR made judge criteria (
llm_judge/agent_judge) concurrent by bolting an async path onto a sync-firstBaseCriterion— each judge checker ended up with two near-duplicate implementations (a sync client + an async client) plus asupports_native_asyncmarker flag to tellSuccessCheckerwhich path to dispatch through. This PR inverts that relationship instead of layering on top of it:BaseCriterion._check_impl_asyncis now the primary implementation surface. A checker overrides exactly one of_check_impl(sync) or_check_impl_async(async) — whichever is its natural form — and the base class derives the other automatically:file_exists,command_executed, ...) keep overriding_check_implonly; the default_check_impl_asyncoffloads it viaasyncio.to_thread.llm_judge/agent_judgenow override only_check_impl_async, usingAsyncAnthropic,httpx.AsyncClient, and a newSubAgentRunner.run_async— no sync client duplicated. The default_check_implderives a sync call viaasyncio.run(...)for any caller that still wants one (tests, etc.).register_criterionenforces that a checker overrides at least one of the two (overriding neither would recurse forever between the defaults).SuccessChecker.check_all_asyncno longer needs an explicit "is this checker async-native" flag — it detects it by introspecting whether_check_impl_asyncis overridden, then gathers those checkers concurrently with each other while the remaining sync checkers still share oneasyncio.to_threadslot (unchanged behavior for the 12 non-judge criteria).SubAgentRunner.run()(sync) is removed — nothing calls it anymore now thatagent_judgeis async-only;run_async()is the sole entrypoint.Net effect on behavior is identical to #58 (judge criteria run concurrently instead of serializing, and don't pin a thread-pool thread for the network wait) — this PR just gets there with one implementation per criterion instead of two.
Test plan
make check/make typecheck/make lintcleanmake verify— 3582 passed, 2 skipped, 90.99% coveragetests/test_check_all_async.py— judge criteria overlap instead of serializing, sync/async batches overlap, ordering preserved, error handling parity,_is_native_asyncdetection,register_criterionguard, and direct sync↔async derivation onBaseCriteriontests/test_judge_anthropic.py/tests/test_judge_bedrock.pyrewritten against the async invokerstests/test_sub_agent_runner.pyconverted torun_asyncCloses#55 (supersedes #58 — closing that PR in favor of this one)
🤖 Generated with Claude Code