fix(eval): drive the agent eval with the OAuth token, and probe it for real - #3403
fix(eval): drive the agent eval with the OAuth token, and probe it for real#3403kovtcharov-amd wants to merge 6 commits into
Conversation
…r real The gate has been red on main and on every PR because the account behind ANTHROPIC_API_KEY is out of credit. Every other Claude workflow in this repo already prefers CLAUDE_CODE_OAUTH_TOKEN and falls back to the key; this one asked for the key alone. `gaia eval agent` drives AND scores each scenario through `claude -p` and never touches the SDK judge, so the CLI credential is the only one this gate needs. ANTHROPIC_API_KEY is blanked when the token is present, deliberately: runner.py adds `--bare` only when the key is set, and `--bare` restricts auth to the key alone, so leaving both would ignore the token. The preflight now asserts the credential is ACCEPTED rather than merely present. Checking only that the variable exists is what let this sit red for weeks: the key was there, the account was empty, and all five scenarios died in three seconds each with their stdout discarded.
Verdict: Request changesSwitching the eval gate to the OAuth token is the right fix and the reasoning behind blanking the API key checks out against the code it cites. The new live probe, though, is written in a way that breaks exactly when it is supposed to work. The blocking issue — the probe can't print its own error message. This step runs under PowerShell's "stop on any error" mode, and the probe captures the CLI's error output. In Windows PowerShell that combination makes the very first line of error output kill the step outright, so a rejected credential ends the run with a raw PowerShell stack trace instead of the actionable message the PR adds one line below it. The same step is also now fragile in the other direction: if the CLI prints any harmless warning to its error stream on a successful call, the preflight goes red for no reason — trading one always-red gate for another. This workflow already solves this exact problem elsewhere in the file, so the fix is a few lines and follows an established local pattern. Worth calling out that the PR's own test plan item — "temporarily point it at a bogus token to check the error is the one a reader can act on" — is the check that would have caught this. Running it before merge is the whole verification here. Real-world evidenceN/A — CI-workflow-only change; no evidence bundle was produced for this PR and this surface can't be exercised from the review lane. My verdict rests on static review plus reading the credential-selection code the PR's rationale depends on. The only meaningful proof is a dispatch of this workflow on the branch, and all three test-plan boxes are still unchecked — including the bogus-token check that targets the issue above. 🔍 Technical details🟡 ImportantProbe runs under The step sets
This file already documents the hazard and works around it in the sibling step:
Assigning to 🟢 MinorBackticks in the missing-credential message escape unevenly (line 504) — inside a PowerShell double-quoted string, The probe has no timeout (line 512) — a Same dead account still gates Strengths
|
|
Verified on this PR's own run, and the credential half is fixed. The scenarios now reach the agent instead of dying at the door. Before, all five errored in 3 seconds; now they run 14–18s, and The gate is still red, on a different and deeper cause. Four scenarios return And the new failure is still invisible for the same reason the old one was. The runner prints the subprocess's stderr while I would still merge this: it removes one real blocker, it is what every other Claude workflow in the repo already does, and the live probe means the next credential failure announces itself in the preflight instead of thirty minutes later as five mystery errors. |
`uv run` resolves the *project* environment rather than the active one, so on a checkout without a synced .venv it creates an empty one and the server dies with ModuleNotFoundError: No module named 'gaia'. A stdio MCP server that exits is CONNECTION_CLOSED to the client, which is what every tool_selection scenario reported once they got far enough to reach it. Reproduced directly: `uv run python -m gaia.mcp.servers.agent_ui_mcp --stdio` creates a fresh .venv and exits 1 before writing a single protocol frame.
13ccb64 to
7ccaeab
Compare
|
Second fix pushed, for the cause the first one exposed. With the credential working, the scenarios got far enough to say what was actually wrong — the trace artifacts carry the agent's own diagnosis, identical across all four:
The eval's MCP config launched that server with Reproduced directly rather than inferred: It now launches with That also explains why this looked environment-specific and was not: the config is checked in, so every runner and every branch hit it the same way. |
|
Third run, and the gate is measuring for the first time. Not green yet, and the residual is a different shape of problem. Two scenarios ran end to end and produced real scores — Three still report CONNECTION_CLOSED. So the Where that leaves things, honestly:
I have not merged it; that is a call for a human. |
On the first run where this gate actually measured anything, scenarios 1-3 died with CONNECTION_CLOSED at 19s, 17s and 36s while 4 and 5 then ran to completion in 197s and 336s. That is a warm-up curve rather than a flake: the server is spawned once per scenario, and a cold `import gaia.mcp.servers. agent_ui_mcp` pulls in most of GAIA and pays first-touch bytecode compilation plus, on this box, an AV scan of everything it opens. Two levers, both cheap: pay the import once in its own step so it is not inside the first scenario's startup budget, and give the client a startup window wide enough that a slow server is not mistaken for a dead one. A genuinely dead server still fails, just at 120s instead of the default.
The evidence contradicts the hypothesis. If MCP_TIMEOUT were taking effect the scenarios would wait 120s before giving up; they fail at 15-22s, the same as before. So the server is exiting, not timing out, and the earlier run where scenarios 4 and 5 passed was variance rather than a warm-up curve. Reverting rather than leaving a speculative change in the PR: the two fixes that remain are each reproduced.
|
Correction: I pushed a third fix on a warm-up hypothesis, the next run contradicted it, and I have reverted it. Recording that here so nobody re-derives it. The hypothesis was that the MCP server's first launches were slow rather than broken — scenarios 1-3 had failed at 19s/17s/36s while 4 and 5 completed in 197s and 336s, which reads as a warm-up curve. So I pre-warmed the import and set The next run failed all five at 15-22s. That is the disproof: had the wider startup window been in effect, a slow server would have been given 120 seconds before being dropped. Failing at the same ~15-20s means the server is exiting, not timing out — and the run where two scenarios passed was variance, not warming. So this PR is back to the two fixes that are each independently reproduced:
What is left is a server that starts and then exits, and its stderr is discarded, so there is nothing to read. That is #3375's fix, and it is now the blocker rather than a convenience — I have spent three runs inferring from timings what one line of captured stderr would have said outright. |
The server is a grandchild — the runner starts `claude -p`, and `claude -p` starts the server — so when it dies the client reports only CONNECTION_CLOSED and the server's own error goes nowhere. Capturing the scenario subprocess's output (#3375) does not reach it either. Three runs of this gate were spent inferring a cause from timings that one line of this log would have stated outright. The launcher execs the real server with stderr tee'd to eval-out/, which the workflow already uploads. stdout is deliberately untouched: it carries the MCP protocol, and one stray byte on it desynchronises the client.
Every eval scenario is driven through the Agent UI MCP server, and the job installed `-e .[dev,eval,ui,api]` — without `mcp`. The server therefore exited with ModuleNotFoundError: No module named 'mcp' before writing a single protocol frame, and the client reported that as CONNECTION_CLOSED. Found by capturing the server's stderr, which is a grandchild process whose output nothing was keeping. The preceding commits fixed a dead credential and a `uv run` launch that built an empty venv; each was hiding this one.
|
Root cause found, and it is one line. The job installed That is why it failed identically on Four commits, each of which had to come before the next could be seen:
I also pushed and reverted a warm-up hypothesis in between; the next run disproved it and I took it out rather than leave a speculative change in the PR. Worth keeping regardless of this gate: the preflight now proves the credential is accepted rather than merely present, and the launcher means the next MCP failure explains itself instead of costing five runs of inference. |
|
The gate is measuring. With the
Three of five now produce real scores against the baseline, where the gate had measured nothing at all. The last two are a path guard doing its job. Indexing refuses the eval corpus: "Access denied: path must be within home directory (C:\Windows\System32\config\systemprofile)". The runner's service account has that as HOME, while the corpus lives under the workspace. I would not take the fix the agent suggests. Both traces recommend widening the allowlist in Scope-wise this PR is done: it takes the gate from measuring nothing to measuring three of five, and the residual is a separate, well-understood environmental fault. |
The
Agent Eval — Gemma-4-E4B consolidationgate is red onmainand on every open PR, and has been for weeks. It is not measuring anything: all fivetool_selectionscenarios die about three seconds in, which is a launch failure rather than a bad answer. A gate that fails identically everywhere teaches reviewers to ignore it, which is worse than having no gate on the day it catches something real.The cause is that the account behind
ANTHROPIC_API_KEYis out of credit. A sibling job that does not discard its subprocess output says so outright:400 — Your credit balance is too low to access the Anthropic API. Every other Claude workflow in this repo already prefersCLAUDE_CODE_OAUTH_TOKENand falls back to the key; this one asked for the key alone.Closes nothing on its own — #3341 stays open for the stdout-swallowing half (#3375, #3368) — but it should turn the gate green again.
Test plan
tool_selectionscenarios run for their usual 200–680s rather than erroring in ~3srag_qualityandcontext_retentionstill report their existing embedder diagnosis (they stay blocked on ci(eval): the agent eval gate has never produced a scorecard — embedder will not load on its runner #3016; this PR does not touch them)Notes for the reviewer
Why the CLI credential is sufficient here.
gaia eval agentdrives and scores each scenario throughclaude -p— the score comes back in the--json-schemapayload.src/gaia/eval/runner.pynever importsClaudeClient, so the SDK judge is not on this path at all. The email quality evals do use the SDK and are unaffected by this change; they will keep failing until the account is funded.Why
ANTHROPIC_API_KEYis blanked rather than left alongside.runner.pyadds--bareonly when that variable is set, and--barerestricts Anthropic auth to the key or anapiKeyHelper— OAuth is never read. Setting both would silently keep using the empty account.The preflight change is the part worth keeping regardless of the credential. It asserted the variable existed. Asserting that a request is accepted — one line, one call — would have turned this into a startup error the first day instead of weeks of a red gate.