fix(chat): isGenuiTurn walks back through messages (LangGraph in-place replace drops tool name)#248
Merged
Merged
Conversation
Live smoke after #247 revealed a third source of streaming jank: the emit-phase assistant message's previous message is the tool result, which LangGraph's in-place replacement strips of its 'name' field. Case 3 of the original isGenuiTurn (prevMsg.role === 'tool' AND prevMsg.name in genuiTools) failed because prevMsg.name was null, so isGenuiTurn returned false on the emit message, and the markdown branch fired showing JSON. Fix: walk backward through agent.messages() from the current index until we find: - an assistant message with tool_calls referencing a GenUI tool (→ this turn produces a surface) - or a human message (→ crossed turn boundary; not GenUI) The walk is bounded by the most recent human message so the lookup stays O(turn length). The function signature gains an optional `index?: number`. The template now passes `i` from the @for. Existing callers without the index still work (the walk-back is skipped, falling through to the direct checks on the message itself).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Live smoke against PR #248's walk-back fix revealed yet another flow we hadn't accounted for: LangGraph projects the sub-LLM's streaming tool_call.arguments as the assistant message's content STRING (not a structured content array) during the streaming phase. The structured array only materialises after streaming completes. So during streaming both detection paths (tool_calls field, function_call content block) come up empty on what will become the tool-call AI message. Add a third detection path: scan the projected content string for stable A2UI v1 envelope markers ("surfaceUpdate", "beginRendering", "dataModelUpdate") and json-render spec markers ("root" + "elements"). Both are canonical contract identifiers per the GenUI schemas — unlikely to false-positive on regular prose. This makes isGenuiTurn robust to LangGraph's streaming oddity without requiring server-side changes to emit_generated_surface.
PR #248's refactor accidentally removed the original case-3 check (prev message is a tool with a GenUI name). The walk-back covers it for the well-formed in-app case, but the unit tests exercise isGenuiTurn(msg, prevTool) without passing an index — so they relied on the removed direct check. Restore the direct prev-message check as a fast path before the walk-back. Now all three layers fire: 1. Direct checks on the message (tool_calls, function_call content blocks, A2UI/json-render content markers). 2. Direct prev-message check (well-formed case). 3. Walk-back through messages bounded by user-message (fallback when the tool message has been stripped of its name).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third pass on the GenUI streaming jank. The classifier patience fix (#246) handled the `-` ambiguity. PR #247 suppressed the markdown branch on GenUI turns. Both correct, but isGenuiTurn was still returning false on the emit-phase message — so the markdown branch fired anyway.
Root cause: `emit_generated_surface` in the Python graph performs an in-place replacement of the ToolMessage via LangGraph's `add_messages` reducer (id-match). The replacement strips the `name` field. So when isGenuiTurn's Case 3 looked at the previous message (`prevMsg.role === 'tool' && prevMsg.name in genuiTools`), `prevMsg.name` was `null` and the check failed.
Fix: walk backward through `agent.messages()` from the current index until we find either:
This makes the detection robust to LangGraph's in-place ToolMessage replacement.
Test plan
Follow-up to #246 and #247.
🤖 Generated with Claude Code