fix(langgraph): coerce dict/str writes in _messages_delta_reducer#7680
Merged
Sydney Runkle (sydney-runkle) merged 8 commits intoMay 1, 2026
Merged
Conversation
_messages_delta_reducer assumed pre-typed BaseMessage objects but HTTP-driven
graphs always receive message input as JSON dicts. add_messages coerces dicts
via convert_to_messages; the delta reducer should do the same.
Changes:
- Added _coerce_one() + _to_msgs() helpers that mirror add_messages coercion
- Handle {"type": "remove", "id": ...} dicts → RemoveMessage directly (convert_to_messages doesn't support this format)
- All other dicts/strings delegated to convert_to_messages
- test_delta_channel_dict_coercion validates dict, dict-update, and RemoveMessage dict inputs
Fixes: Channel 'messages' already exists / AttributeError: 'dict' has no attribute 'id' when using DeltaChannel with HTTP input
Single-pass coercion through `convert_to_messages` (the same helper
`add_messages` uses) instead of per-item `_coerce_one`/`_to_msgs`
wrappers. Drops the non-standard `{"type": "remove"}` dict form
(unsupported by `add_messages`) in favor of `RemoveMessage` instances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`convert_to_messages` is already imported at module level; `chain` moves up alongside it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the `_flatten` generator helper and `chain.from_iterable` / `Iterable` import in favor of a straight loop. Same one-pass coercion, fewer indirections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
State can arrive as raw dicts after checkpoint deserialization or as initial HTTP input; route it through `convert_to_messages` too so the ID-dedup index sees typed BaseMessage objects (matching `add_messages`, which coerces both `left` and `right`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit follow-ups on _messages_delta_reducer: - Top-level tuple writes (a valid MessageLikeRepresentation) were element-expanded by the flatten loop, producing N messages instead of one. Flatten only on `isinstance(w, list)`. - Skip `convert_to_messages(state)` when state is already typed — the reducer's own output is the steady-state input, so the replay hot path shouldn't re-dispatch O(N) per fold. - Soften docstring: this isn't full add_messages parity (REMOVE_ALL_MESSAGES, unknown-id RemoveMessage errors, missing-id UUID assignment, and BaseMessageChunk conversion are not handled). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Christian Bromann (christian-bromann)
added a commit
to langchain-ai/langgraphjs
that referenced
this pull request
May 29, 2026
… cadence helpers (beta) - DeltaChannel: reducer channel storing a sentinel-free omission in blobs and reconstructing state by replaying ancestor writes; count-based snapshot cadence (snapshotFrequency=1000 default) - DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT system bound (default 5000, env override) - messagesDeltaReducer: batching-invariant messages reducer with dict/string coercion (langchain-ai/langgraph#7680) - createCheckpoint channelsToSnapshot/getNextVersion/updatedChannels options, deltaChannelsToSnapshot, and async channelsFromCheckpoint reconstruction - public exports of DeltaChannel + messagesDeltaReducer Ports the end-state of langchain-ai/langgraph#7586, #7634, #7680.
Christian Bromann (christian-bromann)
added a commit
to langchain-ai/langgraphjs
that referenced
this pull request
Jun 1, 2026
… cadence helpers (beta) - DeltaChannel: reducer channel storing a sentinel-free omission in blobs and reconstructing state by replaying ancestor writes; count-based snapshot cadence (snapshotFrequency=1000 default) - DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT system bound (default 5000, env override) - messagesDeltaReducer: batching-invariant messages reducer with dict/string coercion (langchain-ai/langgraph#7680) - createCheckpoint channelsToSnapshot/getNextVersion/updatedChannels options, deltaChannelsToSnapshot, and async channelsFromCheckpoint reconstruction - public exports of DeltaChannel + messagesDeltaReducer Ports the end-state of langchain-ai/langgraph#7586, #7634, #7680.
Christian Bromann (christian-bromann)
added a commit
to langchain-ai/langgraphjs
that referenced
this pull request
Jun 10, 2026
… cadence helpers (beta) - DeltaChannel: reducer channel storing a sentinel-free omission in blobs and reconstructing state by replaying ancestor writes; count-based snapshot cadence (snapshotFrequency=1000 default) - DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT system bound (default 5000, env override) - messagesDeltaReducer: batching-invariant messages reducer with dict/string coercion (langchain-ai/langgraph#7680) - createCheckpoint channelsToSnapshot/getNextVersion/updatedChannels options, deltaChannelsToSnapshot, and async channelsFromCheckpoint reconstruction - public exports of DeltaChannel + messagesDeltaReducer Ports the end-state of langchain-ai/langgraph#7586, #7634, #7680.
Christian Bromann (christian-bromann)
added a commit
to langchain-ai/langgraphjs
that referenced
this pull request
Jun 10, 2026
… cadence helpers (beta) - DeltaChannel: reducer channel storing a sentinel-free omission in blobs and reconstructing state by replaying ancestor writes; count-based snapshot cadence (snapshotFrequency=1000 default) - DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT system bound (default 5000, env override) - messagesDeltaReducer: batching-invariant messages reducer with dict/string coercion (langchain-ai/langgraph#7680) - createCheckpoint channelsToSnapshot/getNextVersion/updatedChannels options, deltaChannelsToSnapshot, and async channelsFromCheckpoint reconstruction - public exports of DeltaChannel + messagesDeltaReducer Ports the end-state of langchain-ai/langgraph#7586, #7634, #7680.
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.
Problem
_messages_delta_reducerassumed writes always contain pre-typedBaseMessageobjects (as noted in its docstring). In practice, HTTP-driven graphs always receive message input as JSON dicts — the same wayadd_messagesreceives them. UsingDeltaChannel(_messages_delta_reducer)with any HTTP input would crash with:This makes
_messages_delta_reducerunusable for the primary motivating use-case (replacingadd_messagesin production LLM graphs).Fix
Mirror the coercion contract of
add_messages:{"role": "human", "content": "..."}) →convert_to_messagesRemoveMessagedicts ({"type": "remove", "id": "..."}) →RemoveMessagedirectly (langchain_core'sconvert_to_messagesdoesn't support this format)BaseMessageobjects → pass through unchangedThe fix is a small
_coerce_one+_to_msgshelper pair that replaces the previous[w] if isinstance(w, BaseMessage) else wgenerator.Tests
Added
test_delta_channel_dict_coercionintest_channels.pycovering:{"role": "human", "content": ..., "id": ...}{"type": "remove", "id": ...}tombstoningAll 23 existing delta-channel tests still pass.
Release Notes: None