Skip to content

fix(langgraph): coerce dict/str writes in _messages_delta_reducer#7680

Merged
Sydney Runkle (sydney-runkle) merged 8 commits into
mainfrom
sr/fix-messages-delta-reducer-dict-coercion
May 1, 2026
Merged

fix(langgraph): coerce dict/str writes in _messages_delta_reducer#7680
Sydney Runkle (sydney-runkle) merged 8 commits into
mainfrom
sr/fix-messages-delta-reducer-dict-coercion

Conversation

@sydney-runkle

Copy link
Copy Markdown
Collaborator

Problem

_messages_delta_reducer assumed writes always contain pre-typed BaseMessage objects (as noted in its docstring). In practice, HTTP-driven graphs always receive message input as JSON dicts — the same way add_messages receives them. Using DeltaChannel(_messages_delta_reducer) with any HTTP input would crash with:

AttributeError: 'dict' object has no attribute 'id'

This makes _messages_delta_reducer unusable for the primary motivating use-case (replacing add_messages in production LLM graphs).

Fix

Mirror the coercion contract of add_messages:

  • Regular message dicts ({"role": "human", "content": "..."}) → convert_to_messages
  • RemoveMessage dicts ({"type": "remove", "id": "..."}) → RemoveMessage directly (langchain_core's convert_to_messages doesn't support this format)
  • BaseMessage objects → pass through unchanged
  • Lists/sequences → element-wise coercion of the above

The fix is a small _coerce_one + _to_msgs helper pair that replaces the previous [w] if isinstance(w, BaseMessage) else w generator.

Tests

Added test_delta_channel_dict_coercion in test_channels.py covering:

  • dict append via {"role": "human", "content": ..., "id": ...}
  • dict update-in-place (same ID)
  • {"type": "remove", "id": ...} tombstoning

All 23 existing delta-channel tests still pass.

Release Notes: None

_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>
@sydney-runkle Sydney Runkle (sydney-runkle) changed the title fix(graph): coerce dict/str writes in _messages_delta_reducer fix(langgraph): coerce dict/str writes in _messages_delta_reducer May 1, 2026
`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>
@sydney-runkle Sydney Runkle (sydney-runkle) merged commit afdfbd5 into main May 1, 2026
66 checks passed
@sydney-runkle Sydney Runkle (sydney-runkle) deleted the sr/fix-messages-delta-reducer-dict-coercion branch May 1, 2026 17:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant