Skip to content

fix(ai): produce new object references in tool-call message updaters#395

Open
braden-w wants to merge 1 commit intoTanStack:mainfrom
braden-w:fix/immutable-tool-call-updates
Open

fix(ai): produce new object references in tool-call message updaters#395
braden-w wants to merge 1 commit intoTanStack:mainfrom
braden-w:fix/immutable-tool-call-updates

Conversation

@braden-w
Copy link

@braden-w braden-w commented Mar 23, 2026

I ran into this while building a Svelte 5 chat UI with tool-call approvals. Svelte 5 uses proxies for reactivity, and $derived(part.state === 'approval-requested') never fired because the part object identity didn't change.

The root cause is four functions in message-updaters.ts that mutate tool-call parts in-place after [...msg.parts]. The shallow copy creates a new array but the elements are the same references, so toolCallPart.state = 'approval-requested' changes the original object in both arrays. This affects any framework using proxy-based reactivity (Svelte 5, Vue 3).

The other updaters (updateToolCallPart, updateTextPart, updateThinkingPart) already produce new objects via spread. This PR aligns the four remaining functions to the same pattern:

const index = parts.indexOf(toolCallPart)
parts[index] = { ...toolCallPart, state: 'approval-requested', approval: { ... } }

Functions fixed: updateToolCallApproval, updateToolCallState, updateToolCallWithOutput, updateToolCallApprovalResponse. Four immutability tests added.

Checklist

Summary by CodeRabbit

  • Bug Fixes

    • Fixed tool-call message updater behavior to ensure immutability by creating fresh object references instead of mutating existing objects in place, preventing unexpected side effects.
  • Tests

    • Added test coverage to validate that tool-call updaters maintain immutability across approval, state, output, and response update scenarios.

Four message-updater functions mutated tool-call parts in-place after
spreading the parts array. The shallow copy preserved original object
references, breaking change detection in frameworks using proxies
(Svelte 5, Vue 3).

Replace in-place mutations with spread copies at the found index,
matching the immutable pattern used by updateToolCallPart,
updateTextPart, and updateThinkingPart.

Add immutability tests for all four fixed functions.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c4222e8-621c-4e19-8cc1-ae2347e9a4d5

📥 Commits

Reviewing files that changed from the base of the PR and between c3583e3 and e00092c.

📒 Files selected for processing (3)
  • .changeset/immutable-tool-call-updates.md
  • packages/typescript/ai/src/activities/chat/stream/message-updaters.ts
  • packages/typescript/ai/tests/message-updaters.test.ts

📝 Walkthrough

Walkthrough

The PR updates tool-call message updater functions to prevent in-place object mutations. Four updater functions (updateToolCallApproval, updateToolCallState, updateToolCallWithOutput, updateToolCallApprovalResponse) now use object spread syntax to create fresh object references instead of mutating existing parts. A changeset documents the patch, and comprehensive immutability tests validate the behavior.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/immutable-tool-call-updates.md
Patch-level changeset documenting the immutability fix for tool-call updaters, describing the shift from in-place mutations to spread-based object replacement.
Message Updater Implementation
packages/typescript/ai/src/activities/chat/stream/message-updaters.ts
Refactored four tool-call updater functions to replace array elements with new object references via spread syntax instead of mutating properties directly, ensuring immutability of tool-call parts.
Immutability Test Coverage
packages/typescript/ai/tests/message-updaters.test.ts
Added four immutability-focused test cases validating that each updater function preserves the original ToolCallPart objects and returns messages with distinct, updated part instances.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hoppity-hop through the code so clear,
No mutations lurk, immutability's here!
Spread syntax spreads a brand new way,
Fresh objects dance in every play! 🌸

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing immutability in tool-call message updaters by producing new object references instead of mutating in-place.
Description check ✅ Passed The description follows the template structure with a comprehensive explanation of changes and includes a completed checklist; however, it omits the suggested '## 🎯 Changes' section heading that structures the template.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

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

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

braden-w added a commit to EpicenterHQ/epicenter that referenced this pull request Mar 23, 2026
Four message-updater functions mutated tool-call parts in-place after
shallow-copying the parts array, breaking proxy-based reactivity in
Svelte 5. Bun patch replaces mutations with immutable spread copies.

Upstream PR: TanStack/ai#395
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