react-langchain: every values event reconverts every message when UI lives in graph state
problem
useStreamThreadRuntime merges the uiStateKey snapshot (stream.values.ui by default) over the live custom channel fold in mergeUIMessages. upstream builds a fresh state object for every values event and only keeps identity for messages: in @langchain/langgraph-sdk 1.11.0, RootMessageProjection.applyValues reconciles messages by id, while stateValuesShallowEqual compares every other key with Object.is. an unchanged ui list therefore arrives as a new array of new entry objects on every superstep, so mergedUiMessages, uiMessagesByParent and convertWithUI all change and every cached conversion misses.
measured on main (f36e94500) through useStreamRuntime with mocked useStream and useChannel, over five rerenders that only replace values.ui with an equal copy:
- 10 root messages and one snapshot UI entry: 50
convertLangChainBaseMessagecalls, 0 of 10 message identities kept. - one subagent with 20 nested messages and a snapshot UI entry on one of them: 100 nested conversions and a new transcript array each time, because
useSubagentTranscriptscompares each subagent's UI entries by identity.
this is separate from #7416, which covers the custom channel fold: a fold that keeps entry identity still hands mergeUIMessages new snapshot entries on every values event. per parent invalidation (#7148, #7155) would narrow the root side to the messages that carry snapshot UI, but those parents and their nested transcripts would still reconvert on every superstep.
fix
recover identity where the snapshot enters the merge: when the snapshot array changes, reuse the previous entry for an id if the new entry is structurally equal to it, so an unchanged snapshot yields the previous entries and the previous merged list. the comparison runs once per values event and is proportional to the snapshot, not to the thread.
Source: assistant-ui/assistant-ui