Add ViBench resources server + agent (PRD-to-web-app benchmark) - #2674
Conversation
|
/claude review |
|
SHIP WITH CARE — one RISK finding (metrics contract), otherwise a well-built environment with strong test coverage. The reward path itself is sound: RISK (inline, app.py:616): the Notes (author's call): |
| "plans_graded_rate": (graded / plans) if plans else 0.0, | ||
| } | ||
|
|
||
| def get_key_metrics(self) -> List[str]: |
There was a problem hiding this comment.
RISK — custom metrics don't work against the framework contract; silently absent in eval, crash on reverify.
AggregateMetricsMixin (nemo_gym/reward_profile.py:590) defines compute_metrics(self, tasks: List[List[Dict]]) — verify responses grouped by task — and get_key_metrics(self, agent_metrics: Dict) (one positional arg). Both overrides here have the wrong signature: compute_metrics(self, verify_responses) iterates the argument as flat dicts, and get_key_metrics(self) takes no arg.
Two consequences:
-
Silently absent in standard eval.
_call_aggregate_metrics(nemo_gym/rollout_collection.py:976) POSTs/aggregate_metricsto the agent server.vibench_agent→OpenCodeSandboxedAgentdoes not proxy/aggregate_metricsto the resources server (contrastsimple_agentapp.py:354, which does). So the base agent's defaultcompute_metrics({}) runs and these methods are never invoked — the whole point of this override (build_failure_rate,mean_seeding_failure_rate,plans_graded_rate, and theget_key_metricsheadline set) never reaches aggregate metrics. -
Crashes on reverify.
nemo_gym/rollout_reverification.py:577POSTs/aggregate_metricsto the resources server. Therecompute_metricsreceivesList[List[Dict]];r.get("reward")on a list →AttributeError, andget_key_metrics(self)called asget_key_metrics_fn(serialized_agent)→TypeError. Aggregation 500s.
FIX: (a) match the mixin signatures — flatten the grouped tasks in compute_metrics, and accept agent_metrics in get_key_metrics returning a dict subset; and (b) add an aggregate_metrics proxy on vibench_agent that forwards to the resources server (as simple_agent does), or these metrics never surface regardless of signature. Reward itself is unaffected (computed in verify), so this is observability, not score corruption.
There was a problem hiding this comment.
Fixed in e1e4bad — both parts confirmed against the framework before changing anything.
Signatures. compute_metrics now takes tasks: List[List[Dict]] and flattens the per-task groups; get_key_metrics(agent_metrics) returns a dict subset and preserves the mean/* default so nothing standard disappears.
Proxy. Added aggregate_metrics on vibench_agent forwarding to the resources server, as simple_agent does. You were right that without it the signatures were moot — build_failure_rate, mean_seeding_failure_rate and plans_graded_rate never ran at all.
Reward is computed in verify() and was unaffected, as you noted.
Tests now call these the way compute_aggregate_metrics does, including a conformance check that asserts the signatures match the mixin via inspect. The old tests passed throughout precisely because they called the methods the way my code expected — worth guarding against directly.
|
Thanks — the RISK finding was right on both counts, and I verified each against the framework before touching anything. Fixed in e1e4bad. Signatures now match Proxy added on Reward is computed in Tests now exercise these the way On the two notes: Gates: |
| return rewrite_loopback_url_for_docker(url) | ||
| return _origin(url) | ||
|
|
||
| def _create_opencode_config(self) -> Dict[str, Any]: |
There was a problem hiding this comment.
Claude suggestion, please take a look:
[P1] This method no longer matches the parent on main
This branch forked at 354babf. Since then #2120 changed the parent to take a request:
main opencode_sandboxed_agent/app.py:503 async def _create_opencode_config(self, request: Request)
here def _create_opencode_config(self)
Rebased on origin/main, the agent tests go to 1 failed / 15 passed with
TypeError: _create_opencode_config() missing 1 required positional argument: 'request'.
The resources server tests stay green at 61 passed / 2 skipped.
This one is worth catching before merge because of what the method does. It is what rewrites the model
URL for the sandbox. Without the rewrite the harness talks to itself, makes no LLM calls, and exports an empty app with no error. Nothing reports that as a failure, including
build_failed, because the build technically succeeded.
There is a second problem in the same method. Main now builds the URL through base_url_for_run
(nemo_gym/base_responses_api_agent.py:147), which adds /ng-rollout/<id> and the token capture suffix.
Line 150 throws that away and rebuilds from get_server_url(), which quietly turns off the token capture
path #2120 added. It is off by default, so nothing breaks today.
Both fix with one change. Take the parent's URL and rewrite only the host.
rewrite_loopback_url_for_docker (line 90) already does that and keeps the path. _origin only strips a
trailing slash or /v1, so add /v1 back:
async def _create_opencode_config(self, request: Request) -> Dict[str, Any]:
config = await super()._create_opencode_config(request)
options = ((config.get("provider") or {}).get("nemo_gym") or {}).get("options")
if isinstance(options, dict) and isinstance(options.get("baseURL"), str):
options["baseURL"] = rewrite_loopback_url_for_docker(options["baseURL"]) + "/v1"
return configOne thing to check. This drops the sandbox_model_base_url branch, so add it back if anyone uses
that option.
There was a problem hiding this comment.
if the opencode_sandboxed_agent keeps changing under you, it may be more stable to implement all here, cc @bxyu-nvidia as author of the base agent class.
There was a problem hiding this comment.
Confirmed and fixed in c49183d. Rebased onto current main first — reproduced exactly what you described: 1 failed / 15 passed with TypeError: _create_opencode_config() missing 1 required positional argument.
Took your suggested shape: the override is now async def _create_opencode_config(self, request) and rewrites only the host of the parent's URL, so the base_url_for_run path survives. Good catch on the token-capture suffix — I was rebuilding from get_server_url() and silently dropping /ng-rollout/<id>.
Kept sandbox_model_base_url: it replaces the origin but now preserves the path suffix, so an explicit address cannot drop the capture path either.
Added a conformance test asserting this signature matches the parent's via inspect. Both failure modes here are invisible at runtime — the harness just talks to itself and exports an empty app that build_failed does not catch — so a mismatch should fail loudly in CI rather than in a rollout.
Verified on the rebased tree: 63 resources server + 17 agent tests, gym env test --resources-server vibench 61 passed, pre-commit run --all-files clean.
There was a problem hiding this comment.
Fair concern — this is the second time a silent drift in the parent has bitten this branch (the other was compute_metrics against AggregateMetricsMixin).
My read is that inlining trades one failure mode for a worse one. Subclassing inherits the whole OpenCode install-and-drive path — version pinning, remote install script, config generation, export parsing — and ViBench overrides only two things: where the sandbox comes from, and rewriting the model URL host. Copying the rest would be several hundred lines that then drift from the base silently and permanently, rather than failing at a signature boundary.
What made both incidents dangerous was that they were invisible at runtime, not that they happened. So I have added conformance tests asserting the overrides match the parent signatures via inspect — a future change like #2120 now fails in CI instead of producing an empty app with no error.
Happy to go either way if @bxyu-nvidia would rather the base class not be subclassed externally — in that case the cleaner split might be a small extension point on OpenCodeSandboxedAgent for the sandbox URL, which would serve any provider whose boxes cannot reach host loopback, not just ViBench.
e1e4bad to
c49183d
Compare
dd1f730 to
ace821a
Compare
|
/ok to test ace821a |
ViBench (https://github.com/ViBench/vibench-public) evaluates whether a model can build a working web application from a PRD. Grading stands the finished app up for real, seeds it through its own UI, and drives it in a browser against a human-written test plan -- so the score reflects what a user can actually reach, not what the diff contains. This wraps ViBench as a single resources server rather than porting its orchestration scripts, which Gym replaces. The topology mirrors resources_servers/swebench: * seed_session starts the build sandbox from ViBench's base image and stages the PRD at /app/prd.txt. The agent never sees the test plans. * Any agent that consumes sandbox_handle can build; the config pairs with opencode_sandboxed_agent. * verify pulls /app out of the agent's sandbox and grades it by shelling into a ViBench checkout's run-seed-then-evaluate.py, once per test plan. One row is one (app, artifact) pair, so the app is built once and graded N times. Reward is the mean normalized score across that artifact's test plans, with per-plan values in reward_components. A plan that fails to seed scores zero rather than dropping out of the mean. Reward is continuous, not binary: ViBench test plans are scored step lists and partial credit is the signal the benchmark is built around. prepare.py generates task rows from a local ViBench checkout; example.jsonl covers 5 apps over 16 test plans. P0 is mvp artifacts only. REVERIFY_MODE is UNSUPPORTED because grading depends on live app and database state and cannot be recomputed from stored rollouts. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
That is the tag ViBench's own build_base_image_if_needed produces and reuses, so the build sandbox and the grading stack share one base image on the host instead of requiring a second, separately-tagged copy. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
gym env start builds an isolated venv per resources server from its requirements.txt; it does not inherit the root venv. The file previously held only a comment claiming no extra dependencies were needed, so the server died at import with ModuleNotFoundError: No module named 'fastapi'. The sandbox extra is required because app.py imports nemo_gym.sandbox, matching swebench. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
sandbox_provider is a reference to a provider instance, and every shipped provider config binds that instance as 'sandbox' -- 'docker' is the child key selecting the provider class, not the reference. Starting with sandbox_provider: docker failed with 'Sandbox provider reference docker is not defined in the merged config'. The provider config also has to be merged in explicitly, so the start command needs a second --config; swapping that one path is how you move to OpenSandbox, Fargate or Enroot without editing this config. Matches swebench and anyswe. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The first cut had the resources server create the build sandbox and hand the agent a sandbox_handle, copying swebench. That cannot work off OpenSandbox: only OpenSandboxProvider implements serialize()/connect(), so on Docker the rollout died with 'provider docker does not support serialize()/connect()'. That is by design, not a gap. NVIDIA-NeMo#2082 excludes node-local providers (Docker, Apptainer, enroot) from ConnectableProvider because the box has no network identity, and routes them through the sandbox server instead -- which is still unmerged in NVIDIA-NeMo#2085. The same issue classifies ViBench correctly: it is 'case 2', where the rollout copies an artifact out and the verifier grades it in a fresh box, so the sandbox never needs sharing. The first cut had accidentally made it case 3, the one shape that does need the sandbox server. So sandbox ownership moves to a new responses_api_agents/vibench_agent, which subclasses OpenCodeSandboxedAgent and overrides only sandbox acquisition and harvesting; installing and driving OpenCode is inherited. It stages the PRD via SandboxSpec.files, tars the built app into a shared artifact_dir, and passes the path to /verify. The resources server now creates no sandbox: seed_session returns PRD text, and verify unpacks the tarball. The tarball comes from a box the model controlled, so unpacking rejects members and links that escape the app dir, and artifact paths outside artifact_dir are refused. Works on Docker today with no dependency on NVIDIA-NeMo#2085. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
BaseRunRequest has no extra=allow, so model_dump() dropped the ViBench task fields and /seed_session rejected the request with 'app: Field required'. OpenCodeSandboxedAgentRunRequest exists precisely to let benchmark params propagate. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The first full rollout scored 0 with build_failed=true and no app. The OpenCode
export explained why: summary {files: 0}, tokens {input: 0, output: 0} -- the
model was never called, and nothing logged an error.
opencode_sandboxed_agent points the in-sandbox harness at
get_server_url(model_server), which is http://127.0.0.1:<port>. Inside a bridged
container that is the container itself, so the policy model is unreachable and
the harness silently does nothing. Sharing the host network namespace makes the
address mean the same thing on both sides -- the lever NVIDIA-NeMo#2082 uses
in its own example config.
Ships as a separate provider config used instead of the stock docker.yaml, since
network: host drops isolation for a container running model-written code and that
should be a deliberate choice, not a default. Also raises exec.default_timeout_s
from 180s, which would kill the build exec regardless.
Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The second rollout built an app (build_failed=false) but every test plan came back seeding_failed with grading_time_s under a second. The cause was run-seed-then-evaluate.py: its build context omits the seeding/ directory that Dockerfile.completed-app requires, so it dies immediately at 'COPY seeding /seeding: not found'. That entry point looks convenient but is broken -- every sibling creates the directory, and run-seed.py:95 makes an empty one when none is supplied. Switch to run-seed.py then run-evaluate-post-seeding.py, which is what ViBench's own results tree drives. That path also takes --test-assets, which the evaluation agent uploads while driving the app and which the previous single-call path had no way to pass -- so rows now carry test_assets_dir. Those fixtures stay grader-only and are never staged into the build sandbox. A grading timeout is now a zero for that plan rather than an exception, so one wedged compose stack cannot take down the rollout. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Two defects found reviewing this branch, the first verified by test: Arbitrary file deletion. verify() unlinked body.artifact_path in a finally block, so a path rejected by _resolve_artifact for reading was still deleted -- any file the server could write. It now resolves first and only ever unlinks the validated path. Blocking call on the async path. _grader_env used synchronous subprocess.run inside async _run_vibench_script, stalling the event loop for up to 120s, and ran once per grading call (six times per rollout). It is now awaited and cached per server. Also: env_creator failure raises instead of degrading to an empty environment, which previously surfaced as 'LLM API KEYS is not set' inside the container and sent debugging to credentials rather than to the call; and a None return code counts as failure rather than success. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded: mean reward 0.696, spread 0.00 to 1.00 with partial credit at 0.51 and 0.97. Replaces the single-task example rollouts. Scanned for secrets before committing -- known key formats, hub endpoints, AGENT_*_API_KEY assignments, Postgres credentials, and a manual read of the captured grader output in the failing task's error fields, which is the path a leak would most likely take. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Redaction: captured grader stdout is stored on PlanResult.error and ships in the committed rollout JSONL, while the environment those scripts run with holds AGENT_*_API_KEY. Scrubbing at the capture point means a stray set -x upstream cannot put a live key in a public file. Aggregate metrics: a mean reward cannot distinguish a weak app from one that never built or could not be seeded, and those need different responses. Each cause is now reported separately, with plans_graded_rate as the check that the verifier ran at all. Tar extraction also passes filter="data", the stdlib guard and the 3.14 default; the explicit checks stay because they name what was refused. Tests: prepare.py went from 0 to 98 percent, app.py to 96, and the agent from no tests at all to covering harvesting and sandbox staging -- including that a tar failure yields no artifact, that artifact names cannot collide across concurrent rollouts, and that test_assets reach evaluation but never the builder. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded, 16/16 plans, no build or seeding failures and no tracebacks. Mean reward 0.834 with a spread from 0.20 to 1.00, so the scale is neither saturated nor binary. The previous validation ran against code that predated the review fixes. This run used a build whose checksum matches the committed file, so the evidence describes what is actually in the branch. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The README and prepare.py claimed feature artifacts were blocked because ViBench ships no reference implementation. That is wrong: 542 RI files are tracked, real app source for 21 of 24 apps, including the setup-environment.sh and start-server.sh the grader requires. The actual blocker is this environment -- seed_session hands the agent a PRD and has no way to stage a starting codebase into the build sandbox. feature-ri is therefore a follow-up, not a dead end; feature-mvp is harder because it starts from a prior rollout's own output. Also drops the 'NVIDIA-NeMo#2082 case 2' framing from code and configs, keeping one pointer in the README. The label means nothing without reading an open issue, and its taxonomy could still change; what a reader needs is the fact that reaching into the agent's sandbox requires serialize()/connect(), which only OpenSandbox implements. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Timeout now SIGTERMs the process group before SIGKILL. ViBench tears its compose project down in a finally block, which SIGKILL skips -- so the wedged-stack case this timeout exists for was leaking postgres, the app, Playwright, the image tag and a port. Verified by test that SIGTERM comes first and SIGKILL only follows for a process that ignores it. The build contract is setup-environment.sh and start-server.sh, the scripts ViBench's prompt requires and its grading stack invokes -- not package.json, which scored any non-Node app as never built. evaluation_timeout_s is now a deadline shared across seed and evaluate, so a plan cannot consume two full windows. A failed asset upload no longer leaks a started sandbox, and a single plan's exception -- an unreadable plan file, a truncated scorecard -- is a zeroed plan rather than a 500 that loses the whole rollout. Only the model server binds 0.0.0.0, which the bridge gateway needs on Linux Docker Engine; the resources server, agent and head server stay on loopback since grader credentials live in those processes. Also: docstring and config now name the scripts actually run, REVERIFY_MODE is a ClassVar, and prepare.py uses a real temp file instead of writing into the ViBench checkout. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
A five-task run scored market_place as a build failure with reward 0: its app shipped a .venv, whose bin/python symlinks to the system interpreter outside the app dir, so the verifier refused the entire tarball as an escaping link. The guard is right; harvesting the venv was not. Dependency trees are excluded for the same reason node_modules already was, and setup-environment.sh rebuilds them. The same task scored 0.97 on an earlier run purely because that build happened not to create one, so this would have kept resurfacing at random. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The previous commit claimed to fix the docstring naming run-seed-then-evaluate.py but never touched vibench.yaml, which still pointed the next reader at the script this code deliberately does not run. It also re-broke the README feature paragraph, dropping the clause that says the reference implementation already exists and only needs staging. _terminate_group's final wait was unbounded, so a process ignoring SIGKILL -- uninterruptible I/O -- would hang the rollout after we had already given up on it. Now bounded by cleanup_grace_s; the hanging-process test drops from ~10s to 0.1s, which is the same defect showing up as test latency. Adds the missing assertions on paths that were correct but unproven: a failed asset upload stops the started sandbox, a truncated scorecard and an unreadable plan file each zero only their own plan, and one plan raising inside gather leaves its siblings' scores intact. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
A forced-timeout run against a real grading script leaked two containers and a network, and they were still there 60s later. The SIGTERM escalation added earlier did not work: CPython does not unwind finally on default SIGTERM, so run-seed.py died before reaching cleanup_compose_project, and killing docker-compose up does not remove anything -- only down does. SIGINT raises KeyboardInterrupt, which does unwind finally. Escalation is now SIGINT -> SIGTERM -> SIGKILL, with the full grace window on SIGINT since cleanup shells out to docker-compose. The same probe now reports zero leftover containers and networks immediately and at 60s. The unit test asserting [SIGTERM, SIGKILL] passed throughout, which is why this needed a real run. Its 'polite process' stub also left returncode as None forever, so the early-exit path was never exercised; a real process sets returncode once wait() returns. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
_run_vibench_script still described SIGTERM before SIGKILL, which is the behaviour that leaked containers. It now defers to _terminate_group rather than restating the mechanism, so the two cannot drift apart again. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
…gent Review found the metrics overrides used the wrong signatures. AggregateMetricsMixin passes compute_metrics rollouts grouped by task (List[List[Dict]]) and calls get_key_metrics(agent_metrics) expecting a dict back; these took a flat list and no argument, returning a list. On the reverify path that raises AttributeError and TypeError. It also never surfaced at all in standard eval: rollout collection POSTs /aggregate_metrics to the agent, and neither vibench_agent nor its parent forwarded it to the resources server, so build_failure_rate, mean_seeding_failure_rate and plans_graded_rate silently never appeared. Adds the proxy, as simple_agent does. Reward is computed in verify() and was never affected. Tests now call these the way the framework does, including a conformance check that asserts the signatures match the mixin -- the previous tests passed because they called the methods the way the code expected. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
… path Rebased onto main, where NVIDIA-NeMo#2120 changed the parent to async _create_opencode_config(self, request). This override still had the old sync no-arg signature, so on main it raised TypeError -- and the method is what rewrites the model URL for the sandbox. Without it the harness talks to itself, makes no LLM calls, and exports an empty app that no failure field reports, because the build technically succeeded. Same method also rebuilt the URL from get_server_url, discarding the rollout prefix and token-capture path that base_url_for_run adds. It now takes the parent's URL and rewrites only the host, so that path survives. sandbox_model_base_url still replaces the origin, but keeps the path too. Tests cover the capture path surviving, the override behaviour, and a conformance check that this signature matches the parent -- the mismatch that caused this is invisible at runtime. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Harvest excludes were ./-anchored, which GNU tar applies only at the top level, so nested node_modules and .venv were still tarred -- and a nested venv symlink makes the verifier reject the whole artifact. Verified on GNU tar 1.34 that unanchored patterns exclude nested trees. This means the earlier venv fix never actually worked. Also excludes export.json, the harness transcript the inherited responses() writes beside the app. Security: env_creator stderr was embedded verbatim in a RuntimeError that reaches PlanResult.error and the committed rollout JSONL, bypassing _redact. Now redacted before the message is built. The 0.0.0.0 model bind is documented as publishing that server and the run's token-capture path on every interface; the narrower gateway bind is noted but not defaulted, since ViBench configures custom bridge pools so the gateway is not a fixed value. A grader misconfiguration was swallowed by gather(return_exceptions=True), turning it into a dataset of silent zeros. It now raises GraderConfigError, which verify re-raises rather than zeroing. The timeout path cancelled communicate() without draining stdout, so an interrupted script could block on a full pipe instead of finishing its docker-compose down -- undoing the cleanup fix. A drain task now runs alongside termination. env_creator's own timeout leaked its subprocess. Also: grading scripts and env_creator now use ViBench's interpreter, as prepare.py already did; VibenchVerifyResponse allows extras so the task fields in the splat survive; add_evaluation_tags raises instead of silently no-opping; the artifact is deleted after grading rather than before; extractall and rmtree moved off the event loop; run() restores the parent's observability wiring so ng_agent_observations is captured; prepare.py picks the goal from the artifact instead of hardcoding zero-to-one; and the unused --workdir flag is gone. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded, 16/16 plans, no build or seeding failures, no tracebacks and no leaked compose projects. Mean 0.785, spread 0.36 to 1.00. market_place is the one to look at: it scored 0.00 with build_failed=True in the previous run, rejected on a nested venv symlink, and grades at 0.97 now that the harvest excludes are unanchored. That is the tar fix landing on the case that exposed it. Rollouts are also ~18 percent smaller because the harness transcript is no longer tarred into the app being graded. Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
CI failed on every shard plus the core Test job, both from causes in this server. test_task_data requires every resources server to ship a task_data.py describing its dataset rows; vibench had none, so "no loadable schema" failed both TestSchemaPresence and the committed-row sweep. The schema mirrors VibenchTaskRequest, the shared model behind seed_session and verify, and records the asset split that matters: asset_dirs is staged into the build sandbox, test_assets_dir is grader-only and must never reach the builder. Data validation then failed for a separate reason: vibench_repo_root was a bare oc.env interpolation with no default, so merely resolving the config raised a ConfigInterpolationError anywhere the variable was unset. Data tooling and CI load this file without ever starting the server, so it now defaults to an obviously-invalid path that resolves cleanly and fails loudly at runtime if left unset. gym env test +should_validate_data=true now exits 0 with "successfully validated". Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
716db71 to
84cc571
Compare
|
/ok to test 8719abe |
`vibench_agent` shipped without a `README.md` in #2674. `ng_test_all` filters testable components to directories containing one, then asserts the filtered count matches the discovered count — so discovery found 174 components and tested 173, failing that assertion in all eight server-suite shards even though every selected module test passed: ``` AssertionError: Mismatch on the number of total modules found (174) and the number of actual modules tested (173)! Extra candidate paths: - responses_api_agents/vibench_agent ``` `nemo_gym/cli/env.py:1114`, on `main` at a2c5222. This adds the missing README. Docs-only — no code or config changes. ## Contents - The build → harvest → verify flow, and that everything about driving OpenCode is inherited from `opencode_sandboxed_agent` (only sandbox acquisition and harvesting are overridden). - Why the agent owns its sandbox instead of sharing one: reaching into the agent's box needs `serialize()`/`connect()`, which only OpenSandbox implements, so the `swebench` shape cannot run on Docker, Apptainer or enroot. See #2082. - Why `configs/docker.yaml` must be used instead of the stock provider config — the stock 180s exec timeout kills long installs, and the harness is told the model is at `http://127.0.0.1:<port>`, which inside a bridged container is the container itself, so it makes zero LLM calls and exports an empty app. - An explicit warning not to "fix" that with `network: host`, and the `0.0.0.0` exposure caveat for the policy-model bind. Setup, task-row generation, reward definition and grading are not duplicated here; they stay in the [resources server README](https://github.com/NVIDIA-NeMo/Gym/blob/main/resources_servers/vibench/README.md). ## Note on the other CI failure The `Test Wheel Use` failure in that run is independent of this change: the wheel test enables prereleases/`unsafe-best-match`, unbounded `mlflow>=3.15.1` resolved to 3.15.2, which selected `sqlalchemy 2.1.0rc1`, whose source metadata uv rejects (`duplicate normalized extra name 'mssql-pymssql'`), aborting `arc_agi_resources_server` startup. That needs a SQLAlchemy pin below 2.1 or prerelease containment in transitive resolution — happy to open a separate issue or PR for it if useful. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com> Co-authored-by: raghavendran ramakrishnan <raramakrishn@nvidia.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Wraps ViBench as a NeMo Gym environment: a model builds a web app from a PRD, and grading stands the app up for real, seeds it through its own UI, and drives it in a browser against a human-written test plan. The score reflects what a user can reach, not what the diff contains.
Adds
resources_servers/vibenchandresponses_api_agents/vibench_agent.Design
The agent owns the build sandbox and copies the finished app out as a tarball; the resources server unpacks it and grades it in a fresh compose project, one per test plan, via ViBench's
run-seed.py→run-evaluate-post-seeding.py.The sandbox is never shared. Reaching into the agent's box needs
serialize()/connect(), which only the OpenSandbox provider implements, so the shapeswebenchuses cannot run on Docker, Apptainer or enroot. See #2082 for the design discussion; this PR has no dependency on it or on #2085.vibench_agentsubclassesOpenCodeSandboxedAgentand overrides only sandbox acquisition and harvesting.Reward
One row is one
(app, artifact)pair: built once, graded across that artifact's test plans. Reward is the mean normalized score, continuous rather than binary, with per-plan values inreward_components.build_failed,seeding_failure_rateandplans_graded_rateare reported separately, because a mean reward cannot distinguish a weak app from one that never built or could not be seeded.REVERIFY_MODEisUNSUPPORTED— grading depends on live app and database state.Validation
Five example tasks graded end to end on a Docker host. Scores spread from 0.00 to 1.00 with partial credit in between, and every zero carried a real scorecard with per-step diagnoses (HTTP 500s, SQL errors) rather than a harness failure.
The reward tracks capability: a weaker policy model scored 0.0 on a task with 0/19 steps, its app unreachable, where a stronger model scored 1.0 on the same task with the same graders.
Scores also vary across repeats of the same model, since it does not build the same app twice. Quantifying that — and separating it from any variance in the LLM-driven verifier — is what reward profiling is for, so
verified: falsestands.data/example_rollouts.jsonlholds a five-task run.Reviewer notes
network: hostis deliberately not used. The in-sandbox harness reaches the policy model atget_server_url(...)→127.0.0.1:<port>, which inside a bridged container is the container itself; the harness then makes zero LLM calls and exports an empty app with nothing logged.configs/docker.yamlinstead addshost.docker.internalviahost-gatewayand the agent rewrites loopback model URLs to it. Onlypolicy_modelbinds0.0.0.0; the servers holding grader credentials stay on loopback. Residual: the sandbox can reach host-published TCP ports, which inference requires.finally, which CPython does not unwind on SIGTERM. Escalation is SIGINT → SIGTERM → SIGKILL. Verified with a forced timeout on a real host: SIGTERM leaked two containers and a network, SIGINT cleaned up fully.artifact_diris a filesystem path shared by agent and resources server. Not a new constraint — grading already shells into a local Docker daemon.docker-composeonPATH. ViBench's scripts use the legacy name; Docker 29.x ships only the plugin. The failure misleads, so it is called out in the README.verifier_metadata, followingswebenchsinceseed_sessionandverifyshare one typed request model. Happy to move them.mvpartifacts only. ViBench ships reference implementations for 21 of 24 apps, sofeature-riis a follow-up; it needs a starting codebase staged into the sandbox the way the PRD already is.example_rollouts.jsonlis ~500 KB for five tasks. Happy to trim.Security
The tarball comes from a box the model controlled, so it is untrusted: unpacking refuses members and links resolving outside the app dir and passes
filter="data", and artifact paths outsideartifact_dirare refused and never deleted. Grader credentials are scrubbed from captured output before it is stored, since that output ships in the rollout JSONL. Test plans andtest_assets/are never staged into the build box.Testing
gym env test --resources-server vibenchpasses. 75 unit tests (60 resources server, 15 agent), coverage 99%.pytest tests/unit_tests/2621 pass.pre-commit run --all-filesclean.Checklist
pre-commit run --all-files).git commit -s).