Judge feedback batch: job-backed run of a pre-existing batch (eval parity) + deep-review fixes#1542
Conversation
…ops, describe Bring the judge_feedback_batch job worker to parity with the eval job worker (the auto-mode e2e integration flagged three gaps): - Progress: the worker reported progress only once at the end, and used train_set_size as the denominator — so when the matching set exceeded max_samples the bar stalled below 100% even on full success. The runner now takes an optional progress_callback and streams (num_judged, error_count, planned_total) per judged chunk; the worker reports live progress and its final snapshot against the planned (capped) count min(train_set_size, max_samples), so success reaches total on full coverage. - Metadata: publish JudgeFeedbackBatchJobProperties via describe() (mirrors EvalJobProperties) — judge/eval names, algorithm, model, mode, run config, tags, max_samples — and render them in the jobs table (previously just a raw "Judge_feedback_batch" label). - Errors: unchanged wiring already surfaces per-item errors to the View Errors UI; improved message quality by unwrapping KilnRunError.original (mirrors EvalJobWorker._error_detail). - API models: add field descriptions to JudgeFeedbackBatchJobResult and the project_id/task_id params. Regenerated api_schema.d.ts. Tests: runner progress_callback streaming, worker describe() + capped-total progress, frontend judge-property rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
…ish) Deep multi-phase review of the judge_feedback_batch feature (0 critical). Fixes: Moderate: - Progress double-counted errored items: the runner's num_judged already includes errored items, so reporting success=num_judged alongside a separate error count breached the processed=success+error contract and overshot 100%. Report success=num_judged-error_count in both the streamed callback and the final snapshot (worker), matching the eval reference's disjoint counters. - Gate-mode task_run_id pairing was defeated by the random shuffle when the tag set exceeded max_samples (two runs sampled disjoint subsets). Gate mode now selects deterministically (sorted by id) before capping; train-signal mode keeps the random minibatch. - Added a JudgeFeedbackBatch model_validator coupling generate_outputs=True to a required run_config_id and rejecting empty target_tags (defense in depth, mirrors EvalRun) — the API request model already validated both. - Documented that num_judged counts attempts (errors/cache included); use len(judged_runs) for a scored-item count. - Added runner tests for the empty candidate set and gate-mode hit_cap=True (set > max_samples), incl. proof that two gate runs cover the same ids. Polish: - Runner: save-error uses _error_detail; concurrency=max(1, concurrency) guard; mean-of-means comment; "Judge feedback batch" wording. - API: judge_feedback_batch_from_id 404 message fixed + accepts a preloaded task (removes the run endpoint's double task load); run docstring covers generate mode. - UI: surface eval_name + stop_after_failures in the jobs table. - Tests: worker run stub gets the full signature; judge-only describe() test. Full checks.sh green. Regenerated api_schema.d.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughUpdates judge feedback batch jobs to stream progress and errors, expose job properties for describe/UI use, enforce batch configuration invariants, and align backend API and web UI rendering with the new job shape. ChangesJudge Feedback Batch Progress, Describe, and Validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Coverage ReportOverall Coverage: 92% Diff: origin/integration/auto-mode-e2e...HEAD
Summary
Line-by-lineView line-by-line diff coveragelibs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.pyLines 57-65 57 (often generic) `format_error_message` text; the underlying cause survives on `.original`, so
58 surface that for the developer-facing error log. Mirrors `EvalJobWorker._error_detail`.
59 """
60 if isinstance(error, KilnRunError) and error.original is not None:
! 61 return str(error.original)
62 return str(error)
63
64
65 def score_passes(Lines 231-239 231 retry_delay: float = 2.0,
232 ):
233 task = judge_feedback_batch.parent_task()
234 if task is None:
! 235 raise ValueError("Judge feedback batch must have a parent task")
236 eval = eval_config.parent_eval()
237 if eval is None:
238 raise ValueError("Eval config must have a parent eval")Lines 376-384 376 error=f"Unexpected error judging item: {_error_detail(scored)}",
377 )
378 errors.append(unexpected_error)
379 if error_callback is not None:
! 380 await error_callback(unexpected_error)
381 continue
382 if scored.error is not None:
383 errors.append(scored.error)
384 if error_callback is not None:
|
There was a problem hiding this comment.
Code Review
This pull request introduces live progress streaming and detailed job properties for judge feedback batch jobs, enhancing both the backend runner and the frontend jobs UI. It also adds deterministic candidate selection in gate mode to ensure stable subsets across runs, along with model validation for batch configurations. The review feedback highlights a potential UI crash in the jobs table if target_tags is undefined for older or malformed jobs, suggesting optional chaining as a safeguard.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…nto leonard/judge-feedback-batch-job-polish
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/desktop/studio_server/jobs/workers/judge_feedback_batch.py`:
- Around line 216-251: The judge/save errors are only being forwarded to
ctx.report_error after runner.run completes, so the error count can update
before any messages exist in the View Errors UI. Update judge_feedback_batch.py
so errors are reported incrementally during execution by wiring an error
callback or observer through the runner path used by report_progress and
result.errors, similar to EvalJobWorker, and keep the final post-run snapshot
only as a catch-up for any remaining errors.
In `@libs/core/kiln_ai/datamodel/judge_feedback_batch.py`:
- Around line 113-124: The after-model validation in
JudgeFeedbackBatch.validate_config is now rejecting older persisted batches when
load_from_file() calls model_validate() directly. Update the loading path or add
a backward-compatibility migration so legacy files can still be read, and keep
the stricter checks only for newly created/edited batches; use the
JudgeFeedbackBatch.validate_config and load_from_file entry points to locate the
fix.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 34d8f9c9-c528-4eb2-8880-e3c2ac794eb5
📒 Files selected for processing (11)
app/desktop/studio_server/jobs/workers/judge_feedback_batch.pyapp/desktop/studio_server/jobs/workers/test_judge_feedback_batch.pyapp/desktop/studio_server/judge_feedback_batch_api.pyapp/web_ui/src/lib/api_schema.d.tsapp/web_ui/src/lib/components/jobs_table.svelteapp/web_ui/src/lib/components/jobs_table.test.tsapp/web_ui/src/lib/stores/jobs_api.tslibs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.pylibs/core/kiln_ai/adapters/eval/test_judge_feedback_batch_runner.pylibs/core/kiln_ai/datamodel/judge_feedback_batch.pylibs/core/kiln_ai/datamodel/test_judge_feedback_batch.py
… works mid-run
The streamed progress callback bumps progress.error per chunk, which makes the
"View Errors" button appear while the job is still running. But the error
MESSAGES were only written to the job error log in a post-run loop over
result.errors — after runner.run() returned. So mid-run the button showed but
the log was empty ("No error messages recorded"); the messages only appeared
once the job completed. This diverged from the eval worker, which logs each
error live via an observer.
Add an error_callback to JudgeFeedbackBatchRunner.run(), fired the moment each
per-item error is collected (both the judge/save error and the unexpected-throw
paths), and wire the worker to write it to the error log immediately. Errors
still appear in the returned result.errors for the synchronous endpoint (which
passes no callback). Removed the worker's post-run loop to avoid double-logging.
Tests: runner test asserting errors are delivered live (interleaved with
progress, not batched to the end); worker stub updated to emit errors via the
callback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
libs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.py (1)
374-393: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUnguarded callback exceptions can abort the whole run and discard already-judged results.
error_callbackandprogress_callbackare awaited directly with no try/except. If the caller's callback raises (e.g. the worker'sreport_progress/report_item_errorinjudge_feedback_batch.pydoing I/O against the job store), the exception propagates out ofrun(), and none of the already-collectedjudged_runs/errors/failing_runsfrom prior chunks get returned — a purely cosmetic reporting failure would fail the entire batch job. The worker layer doesn't wrapawait runner.run(...)in a try/except either, so nothing catches this downstream.Since the whole point of these hooks is to report progress/errors without changing outcome semantics, consider isolating callback failures so a broken reporter can't sink the run.
🛡️ Proposed fix: isolate callback exceptions
errors.append(unexpected_error) if error_callback is not None: - await error_callback(unexpected_error) + await self._safe_callback(error_callback, unexpected_error) continue if scored.error is not None: errors.append(scored.error) if error_callback is not None: - await error_callback(scored.error) + await self._safe_callback(error_callback, scored.error) if scored.run is not None: judged_runs.append(scored.run) if not scored.passed: failing_runs.append(scored.run) # Stream live progress against the planned (capped) count so a background job's bar # advances per chunk and reaches 100% on full coverage. if progress_callback is not None: - await progress_callback(num_judged, len(errors), len(candidates)) + await self._safe_callback( + progress_callback, num_judged, len(errors), len(candidates) + )async def _safe_callback(self, callback, *args) -> None: try: await callback(*args) except Exception: logger.exception( "Callback failed for judge feedback batch %s; continuing run", self.judge_feedback_batch.id, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.py` around lines 374 - 393, The callback hooks in judge feedback batch execution can throw and abort the whole run, so isolate failures in the `JudgeFeedbackBatchRunner.run` flow instead of letting them propagate. Wrap both `error_callback` and `progress_callback` awaits in a small safe helper or local try/except, log the callback failure, and continue so already collected `judged_runs`, `errors`, and `failing_runs` are still returned. Use the existing `error_callback`, `progress_callback`, and `run()` symbols to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@libs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.py`:
- Around line 374-393: The callback hooks in judge feedback batch execution can
throw and abort the whole run, so isolate failures in the
`JudgeFeedbackBatchRunner.run` flow instead of letting them propagate. Wrap both
`error_callback` and `progress_callback` awaits in a small safe helper or local
try/except, log the callback failure, and continue so already collected
`judged_runs`, `errors`, and `failing_runs` are still returned. Use the existing
`error_callback`, `progress_callback`, and `run()` symbols to locate the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e33f0b02-50d5-486d-b245-00c77c3e6739
📒 Files selected for processing (4)
app/desktop/studio_server/jobs/workers/judge_feedback_batch.pyapp/desktop/studio_server/jobs/workers/test_judge_feedback_batch.pylibs/core/kiln_ai/adapters/eval/judge_feedback_batch_runner.pylibs/core/kiln_ai/adapters/eval/test_judge_feedback_batch_runner.py
🚧 Files skipped from review as they are similar to previous changes (3)
- libs/core/kiln_ai/adapters/eval/test_judge_feedback_batch_runner.py
- app/desktop/studio_server/jobs/workers/test_judge_feedback_batch.py
- app/desktop/studio_server/jobs/workers/judge_feedback_batch.py
Defensive: use optional chaining on jp.target_tags so a job record with incomplete properties can't throw a TypeError and crash the whole jobs table render. (Per gemini-code-assist PR review.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
The batch config was written with a raw save_to_file() outside any atomic_write — in both the job worker and the synchronous create-and-run endpoint. Under auto git-sync that new (untracked) file is dirty working-tree state, so the runner's first per-item child write enters atomic_write, whose ensure_clean() treats it as a crashed session and stashes it away (include_untracked=True). Net effect: the batch config is never committed/pushed and is removed from the working tree, orphaning the committed child runs. Wrap the batch-config save in the same save_context used for the child writes (coalescing None -> no-op default_save_context) so it gets its own atomic_write / commit before the runner starts. The batch save and each child save are separate, non-nested atomic_write blocks, so re-entrancy is not a concern. Mirrors how EvalRunner already wraps every per-item write (the eval worker creates no parent entity, so it was already correct). - worker: judge_feedback_batch.py run() wraps the save. - sync endpoint: create_and_run_judge_feedback_batch (@no_write_lock) wraps it via build_save_context(request). - test: asserts the batch save goes through the git context and closes before the runner runs (enter -> exit -> run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
…arity)
Reshape the judge_feedback_batch job from "create-and-run" to "run an existing
batch", mirroring EvalJobWorker. The batch config is created first via the
synchronous POST .../judge_feedback_batches (which persists it under the git-sync
write lock), then the job just runs it by id.
Why:
- The batch id is now caller-supplied and lives in job.params, so it's
retrievable via GET /api/jobs/{id} even if the run fails — closing the gap
where a create-and-run job only surfaced the minted id in its success result.
- The worker no longer writes the config at all, so the earlier "wrap the batch
save in the git-sync context" workaround is gone — the create endpoint owns
that write. One less special case.
- Structurally identical to the eval job (params carry the entity id; results
live on disk), which sets up deriving a disk summary + a real compute_state
later.
Changes:
- JudgeFeedbackBatchJobParams: now {project_id, task_id, judge_feedback_batch_id}
(was a full CreateJudgeFeedbackBatchRequest). run()/describe() load the batch
by id and read its config off disk.
- Job result echoes judge_feedback_batch_id from params (no longer "created").
- Route docstring updated; regenerated api_schema.d.ts.
- Tests: run loads an existing batch, missing-batch 404s, describe/props derived
from the persisted batch. Dropped the now-obsolete config-save-wrap test.
The synchronous create-and-run / run endpoints are left in place (now redundant
with create + job) for callers not yet migrated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
…ints The synchronous create / run / create-and-run endpoints are being superseded by the create + job flow, so mark them DENY_AGENT (the chat assistant should go through POST /api/jobs/judge_feedback_batch/run for dashboard visibility). GETs stay ALLOW_AGENT. Drops the now-unused agent_policy_require_approval import. NOTE: not enforced until the agent-check annotation JSONs are regenerated (the policy lookup reads those, not the live spec). See PR notes re: keeping the create endpoint agent-callable for the new create-then-run-job flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
…nnotations Follow-up to the DENY_AGENT change: - Flip the sync create endpoint back to ALLOW_AGENT — the agent creates the batch here (cheap, no model calls) and then runs it via POST /api/jobs/judge_feedback_batch/run. Only the sync run / create-and-run stay DENY_AGENT (superseded by the job). - Regenerate the agent-check annotation JSONs so the policy is actually enforced (the chat backend reads the dumped JSONs, not the live spec) — fixes the check_api_bindings CI job. run / create-and-run now dump as "deny"; create and the GETs stay "allow". Final judge_feedback_batch agent policy: create -> allow run (existing, sync) -> deny create-and-run (sync) -> deny run as job (/api/jobs/...) -> allow + approval list / get / runs (GET) -> allow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N
…n-existing Judge feedback batch: job runs a pre-existing batch (eval parity)
…1536) * Add failing-train-examples eval API for reflective optimization New endpoint POST .../evals/{eval_id}/eval_config/{eval_config_id}/failing_train_examples that samples an Eval's train set, runs its judge (EvalConfig), and returns the datapoints that fail — with the judge's plaintext feedback — to feed GEPA-style reflection loops. - libs/core/.../eval/failing_examples.py: find_failing_train_examples() shuffles the train set (eval.train_set_filter_id), judges items in concurrent batches via the eval_config_eval path, and stops once `count` failures are found or `max_samples` items are judged ("oversample, return the requested amount"). An example fails only when all output scores fall below the bar (normalize_rating < threshold, default 0.75). Results are persisted as EvalRuns and reused on later calls. - eval_api.py: thin endpoint + request/response models, allowed for the Kiln assistant (ALLOW_AGENT) with a detailed OpenAPI description. Committed the agent-policy annotation so the assistant's policy lookup permits the call. - Regenerated app/web_ui api_schema.d.ts. - Tests: 13 core + 6 API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: reuse loaded eval, resilient persistence, more tests - eval_api.py: resolve the eval config from the already-loaded eval instead of eval_config_from_id (which re-reads the task/eval from disk). Keeps the same 404. - failing_examples.py: wrap EvalRun persistence in its own try/except so a save failure logs and is skipped instead of crashing the whole concurrent batch; the computed scores are still returned. - tests: cover missing scores in example_fails, and judge errors being skipped (still counted as examined) during orchestration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Rework into a persistent Judge Job (config + run + poll) Replace the stateless failing_train_examples endpoint with a durable, runnable JudgeJob model per review feedback. A JudgeJob samples dataset items by tag, judges their existing outputs with an eval config (the judge), and records each item's pass/fail + the judge's feedback as child JudgeJobRuns — surfacing failing examples for reflective optimization. - datamodel/judge_job.py: JudgeJob (child of Task, parent of JudgeJobRun) with target_tags, eval_config_id, run_config_id (metadata), count/max_samples/threshold, latest_status, and an outcome summary. Registered on Task + datamodel __init__. - adapters/eval/judge_job_runner.py: JudgeJobRunner mirrors EvalRunner — judges in eval_config_eval mode, yields Progress for SSE, persists child runs, reuses cached results, and updates status/outcome (running -> succeeded/failed). Keeps the example_fails/score_passes/feedback helpers from the prior engine. - studio_server/judge_job_api.py: create / run (SSE) / create-and-run (SSE) / get / runs / list. The model id is the job id; GET is the poll. Registered in desktop_server; "Judge Jobs" tag added; agent-policy annotations regenerated (ALLOW_AGENT). Removed the standalone endpoint and its annotation; regenerated api_schema.d.ts. - Tests: datamodel, runner, and API (incl. SSE). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: cancel on disconnect, validation, defensive hardening - judge_job_runner: catch GeneratorExit/CancelledError and mark the job `cancelled` (an SSE disconnect previously left it stuck in `running`); add a test. - judge_job_runner: harden score_passes (catch TypeError), feedback extraction (skip non-str values), and tag matching (None-safe). - judge_job_api: validate run_config_id (if provided) and reject a second run with 409 when the job is already running; add tests. - judge_job datamodel: constrain count/max_samples (ge=1) and threshold (0-1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make judge jobs synchronous (per Leonard's feedback) The chat tool drops SSE event payloads and a judge minibatch is short, so SSE buys nothing here. Switch run / create-and-run to synchronous JSON and drop the job-status/poll/cancel layer. - judge_job datamodel: remove latest_status, outcome, JudgeJobStatus, JudgeJobOutcome. A JudgeJob is now just a config (+ JudgeJobRun children). - judge_job_runner: run() returns a JudgeJobRunResult (failing_runs + counts) instead of streaming Progress; no status writes, lock, or GeneratorExit handling. - judge_job_api: run / create-and-run block and return JudgeJobRunResponse (judge_job + failing_runs + counts). Counts are FYI for the caller, not persisted. Removed the SSE helper and the 409 already-running guard. - Tests + api_schema.d.ts updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address Leonard's review: rename task_run_id, collect per-item errors - Fix CI lint failure (ruff format test_judge_job.py). - Rename JudgeJobRun.dataset_id -> task_run_id (more descriptive of the TaskRun it points at). Updates model, runner, API, tests, and regenerated TS schema. - Collect per-item judge/save errors during a run and return them in the sync response (new JudgeJobItemError + JudgeJobRunResult.errors / run-response `errors`). Errors no longer silently swallowed: one bad item doesn't abort the run, the item is left un-persisted, and re-running retries only un-persisted items. A non-empty `errors` list signals partial success to the caller. Per-run aggregate counts stay derived (returned, not persisted) — the durable record remains the per-item JudgeJobRun children. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge jobs: full-coverage gate + return all judged runs Replace `count` (always early-stops) with `stop_after_failures: int | None`: - None (default) = judge the whole matching set up to max_samples (full coverage), so a val gate can pair results by task_run_id. - set = stop once that many failures are found (the cheap train-signal minibatch). Add `judged_runs` (every item judged this run, pass and fail) to the run result and response, keyed by task_run_id — the piece that makes a paired baseline-vs-candidate gate computable instead of an aggregate-count approximation. hit_cap now means coverage was capped (max_samples reached before stop_after_failures, or the matching set exceeded max_samples). Regenerated api_schema.d.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge jobs: retry transient per-item errors Generating/judging invokes a model, so transient rate-limit/connection blips are expected; without a retry they were collected as per-item errors and silently shrank coverage (skewing a gate). Route the judge call through _judge_with_retry, reusing the eval runner's transient-error classification (max_retries=2). Non- transient errors are still collected once, not retried. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge jobs: generate-and-judge mode (scoped candidate gate) Add generate_outputs: when true, run run_config_id on each tagged item to produce a fresh output and judge that — gating a candidate config scoped to the tagged items, in one sync call (no run_comparison, no full-eval scores). run_config_id is required in this mode. - Runner resolves the run config's RunConfigProperties + preloads its skills (mirrors EvalRunner.run_job) and instantiates the evaluator with them. - _judge_with_retry branches to run_task_and_eval; the fresh TaskRun is discarded (allow_saving=False) so the dataset is never polluted. - Skip the result cache in generate mode (generation is non-deterministic). - Record run_config_id on each JudgeJobRun for provenance; lower default concurrency when generating. Regenerated api_schema.d.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge jobs: address review (approval, robustness, eval-type, naming) Per Leonard's review: - Run / Create-and-run endpoints now require agent approval (agent_policy_require_approval) — they make model calls, so the bot shouldn't kick them off without consent (auto-mode still bypasses). Create + GETs stay ALLOW_AGENT. Regenerated the two annotation files. - run()'s asyncio.gather now uses return_exceptions=True: an unexpected throw in one item is converted to a per-item error instead of discarding the whole chunk's results. - Reject reference-answer evals when generate_outputs=false (no reference to compare a pre-existing output against; the judge would error per item). Validated in the API (422) and the runner constructor (last line of defense). - Rename eval_config_for_id -> eval_config_from_id to match the *_from_id convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Rename JudgeJob -> JudgeFeedbackBatch Avoid collision with the upcoming Job management system (and GEPA's "job"), per Leonard's review. Mechanical rename across the datamodel (JudgeFeedbackBatch / JudgeFeedbackBatchRun), runner, API (paths: /judge_jobs -> /judge_feedback_batches), the Task accessor (judge_feedback_batches()), OpenAPI tag, files, tests. Regenerated api_schema.d.ts and the agent-policy annotations (removed the stale judge_jobs files — the endpoints never shipped). Zero external callers, so no compat shim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge feedback batch: return continuous per-dimension scores (P1) The pass/fail bit (example_fails at the 0.75 threshold) discards the continuous signal, so a 2★→3★ improvement reads as zero gradient and a small val slice quantizes to a few loss levels. Add aggregate_normalized_scores() and return mean_normalized_scores (per-dimension mean over judged_runs, 0-1 higher=better) + mean_normalized_score (overall) in the run response — a usable gate/loss metric the caller no longer has to hand-compute from judged_runs[].scores. Part of the loss-function API review; P2/P3/P4/P7 are a separate follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * judge_feedback_batch: surface generation usage (tokens/cost/latency) Stop discarding the candidate run's Usage in generate_outputs mode. JudgeFeedbackBatchRun now carries a per-item `usage`; the runner aggregates it (aggregate_usage) into total_usage / mean_cost / mean_latency_ms on JudgeFeedbackBatchRunResult, and the run API exposes both per-run and aggregate usage. Lets the auto-optimize loop read deterministic cost/latency signals from the same call that gates quality. None on the judge-only path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * judge_feedback_batch: exponential backoff on transient/rate-limit retries Fixed 1s gaps don't let a 429 clear and just re-flood a throttled provider — back off progressively (delay, 2x, 4x) in _judge_with_retry, and raise the default retry_delay 1.0->2.0 so the server-side backoff is gentler. Surfaced by an auto-optimize smoke whose single-sample judge_feedback_batches calls hit provider rate limits (Cerebras/Gemini-Flash candidates). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jobs): add judge_feedback_batch job type (parallel kick-offs) Make judge feedback batch runs job-backed, alongside the existing synchronous endpoints, so an agent can fire many gates and POST /api/jobs/wait on all of them at once instead of blocking on each synchronous call serially. - JudgeFeedbackBatchJobWorker wraps JudgeFeedbackBatchRunner unchanged (mirrors EvalJobWorker). Single-shot: progress reported once, supports_pause=False. - POST /api/jobs/judge_feedback_batch/run (two-segment, approval-gated) — the job-backed counterpart to POST /judge_feedback_batches/run. - Result carries the aggregate scores/usage/latency + the batch id; per-item runs (with the judge's feedback) are persisted, fetched via .../runs. - Worker tests + regenerated api_schema.d.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jobs): agent-check annotation for the judge_feedback_batch job route The chat backend gates call_kiln_api via AgentPolicyLookup, which reads the static agent-check annotation JSON (dumped from the OpenAPI), NOT the live spec. Without an annotation the new POST /api/jobs/judge_feedback_batch/run is rejected as "not allowed", so the assistant falls back to the synchronous endpoint. Regenerated via `make annotations`; marks it allow + requires_approval. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Judge feedback batch: job-backed run of a pre-existing batch (eval parity) + deep-review fixes (#1542) * feat(jobs): judge_feedback_batch worker — stream progress, publish props, describe Bring the judge_feedback_batch job worker to parity with the eval job worker (the auto-mode e2e integration flagged three gaps): - Progress: the worker reported progress only once at the end, and used train_set_size as the denominator — so when the matching set exceeded max_samples the bar stalled below 100% even on full success. The runner now takes an optional progress_callback and streams (num_judged, error_count, planned_total) per judged chunk; the worker reports live progress and its final snapshot against the planned (capped) count min(train_set_size, max_samples), so success reaches total on full coverage. - Metadata: publish JudgeFeedbackBatchJobProperties via describe() (mirrors EvalJobProperties) — judge/eval names, algorithm, model, mode, run config, tags, max_samples — and render them in the jobs table (previously just a raw "Judge_feedback_batch" label). - Errors: unchanged wiring already surfaces per-item errors to the View Errors UI; improved message quality by unwrapping KilnRunError.original (mirrors EvalJobWorker._error_detail). - API models: add field descriptions to JudgeFeedbackBatchJobResult and the project_id/task_id params. Regenerated api_schema.d.ts. Tests: runner progress_callback streaming, worker describe() + capped-total progress, frontend judge-property rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * fix(judge-feedback-batch): address deep code review (6 moderate + polish) Deep multi-phase review of the judge_feedback_batch feature (0 critical). Fixes: Moderate: - Progress double-counted errored items: the runner's num_judged already includes errored items, so reporting success=num_judged alongside a separate error count breached the processed=success+error contract and overshot 100%. Report success=num_judged-error_count in both the streamed callback and the final snapshot (worker), matching the eval reference's disjoint counters. - Gate-mode task_run_id pairing was defeated by the random shuffle when the tag set exceeded max_samples (two runs sampled disjoint subsets). Gate mode now selects deterministically (sorted by id) before capping; train-signal mode keeps the random minibatch. - Added a JudgeFeedbackBatch model_validator coupling generate_outputs=True to a required run_config_id and rejecting empty target_tags (defense in depth, mirrors EvalRun) — the API request model already validated both. - Documented that num_judged counts attempts (errors/cache included); use len(judged_runs) for a scored-item count. - Added runner tests for the empty candidate set and gate-mode hit_cap=True (set > max_samples), incl. proof that two gate runs cover the same ids. Polish: - Runner: save-error uses _error_detail; concurrency=max(1, concurrency) guard; mean-of-means comment; "Judge feedback batch" wording. - API: judge_feedback_batch_from_id 404 message fixed + accepts a preloaded task (removes the run endpoint's double task load); run docstring covers generate mode. - UI: surface eval_name + stop_after_failures in the jobs table. - Tests: worker run stub gets the full signature; judge-only describe() test. Full checks.sh green. Regenerated api_schema.d.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * fix(judge-feedback-batch): report per-item errors live so View Errors works mid-run The streamed progress callback bumps progress.error per chunk, which makes the "View Errors" button appear while the job is still running. But the error MESSAGES were only written to the job error log in a post-run loop over result.errors — after runner.run() returned. So mid-run the button showed but the log was empty ("No error messages recorded"); the messages only appeared once the job completed. This diverged from the eval worker, which logs each error live via an observer. Add an error_callback to JudgeFeedbackBatchRunner.run(), fired the moment each per-item error is collected (both the judge/save error and the unexpected-throw paths), and wire the worker to write it to the error log immediately. Errors still appear in the returned result.errors for the synchronous endpoint (which passes no callback). Removed the worker's post-run loop to avoid double-logging. Tests: runner test asserting errors are delivered live (interleaved with progress, not batched to the end); worker stub updated to emit errors via the callback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * fix(jobs-table): guard target_tags render against missing properties Defensive: use optional chaining on jp.target_tags so a job record with incomplete properties can't throw a TypeError and crash the whole jobs table render. (Per gemini-code-assist PR review.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * fix(judge-feedback-batch): wrap batch-config save in git-sync context The batch config was written with a raw save_to_file() outside any atomic_write — in both the job worker and the synchronous create-and-run endpoint. Under auto git-sync that new (untracked) file is dirty working-tree state, so the runner's first per-item child write enters atomic_write, whose ensure_clean() treats it as a crashed session and stashes it away (include_untracked=True). Net effect: the batch config is never committed/pushed and is removed from the working tree, orphaning the committed child runs. Wrap the batch-config save in the same save_context used for the child writes (coalescing None -> no-op default_save_context) so it gets its own atomic_write / commit before the runner starts. The batch save and each child save are separate, non-nested atomic_write blocks, so re-entrancy is not a concern. Mirrors how EvalRunner already wraps every per-item write (the eval worker creates no parent entity, so it was already correct). - worker: judge_feedback_batch.py run() wraps the save. - sync endpoint: create_and_run_judge_feedback_batch (@no_write_lock) wraps it via build_save_context(request). - test: asserts the batch save goes through the git context and closes before the runner runs (enter -> exit -> run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * refactor(judge-feedback-batch): job runs a pre-existing batch (eval parity) Reshape the judge_feedback_batch job from "create-and-run" to "run an existing batch", mirroring EvalJobWorker. The batch config is created first via the synchronous POST .../judge_feedback_batches (which persists it under the git-sync write lock), then the job just runs it by id. Why: - The batch id is now caller-supplied and lives in job.params, so it's retrievable via GET /api/jobs/{id} even if the run fails — closing the gap where a create-and-run job only surfaced the minted id in its success result. - The worker no longer writes the config at all, so the earlier "wrap the batch save in the git-sync context" workaround is gone — the create endpoint owns that write. One less special case. - Structurally identical to the eval job (params carry the entity id; results live on disk), which sets up deriving a disk summary + a real compute_state later. Changes: - JudgeFeedbackBatchJobParams: now {project_id, task_id, judge_feedback_batch_id} (was a full CreateJudgeFeedbackBatchRequest). run()/describe() load the batch by id and read its config off disk. - Job result echoes judge_feedback_batch_id from params (no longer "created"). - Route docstring updated; regenerated api_schema.d.ts. - Tests: run loads an existing batch, missing-batch 404s, describe/props derived from the persisted batch. Dropped the now-obsolete config-save-wrap test. The synchronous create-and-run / run endpoints are left in place (now redundant with create + job) for callers not yet migrated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * chore(judge-feedback-batch): deny agent access to the sync POST endpoints The synchronous create / run / create-and-run endpoints are being superseded by the create + job flow, so mark them DENY_AGENT (the chat assistant should go through POST /api/jobs/judge_feedback_batch/run for dashboard visibility). GETs stay ALLOW_AGENT. Drops the now-unused agent_policy_require_approval import. NOTE: not enforced until the agent-check annotation JSONs are regenerated (the policy lookup reads those, not the live spec). See PR notes re: keeping the create endpoint agent-callable for the new create-then-run-job flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N * chore(judge-feedback-batch): keep create agent-callable; regenerate annotations Follow-up to the DENY_AGENT change: - Flip the sync create endpoint back to ALLOW_AGENT — the agent creates the batch here (cheap, no model calls) and then runs it via POST /api/jobs/judge_feedback_batch/run. Only the sync run / create-and-run stay DENY_AGENT (superseded by the job). - Regenerate the agent-check annotation JSONs so the policy is actually enforced (the chat backend reads the dumped JSONs, not the live spec) — fixes the check_api_bindings CI job. run / create-and-run now dump as "deny"; create and the GETs stay "allow". Final judge_feedback_batch agent policy: create -> allow run (existing, sync) -> deny create-and-run (sync) -> deny run as job (/api/jobs/...) -> allow + approval list / get / runs (GET) -> allow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZXQFWTXbstDj7vBihcx5N --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(judge-feedback-batch): expose concurrency in job params Add an optional `concurrency` field to JudgeFeedbackBatchJobParams and forward it to JudgeFeedbackBatchRunner.run. Null keeps the runner's mode-aware default (5 when generating outputs, 25 when judging existing ones); values below 1 are clamped to 1 by the runner. Regenerated api_schema.d.ts and added a param-forwarding test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019w3CFeeewrMyMRdTv3SgQm * feat(judge-feedback-batch): validate concurrency >= 1 at schema level Add ge=1 to the concurrency param so invalid input returns a 422 up front (consistent with max_samples / stop_after_failures) instead of being silently clamped by the runner. Update the description, regenerate api_schema.d.ts, and add a validation test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019w3CFeeewrMyMRdTv3SgQm * feat(eval-job): expose concurrency in job params Add an optional `concurrency` field to EvalJobParams and forward it to EvalRunner.run, mirroring the judge feedback batch job param. Null keeps the runner's default (25); ge=1 rejects invalid values with a 422 at the API boundary (matching max_samples-style validation) rather than a runner-side ValueError. EvalRunner.run now accepts int | None and resolves the default internally, keeping 25 a single source of truth. Regenerated api_schema.d.ts; added forwarding + validation tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019w3CFeeewrMyMRdTv3SgQm * fix(eval-job): CI + review — params round-trip and single-source default - test_api: _EVAL_PARAMS now includes the defaulted concurrency=None, so the stored-params round-trip assertion in test_run_eval_job_creates_typed_eval_job matches again (the new optional field is serialized into job.params). - Address gemini review: extract DEFAULT_EVAL_CONCURRENCY (=25) in eval_runner as the single source of truth for the default, use it in EvalRunner.run and interpolate it into the EvalJobParams.concurrency field description so the doc can't drift from the runner default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019w3CFeeewrMyMRdTv3SgQm --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Leonard Q. Marcq <marcqleonard@gmail.com> Co-authored-by: Leonard Q. Marcq <leonard@getkiln.ai>
Brings the
judge_feedback_batchbackground-job worker to parity with the eval job worker, reshapes it to run a pre-existing batch (eval parity), and applies fixes from a deep multi-phase code review of the wholejudge_feedback_batchfeature. Targetsintegration/auto-mode-e2e(the auto-mode e2e integration branch). Includes the work merged from #1543.Job shape: run a pre-existing batch (eval parity)
The job was reshaped from "create-and-run" to "run an existing batch", mirroring
EvalJobWorker:POST .../judge_feedback_batches(persisted under the git-sync write lock); the job then just runs it by id.JudgeFeedbackBatchJobParamsis now{project_id, task_id, judge_feedback_batch_id}(was a fullCreateJudgeFeedbackBatchRequest).run()/describe()load the batch by id and read its config off disk.job.params, it's retrievable viaGET /api/jobs/{id}even if the run fails (the old create-and-run job only surfaced the minted id in its success result). The job result now echoesjudge_feedback_batch_idfrom params.Worker parity
train_set_sizeas the denominator, so the bar stalled below 100% when the matching set exceededmax_samples. The runner now takes an optionalprogress_callbackand streams(num_judged, error_count, planned_total)per judged chunk; live + final progress use the capped denominatormin(train_set_size, max_samples), reaching 100% on full coverage (mirrors the eval worker).successexcludes errors (num_judged - error_count) soprocessed = success + errorholds and the bar can't overshoot.JudgeFeedbackBatchJobProperties+describe()(mirrorsEvalJobProperties) — judge/eval names, algorithm, model, run config (only when generating outputs), tags, max_samples, stop_after_failures — and render them in the jobs table (was a rawJudge_feedback_batchlabel).error_callbackthe moment each is collected — previously the button appeared mid-run (gated by the streamed error count) but the messages only landed in a post-run loop, so the log read "No error messages recorded" until completion. Message quality improved by unwrappingKilnRunError.originallike the eval worker.JudgeFeedbackBatchJobResult,JudgeFeedbackBatchJobParams, and theproject_id/task_idparams.Deep code review fixes
Multi-phase review (runner / worker / data model / REST API / UI / tests). 0 critical, 6 moderate (all fixed), 24 mild (12 fixed, rest documented). Highlights:
num_judgedalready includes errored items, so reportingsuccess = num_judgedalongside a separate error count breached theprocessed = success + errorcontract — nowsuccess = num_judged - error_countin both the streamed callback and the final snapshot.task_run_ids — the pairing the gate exists for (a random shuffle previously picked disjoint subsets pastmax_samples). Train-signal mode keeps the random minibatch.JudgeFeedbackBatchmodel_validator (generate_outputs⇒run_config_id, non-emptytarget_tags) as defense-in-depth (mirrorsEvalRun).hit_cap=True(incl. proof that two gate runs cover the same ids), plus a test asserting errors are delivered live (interleaved with progress)._error_detailon save errors,concurrency = max(1, concurrency)guard, 404 message/wording (Judge feedback batch not found),judge_feedback_batch_from_idaccepts a preloaded task (removes the run endpoint's double task load), docstrings covering generate-outputs mode.Agent policy
The synchronous run / create-and-run endpoints are superseded by the create + job flow, so the agent goes through
POST /api/jobs/judge_feedback_batch/runfor dashboard visibility:create→ allow (cheap, no model calls — the agent creates the batch, then runs it as a job)run(existing, sync) → denycreate-and-run(sync) → deny/api/jobs/...) → allow + approvalRegenerated the agent-check annotation JSONs so the policy is actually enforced (the chat backend reads the dumped JSONs, not the live spec) — fixes the
check_api_bindingsCI job. The synchronous create-and-run / run endpoints are left in place for callers not yet migrated.UI hardening
eval_name,stop_after_failures, tags, etc.).target_tagsrender with optional chaining so a job record with incomplete properties can't throw and crash the whole jobs table (per gemini-code-assist review).Verification
Full
uv run ./checks.sh --agent-modegreen (lint, format, typecheck, py + web tests, schema, build).api_schema.d.tsregenerated.🤖 Generated with Claude Code