Skip to content

bug(v2): output rails silently stop firing after first abort in multi-turn conversations. #2067

Description

@MuneezaAzmat

Did you check docs and existing issues?

  • I have read all the NeMo-Guardrails docs
  • I have updated the package to the latest version before submitting this issue
  • (optional) I have used the develop branch
  • I have searched the existing issues of NeMo-Guardrails

Python version (python --version)

Python 3.13.12

Operating system/version

MacOS 26.3.1

NeMo-Guardrails version (if you must use a specific version and not the latest

No response

Describe the bug

In Colang v2, run output rails in guardrails.co sets $output_rails_in_progress = True before invoking output rails and clears it only on normal completion. If an output rail calls abort (e.g. after a rejection message), the flag is never reset. Subsequent bot say calls skip output rails because _bot_say only runs them when $output_rails_in_progress is false.

This affects any v2 output rail that can abort.

Steps To Reproduce

  1. Use Colang v2 with import guardrails.
  2. Define an output rail that calls bot say then abort on failure (minimal example below).
  3. In one session: trigger the rail → reject → send a new user message that should trigger the rail again.
  4. Observe the subsequent bot response bypasses output rails.
import core
import guardrails

flow output rails $output_text
  if $output_text == "BLOCKME"
    bot say "Rejected."
    abort

@active
flow handle user messages
  while True
    user said something
    bot say "BLOCKME"

flow main
  activate handle user messages
  wait indefinitely

Run test: pytest test_output_rails_abort_multiturn.py -v
test_output_rails_abort_multiturn.py

Expected Behavior

Turn 1 ("hello"):    bot=['Rejected.']  rail_fired=True
Turn 2 ("hi again"): bot=['Rejected.']  rail_fired=True
Turn 3 ("one more"): bot=['Rejected.']  rail_fired=True

Output rail fires on every turn, blocking BLOCKME and returning Rejected. each time

Actual Behavior

Turn 1 ("hello"):    bot=['Rejected.']  rail_fired=True
Turn 2 ("hi again"): bot=['BLOCKME']    rail_fired=False
Turn 3 ("one more"): bot=['BLOCKME']    rail_fired=False

Output rail fires only on the first turn. On all subsequent turns, $output_rails_in_progress is stuck at True (never reset after the abort), so _bot_say skips the await run output rails call entirely and passes BLOCKME through unfiltered.

Root cause:

In guardrails.co [lines 88-99], run output rails sets $output_rails_in_progress = True before calling await output rails $output_text. When the output rail aborts, execution never reaches $output_rails_in_progress = False at the end of the flow, leaving the flag permanently stuck.

Suggested fix

Ensure $output_rails_in_progress is cleared even when output rails abort e.g. reset in guardrails.co after the rail call, or reset on new user input in _user_said as a safety net.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions