Skip to content

fix(chat): drop empty reasoning parts at the render boundary - #3448

Merged
kwakayama merged 2 commits into
mainfrom
fix/empty-reasoning-at-render
Aug 7, 2026
Merged

kwakayama merged 2 commits into
mainfrom
fix/empty-reasoning-at-render

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Completes #3444, which fixed this in the wrong layer.

The gap

#3444 dropped empty reasoning spans during stream assembly (buildCurrentParts). Rendering does not go through that. It reads message.parts straight into groupPartsInOrder, and for a conversation loaded back from storage those parts come from persistence:

stored: [{type:"reasoning", text:""}, {type:"text", text:"answer"}]
groups: ["reasoning","text"]     ← empty "Thought process" still rendered

Empty parts were being persisted before #3444 — buildCurrentParts emitted them, handleFinish passed them to onMessage, storage kept them. So new threads were clean and every existing thread stayed broken.

The fix

Moved to groupPartsInOrder: the single boundary every rendered message crosses, live or restored, whichever producer built the parts.

This is also the layer the spec points at. AG-UI permits a reasoning message that opens and closes with no content events, and explicitly leaves it to the consumer to decide whether to display, suppress, or flag the result. Assembly goes back to reporting the stream as it arrived; display is the consumer's call.

So the assembly-layer guard from #3444 is removed rather than kept alongside the new one. Net effect on the streaming layer is a deletion — one predicate at one boundary instead of the same rule in two places.

The skip runs before the text buffer flushes, matching the tool-result skip directly above it. Otherwise a list or paragraph spanning the empty part renders as two markdown blocks; there is a test for that, and it fails if the guard is moved after the flush.

Carve-outs kept, both tested: a part still streaming stays (it has yet to say anything), and a redacted or signed part stays (it carries meaning without visible text).

Diff

 handler.test.ts     |  7 +++-
 parts-builder.ts    | 10 -----      ← guard removed
 message-parts.test.ts | 49 +++++++
 message-parts.ts    | 26 ++++++--

16 lines of production change, net negative in the streaming layer.

isReasoningPart now narrows to the real ChatReasoningPart instead of a hand-written subset that silently dropped signature and redactedData — which is what let the predicate read them.

Verification

  • agent + chat + react: 1449 passed, 0 failed
  • deno task typecheck clean, deno fmt --check and deno lint clean
  • The text-splitting test mutation-checked: moving the guard after flushText() fails it

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of reasoning content in chat messages.
    • Empty completed reasoning is now hidden, while streaming, signed, redacted, or non-empty reasoning remains visible.
    • Prevented hidden reasoning segments from incorrectly splitting adjacent text groups.
    • Preserved empty reasoning data during processing so it can be handled appropriately by chat consumers.
  • Documentation

    • Updated chat API documentation links and type references.

#3444 dropped them during stream assembly, which misses every conversation
loaded back from storage: those parts were already persisted, and rendering
reads `message.parts` straight through `groupPartsInOrder`. Old threads kept
showing an empty "Thought process" disclosure.

Moved to `groupPartsInOrder`, the single boundary every rendered message
crosses, live or restored, whoever produced the parts. AG-UI permits a
reasoning message that opens and closes with no content events and leaves the
display decision to the consumer, so this is where the decision belongs, and
assembly goes back to reporting the stream as it arrived.

The skip runs before the text buffer flushes, like the tool-result skip above
it, so text either side stays one block instead of splitting a list or
paragraph in two.

Net effect on the streaming layer is a deletion.
@kwakayama
kwakayama requested a review from kojiwakayama as a code owner August 7, 2026 04:03
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aec6414-3c59-4ea5-8d93-832f4648b219

📥 Commits

Reviewing files that changed from the base of the PR and between 5817685 and 8ee2c5e.

⛔ Files ignored due to path filters (1)
  • src/server/handlers/dev/framework-candidates.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (1)
  • docs/api-reference/veryfront/chat.md

📝 Walkthrough

Walkthrough

The change preserves empty completed reasoning spans during streaming assembly. Message grouping now filters those spans while retaining streaming, signed, redacted, and non-empty reasoning parts.

Changes

Reasoning visibility

Layer / File(s) Summary
Preserve empty reasoning spans
src/agent/react/use-chat/streaming/parts-builder.ts, src/agent/react/use-chat/streaming/handler.test.ts
Completed empty reasoning spans are emitted. Streaming tests expect the empty reasoning part in assembled messages.
Filter reasoning during grouping
src/react/components/chat/chat/utils/message-parts.ts, src/react/components/chat/chat/utils/message-parts.test.ts, docs/api-reference/veryfront/chat.md
groupPartsInOrder skips empty completed reasoning without splitting adjacent text. Streaming, signed, redacted, and non-empty reasoning remains visible. API source references point to the relocated definitions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kojiwakayama

🚥 Pre-merge checks | ✅ 5
✅ 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 describes the main change: moving empty reasoning-part filtering to the render boundary.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/empty-reasoning-at-render

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

@kwakayama
kwakayama enabled auto-merge August 7, 2026 06:14
@kwakayama
kwakayama added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 78b8b04 Aug 7, 2026
55 of 56 checks passed
@kwakayama
kwakayama deleted the fix/empty-reasoning-at-render branch August 7, 2026 06:30
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.

1 participant