Skip to content

fix(ai): normalize AG-UI snapshot messages to UIMessage[] in MESSAGES_SNAPSHOT handler#698

Open
amitsaroj wants to merge 5 commits into
TanStack:mainfrom
amitsaroj:fix/messages-snapshot-normalize-parts
Open

fix(ai): normalize AG-UI snapshot messages to UIMessage[] in MESSAGES_SNAPSHOT handler#698
amitsaroj wants to merge 5 commits into
TanStack:mainfrom
amitsaroj:fix/messages-snapshot-normalize-parts

Conversation

@amitsaroj

@amitsaroj amitsaroj commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Fixes #688

Root cause

handleMessagesSnapshotEvent in processor.ts was doing an unsafe type cast:

// Before (unsafe)
this.messages = [...chunk.messages] as Array<UIMessage>

AG-UI MESSAGES_SNAPSHOT messages have the shape { id, role, content } — they do not have the parts array that UIMessage requires. The as Array<UIMessage> cast masked this at compile time.

When any downstream code called message.parts.find(...) — for example, the onToolCallStateChange devtools handler in chat-client.ts — it crashed with:

TypeError: Cannot read properties of undefined (reading 'find')

Fix

Normalize each message through modelMessageToUIMessage when it does not already have parts:

// After (safe)
this.messages = chunk.messages.map((msg) =>
  'parts' in msg
    ? (msg as UIMessage)
    : modelMessageToUIMessage(msg as any, generateMessageId()),
)

This mirrors the pattern already used in normalizeToUIMessage (used by ChatClient.append) and is consistent with how all other message ingestion paths work in this codebase.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • - [ ] I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.

🧪 Test Plan

  1. Send a chat message that triggers a MESSAGES_SNAPSHOT event (e.g., using an AG-UI agent backend that emits snapshot events).
    1. Verify no TypeError: Cannot read properties of undefined (reading 'find') is thrown.
    1. Existing unit tests in packages/ai/tests/stream-processor.test.ts for MESSAGES_SNAPSHOT resets transient state cover the core normalization path.

Summary by CodeRabbit

  • Bug Fixes
    • Normalize streamed chat snapshot messages into a consistent UI message format (convert legacy-shaped messages and assign fallback IDs when missing) so message parts are reliably available and downstream processing/rendering is stable.
  • Tests
    • Added regression tests covering snapshot normalization, ID preservation, empty snapshots, routing of subsequent text chunks, and other edge cases.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e206b50-9d56-4611-966d-baf8d2be1944

📥 Commits

Reviewing files that changed from the base of the PR and between b091adc and 99c4a8b.

📒 Files selected for processing (1)
  • packages/ai/tests/stream-processor.test.ts

📝 Walkthrough

Walkthrough

Replaces unsafe casting of MESSAGES_SNAPSHOT payloads by importing modelMessageToUIMessage and mapping snapshot messages: reuse messages with parts; convert messages without parts (using existing or generated ids) so downstream code can rely on message.parts.

Changes

Safe MESSAGES_SNAPSHOT handling

Layer / File(s) Summary
Safe normalization of MESSAGES_SNAPSHOT payloads
packages/ai/src/activities/chat/stream/processor.ts
Adds modelMessageToUIMessage import and updates MESSAGES_SNAPSHOT handler to map snapshot messages: reuse messages that already have parts, convert others via modelMessageToUIMessage (using an existing or generated id) so each resulting UIMessage includes parts.
Regression tests for snapshot normalization
packages/ai/tests/stream-processor.test.ts
Adds tests verifying AG-UI-shaped snapshot messages are normalized into UI messages with parts, preserve original ids or generate fallbacks, leave already-normalized messages unchanged, handle empty snapshots, and route subsequent TEXT_MESSAGE_CONTENT events correctly.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers:

  • tombeckenham

🐰 I hopped through snapshots, parts awry,
I stitched each message an ID so they’d not cry,
Converted the models, one by one,
Now every part shines in the sun,
A gentle fix — the stream runs spry.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: normalizing AG-UI snapshot messages to UIMessage[] in the MESSAGES_SNAPSHOT handler.
Description check ✅ Passed The description covers the root cause, fix, and test plan. It includes issue reference (#688) and mentions existing test coverage, though the test checkbox is unchecked.
Linked Issues check ✅ Passed The PR fully addresses issue #688 by normalizing AG-UI snapshot messages to UIMessage[] with proper parts arrays and preserving original message IDs for downstream event correlation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the MESSAGES_SNAPSHOT handler: processor.ts receives the normalization fix and stream-processor.test.ts receives regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/activities/chat/stream/processor.ts`:
- Around line 878-882: The snapshot branch is overwriting incoming message IDs
by always calling modelMessageToUIMessage with generateMessageId(); update the
mapping so when converting non-UI messages you preserve the original msg.id and
only generate a new id if msg.id is missing—i.e., in the chunk.messages mapping
(this.messages assignment) pass msg.id (or msg['id']) as the id argument to
modelMessageToUIMessage when present, otherwise call generateMessageId(); this
keeps existing message identity used by ensureAssistantMessage,
TEXT_MESSAGE_CONTENT, toolCallToMessage, and messageStates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d6f8123-d73d-4983-91d7-fbe5fe45edfa

📥 Commits

Reviewing files that changed from the base of the PR and between 769a6c7 and bc8e675.

📒 Files selected for processing (1)
  • packages/ai/src/activities/chat/stream/processor.ts

Comment thread packages/ai/src/activities/chat/stream/processor.ts
@amitsaroj

Copy link
Copy Markdown
Contributor Author

Thanks for the review @coderabbitai — great catch on the ID preservation issue.

The second commit (b091adc) addresses this: instead of always generating a fresh generateMessageId(), the normalization now uses (msg as { id?: string }).id ?? generateMessageId() — preserving the AG-UI message's original id when present and only falling back to a generated ID when the message has no id field.

This ensures downstream event correlation (e.g. TEXT_MESSAGE_CONTENT, TOOL_CALL_*, ensureAssistantMessage) continues to work correctly after a MESSAGES_SNAPSHOT reset.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@amitsaroj: Thanks for the explanation! The approach looks solid — using (msg as { id?: string }).id ?? generateMessageId() is exactly the right pattern here. Preserving the original AG-UI message id ensures that any subsequent events (TEXT_MESSAGE_CONTENT, TOOL_CALL_*, etc.) that reference that ID will correctly correlate with the already-stored message after the snapshot reset, rather than silently breaking due to a mismatched generated ID.

The fix is well-reasoned. 🐇✨

@amitsaroj

Copy link
Copy Markdown
Contributor Author

Hi @tombeckenham and TanStack maintainers — this is a small but impactful fix normalizing AG-UI MESSAGES_SNAPSHOT messages to UIMessage[] format, preventing the TypeError: Cannot read properties of undefined (reading 'find') crash. CodeRabbit's review is clean, no conflicts, and all checks pass. Could someone approve the pending workflows and merge when ready? Thank you!

@tombeckenham

Copy link
Copy Markdown
Contributor

Would you be able to add some tests for this @amitsaroj ?

@amitsaroj

Copy link
Copy Markdown
Contributor Author

Hi @tombeckenham — done! Added 6 new tests in packages/ai/tests/stream-processor.test.ts inside the existing describe('MESSAGES_SNAPSHOT') block (commit 99c4a8b):

  1. Normalizes AG-UI messages (no parts) to UIMessage[] — verifies each message gets a parts array and text content is surfaced as a TextPart
    1. Preserves original AG-UI message id — regression guard for the earlier version that always called generateMessageId() and broke downstream event correlation
    1. No TypeError on parts.find() — directly reproduces the crash from onToolCallStateChange accessing message.parts after a snapshot
    1. Pass-through for messages that already have parts — existing UIMessage snapshots are kept as-is and not re-normalized
    1. Fallback id generation — handles AG-UI messages that have no id field
    1. Stream continuation after snapshotTEXT_MESSAGE_CONTENT events routing to snapshot-hydrated messages work correctly

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.

TypeError: Cannot read properties of undefined (reading 'find') for MESSAGES_SNAPSHOT

2 participants