Skip to content

feat(audio): agent-ready foundation — contract layer, residency, discovery - #2332

Open
shubhamNvidia wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent/foundation
Open

feat(audio): agent-ready foundation — contract layer, residency, discovery#2332
shubhamNvidia wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent/foundation

Conversation

@shubhamNvidia

Copy link
Copy Markdown
Contributor

The layer everything else builds on.

Public surface: agent.py — import nemo_curator.stages.audio.agent. Everything else is private (_agent/), so the supported API is one module.

What's inside _agent/ (8 modules, all private):

module what it does
_agent_ready.py The vocabulary. Defines AgentReady (the mixin a stage inherits) and StageContract, plus the closed sets a contract is written in: Cardinality (1:1 / 1:1 nested-list / 1:N fan-out / N:1 / filter), AudioForm (file / waveform), ErrorPolicy (skip / fail / annotate / unknown), IOSpec, Gates, and ConditionalWrite. Everything else in the package reads these types.
_residency.py Resolves where a stage's audio comes from — an in-memory waveform or a file path — and materialises one from the other when needed. This is what the input_residency / keep_waveform_in_task options in the module PRs are built on. Also does the stable/atomic write helper the stages use.
_roles.py Semantic role names. A key called pred_text and one called text may both be "the transcript"; roles are how the agent decides two stages actually connect instead of matching raw strings.
_agent_registry.py Auto-derives the agent-facing metadata a stage does not have to hand-write: parameter names, types and defaults come from the constructor signature, so describe() stays about I/O and behavior. Also builds the resolved contract for a configured stage.
_catalog.py Discovery. Lists the agent-ready stages, returns a stage's static (instance-free) contract, and answers "which stages produce this role?" — how the agent finds candidates without importing everything.
_composite.py Expands a composite stage into the concrete stages a backend will actually run, so a planner can see through something like SplitASRAlignJoinStage instead of treating it as an opaque box.
_conformance.py The test harness stage authors call: asserts a contract is well-formed, that declared reads/writes match what process() really touches, and that cardinality matches runtime. It can also sweep every stage with no fixtures, which is what keeps contracts honest as stages change.
_planning.py Pipeline-level validation: walks a composed pipeline and reports whether each stage's reads are satisfied by what came before, catching dangling keys and type mismatches before the run starts.

Applied to the first stages

  • common.py + the full preprocessing/ folder made agent-ready, as the first worked slice.
  • In-memory output on the existing preprocessing stagesMonoConversionStage and SegmentConcatenationStage gain keep_waveform_in_task / write_to_disk / output_dir (and residency input on mono), so a preprocessing step feeding a GPU stage can hand its audio over in memory instead of writing a file the next stage immediately re-reads. Writing to disk stays the default, and a stage refuses a configuration with no output sink at all rather than silently producing nothing.

Five new stages

In preprocessing/:

  • ChannelCountStage — record the channel count, filter on it, or convert it (e.g. downmix to mono), in one stage.
  • SampleRateFilterStage — select clips by sample rate without resampling them (previously the only option was a full resample).

In common.py:

  • CreateInitialManifestAudioFolderStage — start a pipeline from a plain folder of audio files. Scans a directory and emits one task per file with its path and a derived id. The existing sources either read an already-written JSONL manifest (ManifestReader) or were dataset-specific (CreateInitialManifest{ReadSpeech,Fleurs}Stage); there was nothing for "here is a folder of WAVs, curate it", which is the most common starting point.
  • PreserveByValueConditionsStage — filter on several conditions at once (AND or OR), against top-level rows or a nested per-segment list. PreserveByValueStage compares exactly one key to one value, so anything like "duration ≥ 5 and MOS ≥ 3.5" needed several stacked stages and still couldn't express OR. Missing keys fail closed rather than silently passing.
  • ManifestCheckpointStage — persist a metadata-only JSONL boundary mid-pipeline and pass rows through unchanged, so a later run can resume from it instead of recomputing expensive GPU work. It refuses to overwrite an existing file, and rejects non-JSON values (e.g. a resident waveform) at the boundary rather than writing something unreadable.

Tests: test_common.py and test_create_manifest_audio_folder.py cover the common.py stages, the preprocessing/ tests cover the preprocessing folder, and the _agent framework tests live in tests/stages/audio/_agent/ — so every stage shipped here ships with its tests.

Note for reviewers: _agent/__init__.py is intentionally import-free (it breaks a common_residency import cycle); please don't add re-exports there.

…overy

Adds the stage-side declaration layer that agent-driven pipeline construction
needs: the `AgentReady` mixin and `StageContract` vocabulary, the input-residency
resolver, semantic role names, and the discovery/planning/conformance code that
reads them. Public entry point is `nemo_curator.stages.audio.agent`; the
implementation lives in the private `_agent/` subpackage.

Stages declare what they read and write instead of a planner inferring it, so a
pipeline can be validated before it runs rather than failing partway through a
GPU pass. Ships `common.py` and the full `preprocessing/` folder as the first
worked slice, including two new stages: ChannelCountStage (record/filter/convert
channel count) and SampleRateFilterStage (select on rate without resampling).

`_agent/__init__.py` is deliberately import-free: `common` imports `_agent_ready`
at module level while `_residency` imports `common`, and that cycle survives only
because reaching `_agent._agent_ready` runs a package __init__ that pulls nothing.

Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
@copy-pr-bot

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

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an agent-facing contract, discovery, planning, residency, and conformance layer for audio stages, then applies it to common preprocessing stages.

  • Adds five audio stages for folder ingestion, conditional filtering, checkpointing, channel handling, and sample-rate filtering.
  • Adds configurable in-memory and disk-backed preprocessing outputs.
  • Adds agent-contract and preprocessing test coverage.

Confidence Score: 3/5

The PR is not yet safe to merge because audio-folder identity collisions can silently reuse the wrong output and successful checkpoints still lack a durable completion state.

The current path-to-ID transformation remains non-injective and feeds collision-prone output filenames, while checkpoint finalization removes ownership without creating the completion marker that setup and retry logic consume.

Files Needing Attention: nemo_curator/stages/audio/common.py

Important Files Changed

Filename Overview
nemo_curator/stages/audio/common.py Adds folder ingestion, multi-condition filtering, checkpointing, and agent contracts; two previously reported correctness issues remain outstanding.
nemo_curator/stages/audio/_agent/_agent_ready.py Defines the agent-facing contract vocabulary and JSON-safe serialization surface.
nemo_curator/stages/audio/_agent/_planning.py Adds pipeline-level validation of task types, roles, keys, and execution gates.
nemo_curator/stages/audio/_agent/_residency.py Adds waveform/file residency resolution and stable audio materialization helpers.
nemo_curator/stages/audio/preprocessing/mono_conversion.py Adds configurable input residency and in-memory or disk-backed mono output.
nemo_curator/stages/audio/preprocessing/concatenation.py Adds configurable in-memory or disk-backed concatenated audio output.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Files[Audio files or manifests] --> Tasks[AudioTask records]
  Tasks --> Preprocessing[Audio preprocessing stages]
  Preprocessing --> Contracts[Agent-ready stage contracts]
  Contracts --> Discovery[Catalog and discovery]
  Contracts --> Planning[Pipeline validation]
  Preprocessing --> Checkpoint[Manifest checkpoint]
  Preprocessing --> Downstream[Inference and filtering stages]
Loading

Reviews (5): Last reviewed commit: "fix(audio): three silent-wrong-data find..." | Re-trigger Greptile

Comment on lines +745 to +746
rel = os.path.relpath(abspath, os.path.abspath(self.data_dir))
item_id = os.path.splitext(rel)[0].replace(os.sep, "__")

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.

P1 Flattened paths produce duplicate IDs

When a recursive folder contains both spk1/utt1.wav and spk1__utt1.wav, replacing separators with __ assigns both tasks the same audio_item_id, causing identity-indexed records or downstream output filenames to collide.

Knowledge Base Used: Audio curation stages

Comment on lines +1017 to +1028
def release_retry_reservation(self) -> None:
"""Remove this run's ownership sidecar after successful execution."""
self._resolve_output()
try:
self._remove_retry_owner_if_owned()
except OSError as exc:
logger.warning(
"ManifestCheckpointStage could not remove its successful retry "
f"ownership record at {self.output_path!r}: {exc}"
)
self._reservation_owned = False
self._reservation_identity = None

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.

P1 Checkpoint completion state is never published

After a checkpoint run succeeds, release_retry_reservation() only removes the ownership sidecar and never creates the _COMPLETE marker consumed by setup and retry logic. Completed and interrupted JSONL files therefore lack a durable completion distinction, so resume tooling cannot safely determine whether all rows were written.

Knowledge Base Used: Curation stage framework

@shubhamNvidia
shubhamNvidia requested review from ayushdg, mohammadaaftabv and sarahyurick and removed request for weijiac0619 August 25, 2026 12:01
shubhamNvidia added a commit to shubhamNvidia/Curator that referenced this pull request Aug 25, 2026
The complete audio agent work as one branch: the same stage-side contracts split
across PRs NVIDIA-NeMo#2332-NVIDIA-NeMo#2342, plus the agent that consumes them.

Stages (nemo_curator/stages/audio/):
  - the _agent/ contract layer (AgentReady, StageContract, roles, input residency,
    discovery, planning, conformance) and the public agent.py entry point
  - describe() contracts and *_key params across common, preprocessing, io,
    filtering, metrics, segmentation, tagging, inference, alm, datasets and
    advanced_pipelines
  - new stages: ChannelCount, SampleRateFilter, CreateInitialManifestAudioFolder,
    PreserveByValueConditions, ManifestCheckpoint, ManifestGroupExport,
    DocumentBatchJsonlWriter

Agent (nemo_curator/audio_agent/):
  - deterministic verbs (discover/describe/validate/smoke/run/report/reuse/delta),
    knowledge cards, recipes, and the host skills

Supporting: resource telemetry used by smoke calibration
(utils/performance_utils.py, opt-in via RESOURCE_PROBE in backends/base.py), the
nemo-curator-audio console script, and the audio_agent CI matrix entry.

Verified: 51/51 stages pass static conformance, the stage-compatibility baseline
hash is unchanged, and whole-repo `ruff check` is clean.

Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
shubhamNvidia added a commit to shubhamNvidia/Curator that referenced this pull request Aug 25, 2026
The complete audio agent work as one branch: the same stage-side contracts split
across PRs NVIDIA-NeMo#2332-NVIDIA-NeMo#2342, plus the agent that consumes them.

Stages (nemo_curator/stages/audio/):
  - the _agent/ contract layer (AgentReady, StageContract, roles, input residency,
    discovery, planning, conformance) and the public agent.py entry point
  - describe() contracts and *_key params across common, preprocessing, io,
    filtering, metrics, segmentation, tagging, inference, alm, datasets and
    advanced_pipelines
  - new stages: ChannelCount, SampleRateFilter, CreateInitialManifestAudioFolder,
    PreserveByValueConditions, ManifestCheckpoint, ManifestGroupExport,
    DocumentBatchJsonlWriter

Agent (nemo_curator/audio_agent/):
  - deterministic verbs (discover/describe/validate/smoke/run/report/reuse/delta),
    knowledge cards, recipes, and the host skills

Supporting: resource telemetry used by smoke calibration
(utils/performance_utils.py, opt-in via RESOURCE_PROBE in backends/base.py), the
nemo-curator-audio console script, and the audio_agent CI matrix entry.

Verified: 51/51 stages pass static conformance, the stage-compatibility baseline
hash is unchanged, and whole-repo `ruff check` is clean.

Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
task.data["is_mono"] = True
task.data["duration"] = mono_waveform.shape[1] / sample_rate
task.data["num_samples"] = mono_waveform.shape[1]
if self.keep_waveform_in_task:

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.

nemo_curator/stages/audio/preprocessing/mono_conversion.py:221-237andnemo_curator/stages/audio/preprocessing/channel_count.py:498-513only avoid assigning the converted tensor whenkeep_waveform_in_task=False; neither removes a waveform that was already present. With supported input_residency="waveform", keep_waveform_in_task=False, write_to_disk=True, both stages wrote a mono [8, 1]WAV but returned the original[2, 8]stereo tensor while claimingis_mono=True/num_channels=1. The next stage in input_residency="auto"prefers that stale tensor and silently bypasses the conversion. Both constructors also accept the no-sink combinationkeep_waveform_in_task=False, write_to_disk=False`, which retains the original
audio while emitting converted metadata.

Reject conversion configurations with neither output sink (and require
update_audio_filepath to imply disk output). After a successful disk-only
conversion, remove waveform_key and sample_rate_key, declare those
removes_keys in the configured contract, and add a resident-input ->
disk-only conversion -> auto-residency consumer regression for both stages.
This is a blocker under the audio review rubric because a valid advertised
configuration silently carries the wrong audio.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, blocker. The existing test only covers file input, so your resident-waveform case is genuinely uncovered. Implementing the full remedy — reject no-sink configs, update_audio_filepath implies disk output, remove waveform_key/sample_rate_key after disk-only conversion and declare them in removes_keys, plus the resident → disk-only → auto-residency regression for both stages. Landing separately so the behavioural change reviews on its own.

sf.write(path, arr, int(sample_rate))
return path

os.makedirs(output_dir, exist_ok=True)

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.

nemo_curator/stages/audio/_agent/_residency.py:244-254 hashes only
arr.tobytes() and sample rate; array shape/channel count and dtype are absent.
Two seconds of mono silence ([1, 32000]) and one second of stereo silence
([2, 16000]) therefore receive the same filename at 16 kHz. The second atomic
replace changed the first task's path from a 32,000-frame mono file into a
16,000-frame stereo file. This is not a cryptographic SHA collision: the
identity omits audio-defining inputs.

Include the canonical array shape and dtype (and any format/subtype parameters)
in the digest, or hash the fully encoded artifact. Add a regression proving
that equal flattened samples with different channel layouts cannot alias.
Adjacent lines 209-215 should also always transpose the documented
channel-first 2-D representation: the current channels < samples heuristic
writes a valid two-channel/one-sample tensor as two mono frames

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — the identity was missing audio-defining inputs. The digest now includes canonical shape, dtype and output format alongside sample rate, and lines 209-215 always transpose the documented channel-first layout. Regression added for equal flattened samples with different channel layouts, and for the two-channel/one-sample case.

# caller never wrote.
walk.past_composite = True

for item in leaves.get(index, []):

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.

nemo_curator/stages/audio/_agent/_planning.py:645-679 builds contracts and
checks reads/gates but never compares the accepts_task_type and
produces_task_type fields that _agent_ready.py:230-234 says enable this
check. A folder source (EmptyTask -> AudioTask) followed by
ManifestReaderStage (FileGroupTask -> AudioTask) returns ok=True,
keys_ok=True, and no issues; executing the second stage on that AudioTask
raises FileNotFoundError while treating dict keys as manifest paths.
This is reachable through the intended repair flow: public
find_producers("audio_filepath") recommends the internal
ManifestReaderStage rather than the public ManifestReader composite.

Track the current produced task type through the walk, accept an explicit
initial_task_type, and emit an error when the next concrete stage cannot
accept it (including a defined policy for unions/subclasses). Cover this exact
chain and two consecutive ManifestReaderStage instances.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The check is already defined on the agent side (task_type_mismatch, error, exact match on the derived type name, skipped when either side is unknown) — the gap is that it isn't in validate_pipeline(). Moving it down: the walk tracks the produced task type, accepts an explicit initial_task_type, and errors when the next stage can't accept it, with a union satisfied by any member and a declared base accepting subclasses. Covering your folder-source → ManifestReaderStage chain and two consecutive ManifestReaderStage instances.

"original_file": original_file,
"num_segments": len(mappings),
"total_duration_sec": total_duration_sec,
self.original_file_key: original_file,

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.

nemo_curator/stages/audio/preprocessing/concatenation.py:290-305 constructs a
new output_data dict, dropping segments and every unrelated upstream key,
but its contract at lines 143-159 inherits
preserves_upstream_keys=True. Independently,
nemo_curator/stages/audio/_agent/_planning.py:510-540 never consults that
contract field. Consequently concatenation followed by a filter reading
upstream text validates with ok=True/keys_ok=True; runtime output has no
text and the filter raises ValueError.

Set the concatenation contract to preserves_upstream_keys=False, and teach
_advance() to clear unavailable keys, their roles/producers, and tensor
residency before folding the replacement stage's writes. Add a regression that
the downstream text read is rejected during validation.

@shubhamNvidia shubhamNvidia Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on both halves. Setting preserves_upstream_keys=False and teaching _advance() to clear unavailable keys, their roles/producers and tensor residency before folding in the replacement writes, with the downstream text read rejected during validation.

# one decomposing composite -- reported as opaque, "we cannot tell", when the executor
# can tell perfectly well that it will run the outer composite and raise.
nested = next((c for c in children if _nested_composite(c)), None)
if nested is not None:

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.

nemo_curator/stages/audio/_agent/_composite.py:182-188 detects exactly the
nested-composite condition that Pipeline._decompose_stages rejects, but stores
it in opaque rather than unrunnable. A two-level composite therefore gets
only [('composite', 'warning')] and report.ok=True; Pipeline.build() raises
TypeError: Nested composition is not supported for the same object.

Assign this branch to unrunnable[index] so validate_pipeline() emits
composite_unrunnable as an error, and add a regression asserting parity with
Pipeline.build().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, moved to unrunnable[index] so validate_pipeline() emits composite_unrunnable as an error, with a parity regression against Pipeline.build(). Kept the check after the single-child branch, matching the executor: its nested rejection only fires inside len(sub_stages) > 1.



@dataclass
class ManifestWriterStage(AgentReady, ProcessingStage[AudioTask, AudioTask]):

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.

nemo_curator/stages/audio/common.py:758-834 makes
ManifestWriterStage agent-ready but supplies no AGENT_STATIC. As a result,
the sanctioned agent.describe_stage("ManifestWriterStage") reports
writes_to_disk=False, no output path, no lifecycle side effect, and no
serialization constraint, while the configured contract reports all four and
setup() truncates the destination. An agent doing the prescribed static
sandbox check can therefore treat a destructive JSON sink as a pure stage.

Add invariant StaticHints(gates=...) matching the configured contract, and a
static-versus-configured regression for every invariant gate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Added invariant StaticHints(gates=...) for all four gates plus per_row_independent. The regression asserts static equals configured rather than a hardcoded list, so the two can't drift.

if TYPE_CHECKING:
from nemo_curator.stages.audio._agent._agent_ready import StageContract

_IMPORTED = False

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.

nemo_curator/stages/audio/_agent/_catalog.py:43-94 explicitly records skipped
modules so a supported CPU install can distinguish “not installed here” from
“Curator cannot do this.” However, nemo_curator/stages/audio/agent.py:63-105
neither exports unavailable_modules() nor includes the failures in catalog
JSON. In the review environment, public discovery returned 12 stages while the
private-only function recorded five missing modules (pyannote, whisperx,
nemo_text_processing twice, and OpenCC). The only supported facade offers no
way to observe that distinction.

Expose unavailable_modules through agent.py and/or include an
unavailable_modules field in the public catalog JSON. Test discovery under a
supported partial dependency set and assert that import failures are visible
without parsing stderr warnings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, unavailable_modules is now exported through nemo_curator.stages.audio.agent and in its all, with a test. Happy to add it to the catalog JSON too if you'd prefer that shape.

@@ -32,11 +32,15 @@
"ALMDataOverlapStage": "nemo_curator.stages.audio.alm",
"AudioDataFilterStage": "nemo_curator.stages.audio.advanced_pipelines",
"BandFilterStage": "nemo_curator.stages.audio.filtering",

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.

nemo_curator/stages/audio/__init__.py:30-74 adds other stages from this PR to
both _LAZY and __all__, but omits
CreateInitialManifestAudioFolderStage. Although the PR presents it as the
user-facing way to start a pipeline from a folder, the conventional import
from nemo_curator.stages.audio import CreateInitialManifestAudioFolderStage
raises ImportError and hasattr(nemo_curator.stages.audio, ...) is false.

Add the class to both public-name tables and cover the top-level import.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, oversight. Added to both _LAZY and all, with the top-level import covered.

Stage source is shared library code. Never change a stage, a threshold, a filter or
windowing logic to make one user's dataset produce the output they hoped for — diagnose
from the data instead. The full guardrail, and what to do when output comes back empty,
is in `nemo_curator/audio_agent/AGENTS.md`. Drive the work through the

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.

nemo_curator/stages/audio/AGENTS.md:14-24,38-43,57-60 tells coding agents they
must use nemo_curator.audio_agent, its CLI, authoring skill, card schema and
card directory; none exists at this head. AGENT_READY.md:293-295 points to a
missing test_agent_simulation_pipelines.py, and the public facade example at
agent.py:34 asks for UTMOSFilterStage, which is not agent-ready in this PR
and raises KeyError. Line 47 then invokes another nonexistent
audio_agent.validate API. Because AGENTS.md is automatically loaded and
uses MUST/sanctioned language, this makes the documented workflow impossible
in the foundation PR itself.

Remove the forward references and use stages/APIs present in this slice, or
land the required package/cards/test harness atomically. Add a smoke/doctest
that executes the public discovery example from a supported install.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Context: these 11 PRs are a split of one branch that also contains nemo_curator/audio_agent, so the agent package, cards, test_agent_simulation_pipelines.py and the UTMOSFilterStage example all resolve there but not at this head (the latter arrives with #2335). Since the branches are meant to end up equivalent to that branch, I'd rather not rewrite the guardrails for a temporary state.

# See the License for the specific language governing permissions and
# limitations under the License.

"""EXEMPLAR per-stage conformance tests — copy these as templates.

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.

tests/stages/audio/_agent/test_agent_conformance_examples.py:15-113 contains
four per-stage exemplar tests, but the test tree contains no reference to
validate_pipeline, composite expansion, audio_stage_catalog,
catalog_as_json, describe_stage, find_producers/find_consumers,
role_index, or the get/list public APIs. The focused CPU slice passes 209
tests, but those tests do not exercise the core paths above; current GitHub
checks also contain no unit-test job. This falls short of CONTRIBUTING.md's
requirement that source changes carry relevant tests and cover at least 80% of
their changes, and the single +6,801-line commit is too large to review safely
without those seams.

Split the framework from stage migrations/new stages where practical, and add
focused test modules mirroring _agent for registry/schema, catalog/partial
imports, composite parity, planner state transitions and residency identity.
At minimum, turn each deterministic reproduction in this review into a
regression before merge.

@shubhamNvidia shubhamNvidia Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Every deterministic reproduction in this review is now a regression in
tests/stages/audio/_agent/test_agent_foundation_regressions.py, including the three
behavioural ones in 813b6bf. The focused modules mirroring _agent exist on the source
branch — say the word and I'll port them into this slice instead of letting them arrive
with the later PRs. Stage migrations are already split across the ten dependent PRs; if
you want the framework cut further, tell me where and I'll restructure.

@shubhamNvidia
shubhamNvidia force-pushed the agent/foundation branch 2 times, most recently from 6dc96a4 to fbf865b Compare September 4, 2026 10:24
Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
Conflict was an add/add in the audio ``_LAZY`` map: upstream added
``RegexSubstitutionStage`` (new ``text_filtering`` package) at the same sorted slot
this branch added ``SampleRateFilterStage``. Both kept. ``__all__`` auto-merged with
both names already present.

Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
Each of these validated clean and then produced, or consumed, the wrong thing at
runtime. Landing together because they share the regression module and two of them meet
in the planner's walk.

1. A disk-only conversion left the pre-conversion waveform on the row.
   ``keep_waveform_in_task=False`` only stopped MonoConversionStage and ChannelCountStage
   from ASSIGNING the converted tensor; it never removed the one already there. With
   ``input_residency="waveform", write_to_disk=True`` -- an advertised combination -- both
   wrote a correct mono WAV, set is_mono=True / num_channels=1, and returned the original
   stereo tensor, which the next stage at ``input_residency="auto"`` prefers. Both
   constructors also accepted ``keep_waveform_in_task=False, write_to_disk=False``, which
   converts into nowhere. Now: both sinkless shapes are rejected in __post_init__
   (update_audio_filepath implies write_to_disk, since without a file the repoint is
   dropped), the keys are removed after a successful write -- after, so a write that
   raises leaves the row as it arrived -- and declared in removes_keys.

2. validate_pipeline never checked task types. ``_agent_ready.py`` documents
   accepts_task_type/produces_task_type as enabling the check and nothing read them, so a
   folder source (EmptyTask -> AudioTask) feeding ManifestReaderStage (FileGroupTask ->
   AudioTask) returned ok=True and then raised FileNotFoundError. The walk now carries the
   produced type, validate_pipeline takes an initial_task_type, and an incompatible
   handover is an error: exact name, a union satisfied by any member, or a declared base
   accepting its subclasses. Only a genuinely unknown type breaks the chain -- an
   unparametrized generic, a contract that will not build, or an unreadable composite
   child. Proximity to a composite does not: a composite hides its inner writes, not the
   generic it is declared over, and treating it as unknown switched the check off for
   every recipe that begins at ManifestReader.

3. preserves_upstream_keys was declared and then ignored. SegmentConcatenationStage
   rebuilds task.data -- segments and every unrelated column gone -- while inheriting
   preserves_upstream_keys=True, and _advance() consulted the field for no stage at all,
   so the four that already declared False were modelled as preserving everything.
   Concatenation followed by a filter reading upstream text validated ok=True and raised
   ValueError on the missing key. Keys, roles and producers now clear before the stage's
   own writes fold in. Tensor residency deliberately does not: ALMDataBuilderStage sets
   the flag for one rebuilding branch while ordinarily still carrying the waveform, and
   clearing residency there retracts the tensor_into_sink block on a pipeline that really
   does hand a resident waveform to a JSON sink.

Every reproduction in the review is a regression in
tests/stages/audio/_agent/test_agent_foundation_regressions.py.

Signed-off-by: Shubham Bhawsar <shbhawsar@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants