Skip to content

fix(agent): deliver non-streamed finalization responses - #5049

Merged
chengyongru merged 1 commit into
mainfrom
fix/stream-finalization-outbound
Jul 23, 2026
Merged

chengyongru merged 1 commit into
mainfrom
fix/stream-finalization-outbound

Conversation

@chengyongru

Copy link
Copy Markdown
Collaborator

Background

Streaming channels intentionally skip the final outbound package when it is marked as a StreamedResponseEvent, because that content is expected to have already arrived through deltas. That suppression mechanism originated in #2365.

Empty-response recovery later made a non-streaming finalization reachable after a stream had already been closed (#2733), and the silent retries added in #2889 amplify the path into multiple stream_end events. #3613 fixed the analogous drop for error/tool-error responses, but empty finalization could still be marked as streamed despite emitting no answer delta.

Root cause

AgentLoop treated the presence of an on_stream callback as proof that the final response content had been streamed. During empty-response finalization, the callback exists, but the final answer comes from chat_with_retry and never passes through the delta callback. ChannelManager then skips the final package, leaving the session populated while streaming clients see no answer.

Fix

  • Track whether the most recently completed answer stream segment actually emitted content.
  • Emit StreamedResponseEvent only when that final segment delivered content.
  • Let non-streaming recovery output remain a regular outbound message so ChannelManager calls channel.send().
  • Keep the existing streamed tool-call regression honest by making its fake provider emit real deltas.

Regression coverage

The new test reproduces the production sequence:

  • two empty streaming responses
  • three StreamEndEvents
  • zero answer deltas
  • a successful non-streaming finalization

It asserts that the final answer is emitted as a regular outbound message rather than StreamedResponseEvent.

Verification

  • pytest tests/agent -q — 1383 passed, 2 skipped
  • ruff check nanobot/ — passed
  • bun run test — 706 passed
  • bun run build — passed
  • Real gateway + headless Playwright with a local OpenAI-compatible fake provider:
    • request sequence: streaming empty, streaming empty, non-streaming finalization
    • final answer rendered in WebUI
    • final answer remained visible after reload
    • public webui-thread route returned exactly one assistant final answer

@chengyongru chengyongru added priority: p1 Important user-impacting issue or project priority with bounded impact or a reasonable workaround. bug Something isn't working fix test regression labels Jul 23, 2026
@chengyongru
chengyongru merged commit 5f054c0 into main Jul 23, 2026
5 checks passed
@chengyongru
chengyongru deleted the fix/stream-finalization-outbound branch July 24, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fix priority: p1 Important user-impacting issue or project priority with bounded impact or a reasonable workaround. regression test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant