Skip to content

fix: emit guardrail halt message to client before closing stream#30807

Open
annguyenNous wants to merge 1 commit into
NousResearch:mainfrom
annguyenNous:fix/guardrail-halt-emit-final-message
Open

fix: emit guardrail halt message to client before closing stream#30807
annguyenNous wants to merge 1 commit into
NousResearch:mainfrom
annguyenNous:fix/guardrail-halt-emit-final-message

Conversation

@annguyenNous

Copy link
Copy Markdown
Contributor

Problem

When the tool loop guardrail fires (max_tool_failures, repeated failures, etc.), the turn exits with guardrail_halt but no final assistant message is emitted to the client. The SSE stream closes silently — from the user's perspective (especially via API/SSE clients like Open WebUI), the conversation just goes silent with no explanation. Indistinguishable from a crash.

Fixes #30770

Root Cause

In agent/conversation_loop.py, the guardrail halt path (line ~3429):

  1. Generates final_response via _toolguard_controlled_halt_response(decision)
  2. Appends it to internal messages history
  3. Breaks out of the loop

But the stream_delta_callback(None) at line 3423 (display flush before tool execution) already closed the display stream. The final_response was never emitted to the client — only stored in internal history.

Fix

After generating the halt message, emit it through both channels:

  • agent._safe_print() — for CLI/TUI users
  • agent.stream_delta_callback(final_response) — for SSE/API clients (Open WebUI, etc.)

The stream_delta_callback(None) is a display flush, not a hard close — the callback is still alive and can accept new text.

Before vs After

Before After
SSE stream closes with no message SSE stream emits halt explanation before closing
TUI shows nothing TUI prints halt message
Indistinguishable from crash Clear explanation of why agent stopped

Tests

  • python -m py_compile agent/conversation_loop.py
  • Existing guardrail tests should still pass (they check internal state, not stream output)

When the tool loop guardrail fires (max_tool_failures, etc.), the
turn exits with guardrail_halt but no final assistant message was
emitted to the client. The SSE stream closed silently —
indistinguishable from a crash.

The stream_delta_callback(None) before tool execution is a display
flush, not a hard close. After generating the halt response, emit
it through both _safe_print (CLI) and stream_delta_callback (SSE)
so clients see the explanation.

Fixes NousResearch#30770
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent loop, run_agent.py, prompt builder P1 High — major feature broken, no workaround labels May 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Competing fix: PRs #30808 and #30813 also address #30770. #30813 is the most comprehensive (covers stream delta, flush, and interim message channels with a reusable helper + 473-line test suite). #30808 is the cleanest minimal fix (reuses _fire_stream_delta). This PR has no tests and swallows exceptions silently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

guardrail_halt exits silently — no final assistant message delivered to client

3 participants