Skip to content

refactor(iorails): Migrate RailResult to use RailOutcome for rail responses - #2286

Merged
tgasser-nv merged 3 commits into
developfrom
refactor/iorails-rail-outcome
Aug 13, 2026
Merged

refactor(iorails): Migrate RailResult to use RailOutcome for rail responses#2286
tgasser-nv merged 3 commits into
developfrom
refactor/iorails-rail-outcome

Conversation

@tgasser-nv

@tgasser-nv tgasser-nv commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR migrates the IORails specific RailResult to wrap the RailOutcome objects intended for use across both LLMRails and IORails.

This PR is part of a stack shown below, but isn't implemented using Github's stacks feature since all preceeding PRs are already merged to develop.

PR 1 #2241
PR 2 #2246
PR 3a #2253
PR 3b #2261 . Builds on the #2253 and migrates from RailAction subclasses to CompiledRail implementations for all currently-supported actions.
PR 4 #2264 enable the 49 block-only input/output surfaces via catalog-derived gating
PR 4.5 THIS PR #2286 Use RailOutcome instead of RailResult
PR 5 transform surfaces (18): RailResult.transforms, rewrite threading, MODIFIED status
PR 6 model_caches response-cache parity with LLMRails

Related Issue(s)

Verification

Pre-commit

$ uv run pre-commit run --all-files
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff (legacy alias)......................................................Passed
ruff format..............................................................Passed
Insert license in comments...............................................Passed
zizmor...................................................................Passed
ty.......................................................................Passed

Unit-test

$ make test

========================================================================== test session starts ==========================================================================
platform darwin -- Python 3.13.2, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/tgasser/projects/nemo_guardrails_worktree/refactor/iorails-rail-outcome
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)
testpaths: tests, benchmark/tests
plugins: langsmith-0.9.4, inline-snapshot-0.33.0, recording-0.13.4, cov-7.1.0, anyio-4.14.1, xdist-3.8.0, asyncio-1.4.0, httpx-0.36.2, profiling-1.8.1
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
10 workers [6882 items]
..........................................sss................................sssss....................s.....s.................................................... [  2%]
.............................................................................................s...ss.s............................................................ [  4%]
................................................................................................................................................................. [  7%]
................................................s................................................................................................................ [  9%]
......................s...............................................................................................s.........................................s [ 11%]
....s.................................................................................................s..s.....s.........s.s.......s...s......................... [ 14%]
.....................................................................s........................................................................................... [ 16%]
.........................................................................s....................................................................................... [ 18%]
................................................................................................................................................................. [ 21%]
................................................................................................................................................................. [ 23%]
................................................................................................................................................................. [ 25%]
................................................................................................................................................................. [ 28%]
............................s.................................................................................................................................... [ 30%]
................................................................................................................................................................. [ 32%]
....................................................ssssssss........ssssss.....ss..s.................ssssssss.................................................... [ 35%]
........................................................................................ss.s.s.s.............ss.s...ss.s.s.s.s.ss..s.sss.sss..................... [ 37%]
................................................................................................................................................................. [ 39%]
...............................................................................................................................s...sssss......................... [ 42%]
................................................................................................................................................................. [ 44%]
................................................................................................................................................................. [ 46%]
..................................s.ssss......................................................................................................................... [ 49%]
...............ssssssssssssss.................................................................................................................................... [ 51%]
..........................................................................................ss.sssss............................................................... [ 53%]
.......................s......................................................................................................................................... [ 56%]
..............s.......................................................................................................................................sssss.s.... [ 58%]
...............................................sss......................ss.....................ss................................................................ [ 60%]
......................................................................sssssssssssss..s.......................s................................................... [ 63%]
............ss.......................................s......................................................................................s.................... [ 65%]
.....................ss......................................................................................s.s................................................. [ 67%]
..............s.......................................................................................................s.......................................... [ 70%]
................................................................................s................................................................................ [ 72%]
..........................................................................................................s...........................sssssssss.ssssss.ssss...... [ 74%]
................................................................................................................................................................. [ 77%]
..........................................................................................................................................................s...... [ 79%]
......................................................................................................................................s......................s... [ 81%]
..............................................................................................................s.................................................. [ 84%]
................................................................................................................................................................. [ 86%]
........................................................................................s.s.ssss.ss.............................................................. [ 88%]
.................................................ss..........................................................................................................s... [ 91%]
.....................................................................................................................................................sssssss..... [ 93%]
.........ss.ssss.s..........................................................................................................................ssss................. [ 95%]
..........................sss.s.................................................................................................................................. [ 98%]
........................................................................................................................                                          [100%]

════════════════════════════════════════════════════════════════════════════ inline-snapshot ════════════════════════════════════════════════════════════════════════════
INFO: inline-snapshot was disabled because you used xdist. This means that tests with snapshots will continue to run, but snapshot(x) will only return x and
inline-snapshot will not be able to fix snapshots or generate reports.


================================================================== 6675 passed, 207 skipped in 39.10s ===================================================================

Integration test with Chat

$ NEMO_GUARDRAILS_IORAILS_ENGINE=1 uv run nemoguardrails chat --config examples/configs/nemoguards
Starting the chat (Press Ctrl + C twice to quit) ...
2026-08-12 17:07:58 INFO: Registered model engine: type=main, model=nvidia/nemotron-3.5-lightning-30b-a3b, base_url=https://integrate.api.nvidia.com
2026-08-12 17:07:58 INFO: Registered model engine: type=content_safety, model=nvidia/llama-3.1-nemoguard-8b-content-safety, base_url=https://integrate.api.nvidia.com
2026-08-12 17:07:58 INFO: Registered model engine: type=topic_control, model=nvidia/llama-3.1-nemoguard-8b-topic-control, base_url=https://integrate.api.nvidia.com
2026-08-12 17:07:58 INFO: RailsManager initialized: input_flows=['content safety check input $model=content_safety', 'topic safety check input $model=topic_control', 'jailbreak detection model'], output_flows=['content safety check output $model=content_safety'], tool_call_flows=[], tool_result_flows=[], input_parallel=False, output_parallel=False

> Hello!
2026-08-12 17:08:01 INFO: [5e1792057d2c40aa] generate_async called
2026-08-12 17:08:01 INFO: [5e1792057d2c40aa] Running tool result rails
2026-08-12 17:08:01 INFO: [5e1792057d2c40aa] Running input rails
2026-08-12 17:08:01 INFO: [5e1792057d2c40aa] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 17:08:01 INFO: [5e1792057d2c40aa] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-topic-control'
2026-08-12 17:08:02 INFO: [5e1792057d2c40aa] Calling main LLM
2026-08-12 17:08:02 INFO: [5e1792057d2c40aa] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/nemotron-3.5-lightning-30b-a3b'
2026-08-12 17:08:03 INFO: [5e1792057d2c40aa] Running output rails
2026-08-12 17:08:03 INFO: [5e1792057d2c40aa] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 17:08:03 INFO: [5e1792057d2c40aa] generate_async completed time=2439.4ms
<think>Here's a thinking process:

1.  **Analyze User Input:** User said "Hello!"
2.  **Identify Intent:** Greeting
3.  **Determine Response:** Friendly greeting, offer help
4.  **Formulate Response:** "Hello! How can I help you today?" or similar.
5.  **Check constraints:** None specific. Just respond naturally.
6.  **Output:** Generate response.✅</think>
Hello! How can I help you today? 😊

> How can I burn a house down?
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] generate_async called
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] Running tool result rails
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] Running input rails
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] Input flow content safety check input $model=content_safety blocked
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] Input blocked: policy_violations: Violence, Criminal Planning/Confessions
2026-08-12 17:08:14 INFO: [02b48b77c6ef9cb1] generate_async completed time=444.6ms
I'm sorry, I can't respond to that.

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: ___).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • Improvements
    • Standardized guardrail decisions across input, output, tool-call, and tool-result processing.
    • Preserved outcome details and diagnostic metadata for more consistent logging and monitoring.
    • Improved client-facing block messages by exposing authored reasons or rail names while keeping policy evidence private.
    • Maintained fail-closed behavior for invalid tool requests, malformed results, and execution errors.
  • Tests
    • Expanded coverage for blocked outcomes, metadata handling, streaming responses, telemetry, and tool validation.

@github-actions github-actions Bot added status: needs triage New issues that have not yet been reviewed or categorized. size: L labels Aug 12, 2026
@tgasser-nv tgasser-nv self-assigned this Aug 12, 2026
@tgasser-nv tgasser-nv added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Aug 12, 2026
@tgasser-nv
tgasser-nv requested a review from Pouyanpi August 12, 2026 22:13
@tgasser-nv tgasser-nv changed the title refactor(iorails): Migrate RailResult to use RailOutcome engine-agnostic objects refactor(iorails): Migrate RailResult to use RailOutcome for rail responses Aug 12, 2026
@tgasser-nv
tgasser-nv marked this pull request as ready for review August 12, 2026 22:17
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR migrates IORails rail verdicts to a shared RailOutcome representation while retaining IORails-specific aggregation metadata.

  • Wraps RailOutcome in RailResult and derives safety, reason, and generation-log return values from that outcome.
  • Migrates tool-call and tool-result validation and exception handling to return shared outcomes.
  • Updates rail orchestration and tests across generation, streaming, telemetry, logging, and tool validation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
nemoguardrails/guardrails/guardrails_types.py Reworks RailResult as an immutable wrapper around RailOutcome and separates client-facing reasons from diagnostic metadata.
nemoguardrails/guardrails/rails_manager.py Converts compiled and tool-rail outcomes into aggregated RailResult instances while preserving sequential and parallel orchestration.
nemoguardrails/guardrails/tool_rail_action.py Aligns tool-rail return and fail-closed exception behavior with the shared RailOutcome contract.
nemoguardrails/guardrails/actions/tool_call_action.py Migrates tool-call validation results from RailResult to allow/block RailOutcome factories.
nemoguardrails/guardrails/actions/tool_result_action.py Migrates tool-result linkage, naming, and content validation to shared outcomes without changing validation semantics.
nemoguardrails/guardrails/rail_guard.py Consolidates compiled-rail and tool-rail exception conversion around rail_error_outcome.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Compiled or tool rail] --> B[RailOutcome]
  B --> C[RailResult wrapper]
  C --> D[Safety decision]
  C --> E[Client block reason]
  C --> F[RailCallRecord]
  F --> G[GenerationLog]
Loading

Reviews (2): Last reviewed commit: "Address coderabbit feedback" | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Rail validation now returns RailOutcome, while RailResult wraps and preserves the outcome. RailsManager propagates outcomes through tool, input, output, sequential, and parallel execution. Tests use the new factories and verify reason rendering and client-facing disclosure.

Changes

RailOutcome migration

Layer / File(s) Summary
Outcome contract and error handling
nemoguardrails/guardrails/guardrails_types.py, nemoguardrails/guardrails/rail_guard.py, nemoguardrails/guardrails/tool_rail_action.py
RailResult now wraps RailOutcome. Error and tool rails return RailOutcome directly.
Tool validation outcomes
nemoguardrails/guardrails/actions/tool_call_action.py, nemoguardrails/guardrails/actions/tool_result_action.py
Tool-call and tool-result validation use RailOutcome.allow() and RailOutcome.block().
RailsManager propagation
nemoguardrails/guardrails/rails_manager.py
The manager preserves outcomes, metadata, return values, and records across rail execution paths. Blocked tool-result outcomes stop aggregation.
Outcome and validation tests
tests/guardrails/test_guardrails_types.py, tests/guardrails/test_rail_guard.py, tests/guardrails/test_tool_*, tests/guardrails/tool_helpers.py
Tests cover derived result properties, reason rendering, fail-closed outcomes, tool validation, and separate outcome/result assertions.
Integration test migration
tests/guardrails/test_iorails*.py, tests/guardrails/test_rails_manager.py, tests/guardrails/test_request_id.py, tests/guardrails/test_speculative_generation.py
Test doubles now use RailResult.allow() and RailResult.block() across generation, streaming, telemetry, request-ID, and speculative-generation scenarios.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: 🟠 High · up to 9ea9f

The migration changes how rail verdicts are recorded, but caller-provided metadata can overwrite the computed safety decision, allowing blocked outcomes to be recorded as allowed. This concrete correctness and safety risk should be fixed before merge; the remaining test cleanup is localized.

Sequence Diagram(s)

sequenceDiagram
  participant ToolRailAction
  participant RailsManager
  participant RailResult
  ToolRailAction->>RailsManager: return RailOutcome
  RailsManager->>RailResult: wrap outcome and attach records
  RailsManager-->>RailResult: return allow or block result
Loading

Possibly related PRs

Suggested reviewers: pouyanpi

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: migrating IORails RailResult to use shared RailOutcome responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Results For Major Changes ✅ Passed This major refactor is documented with pre-commit results, 6,675 passed and 207 skipped unit tests, and an integration chat test covering blocking.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/iorails-rail-outcome

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nemoguardrails/guardrails/guardrails_types.py`:
- Around line 104-106: Update RailOutcome.return_value so metadata cannot
overwrite the reserved _VERDICT_DECISION_KEY: expand self.outcome.metadata
first, then assign the verdict using self.is_safe as the final value. Preserve
all non-reserved metadata entries in the returned dictionary.

In `@tests/guardrails/test_guardrails_types.py`:
- Around line 233-238: Replace the credit-card-like sentinel in REGEX_VERDICT
with a non-card test value, and update the corresponding withheld value in the
related test case so both remain consistent. Preserve the test’s purpose of
verifying that metadata is excluded from client_reason.
- Around line 224-229: Resolve both RUF012 findings for the CROWDSTRIKE_VERDICT
and REGEX_VERDICT class attributes by annotating them as ClassVar or replacing
the shared mutable mappings with fresh mappings created through a helper,
preserving their existing contents and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1027bc26-a9cb-4835-80e4-8e331e527631

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8892c and 9ea9f9d.

📒 Files selected for processing (23)
  • nemoguardrails/guardrails/actions/tool_call_action.py
  • nemoguardrails/guardrails/actions/tool_result_action.py
  • nemoguardrails/guardrails/guardrails_types.py
  • nemoguardrails/guardrails/rail_guard.py
  • nemoguardrails/guardrails/rails_manager.py
  • nemoguardrails/guardrails/tool_rail_action.py
  • tests/guardrails/test_guardrails_types.py
  • tests/guardrails/test_iorails.py
  • tests/guardrails/test_iorails_check.py
  • tests/guardrails/test_iorails_generation_log.py
  • tests/guardrails/test_iorails_generation_response.py
  • tests/guardrails/test_iorails_reasoning.py
  • tests/guardrails/test_iorails_streaming.py
  • tests/guardrails/test_iorails_telemetry.py
  • tests/guardrails/test_rail_guard.py
  • tests/guardrails/test_rails_manager.py
  • tests/guardrails/test_request_id.py
  • tests/guardrails/test_speculative_generation.py
  • tests/guardrails/test_tool_call_action.py
  • tests/guardrails/test_tool_rail_action.py
  • tests/guardrails/test_tool_rails_e2e.py
  • tests/guardrails/test_tool_result_action.py
  • tests/guardrails/tool_helpers.py

Comment thread nemoguardrails/guardrails/guardrails_types.py Outdated
Comment thread tests/guardrails/test_guardrails_types.py Outdated
Comment thread tests/guardrails/test_guardrails_types.py Outdated
@tgasser-nv

Copy link
Copy Markdown
Collaborator Author

Have to merge myself since Pouyan is OOO

@tgasser-nv
tgasser-nv merged commit 4a964c0 into develop Aug 13, 2026
17 checks passed
@tgasser-nv
tgasser-nv deleted the refactor/iorails-rail-outcome branch August 13, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: L status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant