Skip to content

2/3 Add the redesigned VeRO system (vero/) - #45

Open
varunursekar wants to merge 60 commits into
pr1-legacy-relocatefrom
pr2-add-vero
Open

2/3 Add the redesigned VeRO system (vero/)#45
varunursekar wants to merge 60 commits into
pr1-legacy-relocatefrom
pr2-add-vero

Conversation

@varunursekar

@varunursekar varunursekar commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Second of three stacked PRs splitting #41. Base: pr1-legacy-relocate.

Adds the v0.5 system at the repo root: vero/ (core + harbor eval sidecar, build compiler, runtime/observability), vero-tasks/, runs/. Root README points to legacy/. Pure adds (197 files) + README/.gitignore.

Review after 1/3. 3/3 adds harness-engineering-bench/ on top.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the complete v0.5 VeRO system (197 new files) covering the core optimization engine, Harbor eval sidecar, build compiler, and runtime/observability stack. The change is a pure addition with no modifications to existing files.

  • Evaluation engine + budget ledger: The EvaluationEngine and BudgetLedger implement correct cancellation-safe patterns via a new _drain_write helper that centralises the previously hand-rolled shield/rollback logic; the reserve, refund, and engine exception handlers are all consistent.
  • Harbor sidecar: The CanonicalVerifier, EvaluationSidecar, and InferenceGateway are well-structured with atomic writes, timing-safe token comparison, and a principle-of-least-privilege agent/admin boundary. _safe_extract_tar now applies filter=\"data\" matching extract_harbor_session_archive.
  • run_result_to_messages in vero/src/vero/utils/tokens.py: The helper reduces every output or content list to its first element; in a Responses API message that carries parallel tool calls, all but the first call are silently discarded.

Confidence Score: 4/5

Safe to merge; the one functional concern is confined to a utility helper used in agent conversation history and does not affect the evaluation engine, budget ledger, or Harbor security boundary.

The evaluation engine, budget ledger, sidecar auth, and Harbor compiler are carefully implemented and the cancellation-safety issues from the previous review cycle have been addressed. The only confirmed defect is in run_result_to_messages, which silently drops all but the first content block from a multi-block Responses API output item — a scenario that arises whenever an agent makes parallel tool calls.

Files Needing Attention: vero/src/vero/utils/tokens.py — the process_item helper truncates multi-block output/content lists

Important Files Changed

Filename Overview
vero/src/vero/utils/tokens.py run_result_to_messages silently truncates multi-block output/content to first element, dropping parallel tool calls
vero/src/vero/evaluation/engine.py Authorization, budget reservation/refund, and cancellation-safe shield patterns are all correctly implemented; previous review issues are resolved
vero/src/vero/evaluation/store/budget.py New _drain_write helper centralizes the reserve/refund cancellation-safe write pattern; CancelledError propagation is correct in both reserve and refund paths
vero/src/vero/sidecar/auth.py Admin token generation uses secrets module, atomic write with strict permissions (0o400/0o700), and timing-safe comparison; well-implemented
vero/src/vero/sidecar/verifier.py CanonicalVerifier candidate selection, re-scoring, and finalization logic is well-structured with idempotent durable writes; baseline floor semantics are correctly handled
vero/src/vero/harbor/build/compiler.py _safe_extract_tar now includes filter="data" matching session.py's defensive posture; previous review concern is resolved
vero/src/vero/gateway/inference.py Inference gateway with token authentication, per-scope budgets, and cancellation-safe semaphore + lock pattern; streaming SSE usage extraction handles both Chat and Responses API formats

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
vero/src/vero/utils/tokens.py:22-26
**Multi-block output silently dropped to first item**

When an item has more than one entry in its `output` or `content` list — for example, a Responses API message that contains both a text block and one or more parallel tool-call blocks — `process_item` replaces the entire list with just the first element. Every tool call after the first is silently discarded. When the result is later passed as conversation history to another model invocation, the model will not see that those tool calls were ever made, which will cause the agent to mis-attribute or duplicate tool calls on the next turn.

The test `test_run_result_to_messages_uses_first_content_item` confirms this is the current behavior, but parallel tool-use is a common pattern in the Responses API. If truncation to the first block is intentional (e.g. this function is only ever called in contexts where multi-block output cannot arise), the docstring should state that precondition explicitly.

Reviews (31): Last reviewed commit: "Tell the optimizer its results are persi..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Introduce the v0.5 codebase: vero/ (core + harbor eval sidecar, build
compiler, runtime/observability), vero-tasks/, and runs/. Root README points
to legacy/ for the original-paper code.
@socket-security

socket-security Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​orjson@​3.11.910010010010070
Addedpypi/​wandb@​0.28.074100100100100
Addedpypi/​litellm@​1.92.074100100100100
Addedpypi/​openai@​2.46.090100100100100
Addedpypi/​pydantic@​2.13.4100100100100100
Addedpypi/​fastapi@​0.139.0100100100100100

View full report

Comment thread vero/src/vero/optimization/optimizer.py Outdated
Comment thread vero/src/vero/harbor/build/compiler.py Outdated
varunursekar and others added 12 commits July 23, 2026 11:51
…back #1)

Addresses the 'baseline floor fails open' regression:
- baseline_floor now defaults to false. The floor gates a ship on a
  validation comparison while the reward is on the (possibly
  differently-distributed) target, so it is opt-in.
- Pin the fixed seed's score to avoid re-scoring it every run
  (reproducibility + one fewer eval): VerificationTarget.baseline_reward
  (per-target, post scale/offset) and VerificationSelection
  .baseline_selection_score (for the floor). Plumbed through the build
  config + compiler.
- Fail safe when the floor is on, unpinned, and the seed re-score can't be
  measured: ship nothing (NoCandidateError -> shipped=false + infra error)
  instead of shipping the best candidate unverified (the fail-open bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1)

Add an admin-only way to generate the number to pin: CanonicalVerifier
.measure_baseline(replicates) admin-scores the fixed seed N times on the
selection partition and each target and returns per-key mean/stddev. Exposed
as POST /score/baseline and 'vero harbor score-baseline --replicates N'.
Reuses the existing admin scoring machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atten

The benchmark-config tests read harness-engineering-bench/ (a separate branch
in the stacked split), so skip them when it isn't checked out. Update the
paths for the candidates/ -> top-level flatten (all benchmarks are now
top-level siblings of gaia/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
min_aggregate_cases defaulted to 1 — a vacuous floor that let an agent read
held-out validation labels one case at a time via single-case aggregate
evals. Default to 5 in both the sidecar policy and the build AgentAccessSpec
so a build that omits it is safe rather than unfloored (shipped builds
already set 5 explicitly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-enabling harness isolation broke the eval: 'uv run' as the unprivileged
harness got a fresh empty uv cache and couldn't resolve the candidate package
from it (No module named 'gaia_agent'), with no network to download at eval
time. Pre-warm /home/harness/.cache/uv from the build (root) cache in the
sidecar image, matching the UV_CACHE_DIR the backend sets for the harness user.

Also fixes a test regression from feedback #2: with the k-anonymity floor
defaulting to 5, the instruction now reads 'must include at least 5 cases'
rather than 'arbitrary subsets'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ack #8)

The mean-of-k recorded only score/n_attempts/n_scored, so an outage-diluted
cell (dead attempts zero-filled) was indistinguishable from a genuinely clean
low cell. Add n_dead_infra (dead-to-infrastructure attempts, classified via
the existing error taxonomy) and n_clean (the rest), so the split is visible
in the live metrics rather than only reconstructable from raw trial records.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- optimizer.run: validate self.max_proposals >= 0 before the proposal_limit
  guard, so the specific 'must be non-negative' error is reachable (was a
  dead branch that always tripped the generic message first).
- compiler._safe_extract_tar: extractall(filter='data') to strip device
  files / setuid bits and neutralize unsafe links, matching
  extract_harbor_session_archive (also silences the py3.14 deprecation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A cold-start walkthrough of the harbor path: the three-container trust
boundary, a run end-to-end, the evaluation core, disclosure/selection, the
five security mechanisms, and a suggested outside-in review order keyed to
the source files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update harbor_requirement pin from harbor[modal]==0.18.0 to ==0.20.0
across the README and harbor tests. Remove the PYTHONPATH injection in
the harness eval launch: it was a no-op (uv strips PYTHONPATH from the
child env) and did not fix the "No module named <agent>" failure under
the dropped uid, so it and its test assertion are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The candidate is checked out at <mktemp>/repository; `mktemp -d` makes
that parent 0700 root. The eval launch chowned the repository (and the
staging tree) to the harness user but not the mktemp parent, so once the
process dropped to harness it could not traverse into its own workspace
to resolve the editable candidate package's absolute path — every eval
(baseline included) failed with "No module named <agent>" while harbor
itself still loaded from the harness-owned cache.

Grant the harness user traversal on that parent (chmod o+x) alongside the
existing chowns. o+x suffices because run_as drops to the user's own
uid+gid, so it is "other" relative to the root-owned parent. The dir
holds only candidate code, so widening traversal leaks no trusted data.

Reproduced and fixed against the real compiled sidecar image with a
faithful user+group+extra_groups privilege drop: 0700 -> "No module
named gaia_agent"; 0701 -> import resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a reachability probe: after chowning/granting traversal, run `test -r
<project_path>` as the dropped user before launching harbor. Readability
requires traversing every ancestor, so any provisioning gap (a missing
chown, a non-traversable parent) is caught here with a clear message
instead of resurfacing as a cryptic "No module named <agent>" several
retries downstream — which is exactly how the checkout-parent bug hid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the harness provisioning commands and reachability probe into
vero.harbor.isolation as a single source of truth, and have the eval
launch use them, so the runtime and the test can't drift.

Add tests/test_v05_harbor_isolation_container.py: inside a throwaway
Linux container it loads the real sandbox.py + isolation.py standalone
(stdlib-only, no install) and asserts, against the real uid/gid drop and
the real provisioning commands, that (1) a checkout under a 0700 mktemp
parent is unreachable to the dropped user when only the leaf is chowned
(the regression sentinel for the shipped bug), (2) it becomes reachable
after harness_grant_commands and the probe agrees, and (3) trusted
root-only state stays unreadable to the dropped user. Skips when no
docker daemon is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread vero/src/vero/evaluation/store/budget.py Outdated
varunursekar and others added 4 commits July 23, 2026 17:08
Agents don't know what "vero" is; every name they meet should say what it
holds. The workspace context directory is now .evals/ with results/ (was
evaluations/), tasks/ (was cases/), and plan.json (was evaluations.json).
Host-side paths (~/.vero, ./.vero/session) are operator-facing and unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One well-named entry point over the .evals/ context — the new avatar of the
legacy VeroAgent's three structured tools. Runner subcommands (run/status/
result/submit) delegate to the sidecar client lazily; the viewers (list/show/
cases/trace/diff/plan/tasks) are unprivileged stdlib readers of the disclosure-
projected tree, with bounded, paginated, metadata-first output. The compiler
bakes skills/evals/SKILL.md into the optimizer workspace by default
(include_evals_skill), and the instruction template now teaches `evals`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
min_aggregate_cases moves onto the core EvaluationAccessPolicy (omitted
resolves to 5 under aggregate disclosure, 1 otherwise) and the engine refuses
agent-chosen aggregate subsets below the floor after cost resolution, so the
protection holds on every path — programmatic API and local runs included —
not just compiled harbor deployments. The sidecar keeps its earlier check as
defense-in-depth and now passes the floor through its explicit authorization.

AgentAccessSpec.to_access_policy() is the single typed translation from build
spec to runtime policy; the compiler emits it nested in serve.json and
SidecarEvaluationPolicy holds it as `access`, replacing the hand-rolled flat
keys (disclosure/agent_evaluable/min_aggregate_cases/expose_case_resources)
that had to be kept in sync across three representations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidecar's context writer predates the .evals rename and had its own
literals: exposed task resources landed in .evals/cases/ (README, skill,
and instruction docs all say tasks/) and plan.json was never written in
the harbor topology, breaking the documented 'evals plan' first step.
Caught live in the first gaia run of the new stack.
Comment thread vero/src/vero/evaluation/budget.py Outdated
AgentContextDirectory now owns write_case_resources and
write_evaluation_plan; both WorkspaceContextManager and the sidecar
feed it ContextPlanEntry rows (policy, evaluation set, resolved
budget), so the tree layout can no longer drift between topologies.
Top-level names (results/, tasks/, candidates/, plan.json) are module
constants. Behavior note: case-resource exposure now keys on
expose_case_resources alone (which model validation already ties to
agent_visible); the sidecar previously also required agent_can_evaluate.
…ecar

The gateway now keeps a size-rotated JSONL log of every request it
proxies or denies (scope, attribution, model, status, latency, tokens,
and head+tail-truncated request/response bodies; SSE responses are
captured from the chunks already tapped for usage). On by default in
compiled tasks at /state/inference/requests with a 16KB per-body cap
(inference_gateway.log_requests / request_log_body_bytes).

The trusted sidecar - the only credential holder - mirrors the gateway
state into the existing W&B run: a poller logs per-scope usage series
(live optimizer producer-scope burn included) every 30s and ships
rotated log files as artifacts, sharing the sink step counter. At
finalize the gateway ledger and request log are copied into the
session's artifacts so /session/export preserves every
request-response, W&B or not.
The loader resolves an existing local task_source to an absolute path,
while the committed partition manifest records it relative to itself;
compare resolved locations before rejecting. Registry sources still
compare literally.
@shehabyasser-scale

Copy link
Copy Markdown
Collaborator

Really nice split. Breaking #41 into relocate-legacy / add-system / add-benchmarks makes it reviewable again (Greptile passes each now), keeps the old stack intact under legacy/, and reads as clean pure-adds. I went through the system tree closely against the legacy Harbor stack.

Two things I'd flagged on the earlier v0.5 snapshot are already handled here, thanks for that:

  • Baseline floor fails safe now (verifier.py:383, raises NoCandidateError if the seed can't be re-scored rather than shipping it unverified).
  • k-anonymity floor defaults to 5 (build/config.py:29, and resolved to 5 under AGGREGATE in evaluation/models.py:707).
  • The in-process .veroaccess ACL being gone is fine: it's replaced by the privilege-drop + root-owned session dir isolation. Might be worth a one-line note in the PR that this was intentional.

One integrity concern I'd want resolved before this is relied on for competitive selection:

Infra-classified cases are excluded from the aggregate rather than scored at failure_score. An all-dead "infrastructure" case returns CaseStatus.ERROR (backend.py:1083-1105, the comment there literally says "excluded from the aggregate") and is dropped from informative_scores, so the mean divides by the informative count only (backend.py:1324-1330). The transient-infra classification matches the candidate process's own exception type name + message against a regex allowlist (error_taxonomy.py:131-159: rate.?limit|...|time(?:d.?)?out|connection|...). Budget and auth are detected out of band (good), but transient-infra is not.

If a candidate-attributable failure can reach that path (a candidate that raises ConnectionError, or emits a "timeout"/"connection" message, on a hard case), that case drops out of its own denominator and inflates the mean over the rest. That's the one-sided-selection lever the legacy code deliberately kept off: legacy always zero-filled dead attempts and documented infra retry as trusted-candidate-only. Here there's no trusted-only gate and infrastructure_max_attempts defaults to 3 (on).

Could you confirm whether the routing prevents a candidate-raised exception from being classified transient-infra? If it doesn't, I think the fix is:

  1. Score candidate-attributable all-dead cases at failure_score (restore the zero-fill invariant); restrict exclusion to harness-produced signals (coverage gaps, gateway-ledger budget events).
  2. Default infrastructure_max_attempts to 1, and gate retry > 1 to trusted / finalization evaluations only.

One more (high), in scale-vero-tasks: a single non-finite metric poisons the whole report. runner.py:154 writes score/metrics unguarded, so a NaN/inf from one case makes vero reject the report and collapse the entire run to the failure value, discarding all per-case data. Suggest enforcing finiteness at the TaskResult boundary.

Smaller, non-blocking follow-ups:

  • Whole-run retry replaces rather than merges groups (backend.py:1222), so with aggregate_attempts: best it's a best-of-3 amplifier tripped by one missing task.
  • A candidate self-timeout is classed transient-infra and excluded (backend.py:1041-1044) instead of scored at failure_score.
  • The legacy operator ERROR alarm and the partly-zero-filled-mean WARNING are gone (raw n_dead_infra/n_clean counts survive as metrics, but there's no grep-able signal in the logs).
  • No per-case infra_retry audit marker, so a score that survived N re-rolls looks identical to a first-attempt score.
  • DGM / Evolutionary counters + seeded RNG reset on resume (strategy.py:241), silently shifting the search distribution.

Happy to pair on the aggregate/retry piece if that's useful.

varunursekar and others added 7 commits July 25, 2026 15:07
The proxy route was declared in inference.py and then independently rebuilt by
string concatenation in four places that knew nothing about it: launch.json, the
compose file's OPENAI_BASE_URL, the seed script, and the trusted backend. Five
copies of three conventions — the scope segment, the attribution segment, and
/v1. Change the route and four sites keep producing URLs the gateway 404s or
403s on, inside a container, at run time.

Put the route shape in the layout as one string and derive everything from it:
scope_route for the gateway to register, scope_url for callers using this
layout's gateway, and scope_path for the trusted backend, which reads its base
URL from the deployment config rather than assuming ours. The optimizer's
attribution segment was duplicated three times and is now a field too. The
templates receive a precomputed producer_base_url instead of concatenating.

The test asserts on the compiled artifacts — compose, launch.json, and the seed
script — rather than on the constant, so it still holds if a template goes back
to building the URL by hand. Verified by doing exactly that with a wrong shape:
it fails.

Also folded in, both hygiene rather than risk: VERO_EVAL_URL was a
producer/consumer pair split between the compose template and the CLI that reads
it, and the gateway's upstream env var names were the last constants still local
to the compiler.

The OPENAI_* scrub set is now a named constant with the invariant written down.
Worth being precise about what that invariant is, since it looks alarming: the
blanking loop is default-deny, so a new credential added to `secrets` is blanked
for the optimizer automatically. The exclusion list is the explicit-allow
exception for the two names the template sets deliberately, and forgetting one
emits a duplicate compose key rather than leaking anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
task.toml.j2 hardcoded build_timeout_sec = 1800 while the timeout_sec two lines
above it was already parametrized, so one timeout in the same generated file was
tunable and the other was not. The outer build is not trivial — for a source
build it copies the vero tree, installs it plus harbor into the sidecar, syncs
the baseline, and pre-warms a uv cache — and there was no way to raise the limit
without editing the template.

Typed as an int to match the neighbouring verifier timeout, so the default still
renders as 1800 and compiled output is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
harbor/ held fourteen flat modules that are not peers: they run in three
different processes on either side of a trust boundary, and nothing in the
layout said so. Most of them were not Harbor code at all. The sidecar stack --
sidecar, app, serve, verifier, transport, auth, isolation, session -- imports
only itself, never backend, build, deployment or cli, and inference.py imports
only layout. deployment.py is the single module binding the two, which is
exactly its job. The nested-backend work sharpened the point: a compiled task
can now use a command backend and no HarborBackend at all, while every module
implementing it still lived under harbor/.

Split those out to vero/sidecar/ and vero/gateway/, hoist layout.py to the top
level so gateway/ needs no backwards edge, and leave harbor/ holding what is
genuinely Harbor: build/, backend, generation, deployment, cli. Regroup
evaluation/ along its own dependency layers into backends/, scoring/ and
store/. A reviewer can now read the trust architecture off the tree.

Package docstrings carry what the tree cannot: that nothing in sidecar/ is
Harbor-specific, and that its co-location is load-bearing -- HTTP carries the
API, but candidate code, the .evals context and the admin token all move
through volumes shared with main, so it is not deployable away from the task it
serves. That is also why it keeps the name.

evaluation/ is an internal regroup: every name is still re-exported from
vero.evaluation, so only deep importers moved. harbor/ -> sidecar//gateway/ is
a real move, so those exports were relocated rather than aliased back; leaving
compatibility shims in harbor/__init__ would preserve the false impression this
commit exists to remove.

Pure moves and import rewrites. No logic changes: 399 passed / 9 skipped before
and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
circle_factory built SidecarEvaluationPolicy with flat disclosure and
expose_case_resources keyword arguments, but those moved under a nested
access=EvaluationAccessPolicy some time ago. The sidecar container therefore
died at startup with three validation errors before serving anything.

Predates the package restructure -- the same call fails identically on
pr2-add-vero -- and no test covers it, because nothing exercises the example
factories. Found by actually booting the container: it now starts and serves
/health and /status, reporting full disclosure on development, aggregate on
validation, and the budget ledger.

ale_factory.py in harness-engineering-bench has the same stale construction and
is broken the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The example only had the hand-written path: circle_factory.py wiring a
CommandBackend directly, with the agent handed raw OPENAI_* credentials. That
is the same shape as ale-bench, and it means the one worked example of a
non-agent target is also an example of an unmetered optimizer.

The nested-backend refactor makes the compiled path available to such a target,
so add a build.yaml beside the hand-written task: same packing.py, same
evaluate.py, but `evaluation_backend: command` and an inference gateway. The
optimizer now reaches its model only through a scoped, allow-listed, metered
token; the raw upstream stays in the gateway container and is scrubbed to "" in
main. Two wirings of one problem, directly comparable.

Verified end to end with codex/gpt-5.4: reward 2.6324 against a 0.9598 baseline
and a ~2.635 known optimum, 0 exceptions in 20m46s. The gateway metered every
optimizer call -- 57 requests, 3.6M tokens, no denials -- and the evaluation
scope stayed at zero, since a command harness makes no model calls of its own.

passthrough_environment is set and commented because it is easy to get wrong:
CommandBackend deliberately runs the harness with PATH=os.defpath, so python
(in /usr/local/bin on the base image) is not found unless PATH is forwarded.
The failure surfaces only when the harness first runs, after the agent has
already spent its budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_v05_harbor_build.py carried two tests that read build YAMLs out of
harness-engineering-bench, guarded by a skipif on that directory existing. It
does not exist on this branch, so they always skipped here and only ran one
branch up -- where they had been failing for a while against stale expectations
nobody saw. A test that can silently skip on the branch that owns it is a test
that rots.

Drop them and the BENCHMARK_ROOT/_requires_benchmarks harness; they move to
pr3-harness-bench, where the benchmarks are always present and the guard can go
away entirely. Suite is unchanged at 399 passed, with skips down from 9 to 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
report.py was the largest module in the codebase at 43K, but 57% of that was
one raw string: a complete 25K HTML document with its own CSS and JavaScript,
pasted into _REPORT_HTML. Inside a string literal it gets no highlighting, no
formatter, no linter, and no diff worth reading -- and it made the module look
like a 43K Python problem when the Python is 19K.

Extract it verbatim to templates/report.html, loaded through
importlib.resources the way the packaged evals skill already is. Non-Python
artifacts get their own directory rather than sitting beside the modules,
matching harbor/build/templates and skills/evals. Substitution is unchanged:
still one replace of __VERO_REPORT_DATA__.

The extracted text is byte-identical to the old literal, so generated reports
are unchanged by construction rather than by inspection. The wheel ships the
file alongside the existing Jinja templates and skills/evals/SKILL.md, checked
by building one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread vero/src/vero/evaluation/store/budget.py Outdated
varunursekar and others added 4 commits July 25, 2026 22:27
The main container blanks every declared secret except the two the compose
template assigns itself -- the optimizer's scoped token and its gateway URL.
GATEWAY_ROUTED_CREDENTIALS named that exemption in Python while the template
spelled the same names out again, five copies of OPENAI_API_KEY and four of
OPENAI_BASE_URL between them.

The duplication is one-directional and quiet. Blanking is default-deny, so a
newly declared secret is scrubbed automatically and adding one is safe. The
failure is the other way: a name added to the exemption but not assigned by the
template would be neither blanked nor set, leaving whatever the host exported
in the optimizer's container -- the one outcome the scrub exists to prevent.

Move both names onto LAYOUT beside the other env-name fields, derive the
exemption from LAYOUT.routed_credential_envs, and have the compose file and
codex's provider config reference them. Nine literals become one definition.

Add a test that renders a compiled task and asserts every exempt name is
positively set with a scoped value, and that the raw upstream is blanked in the
same container. Verified it fails as intended by adding a third name to the
exemption: "STRAY_KEY is skipped by the scrub but never set".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
config.py held three unrelated things in 953 lines: the leaf models a
build.yaml composes, HarborBuildConfig itself, and the ${NAME:-default}
substitution language plus the YAML reader. Split into specs.py (313),
config.py (635), and loader.py (155). Only two places imported deeply;
build/__init__.py re-exports the same eleven names.

validate_references was 136 lines running six unrelated checks, including
45 lines of task_manifest JSON parsing. It is now six named validators in
the original order, so error precedence is unchanged and a rejected build
points at the rule it broke. The manifest check reads and parses the file
once instead of twice.

The fields are grouped into six private base classes, each with its own
docstring and the validators that concern only itself. Grouping by
inheritance rather than nesting keeps the wire format flat, so every
checked-in build.yaml parses identically -- verified by loading all five
benchmark configs and by comparing the old and new field sets (62 each,
same annotations and defaults).

That retires a duplicate list: the Harbor-only fields were enumerated once
as declarations and once in a hand-maintained frozenset, and
_HARBOR_ONLY_FIELDS is now derived from _HarborEvaluationFields. The two
had already drifted -- reward_key is Harbor-only in fact, since a command
harness reports its own reward, but was missing from the list, so setting
it on a command build passed validation and did nothing. It is now in the
group and rejected.

Two tests. One sweeps the derived set, so a field added to the group is
covered the day it lands; the other asserts the groups stay flat on the
wire and that every field belongs to one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The package restructure moved the sidecar stack, the gateway, and the
evaluation store without updating this doc, leaving fourteen paths that
resolve to nothing: harbor/sidecar.py, harbor/verifier.py,
harbor/session.py, harbor/app.py, harbor/inference.py, and
evaluation/budget.py. Since the doc's whole purpose is a suggested reading
order, a dead path is worse here than in a code comment.

Also names the four modules the build pipeline now passes through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of reserve's charge write, reserve's rollback write, and refund's
write hand-rolled the same shield-and-drain loop, and each got the same
detail wrong at least once: a write failure replacing the CancelledError
of the run being torn down. Two were patched individually in review; the
third, reserve's charge write, still called write.result() unguarded, so
an OSError there discarded the cancellation and the caller saw the wrong
exception.

_drain_write now owns the loop and returns both outcomes rather than
raising one, which is what makes the invariant checkable: no call site can
propagate a write error in place of a cancellation, because it has to
decide about both. Draining to completion is preserved -- an abandoned
write could overwrite a later one once the ledger lock is released.

Behaviour is unchanged except in the case that was broken. Reserve still
rolls its charge back on cancellation, and now skips the rollback when the
charge never landed. Refund still commits, since returning budget is safe
even for a run that is going away; that asymmetry is now stated rather
than implied.

The new test fails on the old code with OSError: disk gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varunursekar added a commit that referenced this pull request Jul 26, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_execute_record shields _record and refund in both exception handlers but
left the success-path _record bare, so a cancellation arriving after the
evaluation finished could abandon it part-way -- the budget charged for a
run the engine never finished publishing.

The mechanism is narrower than "the database write is lost": that write is
an asyncio.to_thread, and a cancelled await does not stop the thread, so
the file lands either way. What is actually skipped is everything after it
-- the listener loop, where the trusted side mirrors an evaluation to W&B
and the session archive -- plus the whole of _record when the cancellation
arrives before its lock is acquired.

Shield does not wait for the shielded coroutine, only protect it: the
caller still unwinds at once while _record completes in the background.
That is already the contract of the four shielded calls beside it, so this
makes the path consistent rather than introducing a new guarantee.

The test cancels during a listener, the one genuinely interruptible step,
and times out on the unshielded code.

Reported by greptile on #45.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varunursekar added a commit that referenced this pull request Jul 26, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varunursekar and others added 2 commits July 26, 2026 17:29
A held-out target can now set n_attempts/aggregate_attempts to score each
case several times and combine them (e.g. 3x mean to average a noisy final
eval), while search and selection keep the global attempts. Backends are
per-partition, so the override is injected into the target partition's
backend at compile time; a config validator rejects overriding an
agent-evaluable partition (shared with search) or a command backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uction

The instruction's insights paragraph was gated on overlay_present, which is
true whenever the always-baked evals skill is added -- so every optimizer was
told to dispatch an insights-generator subagent and read skills/insights that
no benchmark ships. The insights skill is retired; drop the paragraph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
varunursekar added a commit that referenced this pull request Jul 27, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varunursekar and others added 3 commits July 26, 2026 21:28
The optimizer interacts with VeRO through the `evals` CLI; treat it as a tool.
Three fixes so we stop handicapping it:

- Both eval modes with trade-offs. `evals run` blocks and returns the result by
  default (one call, stay in-loop); --detach is only for running several evals
  at once, then poll `evals status`. Every doc previously pushed --detach, which
  against Claude Code's 10-min Bash cap trained the detach/poll/end-turn pattern
  that ended a headless run after one eval. Updated the instruction template,
  the evals skill, and the CLI group help.
- Power-flags made discoverable. `evals run` options were bare; add help text so
  `evals run --help` self-documents subset iteration (--start/--stop/--case-id),
  replication (--seed), and the per-case/whole-eval wall overrides.
- Submit semantics. `evals submit` now explains it nominates your best candidate
  to ship, that you should beat the baseline first, and that not submitting
  falls back to auto-best then last-commit.

Also state the per-case wall limit as a rule (a slower candidate can be stopped
and score the failure value) so the optimizer treats latency as a constraint,
worded to stay out of the budget-blind redaction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: `evals status JOB` now reports client-derived `elapsed_seconds` and, for a
subset run, `requested_cases`, so a polling agent sees progress instead of a
bare `running`.

E: `evals wait JOB` blocks until a detached job finishes and prints its result
-- the blocking companion to `evals run --detach`, so the agent never hand-rolls
a poll loop (the pattern that hit the 10-min Bash cap and Exit 143). Optional
--timeout returns the still-running status cleanly (exit 0), so it is safe to
re-call. Instruction + evals skill updated to point at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tokens were the odd metric out — a per-evaluation sum while latency already had
per-case statistics. Token and latency distributions are unbounded and
heavy-tailed, so a mean alone hides the shape: give both a mean, a median, and a
max over cases. The agent-reported token counts are recorded per case and get
the full treatment; the trusted gateway figure is metered per evaluation, so it
contributes its sum plus a derived mean_case_inference_* (median and max need
post-hoc per-case attribution). Accuracy, being bounded, keeps mean and stddev.

Budget-blind redaction now matches these markers anywhere in a metric name, not
just as a prefix: mean_case_inference_input_tokens is the same budget signal as
inference_input_tokens and must not reach the agent. Latency carries no marker
and stays visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
varunursekar added a commit that referenced this pull request Jul 27, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varunursekar and others added 2 commits July 26, 2026 23:47
…jected

A live optimizer followed the instruction's advice to "pass --seed N to
reproduce a noisy comparison exactly" and got back
`400: invalid evaluation request`. Two separate defects met there.

HarborBackend.validate_request rejects request.seed outright, and every
Harbor-scored build renders that advice, so the flag could never work where
it was recommended. Gate it on a new seed_supported context flag: a command
backend does its own sampling and keeps the advice; a Harbor build is told to
replicate by re-running the identical selection, and that --seed is refused —
naming the flag so the rejection is not a surprise.

The rejection message also never reached the agent. Both the blocking handler
and the detached job path collapsed EvaluationRequestError to a bare
"invalid evaluation request", discarding a message that named exactly which
backend capability refused the request. Pass it through on both paths. This
is the same reasoning that already appends the exception type to unmapped
failures. Denials stay opaque: their message is policy, not capability, and
test_http_app_redacts_access_denial_details still pins that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An optimizer sizing a subset had no way to learn how many cases a partition
holds. Observed live: it asked for `--start 20 --stop 70` of a 49-case
development partition, and lost two evaluations to the rejection before
backing off to a range it had already proven safe.

write_evaluation_plan now asks each backend to cost its own base selection and
records the count, which `evals plan` surfaces as a `cases` column. The call is
advisory: a backend that cannot cost itself reports no count rather than
failing the plan for every partition.

The rejection itself was already well worded ("case range stops at 70, but the
Harbor evaluation set contains 49 cases") and was being discarded by the
opaque request-error mapping fixed in the previous commit. Together the agent
now both sees the bound up front and is told it on the way out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +191 to +208
except asyncio.CancelledError as error:
message = "evaluation was cancelled"
await asyncio.shield(
self._persist_failure(
store=store,
evaluation_id=evaluation_id,
backend_id=backend_id,
backend=backend,
request=request,
objective_spec=objective_spec,
principal=principal,
created_at=created_at,
code="evaluation_cancelled",
message=message,
status=EvaluationStatus.CANCELLED,
)
)
raise EvaluationCancelledError(evaluation_id, message) from error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Double-cancellation race leaks budget reservation

When asyncio.timeout fires it internally cancels the task, and the resulting CancelledError is correctly caught at line 191. The handler then calls await asyncio.shield(self._persist_failure(...)). If quiesce_agent_evaluations delivers a second task.cancel() while that shielded await is in progress, Python raises a new CancelledError from inside the except asyncio.CancelledError: block before raise EvaluationCancelledError(...) is reached. The engine only catches EvaluationCancelledError and EvaluationExecutionError; a raw CancelledError bypasses both handlers, so neither _record nor budget_ledger.refund is called — the reservation is permanently leaked.

Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/evaluation/evaluator.py
Line: 191-208

Comment:
**Double-cancellation race leaks budget reservation**

When `asyncio.timeout` fires it internally cancels the task, and the resulting `CancelledError` is correctly caught at line 191. The handler then calls `await asyncio.shield(self._persist_failure(...))`. If `quiesce_agent_evaluations` delivers a second `task.cancel()` while that shielded await is in progress, Python raises a new `CancelledError` *from inside* the `except asyncio.CancelledError:` block before `raise EvaluationCancelledError(...)` is reached. The engine only catches `EvaluationCancelledError` and `EvaluationExecutionError`; a raw `CancelledError` bypasses both handlers, so neither `_record` nor `budget_ledger.refund` is called — the reservation is permanently leaked.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

The env-var mitigation is not sufficient. Harbor forces
ENABLE_BACKGROUND_TASKS=1 and the build's agent_env overrides it back to 0
(env.update(self._resolved_env_vars) runs after the forced defaults, and the
raised BASH_MAX_TIMEOUT_MS demonstrably took effect — one call blocked 10.8
minutes, impossible under the 600s default). The variable gates *automatic*
backgrounding; it does not remove the Bash tool's run_in_background parameter,
which the model can still choose.

Observed live: the optimizer reached the validation stage, detached two jobs,
then backgrounded both `evals wait` calls, polled twice, said it would wait for
a notification, and ended its turn. The headless run exited there, killing the
waits. The previous wording only warned against ending a turn on a detached
job; backgrounding the wait itself reads as actively waiting, so it did not
land.

Name the actual failure: run every evals call in the foreground, and say that
a background task, a scheduled wake-up, or a promise to report back later ends
the run outright.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +209 to +238
except TimeoutError as error:
message = f"evaluation exceeded {request.limits.timeout_seconds} seconds"
await self._persist_failure(
store=store,
evaluation_id=evaluation_id,
backend_id=backend_id,
backend=backend,
request=request,
objective_spec=objective_spec,
principal=principal,
created_at=created_at,
code="evaluation_timeout",
message=message,
)
raise EvaluationExecutionError(evaluation_id, message) from error
except Exception as error:
message = str(error) or type(error).__name__
await self._persist_failure(
store=store,
evaluation_id=evaluation_id,
backend_id=backend_id,
backend=backend,
request=request,
objective_spec=objective_spec,
principal=principal,
created_at=created_at,
code="backend_error",
message=message,
)
raise EvaluationExecutionError(evaluation_id, message) from error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 _persist_failure unshielded in TimeoutError and Exception handlers

The CancelledError handler above (line 193) correctly wraps _persist_failure in asyncio.shield. The TimeoutError and Exception handlers do not. When asyncio.timeout fires, it absorbs the timeout's internal cancellation and re-raises as TimeoutError — but if quiesce_agent_evaluations has also called task.cancel(), that external cancellation is still pending. The first await inside the unshielded _persist_failure (typically await store.save(...)asyncio.to_thread) will deliver that cancellation. The CancelledError propagates past the raise EvaluationExecutionError(...) line, exits Evaluator.evaluate as a raw CancelledError, bypasses both EvaluationCancelledError and EvaluationExecutionError handlers in the engine, and leaves the budget reservation permanently charged. The same race applies to the except Exception: path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/evaluation/evaluator.py
Line: 209-238

Comment:
**`_persist_failure` unshielded in `TimeoutError` and `Exception` handlers**

The `CancelledError` handler above (line 193) correctly wraps `_persist_failure` in `asyncio.shield`. The `TimeoutError` and `Exception` handlers do not. When `asyncio.timeout` fires, it absorbs the timeout's internal cancellation and re-raises as `TimeoutError` — but if `quiesce_agent_evaluations` has also called `task.cancel()`, that external cancellation is still pending. The first `await` inside the unshielded `_persist_failure` (typically `await store.save(...)``asyncio.to_thread`) will deliver that cancellation. The `CancelledError` propagates past the `raise EvaluationExecutionError(...)` line, exits `Evaluator.evaluate` as a raw `CancelledError`, bypasses both `EvaluationCancelledError` and `EvaluationExecutionError` handlers in the engine, and leaves the budget reservation permanently charged. The same race applies to the `except Exception:` path.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

varunursekar added a commit that referenced this pull request Jul 27, 2026
Six findings from the reviews on #45/#46 that survived checking against
the current tree. Each was verified against the live litellm endpoint the
gateway proxies to, not inferred from a commit message.

gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and
deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and
get 400 "This model does not support image inputs", which caps achievable
reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns
200 for every request shape the gaia agent sends -- image input, hosted
web_search, reasoning.effort, parallel_tool_calls -- confirmed through the
SDK path the agent actually uses. The name is unprefixed on purpose: every
agent sends model_name.removeprefix("openai/"), so an openai/-prefixed
name would be allow-listed in one form and requested in another and the
gateway would deny it. The gaia held-out baseline now needs re-measuring.

tau3 executes every tool call the model returns instead of only the first.
deepseek-v4-flash does return two in a turn, and the usual guard is not
available: litellm rejects parallel_tool_calls for fireworks_ai with
UnsupportedParamsError, so the reviewer's suggested one-liner would have
400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a
customer-service agent, a verified identity with no message sent.

officeqa and swe-atlas-qna run their tool dispatch inside the try rather
than an else, and catch KeyError/TypeError/ValueError alongside
JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict,
so the model can return valid JSON missing a required key; that ended the
trial instead of feeding the model an error. Widening the except alone
would have caught nothing, since the lookups were in the else.

gaia's client gets max_retries=8, the only one of the five without it. A
within-trial transient failure scores at the failure value for competitive
evaluations, so an unretried 429 cost a candidate a 0.0.

tau3 quotes the MCP session id before interpolating it into a shell
command, for the same reason the payload beside it is base64-encoded.

per_trial_tokens buckets unattributed records under a name rather than
None, which crashed sorted() as soon as one appeared beside an attributed
record.

Not actioned: the browsecomp-plus grader was reported as vulnerable to
format-string injection from curly braces in the agent's response.
str.format does not rescan substituted values, so there is nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sposable

Observed live: run #4's optimizer piped `evals run` through `tail -60`, and the
payload is long enough that the score fell off the top -- it kept the per-case
wall metrics and discarded its own objective value. Truncating a large JSON blob
is what any agent will naturally do, so the fix is not to reformat the output but
to say plainly that nothing is lost by truncating it.

The full record is written under .evals/results/ before `evals run` returns and
stays there for the whole run, so `evals list` / `show` / `cases` / `diff` can
recover any of it. The instruction previously mentioned .evals/results/ only in
the exposed-partitions section, framed as a place to inspect failed trajectories
-- not as the durable home of every result. Say it in the workflow step where the
truncation actually happens, and name the failure it prevents: re-running an
evaluation to recover a number that was already on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +22 to +26
if key in item:
if isinstance(item[key], list):
item[key] = item[key][0] if item[key] else ""
return item

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Multi-block output silently dropped to first item

When an item has more than one entry in its output or content list — for example, a Responses API message that contains both a text block and one or more parallel tool-call blocks — process_item replaces the entire list with just the first element. Every tool call after the first is silently discarded. When the result is later passed as conversation history to another model invocation, the model will not see that those tool calls were ever made, which will cause the agent to mis-attribute or duplicate tool calls on the next turn.

The test test_run_result_to_messages_uses_first_content_item confirms this is the current behavior, but parallel tool-use is a common pattern in the Responses API. If truncation to the first block is intentional (e.g. this function is only ever called in contexts where multi-block output cannot arise), the docstring should state that precondition explicitly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: vero/src/vero/utils/tokens.py
Line: 22-26

Comment:
**Multi-block output silently dropped to first item**

When an item has more than one entry in its `output` or `content` list — for example, a Responses API message that contains both a text block and one or more parallel tool-call blocks — `process_item` replaces the entire list with just the first element. Every tool call after the first is silently discarded. When the result is later passed as conversation history to another model invocation, the model will not see that those tool calls were ever made, which will cause the agent to mis-attribute or duplicate tool calls on the next turn.

The test `test_run_result_to_messages_uses_first_content_item` confirms this is the current behavior, but parallel tool-use is a common pattern in the Responses API. If truncation to the first block is intentional (e.g. this function is only ever called in contexts where multi-block output cannot arise), the docstring should state that precondition explicitly.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

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.

2 participants