Source code for nemo_rl.environments.call_selection

# Copyright (c) 2026, NVIDIA CORPORATION.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Select captured calls for training, independently of row reconstruction.

A selection policy is a synchronous callable ``(records, result)`` returning
``(selected_records, terminal_response_id)``. It must return unique, unchanged
records from this attempt and identify the scored response within its selection.
Order determines root/path priority, not storage ancestry. Policies choose
membership; shared capture validation and reconstruction remain authoritative.
"""

from __future__ import annotations

import importlib
from collections import defaultdict
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, cast

if TYPE_CHECKING:
    from nemo_gym.token_id_capture.staging.records import CallRecord

type SelectionPolicy = Callable[
    [list[CallRecord], dict[str, Any]], tuple[list[CallRecord], str | None]
]


[docs] def _output_items( result: dict[str, Any], *, captured_response_ids: set[str] ) -> tuple[list[dict[str, Any]], dict[int, str | None]]: """Normalize ordinary output, retaining envelope boundaries and identities.""" # Gym is optional; resolving policies must also work in non-Gym config tools. from nemo_gym.responses_converter import _RESPONSE_OUTPUT_BOUNDARY_TYPES responses = result.get("responses") or [result.get("response") or {}] sessions = {(r.get("metadata") or {}).get("session_id") for r in responses} if len(sessions) > 1: raise ValueError("Multiple sessions require qualified training membership") items = [] terminals = {} for response in responses: response_items = [] output = response.get("output") if not isinstance(output, list): raise ValueError("Expected ordinary response output") if response.get("contains_transitions"): seen = {} for snapshot in output: if not isinstance(snapshot, list): raise ValueError("Malformed transition snapshot") for item in snapshot: if not isinstance(item, dict): raise ValueError("Malformed ordinary output item") identity = item.get("id") if identity and identity in seen: if item != seen[identity]: raise ValueError( "Snapshot reused an item ID with different content" ) continue if identity: seen[identity] = item response_items.append(item) else: response_items.extend(output) if any(not isinstance(item, dict) for item in response_items): raise ValueError("Malformed ordinary output item") authored = [] for position, item in enumerate(response_items): item_type = item.get("type") if ( item.get("role") == "assistant" or item_type in _RESPONSE_OUTPUT_BOUNDARY_TYPES ): # Gym's generation boundary includes types that capture cannot # yet fingerprint. Never silently discard these from history. if item_type not in (None, "message", "reasoning", "function_call"): raise ValueError( "Unsupported generated output item: " f"type={item_type!r}, item_id={item.get('id')!r}, " f"output_index={position}" ) authored.append(item) native_id = response.get("id") if native_id in captured_response_ids and not authored: raise ValueError("No accepted captured generations in response output") items.extend(authored) if authored: terminals[len(items)] = ( native_id if native_id in captured_response_ids else None ) return items, terminals
[docs] def select_captured_calls( records: list[CallRecord], result: dict[str, Any] ) -> tuple[list[CallRecord], str | None]: """Resolve references and validate any accompanying ordinary output. Stable IDs constrain content matching rather than overriding contradictions. Content-only matches must be unique locally; this is deliberately not an exponential search over possible transcript interpretations. """ # Defer the optional Gym dependency until the policy is executed. from nemo_gym.token_id_capture.completion import output_item_fingerprint by_response: dict[str, CallRecord] = {r.response_id: r for r in records} by_call: dict[str, CallRecord] = {r.model_call_id: r for r in records} if len(by_response) != len(records) or len(by_call) != len(records): raise ValueError("Capture contains reused call or response identities") terminal = by_response.get((result.get("response") or {}).get("id") or "") selected = [] referenced: list[CallRecord] | None = None turns = (result.get("ng_trajectory") or {}).get("turns") or [] if any(turn.get("model_calls") for turn in turns): referenced = [] for turn_index, turn in enumerate(turns): refs = turn.get("model_calls") or [] if not refs: raise ValueError( "Only part of the returned trajectory has call references: " f"turn={turn_index}" ) for ref_index, ref in enumerate(refs): location = ( f"turn={turn_index}, reference={ref_index}, " f"response_id={ref.get('response_id')!r}, " f"model_call_id={ref.get('model_call_id')!r}" ) witnesses = [] for key, index in ( ("response_id", by_response), ("model_call_id", by_call), ): if ref.get(key): if ref[key] not in index: raise ValueError( "Trajectory references a call outside this capture: " + location ) witnesses.append(index[ref[key]]) if not witnesses or any(r != witnesses[0] for r in witnesses): raise ValueError( "Missing or contradictory trajectory call identity: " + location ) referenced.append(witnesses[0]) if referenced is None or "response" in result or "responses" in result: items, envelope_ids = _output_items( result, captured_response_ids=set(by_response) ) terminals = { end: by_response[identity] for end, identity in envelope_ids.items() if identity is not None } fingerprints = [output_item_fingerprint(item) for item in items] by_item_id: dict[str, set[str]] = defaultdict(set) by_first: dict[str, list[CallRecord]] = defaultdict(list) for record in records: if record.output_items: by_first[record.output_items[0].fingerprint].append(record) for item in record.output_items: if item.id: by_item_id[item.id].add(record.model_call_id) # Exact later identities can disambiguate an earlier retry through a # verified parent. They never add unreturned actions to the selection. required = set() identified = {terminal.model_call_id} if terminal is not None else set() identified.update(record.model_call_id for record in terminals.values()) for item in items: matches = by_item_id.get(item.get("id") or "", set()) if len(matches) > 1: raise ValueError("Served item identity was reused by multiple calls") identified.update(matches) for identity in identified: visited = set() ancestor: str | None = identity while ancestor is not None: if ancestor in visited or ancestor not in by_call: raise ValueError("Broken captured parent chain") visited.add(ancestor) required.add(ancestor) ancestor = by_call.get(ancestor).parent_call_id position = 0 envelope_selected_start = 0 while position < len(items): matches = [] for candidate in by_first.get(fingerprints[position], []): # References constrain the same whole-call content check. They # cannot override edited, missing, or synthetic output items. if referenced is not None and ( len(selected) >= len(referenced) or candidate != referenced[len(selected)] ): continue evidence = candidate.output_items or [] end = position + len(evidence) # Whole calls cannot cross response envelopes. A native ID must # occur inside its own envelope, but need not be its last call. if any(position < stop < end for stop in envelope_ids): continue if ( end in terminals and terminals[end] != candidate and terminals[end] not in selected[envelope_selected_start:] ): continue if [item.fingerprint for item in evidence] != fingerprints[ position:end ]: continue if any( by_item_id.get(item.get("id") or "", {candidate.model_call_id}) != {candidate.model_call_id} for item in items[position:end] ): continue matches.append(candidate) corroborated = [r for r in matches if r.model_call_id in required] if len(matches) > 1 and len(corroborated) == 1: matches = corroborated if len(matches) != 1: expected = ( referenced[len(selected)] if referenced is not None and len(selected) < len(referenced) else None ) reason = ( "multiple captured calls match" if matches else "no whole captured call matches content and identity" ) raise ValueError( "Missing or ambiguous captured generation in ordinary output: " f"authored_output_index={position}, " f"item_id={items[position].get('id')!r}, reason={reason}; " f"expected_call={expected.model_call_id if expected else None!r}, " f"item_identity_calls={sorted(by_item_id.get(items[position].get('id') or '', set()))}, " f"matching_calls={[r.model_call_id for r in matches]}" ) selected.append(matches[0]) position += len(matches[0].output_items or []) if position in envelope_ids: envelope_selected_start = len(selected) if referenced is not None and len(selected) != len(referenced): raise ValueError( "Returned output is missing referenced generations: " f"authored_output_index={len(items)}, " f"remaining_calls={[r.model_call_id for r in referenced[len(selected) :]]}" ) else: selected = referenced if not selected: raise ValueError("No accepted captured generations") if len({record.model_call_id for record in selected}) != len(selected): raise ValueError("Accepted generations are duplicated") if terminal is not None and terminal not in selected: raise ValueError("Scored response is absent from accepted history") declared = result.get("terminal_response_id") if declared: if declared not in {record.response_id for record in selected}: raise ValueError("Declared terminal is absent from accepted history") if terminal is not None and declared != terminal.response_id: raise ValueError("Declared terminal differs from the scored response") terminal_response_id = declared or (terminal.response_id if terminal else None) if terminal_response_id is None: # Returned history is ordered; preserve the existing last accepted call # convention when envelopes use harness-local rather than served IDs. terminal_response_id = selected[-1].response_id return selected, terminal_response_id
[docs] def select_all_captured( records: list[CallRecord], result: dict[str, Any] ) -> tuple[list[CallRecord], str | None]: """Include all calls except unusable, unscored leaves, retaining attribution.""" # Defer the optional Gym dependency until the policy is executed. from nemo_gym.token_id_capture.staging import resolve_terminal attribution = resolve_terminal( records, result.get("response"), declared_response_id=result.get("terminal_response_id"), ) if not attribution.attributed: raise ValueError( "all_captured requires an identifiable scored terminal: " + attribution.reason ) terminal_response_id = next( record.response_id for record in records if record.model_call_id == attribution.model_call_id ) # Unusable parents or the scored call still invalidate the owner downstream. parents = {record.parent_call_id for record in records} selected = [] for record in records: unusable = record.response_status in ("failed", "unknown") or ( record.response_status == "incomplete" and bool(record.finish_reason) and record.finish_reason not in ("length", "max_tokens", "max_output_tokens") ) if ( not unusable or record.model_call_id == attribution.model_call_id or record.model_call_id in parents ): selected.append(record) return selected, terminal_response_id
SELECTION_POLICIES: dict[str, SelectionPolicy] = { "returned_history": select_captured_calls, "all_captured": select_all_captured, }
[docs] def resolve_selection_policy(name: str) -> SelectionPolicy: """Resolve a built-in name or an importable ``package.module:function``.""" if name in SELECTION_POLICIES: return SELECTION_POLICIES[name] module_name, separator, function_name = name.partition(":") if not separator or not module_name or not function_name.isidentifier(): raise ValueError( f"Unknown selection_policy {name!r}: expected a built-in " f"({', '.join(SELECTION_POLICIES)}) or 'package.module:function'" ) try: policy = getattr(importlib.import_module(module_name), function_name) except (ImportError, AttributeError) as error: raise ValueError(f"Cannot load selection_policy {name!r}: {error}") from error if not callable(policy): raise ValueError(f"selection_policy {name!r} is not callable") return cast(SelectionPolicy, policy)