Repository navigation
feat: declare failure_reason on BaseVerifyResponse - #2552
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
a9aac53 to
59208ab
Compare
|
Hi — we hit exactly the gap this PR addresses, from the RL-training side, and we'd like to land our half with this PR rather than propose a competing one. Context: we run browser-based RL on Gym (one isolated cloud browser per rollout, verified by an LLM judge). On a GB200 run at 64-way concurrency, orphaned environment sessions caused 1172 of 1280 rollouts across steps 61–80 (91.6%) to fail in session reset. Every one of those entered GRPO as a legitimate
The good news is that the boolean already exists in this repo — it's just not on the contract:
So the semantics are settled and have a first-party consumer — but because the field hangs off one agent family's private config model, it only works for three SWE-shaped agents, and NeMo-RL has to hard-code that private schema path. Every other environment in the repo gets nothing, and verl gets nothing at all ( Would you be open to landing both fields together on class BaseVerifyResponse(BaseVerifyRequest):
reward: float
mask_sample: bool = False # machine-readable: reward doesn't reflect policy quality
failure_reason: Optional[str] = None # human-readable cause (this PR)Both default-inert, so existing environments are unchanged. Gym would state the fact and stop there — whether to mask the loss, resample, exclude from group statistics, or drop the group stays the training framework's decision, and the three frameworks we checked do genuinely differ there. We're happy to do the work: contribute the |
|
@waple0820 thank you for the comment! Could you please also take a look at #2383 #2384 #2385 #2386 #2387 #2388 #2410 as well? Those are all related PRs, in the spirit of this one, that handle resiliency issues I personally found to be critical. In fact this PR was opened to address reviewer comments on #2384. It would be very useful to have your feedback on whether those fixes also look important to you. |
59208ab to
4b4993f
Compare
| # Elaborates on why `reward` may not be fully genuine, allowing the upstream | ||
| # training loop to decide on how to treat the response. | ||
| # For example, timeouts may result in reward=0, and we should be honest about the reason. | ||
| failure_reason: Optional[str] = None | ||
|
|
There was a problem hiding this comment.
echoing @waple0820 's comment, I believe it'd be useful to have these 3 fields:
- mask_sample: signoff from the verifier/environment around using this reward downstream
- failure_kind: a diagnosis/category of failures that we can use for grouping in telemetry
- failure_reason: freeform error message
mask_sample=False but failure_kind+failure_reason being set can indicate a degraded but scored state as legitimate
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
ebd6c32 to
989be05
Compare
|
/ok to test 989be05 |
|
/ok to test 700bf92 |
700bf92 to
35dff70
Compare
Signed-off-by: Teodor-Dumitru Ene <teodord.ene@gmail.com>
Signed-off-by: Teodor-Dumitru Ene <teodord.ene@gmail.com>
02ad6f8 to
935e673
Compare
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
An environment that knows it failed - lost session, unavailable judge, OOM-killed container, reset timeout - can only return reward=0.0 today, which is indistinguishable from a policy that genuinely scored zero. The field already exists in this repo, just not on the contract: `mask_sample` lives on SWEBenchWrapperInstanceConfig (responses_api_agents/swe_agents/app.py), anyterminal_agent and anyswe_agent, and NeMo-RL already consumes it end to end through full_result["instance_config"]["mask_sample"]. Because it hangs off one agent family's private config model it covers three SWE-shaped agents only, NeMo-RL has to hard-code that private path, and verl gets nothing. Promote it to BaseVerifyResponse, alongside the human-readable failure_reason proposed in NVIDIA-NeMo#2552: * mask_sample: bool = False - machine-readable, for training frameworks * failure_reason: Optional[str] = None - for triage and logs Gym states the fact and stops there. Whether to mask the loss, resample the episode, exclude the sample from group statistics or drop the group stays the training framework's decision; the frameworks we checked genuinely differ. Defaults are inert, so existing environments are unchanged. swe_agents and anyswe_agent now mirror their instance-config flag onto the contract field and keep the old location for one release; anyterminal_agent already surfaces it via TerminalBenchMetrics. Closes NVIDIA-NeMo#2608 Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
…ard (NVIDIA-NeMo#2611) Closes NVIDIA-NeMo#2608 `BaseVerifyResponse` carries a single scalar for the outcome, so an environment that *knows* it failed — lost session, unavailable judge, OOM-killed container, reset timeout — can only return `0.0`, which is indistinguishable from a policy that genuinely scored zero. The concept is not new to this repo. It exists in four private locations: - `mask_sample: bool = False` on `SWEBenchWrapperInstanceConfig` (`responses_api_agents/swe_agents/app.py:272`), also `anyterminal_agent/app.py:108` and `anyswe_agent/app.py:180`, documented at `swe_agents/README.md:670`. - `conversational_tool_use/simulation/app.py:112` keeps its own `{"mask_sample": ...}` dict. - `nemo_gym/token_id_capture/delivery.py:49` already defines `MASK_SAMPLE_KEY = "mask_sample"` as a **top-level** rollout field, described there as what "a consumer reads to exclude a rollout from the loss". - NeMo-RL consumes only the first one, through `full_result["instance_config"]["mask_sample"]` (`nemo_rl/experience/rollouts.py:229-241`), which zeroes `loss_multiplier` in `nemo_rl/algorithms/grpo.py:2104`. So core already agrees what the key is called and what it means; it is reachable only if your environment happens to be SWE-shaped or if token capture produced it. verl gets nothing. This is the same objection raised on NVIDIA-NeMo#2384 — that a fix "should be applied uniformly, to other benchmarks as well" rather than to one harness. Putting the field on the contract is what makes it uniform. ## What this changes **1. The contract** (`nemo_gym/base_resources_server.py`) ```python class BaseVerifyResponse(BaseVerifyRequest): reward: float mask_sample: bool = False # is this reward a valid measurement failure_kind: Optional[str] = None # which kind of failure, from NVIDIA-NeMo#3179's vocabulary failure_reason: Optional[str] = None # what happened, for a person ``` The three are orthogonal. `mask_sample` decides usability, `failure_kind` groups in logs and metrics, `failure_reason` explains one occurrence. A sample that degraded but was still measured validly names a kind and stays unmasked. `failure_kind` validates against `nemo_gym.failure_kinds` and warns rather than rejects — an unregistered name is a migration signal, and failing the response over it would replace a visible wrong label with an invisible lost failure. An environment that needs something the shared set should not grow uses `<server>:<kind>`. Gym states the fact and stops there. Masking, resampling, excluding from group statistics or dropping the group stays the training framework's decision — the three frameworks we checked genuinely differ. `swe_agents` and `anyswe_agent` mirror their instance-config flag onto the contract field and keep the old location for one release. `anyterminal_agent` already surfaces it through `TerminalBenchMetrics`. **2. Something that reads it** (`nemo_gym/reward_profile.py`, `nemo_gym/rollout_collection.py`) A declaration nothing consumes is not worth much, and the consumer that matters is the one that publishes the score. `compute_aggregate_metrics()` computes quality from the scored subset: the profiler input, a custom `compute_metrics()`, and key-metric selection all skip masked samples. Masked rows stay in the input and are reported as coverage instead — `coverage/measured_rollouts|masked_rollouts` and `coverage/measured_tasks|fully_masked_tasks`. A run where every sample is masked publishes coverage rather than inventing a zero, and a fully masked task is counted without contributing a group. `mask_sample` is stripped before profiling, so it is no longer averaged as though the flag were a measurement. `perf_summary` deliberately keeps using every response, masked included: a masked sample still executed and still reported its latency, so observability coverage should not shrink because a rollout was excluded from scoring. Progress telemetry gets the same split while a run is still going, over the results that reach the main rollout output: - `progress/<agent>/reward` is unchanged — it still averages over every rollout, so an existing dashboard reads the number it read before. - `progress/<agent>/reward_unmasked` is that same average with the masked rollouts removed. - `progress/<agent>/masked_pct` is how many were removed. - `progress/<agent>/failed` and `/omitted` count the attempts that never reach the main output at all, so a sidecar failure is never folded into a quality average as a reward of zero. The gap between the two reward series is the score a run lost to its own infrastructure rather than to the policy. On the run that motivated this, 1172 of 1280 rollouts across steps 61–80 failed in session reset and entered GRPO as legitimate zeros; nothing in the metrics said so. Every new key appears only once something is actually masked, failed or omitted, so a healthy run publishes exactly what it published before. ## Scope Run-level reconciliation is no longer in this PR. NVIDIA-NeMo#3302 implements it for NVIDIA-NeMo#2135, using the materialized inventory and attempt journal to account for omitted and unknown work offline — neither of which this contract can see. What remains here is the per-sample half. ## Relationship to the other failure-handling work - **NVIDIA-NeMo#2552** (merged) landed `failure_reason` and its comment already points here: *"Machine-readable handling belongs to `mask_sample`/`failure_kind`."* This PR is that half. - **NVIDIA-NeMo#2728** owns `failure_kind` and the shared registry, and already reserves `session_lost` with `source="NVIDIA-NeMo#2611"`. No overlap — this PR does not touch `failure_kind`. - **NVIDIA-NeMo#2750** lists this under *Failure fields and artifact integrity*. The design document's rule — *"the producer returns the real result with `mask_sample=True`, a stable `failure_kind`, and a human-readable `failure_reason`"* — is exactly what the three fields together express once this lands. ## Compatibility Defaults are inert; an unmodified environment behaves identically, and a run that masks nothing publishes exactly the keys it published before. One inheritance detail worth naming: `AnySweVerifyResponse` inherits `mask_sample` from both `SWEBenchMetrics` and `BaseVerifyResponse`, and the metrics model wins the MRO, so the contract field resolved to `Optional[bool] = None` with no description on that response. It is restated on the subclass so the schema keeps the contract's type, default and description. ## Tests - `tests/unit_tests/test_base_resources_server.py` — defaults, round trip, that a plain `reward=0.0` is *not* implicitly masked, and that a degraded-but-scored rollout stays unmasked. - `tests/unit_tests/test_aggregate_metrics.py` — a masked zero does not halve the score, the flag is not published as a quality metric, a run that masks nothing is byte-identical, a fully masked run reports coverage instead of a zero, a fully masked task is counted but not scored, custom `compute_metrics()` sees only scored samples, and a fully masked run still reports observability coverage. - `tests/unit_tests/test_rollout_collection.py` — the disposition account closes for every combination and the residual never goes negative; a healthy run adds no keys; a partially masked agent reports its share and the score without it; a fully masked agent publishes no average; failed and omitted attempts are counted, never averaged. - `responses_api_agents/anyswe_agent/tests/test_app.py` — an end-to-end `/run` that constructs and serializes the response. Reverting the fix makes it fail with `TypeError: got multiple values for keyword argument 'mask_sample'`. `uv run --extra dev pytest tests/unit_tests -q` → 3383 passed. Two failures on this branch, `test_e2b_provider` and `test_enroot_provider`, reproduce on a clean `upstream/main` and are unrelated. `pre-commit` clean. --------- Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com> Co-authored-by: waple0820 <232305951+waple0820@users.noreply.github.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
…coring it zero `verify` returned `reward=0.0` whenever the browser session was missing, which is the conflation this environment exists to demonstrate: a rollout whose session was lost is not a policy that solved nothing, and downstream the two are identical. It now reports `failure_reason` on that path, using the field NVIDIA-NeMo#2552 added to `BaseVerifyResponse`. A browser that died mid-episode was worse than mis-scored. `_score` reads the live page, so a dead browser raised out of `verify`; only `JudgeError` is converted to a routed row, so that exception ended the whole collection run rather than the one rollout it belonged to. Browser reads are now wrapped and reported the same way. An unsupported scoring key still raises, because a dataset typo is a configuration error rather than an infrastructure failure, and failing on the first rollout is the intended behaviour. Building the response by spreading the request and also passing `reward` as a keyword was a latent `TypeError: got multiple values`: `BrowserVerifyRequest` allows extra fields, so a caller putting `reward` or `failure_reason` in the body crashed verify. Response-owned fields are now dropped from the spread. The same shape was found and fixed in `anyswe_agent` on NVIDIA-NeMo#2611. Closing the previous browser on a re-seeded session swallowed every exception. A browser we could not close is a resource the run still holds, so it is logged. Tests build and serialize the real response, since the collision only appears at construction time. Reverting any one of the three fixes fails a test. Signed-off-by: waple0820 <feng.wang@lexmount.com>
What does this PR do?
Declares
failure_reasonfield onBaseVerifyResponseto standardize a way to report why a returned reward may not reflect the policy quality.failure_reasonis meant to be human-readable; see #2750 for the whole design document.Checklist
pre-commit run --all-files) (so CI lint/format/copyright pass).git commit -s) (so the DCO check passes).