Repository navigation
feat(swe_agents): support legacy Switchyard beta token capture - #2026
linj-glitch wants to merge 13 commits into
Conversation
Signed-off-by: Lin Jia <linj@nvidia.com>
- name the retrieval envelope's record list field (completions) and exact ordering keys (captured_at, uuid) - note serve --config rejects --enable-rl-logging and that retrieval works from any capture-enabled process sharing the rl-log-dir - require exactly one HTTP attempt per direct policy call: transport retries can duplicate capture records and mask the sample - document that token-field stripping is a no-op on the direct path - rename ng_switchyard_trace_error -> switchyard_trace_error to match existing agent_error_kind-style naming (no ng_ prefix convention) - cite the existing mask_sample flag and verify_golden_patch gate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
Implements the MVP from gym_switchyard_integration.md:
- SWEBenchWrapperConfig.switchyard_base_url (optional; absent = unchanged
behavior) and SWEBenchWrapperInstanceConfig.switchyard_session_id
(uuid4 hex, generated per OpenHands agent run, never for golden-patch
verification)
- OpenHandsHarnessProcessor exports SWITCHYARD_BASE_URL/SESSION_ID into
the agent container script only; the eval container is untouched
- switchyard_trace.py validates a retrieved capture session fail-closed
(schema, per-record token triples, session consistency, strict-history
extension) and reconstructs one token-annotated Gym rollout via the
existing VLLMConverter/split helpers
- SWEBenchWrapper.run retrieves GET /v1/sessions/{id}/completions after
the agent exits and replaces input/tools/output on success; any
failure masks the sample (mask_sample=true, switchyard_trace_error)
while preserving the OpenHands patch/eval/reward
Requires the nv-OpenHands fork commit adding direct Switchyard transport
to NemoGymClient (branch switchyard-direct-transport); the three config
pins will be bumped once that commit is pushed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Replace the planned nv-OpenHands client change with configuration Gym already controls, so the pinned fork commit needs no modification and no pins move: - write the Switchyard route id as the agent TOML model (Switchyard dispatches on it and substitutes the route target's model upstream) - ride the capture session on completion_kwargs.proxy_x_session_id, which lands verbatim in the pinned client's request body and which Switchyard strips before forwarding (body-field session support is added on the Switchyard PR #63 branch) - rewrite the model-server entry of the agent container's NEMO_GYM_CONFIG_DICT to Switchyard's host/port; the eval container keeps the original - validate switchyard_base_url as http://<host>:<port> at server startup (ServerClient can only target plain host:port) The pinned client keeps writing completion logs and metrics, so the fallback rollout and non-Switchyard behavior are unchanged. Doc updated to the zero-fork architecture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
How to activate Switchyard trace collection (YAML)Everything is off by default — enabling it is one key on the swe_agents server config. The three OpenHands configs now carry a commented example; uncomment and point it at your proxy: swe_agents:
responses_api_agents:
swe_agents:
agent_framework: openhands
# Route policy calls through Switchyard and rebuild the rollout with
# exact token ids. Must be exactly http://<host>:<port> (validated at
# startup) and reachable from this process AND the agent container.
switchyard_base_url: http://switchyard-host:4000Equivalently, keep the shipped configs untouched and set it per run via a Hydra override / overlay config on top of Prerequisite — the Switchyard side must be a capture-enabled serve (PR NVIDIA-NeMo/Switchyard#63 at # single-vllm.yaml
routes:
policy_model:
type: model
target:
model: <actual-model-served-by-vllm>
base_url: http://<vllm-host>:<port>/v1
api_key: dummy
format: openai
token_capture_engine: vllmswitchyard \
--routing-profiles single-vllm.yaml \
--enable-rl-logging \
--rl-log-dir /data/switchyard-traces/<job-id> \
-- serve(Use the What you get / what can happen:
Full details in 🤖 Generated with Claude Code |
The module is harness-neutral by design (depends only on Switchyard's record schema and the shared Responses converter), so it lives in the core library where any agent server can reuse it for Switchyard trace collection; its tests move to tests/unit_tests accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
…guity Two trainer-boundary hardenings: - mask_sample becomes a first-class top-level field on BaseVerifyResponse (default false). swe_agents populates it from its instance config; anyterminal_agent already surfaced it via TerminalBenchMetrics. This gives trainers one documented place to read the unreliable-reward signal instead of agent-specific config layouts (NeMo RL maps it to a zero loss multiplier). - Switchyard trace reconstruction now validates that each record's prompt token ids extend the prior record's prompt+generation tokens — the exact contiguity the trainer hard-asserts when concatenating the triples. A violating session masks the sample instead of crashing the training step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
Serves Gym model calls through a Switchyard routing proxy, so any benchmark
Gym already supports can be evaluated against a router without touching
harness code:
gym eval run --benchmark <name> --model-type switchyard_model
Switchyard is integrated at the model layer rather than the agent layer. The
existing Switchyard work in Gym (#2026 token capture, #2080 OpenCode) wires
one harness at a time under responses_api_agents/swe_agents; putting the
router behind a Responses API model instead makes it available to every
benchmark at once, which is what evaluating the router itself requires.
Two modes. Attach (default) points at a proxy run outside Gym, which keeps
proxy lifecycle and image pinning with the eval harness so a run can be
compared against a scaled-evals run of the same Switchyard commit. Launch
starts the proxy from a routing profile and shuts it down at exit, for a
single-command run.
Gym's rollout-attempt id is forwarded as Switchyard's opaque session id, so
proxy-side routing decisions and costs join back to the rollout that produced
them. The correlation prefix is stripped by Gym's capture middleware before
handlers run, so a thin outer middleware republishes the id for the request.
The nemo-switchyard dependency is scoped to this package rather than Gym's
core dependencies: only runs that route through Switchyard pay for it, and
attach mode needs no dependency at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Serves Gym model calls through a Switchyard routing proxy, so any benchmark
Gym already supports can be evaluated against a router without touching
harness code:
gym eval run --benchmark <name> --model-type switchyard_model
Switchyard is integrated at the model layer rather than the agent layer. The
existing Switchyard work in Gym (#2026 token capture, #2080 OpenCode) wires
one harness at a time under responses_api_agents/swe_agents; putting the
router behind a Responses API model instead makes it available to every
benchmark at once, which is what evaluating the router itself requires.
Two modes. Attach (default) points at a proxy run outside Gym, which keeps
proxy lifecycle and image pinning with the eval harness so a run can be
compared against a scaled-evals run of the same Switchyard commit. Launch
starts the proxy from a routing profile and shuts it down at exit, for a
single-command run.
Gym's rollout-attempt id is forwarded as Switchyard's opaque session id, so
proxy-side routing decisions and costs join back to the rollout that produced
them. The correlation prefix is stripped by Gym's capture middleware before
handlers run, so a thin outer middleware republishes the id for the request.
The nemo-switchyard dependency is scoped to this package rather than Gym's
core dependencies: only runs that route through Switchyard pay for it, and
attach mode needs no dependency at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Serves Gym model calls through a Switchyard routing proxy, so any benchmark
Gym already supports can be evaluated against a router without touching
harness code:
gym eval run --benchmark <name> --model-type switchyard_model
Switchyard is integrated at the model layer rather than the agent layer. The
existing Switchyard work in Gym (#2026 token capture, #2080 OpenCode) wires
one harness at a time under responses_api_agents/swe_agents; putting the
router behind a Responses API model instead makes it available to every
benchmark at once, which is what evaluating the router itself requires.
Two modes. Attach (default) points at a proxy run outside Gym, which keeps
proxy lifecycle and image pinning with the eval harness so a run can be
compared against a scaled-evals run of the same Switchyard commit. Launch
starts the proxy from a routing profile and shuts it down at exit, for a
single-command run.
Gym's rollout-attempt id is forwarded as Switchyard's opaque session id, so
proxy-side routing decisions and costs join back to the rollout that produced
them. The correlation prefix is stripped by Gym's capture middleware before
handlers run, so a thin outer middleware republishes the id for the request.
The nemo-switchyard dependency is scoped to this package rather than Gym's
core dependencies: only runs that route through Switchyard pay for it, and
attach mode needs no dependency at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
…ion provenance (#2141) Part of #2088. This PR now also absorbs #2600 (routing-condition provenance) so the whole feature reviews in one place. Adds `responses_api_models/switchyard_model/`, a model server that routes Gym model calls through a Switchyard proxy, so any benchmark Gym supports can run against a router without harness changes — unlike the per-harness integrations in #2026/#2080, which are right for RL trace capture but not for evaluating the router itself. Gym either hosts the router in-process from a native Switchyard TOML deployment (`deployment=routes.toml`; a bad deployment fails at startup, nothing outlives the process, and multi-worker configs are rejected since each worker would host its own proxy) or attaches to an externally run proxy via `switchyard_base_url`, which does support multiple workers. A caller-supplied `model` is always overridden with the configured route so an agent can't bypass the router, and Gym forwards its rollout-attempt id as the Switchyard session id (`x-switchyard-session-id`) so routing decisions and costs join back to the rollout that produced them. That id only arrives when model-call capture or token capture is enabled, so the server validates the requirement at startup: a warning under the default config, a refusal to start when forwarding was requested explicitly but no capture is on. From #2600: set `condition_dir` and the server records the routing condition the run served under — `switchyard-condition.json` at startup (route, mode, proxy URL, deployment SHA-256 plus an archive with inline `api_key` and all `extra_headers` values redacted, and the proxy's identity: the local wheel version when hosting, a caller-supplied `proxy_provenance` mapping when attaching) and `switchyard-stats.json` at shutdown (per-target requests, errors, tokens, and latency plus classifier-side usage from `/v1/stats`, fetched through Gym's shared async client as the configured caller and hard-capped inside the shutdown window). The snapshot labels its scope, since a hosted proxy's counters cover exactly the run while an attached proxy's aggregate everything it has served. Both writes are best-effort and cannot take down serving. This makes two routed runs comparable and a hosted run reproducible from the archived deployment; the docs page gains a "Compare routing conditions" workflow that aligns rollout rows by task and rollout index, reports rollouts missing a counterpart, and includes the classifier tokens the selected model's usage omits. The only dependency is `nemo-switchyard==0.2.0`, pinned exactly in this server's own pyproject.toml (Gym core never imports Switchyard); 0.2.0 is the floor because it is the first release whose Responses translation passes `NeMoGymResponse` validation (NVIDIA-NeMo/Switchyard#144), and the ceiling because upstream has already removed the Python serve stack this PR originally drove. 47 tests pass with 100% coverage on app.py, including an integration suite that drives the real 0.2.0 wheel with no mocks (chat/responses round trips, route override, stats, provenance files written across a real shutdown), and a live routed rollout ran against this revision with reward 1.0 (results in [this comment](#2141 (comment))). Two reviewer notes. `ModelCallRecord.model_ref` names the Gym model server, not the routed target, so per-call attribution must be read from captured payloads — promoting it to a first-class record field is the follow-up flagged for #2088 and needs a core-schema design pass. And `harbor_agent` overwrites `response["model"]` with the policy model name, so Harbor-run benchmarks report the route name for every call; flagging in case that overwrite is unintentional. --------- Signed-off-by: Lin Jia <linj@nvidia.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Merge current main, preserve native capture and current harness pins, reject incompatible capture modes, and keep prefilled assistant history in rollout input. Document that the external capture endpoint is available only on the Switchyard beta branch and still needs a real OpenHands deployment rollout. Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
|
/ok to test 05f448b |
Signed-off-by: Lin Jia <linj@nvidia.com>
|
/ok to test cde8b3d |
Signed-off-by: Lin Jia <linj@nvidia.com>
|
Correction to the older activation example above: this adapter targets Switchyard's In the beta routing YAML, The PR remains a draft: the previous real OpenHands/beta-proxy run captured valid token records but produced no patch, so an end-to-end model → patch → grading smoke is still missing. The additive merge |
|
/ok to test 4e0b01b |
|
Updated evidence at Four real capture records have aligned valid token/log-probability arrays, but the first response's prose after two tool calls disappeared from the parsed assistant/history. The first 7,367 prompt + 1,456 generated tokens total 8,823; the next actual prompt has 7,704 tokens and diverges at index 7,613. The adapter correctly rejected that history. We stopped after the conclusive mismatch; the real wrapper returned The PR remains draft. This identifies a tool-parser/history serialization limitation beyond reasoning mode; neither the guard nor the template was weakened. The body preserves the failed attempts and distinguishes baseline tests from fresh rollout outcomes. Required |
This optional adapter routes OpenHands policy calls through the legacy Switchyard beta proxy and reconstructs Gym training rollouts from exact prompt/generated token IDs and log probabilities, preserving patch/evaluation results and masking invalid traces; it is off by default and needs no separate issue beyond Switchyard #63.
It requires that PR's
rl-token-capture-betaimplementation at97424fc108e267a0885ce6d7f34806fd3b9ba3bd, which did not merge into Switchyard main: current native deployments must not be assumed compatible, and startup rejects simultaneous native Gym capture or OpenCode use, as explained in the integration guide.Head
4e0b01b5023fead76df081becb6e9b254d2bd30fadditively merges mainc155b77c8420283982ec30347a46b92ae6ee1af5, resolving an obsolete whitespace conflict while retaining main'sng_verifier_invocationsfield and the existing adapter behavior.With Python 3.13.14 and imports asserted to use this checkout,
python -m pytest -q -o addopts= --import-mode=importlib responses_api_agents/swe_agents/tests tests/unit_tests/test_switchyard_trace.py tests/unit_tests/test_base_resources_server.pypassed 323 tests; full pre-commit, actual PR-diff whitespace checks, and complete diff/body/comment guardword scans passed, and the merge commit has DCO sign-off.Earlier validation at
cde8b3dincluded 489 SWE/Switchyard/converter/base-response tests plus 43 AnySWE tests; prior regressions verified prefilled-history reconstruction and incompatible-mode rejection, and the changed-file secret scan passed with only a documented fixed test-session ID allowlisted.Keep this PR draft. Earlier Apptainer/OpenHands/Qwen3-1.7B trials captured 42 schema-1 records but produced no patch. A new bounded run at this head used Qwen3-8B revision
b968826d9c46dd6066d109eabc6255188de91218, pinned OpenHands, the originalpsf__requests-1766issue, and the beta proxy. It produced anauth.pypatch that the real verifier applied; the suite returned 45 passed / 46 failed, including httpbin DNS failures, withresolved=false. This demonstrates patch application and evaluation execution, not a reliable correctness result.The new run captured six individually valid schema-1 records, with generated-token/log-probability lengths 771, 1225, 692, 1132, 1093, and 2048. Final
run()postprocessing of the saved actual response against the live capture endpoint rejected record 1 because its prompt did not extend the prior prompt plus generation. Qwen reasoning tokens were absent from the next OpenHands prompt, so the guard correctly returnedmask_sample=truewithout emitting a token trajectory. An offline check of the official Qwen template with explicitenable_thinking=falsealso drops the empty reasoning prefix on the next tool round trip; disabling thinking alone does not establish compatibility. No token-prefix guard was relaxed.A later bounded trial used the official Qwen2.5-7B-Instruct template at revision
a09a35458c702b33eeacc393d103063234e8bc28, using vLLM 0.22.0 with the Hermes tool parser and without a reasoning parser or custom template, through the actualSWEBenchWrapper.run()path. The original coveragepy R2E rowc1bfa7352368b63f3a9b30c02f242408d07a7ab2and its public image were used; the agent-visible data excluded solutions and grading fields. The original native R2E verifier separately established an unpatched baseline of 6 passed / 1 failed.That live trial recorded four individually valid captures but also failed the token-history contract. The first response generated two task-tracker calls followed by prose; both the parsed assistant message and the next request retained only the calls. Its 7,367 prompt tokens plus 1,456 generated tokens made an 8,823-token sequence; the actual next prompt had 7,704 tokens and diverged at token index 7,613. Later matching transitions cannot repair that earlier loss. After this conclusive mismatch, the trial was intentionally stopped and the real wrapper completed cleanup with
mask_sample=trueand the record-1 prefix error. No patch was produced or patched verifier run completed; the separate unpatched baseline is not a rollout grading result.Keep this PR draft. The observed blocker includes loss of generated text during tool parsing/history serialization, beyond Qwen3 reasoning-template behavior. A representative model → patch → reliable verifier → valid token-trajectory run remains outstanding for a compatible deployment. No prefix guard was weakened, no custom template was added, and these tests do not establish compatibility with native Switchyard. No PR code changed for these trials. Required
Nemo_CICD_Test,Lint check, andDCOchecks pass at this head; the runtime compatibility gap and maintainer review still prevent readiness.