Skip to content

Let judges deliver their scores later - #191

Draft
kentare wants to merge 4 commits into
mainfrom
feat/evaluator-v2
Draft

kentare wants to merge 4 commits into
mainfrom
feat/evaluator-v2

Conversation

@kentare

@kentare kentare commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A Langfuse judge scores a trace a few seconds after it arrives and cannot be called on request, but evaluator@1 only allowed an answer within one request. ADR 0040 replaces it with evaluator@2. Closes #96. First of a stack: #94 and #97 follow on top.

  • An evaluator answers at once or pending, and the runner asks again with collect every 5 seconds, for at most 5 minutes from when the last case was sent (packages/eval/src/runner.ts). Each collect gets a signal that aborts at the deadline, and nothing that arrives after it is taken. What has not arrived is recorded as missing, and the run's incomplete names the judge and the count ("no score from judge:x for 20 of 84 cases").
  • Cancelling an eval task stops its cases, its evaluators and its wait for scores, and the signal reaches the judge's requests.
  • A score has a state: scored, not-applicable, pending, failed or missing (packages/eval/src/schema.ts). A score stored before reads with the state its value implies.
  • A gate whose metric has a missing or failed score in its split fails with that count, also when they are in a no-regression gate's baseline, and never averages over what arrived.
  • GET /eval-tasks/{task} answers waiting-for-scores with how many are pending, on the instance running the eval.
  • An HTTP judge answers pending with a ticket and is collected in batches of 50. A score needs a value or a state, a malformed collected score fails its own case, and a failed batch keeps the batches that arrived. docs/bring-your-own-logic.md documents the protocol. One contract suite covers immediate and pending answers, and it collects the ticket it was given.
  • Registering a plug-in under evaluator@1 is refused, and one stored under it judges nothing until it is registered again.
  • Docs: docs/api/work.md, docs/api/README.md (API changes), docs/bring-your-own-logic.md, docs/architecture/modules/eval.yaml. i18n: apps/api/src/i18n/{en,nb}/errors.ts.

Not in this PR: a restart during the wait marking the run interrupted. That needs the lease on EvalRun, which #194 adds.

How it was tested

  • CI on this head: check and the three test shards pass. The top of the stack (Delete what genki sent to an agency's Langfuse when it purges the case #197) with main merged passes npm run check: 470 files, 5092 tests.

  • New tests: "lands a pending judge's scores in the run and its metrics once they are collected", "records a score still missing 5 minutes after the last case was sent as missing, naming the judge and the count", "takes no score that arrives after the deadline, and bounds each collect by the time left", "cancels a collect call still open at the deadline", "stops running cases once the eval run is cancelled", "cuts a pause for scores short when the eval run is cancelled", "stops asking the judge once the eval task is cancelled", "fails the run when a collected score cannot be recorded on its case, as for an immediate score", "records nothing for a score the judge answers pending again", "reads a run stored before scores had a state, or before incomplete existed", "fails a gate whose metric has a missing or failed score in its split, in the run or its baseline, naming the count", "passes an evaluator that answers pending and delivers when collected", "fails when the ticket it was given is never collected, even if asking again answers at once", "refuses a score with neither a value nor a state, instead of reading it as not applicable", "refuses a pending score without a ticket", "keeps the scores of the batches that arrived when a later batch fails", "records a malformed collected score as failed with the reason, and keeps the rest", "passes the signal on to every request, when it judges and when it collects", "asks for at most 50 tickets per collect call", "passes the signal on to the egress request", "judges nothing with a stored evaluator@1 plug-in until it is registered again under evaluator@2", "refuses an evaluator plug-in that claims evaluator@1", "answers that a run is waiting for scores, and for how many, while its judge has not delivered".

  • Rules broken on purpose: each turns a test red. The review found seven that no test stated (a cancel cutting the pause short, a collect cancelled at the deadline, a score answered pending again, a cancelled eval task, a pending score without a ticket, the request signal, a run stored before incomplete); each now has the test above that fails when it is broken.

  • npm run check passes

Checklist

  • Every new rule has a test whose name states it
  • User-facing text is in the i18n catalogues, in bokmål and English, with no em-dashes
  • Vendor SDKs stay in packages/providers-*
  • ADRs, docs and docs/architecture/modules are updated if a contract, boundary or module changed
  • No secrets, personal data or local paths

A Langfuse judge scores a trace a few seconds after it arrives and
cannot be asked on request, while evaluator@1 only allowed an answer
within one request. evaluator@2 replaces it (ADR 0040, issue #96).

- An evaluator answers at once or pending, and the runner asks again
  with collect every 5 seconds, for at most 5 minutes from when the
  last case was sent. What has not arrived then is recorded as
  missing, and the run's incomplete names the judge and the count
  ("no score from judge:x for 20 of 84 cases").
- A score has a state: scored, not-applicable, pending, failed or
  missing. Only a scored score has a value, and only a not-applicable
  one does not apply.
- A gate whose metric has a missing or failed score in its split
  fails with that count, and never averages over what arrived.
- GET /eval-tasks/{task} answers waiting-for-scores with how many are
  pending, on the instance running the eval. #94 stores the run's
  state under a lease, so a restart during the wait is handled there.
- An HTTP judge answers pending with a ticket and is collected in
  batches of 50 (docs/bring-your-own-logic.md). One contract suite
  covers immediate and pending answers.
- Registering a plug-in under evaluator@1 is refused, and one stored
  under it judges nothing until it is registered again under
  evaluator@2.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The no-regression gate can pass with an incomplete baseline—even without any comparison—leaving a critical correctness issue unresolved.

7 open issues
What changed in this PR

This PR introduces evaluator@2 so evaluation runs can collect scores asynchronously, supporting judges such as Langfuse. It lays the groundwork for the unified eval runner and tracing integrations in #94 and #97.

Changes:

  • Adds explicit score states, deferred collection, incomplete-run summaries, and waiting status.
  • Updates publishing gates to reject missing or failed candidate scores.
  • Adds ticket-based HTTP collection, rejects obsolete evaluator contracts, and updates tests and documentation.
File Description
packages/​jobs/​src/​evaluators.ts Adds explicit states to job evaluator scores.
packages/​eval/​src/​strategy.ts Emits progress and waiting notices.
packages/​eval/​src/​service.ts Exposes waiting status and pending counts.
packages/​eval/​src/​schema.ts Defines score states and incomplete-run summaries.
packages/​eval/​src/​runner.ts Collects deferred scores and records missing results.
packages/​eval/​src/​platform.test.ts Tests waiting status through the eval service.
packages/​eval/​src/​language-gates.test.ts Updates fixtures for the score schema.
packages/​eval/​src/​gates.ts Checks gated metrics for unscored results.
packages/​eval/​src/​evaluators.ts Extends the evaluator interface with collection.
packages/​eval/​src/​eval.test.ts Tests deferred scoring, deadlines, notices, and gates.
docs/​bring-your-own-logic.md Documents the HTTP judge protocol.
docs/​architecture/​system-atlas.html Updates evaluator descriptions and score schema.
docs/​architecture/​modules/​eval.yaml Documents deferred evaluator support.
docs/​api/​work.md Explains waiting status and incomplete scores.
apps/​api/​src/​stores.contract.test.ts Updates stored-run fixtures.
apps/​api/​src/​routes/​console-routes.test.ts Updates console score and run fixtures.
apps/​api/​src/​quality.test.ts Updates the eval-run fixture.
apps/​api/​src/​plugins.ts Rejects incompatible plug-in contracts.
apps/​api/​src/​plugin-routing.test.ts Tests deferred HTTP judges and contract rejection.
apps/​api/​src/​plugin-contracts.ts Implements evaluator@2 checks and batched collection.
apps/​api/​src/​plugin-contracts.test.ts Tests immediate and deferred contract responses.
apps/​api/​src/​openapi/​work.ts Documents the waiting-status response.
apps/​api/​src/​i18n/​nb/​errors.ts Adds the Bokmål outdated-contract message.
apps/​api/​src/​i18n/​en/​errors.ts Adds the English outdated-contract message.
apps/​api/​src/​external-plugins.ts Uses the new contract and excludes obsolete evaluators.
apps/​api/​src/​external-plugins.test.ts Tests re-registration of stored obsolete evaluators.

Review effort: Balanced

Comment thread packages/eval/src/gates.ts Resolved
Comment thread apps/api/src/plugin-contracts.ts Resolved
Comment thread apps/api/src/plugin-contracts.ts Resolved
Comment thread apps/api/src/plugin-contracts.ts Resolved
Comment thread packages/eval/src/runner.ts Resolved
Comment thread packages/eval/src/runner.ts Resolved
Comment thread packages/eval/src/strategy.ts Resolved
- The runner stops asking at the deadline, bounds each collect by the
  time left and takes nothing that arrives after it. A collect call
  still open at the deadline is cancelled.
- Cancelling the eval task stops the wait: the task's signal reaches
  the runner, its pauses and the judge's requests.
- Only the collect call is caught, so a collected score that cannot be
  recorded on its case fails the run, as an immediate score does.
- A no-regression gate fails when its baseline has missing or failed
  scores in the gated split, naming the count.
- The contract check collects the ticket the pending answer gave,
  instead of asking again.
- An HTTP judge's score needs a value or a state, and a failed collect
  batch no longer discards the batches that arrived.
@kentare
kentare added this pull request to stack #199 October 30, 2026 04:36
- A malformed score in a collect answer fails its own case with the
  reason; the judge's other scores are kept.
- Cancelling an eval run stops it before the next case and the next
  evaluator, and an HTTP judge's requests get the run's signal when it
  judges and when it collects, also in the registration check.
- A score stored before scores had a state reads with the state its
  value implies, so older run files still parse.
- Tests now state the rules that had none: a pause cut short by a
  cancel, a collect cancelled at the deadline, a score answered pending
  again, a cancelled eval task, a pending score without a ticket, at
  most 50 tickets per call, and the judge sender's signal.
- docs/api/README.md lists the evaluator@2 changes.
@kentare

kentare commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review. Copilot and a claim-checking review are done (no blind review) and CI passes. Open: a run's incomplete counts each case and repeat as a case ("for 2 of 4 cases" with two repeats), as the gates do.

This branch has not been deployed

No deployments
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.

Let judges deliver their scores later

2 participants