feat: Switchyard token-capture integration for OpenHands SWE runs (zero-fork) - #2026
feat: Switchyard token-capture integration for OpenHands SWE runs (zero-fork)#2026linj-glitch wants to merge 7 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>
What
Opt-in routing of OpenHands policy calls through a token-capture Switchyard proxy, so every call's exact vLLM prompt token ids, generation token ids, and sampled-token log probabilities are captured on the wire and returned in Gym's existing trainer-facing Responses API rollout (
TokenIDLogProbMixinitems). Reward, patch production, and evaluation are unchanged.Zero-fork architecture
No nv-OpenHands change; the pinned fork commit and YAML pins do not move. The pinned client is steered entirely by configuration Gym writes per run:
modelbecomes the Switchyard route id (Switchyard dispatches on it and substitutes the target model upstream);completion_kwargs.proxy_x_session_id, which lands verbatim in the client's request body — Switchyard resolves it (header > body > launcher API key) and strips it before forwarding;NEMO_GYM_CONFIG_DICTis rewritten to Switchyard's host:port (eval container keeps the original).The body-field session channel is the cross-repo counterpart, landed on Switchyard PR #63 as
eb351a7. Gym has no dependency on the Switchyard package — the coupling is a wire contract (OpenAI chat completions in,proxy_x_session_idcorrelation, versioned record JSON out), guarded by aschema_versiongate.Activation and safety
switchyard_base_url, generated scripts/configs are byte-identical to today (tested).mask_sample=true,switchyard_trace_error) while preserving the OpenHands patch, eval metrics, and reward. Strict-history reconstruction never repairs a divergent chain.Changes
nemo_gym/switchyard_trace.py(new): fail-closed session validation, strict-history reconstruction, tool mapping. Shared infrastructure — depends only on Switchyard's record schema and the shared Responses converter, so any agent server (not just swe_agents) can adopt Switchyard trace collection with it.responses_api_agents/swe_agents/app.py:switchyard_base_urlconfig + startup URL validation, per-run session UUID, agent-container TOML/config-dict routing, post-run retrieval + masking inrun(),switchyard_trace_erroron the verify response.gym_switchyard_integration.md: full design doc (updated to the zero-fork architecture).tests/unit_tests/test_switchyard_trace.py(validation matrix, two-call tool-trajectory reconstruction) andtest_app.pyadditions (gating, TOML/config-dict routing, run success/failure/masking, URL validation).Testing
tests/unit_tests/test_switchyard_trace.py+tests/unit_tests/test_responses_converter.py: 226 passed; ruff clean.🤖 Generated with Claude Code