Skip to content

fix: make the online eval run safe for production - #25

Merged
RobertCrupa merged 12 commits into
mainfrom
fix/online-evals-blockers
Oct 9, 2026
Merged

RobertCrupa merged 12 commits into
mainfrom
fix/online-evals-blockers

Conversation

@RobertCrupa

Copy link
Copy Markdown
Contributor

Part of apify/ai-team#249, stacked on the #271 PR. Fixes three blockers that a production-readiness review of the online evals stack found. The unit tests did not catch any of them.

1. The stack did not merge into main (fixed by the rebase, not in this PR)

The stack was based on poc/workflow-runner, but main got the PoC as squash PRs #7 to #13. A test merge conflicted in 21 files. PRs #1 to #5 are now rebased onto main. Two old merge commits had quietly adapted the stack to main; the rebase redoes them as commits:

  • fix: read the online judge reply from judgeLlmCall's result (in the #269 PR). On main, judgeLlmCall returns {json, usage, ...}. Without this, every online judgement fails with InvalidJudgeReplyError. The build passed; only typecheck caught it.
  • test: pin the online score id to JUDGE_IMPL_VERSION 0.4.0 (in the #270 PR).

2. The checkpoint did not carry over between runs

actorCheckpointStore() used Actor.getValue / Actor.setValue, so the run's default key-value store. Every platform run gets a new default store, so every scheduled run read null and fell back to now - 24h. Results: the traffic from 05:27 to 06:00 UTC was never judged, and a failed or timed-out run lost its window (the documented retry had no effect).

  • The checkpoint is now in the named store apify-ai-online-state, one record per environment (ONLINE_CHECKPOINT-<environment>), so a staging run never moves the prod checkpoint.
  • New test test/checkpoint-store.test.ts runs against the SDK's real local storage and changes the default store id between "runs", like the platform. It fails on the old code (expected null to deeply equal {...}) and passes now.
  • JUDGE_IMPL_VERSION is not bumped: it is part of every score id, and scoring did not change.

3. The scheduled run would use the wrong Langfuse project

Online mode must read "Apify AI Agent". The Actor env keys belong to "MCP Agent Evals" and must stay there, because the workflow runner starts the judge without keys. The schedule sent no keys and its README said to put them in the Actor env, which breaks one mode or the other.

  • New guard (src/online-project.ts): before any other Langfuse call, online mode resolves the keys' project with GET /api/public/projects and fails the run unless its name or id equals the new langfuseProject input (default Apify AI Agent). Dataset-run mode does not change. 8 tests.
  • The schedule now runs an Apify Task (RUN_ACTOR_TASK). schedule/online-task.json is the POST /v2/actor-tasks body with placeholders; the keys go into the Task's secret inputs, typed in the Console. .gitignore blocks filled-in *.local.json copies.
  • Compatible with feat: Langfuse project keys as mandatory Actor input #15 (keys as required input). After feat: Langfuse project keys as mandatory Actor input #15, drop the sentences about the env-key fallback.

Verification

On the top of the stack with this PR: npm ci, build, npm test for contract (26), judge (179), workflow-runner (42) and tools (8), scenarios:check, judge typecheck, and Prettier all pass. ESLint is clean on the changed files; the remaining errors are older lines from main.

Not verified yet

  • The guard default is the project name. The project id and whether production has apify-ai traces since apify-ai-agent v0.3.4 are not checked yet: that needs the "Apify AI Agent" keys.
  • Whether POST /v2/actor-tasks encrypts isSecret input. The README avoids the question: type the keys in the Console.
  • Nothing is created on Apify or in Langfuse by this PR.

@RobertCrupa
RobertCrupa added this pull request to stack #6 October 7, 2026 09:03
@RobertCrupa
RobertCrupa removed this pull request from stack #6 October 7, 2026 09:08
@RobertCrupa
RobertCrupa added this pull request to stack #26 October 7, 2026 09:08

@vojtechj-apify vojtechj-apify 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.

All three fixes are right, and the checkpoint test that switches the default store between runs is a good reproduction of the bug.

  • The guard defaults to the project name. Has it been run with the real "Apify AI Agent" keys yet?
  • "Prettier passes" holds only for the new files. A few files inherited from the base commit are flagged. That goes away with the rebase onto main.
  • Merge order with #15: both change how the Langfuse keys are handled, and they conflict. Which one goes first?

Comment thread actors/judge/src/select.ts
RobertCrupa added a commit that referenced this pull request Oct 8, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa

Copy link
Copy Markdown
Contributor Author

Addressed every open review thread on #1 to #5 and #25 here (commits 3982874..3354391), each thread has a reply with its commit. Verified end to end: unit tests (195), typecheck, and two cloud runs of build 0.1.2 of eval-judge in online mode over the prod window 2026-10-07 11:00 to 12:00 UTC with maxItems: 5: run 1 judged and wrote 5 traces and created rollup-2026-10-07 (https://console.apify.com/actors/runs/2MqlKJtws8cujvsC0); run 2 over the same window skipped the already-judged trace, wrote 2, failed 2 on reconstruction (tolerated, under the 50% hold) and merged into the rollup (https://console.apify.com/actors/runs/SV1a7mspsDqisn01y). Both score copies read back with environment: prod and the run copies under apify-ai-online-2026-10-07.

RobertCrupa added a commit that referenced this pull request Oct 8, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from 3354391 to 7c5e318 Compare October 8, 2026 12:11
RobertCrupa added a commit that referenced this pull request Oct 8, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from 7c5e318 to e75e949 Compare October 8, 2026 13:02
RobertCrupa added a commit that referenced this pull request Oct 8, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from e75e949 to 177bebc Compare October 8, 2026 13:27
RobertCrupa added a commit that referenced this pull request Oct 8, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from 177bebc to c523147 Compare October 8, 2026 13:31
RobertCrupa added a commit that referenced this pull request Oct 9, 2026
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from c523147 to 53098a9 Compare October 9, 2026 10:52
Base automatically changed from chore/online-evals-schedule-alerting to main October 9, 2026 10:52
…team#267)

actorCheckpointStore() used Actor.getValue/setValue, which act on the run's
default key-value store. Every platform run gets a new default store, so
every scheduled run read null and fell back to now-24h. The 33 min between
the window end and the next 06:00 run were never judged, and a failed run
lost its window although finishOnlineRun() held the checkpoint back.

The checkpoint now lives in the named store apify-ai-online-state
(CHECKPOINT_STORE_NAME), under ONLINE_CHECKPOINT-<environment>, so a
staging run never reads or moves the prod checkpoint. The CheckpointStore
seam and the override rule are unchanged.

New test: writes through actorCheckpointStore() on SDK local storage, then
switches ACTOR_DEFAULT_KEY_VALUE_STORE_ID as a new run does and reads back.
It failed with null before this change.

JUDGE_IMPL_VERSION is not bumped: it is part of every online score id, and
this change does not change how a trace is scored.
…ction project (ai-team#271)

The Actor env keys point at the dataset-run project (MCP Agent Evals),
because the workflow runner starts the judge without keys. An online run
that fell back to them selected from and wrote scores into that project
without an error. Online mode now resolves the keys' project with
GET /api/public/projects before any other Langfuse call and fails unless
it matches the new langfuseProject input (name or id, default
Apify AI Agent). Dataset-run mode is unchanged.
…Langfuse keys (ai-team#271)

The schedule ran the Actor with no keys, and the README said to put them
into the Actor env vars. Those must stay on the dataset-run project, so
the daily run read and scored the wrong project. The schedule now runs a
Task (RUN_ACTOR_TASK) whose input holds the online settings and the
Apify AI Agent keys as secret input. online-task.json is the Task body
with placeholders only; the schedule README covers the Task, the keys
and the creation steps, and the Actor README describes online keys.
…and drop the unused checkpoint write (ai-team#267 review)

A root span that starts in the last AGENT_REQUEST_TIMEOUT_MS of the window
may still be running at the window end, so one quiet turn straddling the end
(1 in, 0 completed) no longer fails the run or a fixed-bound backfill;
isCompletionGateBroken() requires a root started before window.end - 30 min.
requestedWindow() returns the raw bounds so the empty-window log does not
recompute them. selectTraces() only returns the checkpoint record now: the
writeCheckpoint option and Selection.checkpointWritten had no caller left.
…69 review)

Deduplicating messages on role plus text dropped a real repeat such as
user: "yes" ... user: "yes", pointed the prompt at the first one and lost
everything between the two. collectItems() now skips, per generation, the
longest prefix of its input that matches message for message what earlier
generations already contributed, and never dedups by text. Also drops the
stale module-comment reference.
…arn once per batch on a schema mismatch (ai-team#269 review)

Nothing escaped the fence literals, so a tool result or user message
containing <<<APIFY_AI_TURN_DATA_END>>> closed the fence. The delimiters now
live in online-render.ts (re-exported from online-judge.ts) and
renderTurnForJudge() replaces every occurrence inside the turn with a visible
marker, looping until none is left so a nested one cannot reassemble.
The toolSchemaHash mismatch is expected on every trace until the agent fix,
so judgeOnlineTrace() logs it at debug and main.ts warns once per batch.
… window above 50% judge failures (ai-team#270 review)

- Each trace's scores are written right after it is judged (main.ts), so a
  timeout at trace 95 of 100 leaves 94 in Langfuse; finishOnlineRun() now
  takes what was written and does only the rollup and the checkpoint.
- Both score copies set environment and carry it in metadata; the run id
  and the rollup item id are suffixed with the environment for every
  environment but prod, so a staging backfill never joins prod's run or
  rollup (also closes the #25 thread on finishing the per-environment
  design).
- TooManyJudgeFailuresError replaces AllJudgementsFailedError: the window is
  held when more than MAX_JUDGE_FAILURE_RATIO (0.5) of the traces given to
  the judge failed, not only when all did.
- Rollup comments say what is true after a retry (scores are the source of
  truth; n and passes count what each run wrote); rollupItemId() is used
  wherever the id is built; main.ts uses DEFAULT_ONLINE_JUDGE_MODEL,
  DEFAULT_JUDGE_MODEL and HOLISTIC_SCORE_NAME instead of literals.
…e Task not the Actor (ai-team#271 review)

llm.ts makes three attempts of 120 s with 2 s sleeps, 364 s worst case per
trace (the doc comment said two attempts); 25 waves at concurrency 4 is
152 min, so timeoutSecs is 10800. The run-status alert goes on the
apify-ai-online-evals Task, because the workflow runner calls the same Actor
in datasetRun mode and an Actor-level alert would report offline failures
too. The pass-rate alert filter can add environment = prod now that scores
carry it.
…d document the changed online behaviour (ai-team#268 review)

Score configs cannot be deleted, so the script now resolves the keys'
project first and stops unless it is Apify AI Agent (LANGFUSE_PROJECT
overrides). The Actor README follows the review changes: settled completion
gate, per-trace writes, environment on scores and ids, judge failure ratio,
fence-marker stripping, rollup semantics after a retry.
…existed (ai-team#270)

The first cloud run after the environment change replaced the day's existing
rollup because isRollup() demanded the new field; an older item is still a
rollup and merges as the fresh (prod) environment.
@RobertCrupa
RobertCrupa force-pushed the fix/online-evals-blockers branch from 53098a9 to 010fb79 Compare October 9, 2026 10:52
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 9, 2026
@apify-service-account apify-service-account added the t-ai Issues owned by the AI team. label Oct 9, 2026
@RobertCrupa
RobertCrupa merged commit d3edfa2 into main Oct 9, 2026
3 of 7 checks passed
@RobertCrupa
RobertCrupa deleted the fix/online-evals-blockers branch October 9, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants