Skip to content

feat(swe_agents): support legacy Switchyard beta token capture - #2026

Draft
linj-glitch wants to merge 13 commits into
mainfrom
feature/switchyard-openhands-integration
Draft

linj-glitch wants to merge 13 commits into
mainfrom
feature/switchyard-openhands-integration

Conversation

@linj-glitch

@linj-glitch linj-glitch commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator

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-beta implementation at 97424fc108e267a0885ce6d7f34806fd3b9ba3bd, 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 4e0b01b5023fead76df081becb6e9b254d2bd30f additively merges main c155b77c8420283982ec30347a46b92ae6ee1af5, resolving an obsolete whitespace conflict while retaining main's ng_verifier_invocations field 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.py passed 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 cde8b3d included 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 original psf__requests-1766 issue, and the beta proxy. It produced an auth.py patch that the real verifier applied; the suite returned 45 passed / 46 failed, including httpbin DNS failures, with resolved=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 returned mask_sample=true without emitting a token trajectory. An offline check of the official Qwen template with explicit enable_thinking=false also 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 actual SWEBenchWrapper.run() path. The original coveragepy R2E row c1bfa7352368b63f3a9b30c02f242408d07a7ab2 and 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=true and 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, and DCO checks pass at this head; the runtime compatibility gap and maintainer review still prevent readiness.

Signed-off-by: Lin Jia <linj@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

linj-glitch and others added 4 commits July 14, 2026 13:46
- 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>
@linj-glitch linj-glitch changed the title docs: add switchyard openhands implementation handoff feat: Switchyard token-capture integration for OpenHands SWE runs (zero-fork) Jul 14, 2026
@linj-glitch

Copy link
Copy Markdown
Collaborator Author

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:4000

Equivalently, keep the shipped configs untouched and set it per run via a Hydra override / overlay config on top of swebench_openhands.yaml.

Prerequisite — the Switchyard side must be a capture-enabled serve (PR NVIDIA-NeMo/Switchyard#63 at eb351a7+), with a route whose id matches this config's model_server.name (policy_model in the shipped configs):

# 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: vllm
switchyard \
  --routing-profiles single-vllm.yaml \
  --enable-rl-logging \
  --rl-log-dir /data/switchyard-traces/<job-id> \
  -- serve

(Use the --routing-profiles ... -- serve form: serve --config is the Rust profile server and rejects --enable-rl-logging.)

What you get / what can happen:

key absent behavior byte-identical to main — no Switchyard codepath runs
enabled, capture healthy rollout input/output items carry exact prompt_token_ids / generation_token_ids / generation_log_probs; response metadata records the session id + record UUIDs
enabled, anything fails sample is masked (mask_sample: true, reason in switchyard_trace_error) — patch, eval metrics, and reward are preserved

Full details in gym_switchyard_integration.md.

🤖 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>
@linj-glitch
linj-glitch requested a review from bxyu-nvidia July 14, 2026 22:36
…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>
@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Jul 17, 2026
linj-glitch added a commit that referenced this pull request Aug 18, 2026
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>
linj-glitch added a commit that referenced this pull request Aug 25, 2026
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>
ananthsub pushed a commit that referenced this pull request Aug 26, 2026
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>
ananthsub pushed a commit that referenced this pull request Aug 26, 2026
…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>
@linj-glitch
linj-glitch marked this pull request as draft October 5, 2026 06:12
@github-actions github-actions Bot removed the sla:review-overdue Review response is over the one-business-day SLA label Oct 5, 2026
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>
@svcnvidia-nemo-ci svcnvidia-nemo-ci added area:agent Agent harnesses and Responses API agent behavior feature New capabilities, enhancements, or enablement work labels Oct 20, 2026
@linj-glitch

Copy link
Copy Markdown
Collaborator Author

/ok to test 05f448b

@linj-glitch linj-glitch changed the title feat: Switchyard token-capture integration for OpenHands SWE runs (zero-fork) feat(swe_agents): support legacy Switchyard beta token capture Oct 24, 2026
Signed-off-by: Lin Jia <linj@nvidia.com>
@linj-glitch

Copy link
Copy Markdown
Collaborator Author

/ok to test cde8b3d

@linj-glitch

Copy link
Copy Markdown
Collaborator Author

Correction to the older activation example above: this adapter targets Switchyard's rl-token-capture-beta implementation, merged from Switchyard #63 as 97424fc108e267a0885ce6d7f34806fd3b9ba3bd; it should not be assumed compatible with current native Switchyard deployments.

In the beta routing YAML, token_capture_engine: vllm belongs at the route level, alongside type and target, rather than inside target. The current gym-switchyard-integration.md has the corrected example and deployment restrictions. Do not enable this external adapter together with native Gym token capture.

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 4e0b01b5023fead76df081becb6e9b254d2bd30f resolves the main-branch conflict while preserving the new verifier-invocation metadata. The final source-bound SWE/Switchyard/base-response suite passed 323 tests, and full pre-commit checks passed. Draft status and the rollout limitation are unchanged.

@linj-glitch

Copy link
Copy Markdown
Collaborator Author

/ok to test 4e0b01b

@linj-glitch

linj-glitch commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Updated evidence at 4e0b01b5023fead76df081becb6e9b254d2bd30f: the earlier Qwen3-8B Requests run produced an applied patch but had DNS-limited grading and a reasoning-token prefix failure. A subsequent official Qwen2.5-7B trial used the actual SWEBenchWrapper.run() path with the original coveragepy R2E task, vLLM 0.22.0 and the Hermes tool parser, no custom template, and no reasoning parser. Agent-visible data excluded solutions and grading fields; the original verifier separately reproduced the unpatched 6-passed / 1-failed baseline.

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 mask_sample=true and the prefix error after cleanup. No model patch or patched-verifier result was obtained in this trial.

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 Nemo_CICD_Test, Lint check, and DCO checks now pass at this head. Draft status, the runtime compatibility gap, and maintainer review remain; the head is unchanged.

This branch was previously deployed

1 inactive deployment
public — 4e0b01b5 Deployed Oct 28, 2026 by copy-pr-bot[bot] via release / finalize / notify #8024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Agent harnesses and Responses API agent behavior feature New capabilities, enhancements, or enablement work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants