Skip to content

feat(core): report infrastructure failure from verify, not just a reward - #2611

Open
waple0820 wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
waple0820:feat/verify-response-mask-sample
Open

feat(core): report infrastructure failure from verify, not just a reward#2611
waple0820 wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
waple0820:feat/verify-response-mask-sample

Conversation

@waple0820

@waple0820 waple0820 commented Aug 19, 2026

Copy link
Copy Markdown

Closes #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 #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)

class BaseVerifyResponse(BaseVerifyRequest):
    reward: float
    mask_sample: bool = False              # machine-readable
    failure_reason: Optional[str] = None   # from #2552, already on main

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.

The three fields are orthogonal, per the review discussion on #2552: a rollout that degraded but was still scored legitimately keeps mask_sample=False while naming a failure_kind/failure_reason.

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/rollouts_total|scored|masked and coverage/tasks_total|scored|fully_masked. 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.

Relationship to the other failure-handling work

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 — 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.

@copy-pr-bot

copy-pr-bot Bot commented Aug 19, 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.

@waple0820

waple0820 commented Aug 27, 2026

Copy link
Copy Markdown
Author

@ananthsub rebased onto main and reshaped around the three fields as you described them here: #2552 (comment)

  • This PR is mask_sample only. failure_reason landed in feat: declare failure_reason on BaseVerifyResponse #2552, failure_kind belongs to feat: add shared failure-kind registry #2728 — it touches neither, and it does not re-declare what main already has.
  • The contract comment now states the orthogonality, including the case you named: a rollout that degraded but was still scored legitimately keeps mask_sample=False while naming a failure_kind/failure_reason.
  • Added the half that reads it. The flag was a declaration nothing consumed, so rollout_collection now reports progress/<agent>/masked_pct and progress/<agent>/reward_unmasked next to the existing progress/<agent>/reward, which is unchanged. The gap between the two reward series is the score a run lost to its own infrastructure rather than to the policy. Both keys appear only once something is actually masked, so a run that reports no masking exports exactly what it exported before.

Two things worth checking against your design:

nemo_gym/token_id_capture/delivery.py:49 already defines MASK_SAMPLE_KEY = "mask_sample" as a top-level rollout field meaning "exclude this rollout from the loss", and token capture already keeps a masked count with a max_mask_fraction abort. So core agrees on the name and the meaning; an environment that reports the same fact through verify is simply invisible to that accounting today. This PR routes it into the metrics but deliberately leaves the abort guardrail alone — coupling env-reported masking to max_mask_fraction looks like your call, not mine.

In #2728 session_lost is reserved with source="#2611". This PR does not produce that kind — it produces mask_sample. The producer for session_lost is the browser environment in #1865, and it will set both once the registry exists.

Ordering is yours: happy to land after #2728 and drop anything that overlaps, or before it if that is easier.

@ananthsub
ananthsub self-requested a review August 27, 2026 19:58
Comment thread nemo_gym/base_resources_server.py Outdated
class BaseVerifyResponse(BaseVerifyRequest):
reward: float

# True when ``reward`` does not reflect policy quality because the environment or its

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description is too specific to RL training. Gym evaluation and other library callers can consume the same field, while loss masking, resampling, and group handling are only some possible policies. It is also an ordinary comment, so it will not appear in Pydantic's generated schema.

instead, lets add this with Field(description=...) that defines the result semantics: whether this completed sample should be excluded from evaluation scores and other downstream quality calculations because its reward is not a valid measurement of the evaluated system

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

One prerequisite this needed: AnySweVerifyResponse(SWEBenchMetrics, BaseVerifyResponse) inherits mask_sample from both, and SWEBenchMetrics wins the MRO, so on that response the field resolved to Optional[bool] = None with no description — the contract field was invisible in its schema whatever the base declared. Restated on the subclass so the type, default and description survive.

Comment thread nemo_gym/rollout_collection.py Outdated


def _masking_step_metrics(agent_name: str, total: int, unmasked: Counter) -> Dict[str, float]:
"""Progress metrics for the rollouts an environment reported as masked.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper changes progress telemetry only; it does not make the final evaluation result mask-aware. Later, every persisted result is sent to /aggregate_metrics, where the generic profiler still averages masked rewards and treats mask_sample itself as a numeric metric. For example, a valid reward of 1 plus a masked reward of 0 still publishes mean/reward=0.5.

Please apply masking centrally in compute_aggregate_metrics(), including the input to custom compute_metrics() and key-metric selection. Masked rows should remain persisted for coverage, and fully masked tasks should publish coverage without inventing a zero quality score.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread nemo_gym/rollout_collection.py Outdated
metrics = agent_name_to_metrics[agent_name]
metrics.update({k: v for k, v in result.items() if isinstance(v, (int, float)) and not k.startswith("_")})
agent_name_to_counts[agent_name] += 1
if not result.get(MASK_SAMPLE_KEY):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reward_unmasked currently includes failure-sidecar and non-persisted results because these counters are updated for every returned dictionary. Please update quality metrics only for results eligible for the main rollout output. Track failed and omitted results separately.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

reward=1.0 if metrics.resolved else 0.0,
# Report it on the contract as well; `instance_config.mask_sample` stays
# for one release so existing consumers keep working.
mask_sample=bool(instance_config.mask_sample),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SWEBenchMetrics.model_dump() already contains mask_sample, so this explicit keyword followed by **metrics.model_dump() raises TypeError: got multiple values for keyword argument 'mask_sample' on every result. The value here also comes from instance_config, which remains at its default, while the computed decision is written to metrics.mask_sample.

Please resolve the value once from metrics.mask_sample, copy it into the existing compatibility instance_config field, exclude the key from the expanded metrics mapping, and pass it once at the top level.

An end-to-end /run test should construct and serialize the response so this path is covered.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
We added the end-to-end /run test. Reverting the fix makes it fail with exactly the TypeError you predicted.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 27, 2026
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 28, 2026
… telemetry

Addresses review on NVIDIA-NeMo#2611.

`mask_sample` moves from a comment to `Field(description=...)` so it reaches the
generated schema, and the description states the result semantics rather than one
consumer's policy: whether a completed sample should be excluded from evaluation
scores and other downstream quality calculations because its reward is not a
valid measurement of the evaluated system.

`compute_aggregate_metrics()` now applies that centrally. Quality metrics, the
input to a custom `compute_metrics()`, and key-metric selection all come from the
scored subset; masked rows stay in the input and are reported as coverage. A run
where every sample is masked publishes coverage instead of inventing a zero, and
`mask_sample` is no longer coerced into a numeric metric of its own. The keys
appear only once something is masked, so a run that masks nothing is unchanged.

Progress accounting is restricted to results that reach the main rollout output.
A sidecar failure and a kill-shaped omission are counted as such rather than
averaged in as rewards of zero.

`anyswe_agent` raised `TypeError: got multiple values for keyword argument
'mask_sample'` on every result, because `SWEBenchMetrics` already carries the key
that was also passed explicitly. It also read `instance_config.mask_sample`,
which keeps its default, while `_should_mask_sample()` writes the decision to
`metrics.mask_sample`. The value is now resolved once from the metrics, mirrored
onto the compatibility field, and passed once. `SWEBenchMetrics.mask_sample` is
declared `Optional[bool] = None` and wins the MRO on `AnySweVerifyResponse`, so
the contract field is restated there to keep its type, default and description.

An end-to-end `/run` test constructs and serializes the response, and fails with
the original `TypeError` without the fix.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
@waple0820
waple0820 force-pushed the feat/verify-response-mask-sample branch from f95c306 to ceb910e Compare August 28, 2026 03:49
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 28, 2026
… telemetry

Addresses review on NVIDIA-NeMo#2611.

`mask_sample` moves from a comment to `Field(description=...)` so it reaches the
generated schema, and the description states the result semantics rather than one
consumer's policy: whether a completed sample should be excluded from evaluation
scores and other downstream quality calculations because its reward is not a
valid measurement of the evaluated system.

`compute_aggregate_metrics()` now applies that centrally. Quality metrics, the
input to a custom `compute_metrics()`, and key-metric selection all come from the
scored subset; masked rows stay in the input and are reported as coverage. A run
where every sample is masked publishes coverage instead of inventing a zero, and
`mask_sample` is no longer coerced into a numeric metric of its own. The keys
appear only once something is masked, so a run that masks nothing is unchanged.

Progress accounting is restricted to results that reach the main rollout output.
A sidecar failure and a kill-shaped omission are counted as such rather than
averaged in as rewards of zero.

`anyswe_agent` raised `TypeError: got multiple values for keyword argument
'mask_sample'` on every result, because `SWEBenchMetrics` already carries the key
that was also passed explicitly. It also read `instance_config.mask_sample`,
which keeps its default, while `_should_mask_sample()` writes the decision to
`metrics.mask_sample`. The value is now resolved once from the metrics, mirrored
onto the compatibility field, and passed once. `SWEBenchMetrics.mask_sample` is
declared `Optional[bool] = None` and wins the MRO on `AnySweVerifyResponse`, so
the contract field is restated there to keep its type, default and description.

An end-to-end `/run` test constructs and serializes the response, and fails with
the original `TypeError` without the fix.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-customer Waiting on the original author to respond label Aug 28, 2026
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 28, 2026
…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>
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Aug 30, 2026
@waple0820
waple0820 force-pushed the feat/verify-response-mask-sample branch from ceb910e to df1d43f Compare August 31, 2026 06:15
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 31, 2026
… telemetry

Addresses review on NVIDIA-NeMo#2611.

`mask_sample` moves from a comment to `Field(description=...)` so it reaches the
generated schema, and the description states the result semantics rather than one
consumer's policy: whether a completed sample should be excluded from evaluation
scores and other downstream quality calculations because its reward is not a
valid measurement of the evaluated system.

`compute_aggregate_metrics()` now applies that centrally. Quality metrics, the
input to a custom `compute_metrics()`, and key-metric selection all come from the
scored subset; masked rows stay in the input and are reported as coverage. A run
where every sample is masked publishes coverage instead of inventing a zero, and
`mask_sample` is no longer coerced into a numeric metric of its own. The keys
appear only once something is masked, so a run that masks nothing is unchanged.

Progress accounting is restricted to results that reach the main rollout output.
A sidecar failure and a kill-shaped omission are counted as such rather than
averaged in as rewards of zero.

`anyswe_agent` raised `TypeError: got multiple values for keyword argument
'mask_sample'` on every result, because `SWEBenchMetrics` already carries the key
that was also passed explicitly. It also read `instance_config.mask_sample`,
which keeps its default, while `_should_mask_sample()` writes the decision to
`metrics.mask_sample`. The value is now resolved once from the metrics, mirrored
onto the compatibility field, and passed once. `SWEBenchMetrics.mask_sample` is
declared `Optional[bool] = None` and wins the MRO on `AnySweVerifyResponse`, so
the contract field is restated there to keep its type, default and description.

An end-to-end `/run` test constructs and serializes the response, and fails with
the original `TypeError` without the fix.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 31, 2026
… telemetry

Addresses review on NVIDIA-NeMo#2611.

`mask_sample` moves from a comment to `Field(description=...)` so it reaches the
generated schema, and the description states the result semantics rather than one
consumer's policy: whether a completed sample should be excluded from evaluation
scores and other downstream quality calculations because its reward is not a
valid measurement of the evaluated system.

`compute_aggregate_metrics()` now applies that centrally. Quality metrics, the
input to a custom `compute_metrics()`, and key-metric selection all come from the
scored subset; masked rows stay in the input and are reported as coverage. A run
where every sample is masked publishes coverage instead of inventing a zero, and
`mask_sample` is no longer coerced into a numeric metric of its own. The keys
appear only once something is masked, so a run that masks nothing is unchanged.

Progress accounting is restricted to results that reach the main rollout output.
A sidecar failure and a kill-shaped omission are counted as such rather than
averaged in as rewards of zero.

`anyswe_agent` raised `TypeError: got multiple values for keyword argument
'mask_sample'` on every result, because `SWEBenchMetrics` already carries the key
that was also passed explicitly. It also read `instance_config.mask_sample`,
which keeps its default, while `_should_mask_sample()` writes the decision to
`metrics.mask_sample`. The value is now resolved once from the metrics, mirrored
onto the compatibility field, and passed once. `SWEBenchMetrics.mask_sample` is
declared `Optional[bool] = None` and wins the MRO on `AnySweVerifyResponse`, so
the contract field is restated there to keep its type, default and description.

An end-to-end `/run` test constructs and serializes the response, and fails with
the original `TypeError` without the fix.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
@waple0820
waple0820 force-pushed the feat/verify-response-mask-sample branch from df1d43f to b05103d Compare August 31, 2026 06:15
waple0820 added a commit to waple0820/Gym that referenced this pull request Aug 31, 2026
…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>
`mask_sample` on the verify response was a declaration nothing read. Token
capture already keeps a masked count for the rollouts it cannot rebuild, so
this is the same accounting for the masking an environment reports itself.

`progress/<agent>/reward` keeps averaging over every rollout, so an existing
dashboard reads the number it read before. `reward_unmasked` is that average
with the masked rollouts removed; the gap between the two is the score a run
lost to its own infrastructure rather than to the policy. Both keys appear
only once something is actually masked, so a run that reports no masking
exports exactly what it exported before.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
… telemetry

Addresses review on NVIDIA-NeMo#2611.

`mask_sample` moves from a comment to `Field(description=...)` so it reaches the
generated schema, and the description states the result semantics rather than one
consumer's policy: whether a completed sample should be excluded from evaluation
scores and other downstream quality calculations because its reward is not a
valid measurement of the evaluated system.

`compute_aggregate_metrics()` now applies that centrally. Quality metrics, the
input to a custom `compute_metrics()`, and key-metric selection all come from the
scored subset; masked rows stay in the input and are reported as coverage. A run
where every sample is masked publishes coverage instead of inventing a zero, and
`mask_sample` is no longer coerced into a numeric metric of its own. The keys
appear only once something is masked, so a run that masks nothing is unchanged.

Progress accounting is restricted to results that reach the main rollout output.
A sidecar failure and a kill-shaped omission are counted as such rather than
averaged in as rewards of zero.

`anyswe_agent` raised `TypeError: got multiple values for keyword argument
'mask_sample'` on every result, because `SWEBenchMetrics` already carries the key
that was also passed explicitly. It also read `instance_config.mask_sample`,
which keeps its default, while `_should_mask_sample()` writes the decision to
`metrics.mask_sample`. The value is now resolved once from the metrics, mirrored
onto the compatibility field, and passed once. `SWEBenchMetrics.mask_sample` is
declared `Optional[bool] = None` and wins the MRO on `AnySweVerifyResponse`, so
the contract field is restated there to keep its type, default and description.

An end-to-end `/run` test constructs and serializes the response, and fails with
the original `TypeError` without the fix.

Signed-off-by: waple0820 <232305951+waple0820@users.noreply.github.com>
@waple0820
waple0820 force-pushed the feat/verify-response-mask-sample branch from b05103d to 2bbc5a3 Compare September 1, 2026 06:59
@cafzal

cafzal commented Sep 5, 2026

Copy link
Copy Markdown

Independent evaluation evidence supports the core distinction in this PR: one rollout with an agent/infrastructure failure and its fresh recovery with a valid-but-wrong result both produced scalar reward 0. Treating both as ordinary zeros would hide invalid-execution denominator drift; treating both as failures would hide a real policy miss. A top-level, producer-owned masking fact is general across SQL, coding, browser, sandbox, tool-use, and judge-backed environments and avoids coupling consumers to a SWE-specific private config path.

The strongest validation properties are already reflected in this PR's plan: valid zero versus masked zero, inert defaults for unmodified environments, fully masked groups producing coverage rather than an invented score, custom metrics receiving only scored samples, and observability retaining all attempts. I would additionally preserve an exact reconciliation invariant across scored, masked, failed, omitted, and unknown dispositions as #2750 evolves.

Given that #2611 already owns the implementation, I will not open a duplicate PR for #2608.

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request Issue reported or requested by someone from the community

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(core): move mask_sample onto BaseVerifyResponse so any environment can report an infrastructure failure

4 participants