Files
langgraph/libs
b7d053b256 fix(langgraph): give every message written to a messages DeltaChannel an id (#9275)
A DeltaChannel replays its saved writes on every read, and
`_messages_delta_reducer` doesn't fill in missing ids, so the loop gives
messages their ids before saving them (`ensure_message_ids`). That
missed several cases:

- Strings and `(role, content)` tuples, alone or in a list (`[("user",
"hi")]`), and messages inside an `Overwrite` were saved with no id, so
nothing could target them by id, for example `RemoveMessage`. A regular
messages channel gives them ids.
- A raw `Pregel` graph's input to a DeltaChannel got its ids after it
was applied, and none with exit durability or on a new thread; a dict
given as input read back with an id its nodes never saw. A
`Command(update=...)` sent to an older checkpoint got none either.

Now, in a DeltaChannel whose reducer is `_messages_delta_reducer`, every
message-like the reducer accepts becomes a message with an id before the
writes are saved or applied: in `put_writes`, before input is applied,
for a forked `Command`, and in `update_state`. Anything that can't
become a message is left as it is, so it fails where it always has.
Other DeltaChannels keep the rule they had (message objects, and message
dicts in a list, get ids), since they may hold other data and
deepagents' messages channel, which uses its own copy of the reducer,
relies on it; the dicts in an `Overwrite` there are left as they are.
Lists and dicts the caller passed in are never changed: a write that
changes is replaced. So the id step can recognize the reducer without an
import cycle, `_messages_delta_reducer` now lives in
`langgraph.pregel._messages`; `langgraph.graph.message` re-exports it.

Visible changes: in a messages DeltaChannel, a node's update streams as
messages with ids, and a string is stored as a message (about 170 bytes
more each).

JS twin: langchain-ai/langgraphjs#2992. Thanks to @HarshShroff for
testing every `Overwrite` form and for the tuple fix.

## Tests

`test_delta_channel_id_stability.py` and
`test_delta_channel_update_state.py`: a write of every shape the reducer
accepts gets an id that every read keeps, as input, from a node and
through `update_state` (sync, async and as input); a list written twice
adds its messages twice and stays as it was; a forked `Command` gets an
id; a message dict the reducer rejects in a list still fails the run
before it's saved; a raw `Pregel` input gets an id on a new thread with
every durability, and its nodes see the ids that are saved; another
DeltaChannel keeps the dicts in an `Overwrite`. 36 of the 87 tests in
these files fail on main. The langgraph and prebuilt suites pass, and
lint is clean.

---------

Co-authored-by: Harsh Shroff <29751900+HarshShroff@users.noreply.github.com>
2026-10-12 20:33:45 +00:00
..
2026-10-07 09:19:34 -04:00