mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-11 20:27:54 +02:00
fix(message): assign UUIDs to ID-less messages in _messages_delta_reducer
Without UUID assignment, message eviction and RemoveMessage tombstoning fail for messages created without explicit IDs. Matches the behavior of add_messages. IDs are now assigned inline within the existing iterations over state_msgs and msgs to avoid an extra pass.
This commit is contained in:
@@ -257,9 +257,10 @@ def _messages_delta_reducer(
|
||||
|
||||
Raw dict / string / tuple inputs are coerced to typed `BaseMessage`
|
||||
objects so that HTTP-driven graphs work without a separate coercion
|
||||
step. This is not full `add_messages` parity — `REMOVE_ALL_MESSAGES`,
|
||||
unknown-id `RemoveMessage` errors, missing-id UUID assignment, and
|
||||
`BaseMessageChunk` conversion are not handled here.
|
||||
step. Messages without IDs are assigned UUIDs (matching `add_messages`
|
||||
behavior) so that message eviction and `RemoveMessage` tombstoning work
|
||||
correctly. `REMOVE_ALL_MESSAGES` and `BaseMessageChunk` conversion are
|
||||
not handled here.
|
||||
|
||||
Example::
|
||||
|
||||
@@ -289,15 +290,19 @@ def _messages_delta_reducer(
|
||||
state_msgs = cast("list[AnyMessage]", convert_to_messages(state))
|
||||
msgs = cast("list[AnyMessage]", convert_to_messages(flat))
|
||||
|
||||
index: dict[str, int] = {
|
||||
m.id: i for i, m in enumerate(state_msgs) if m.id is not None
|
||||
}
|
||||
# Build index and assign missing IDs in one pass (parity with add_messages
|
||||
# so that eviction and RemoveMessage tombstoning work on ID-less messages).
|
||||
index: dict[str, int] = {}
|
||||
for i, m in enumerate(state_msgs):
|
||||
if m.id is None:
|
||||
m.id = str(uuid.uuid4())
|
||||
index[m.id] = i
|
||||
result: list[AnyMessage | None] = list(state_msgs)
|
||||
for msg in msgs:
|
||||
if msg.id is None:
|
||||
msg.id = str(uuid.uuid4())
|
||||
mid = msg.id
|
||||
if mid is None:
|
||||
result.append(msg)
|
||||
elif isinstance(msg, RemoveMessage):
|
||||
if isinstance(msg, RemoveMessage):
|
||||
if mid in index:
|
||||
result[index[mid]] = None
|
||||
del index[mid]
|
||||
|
||||
@@ -284,6 +284,28 @@ def test_messages_delta_reducer_tuple_write_is_one_message() -> None:
|
||||
assert result[0].content == "hi"
|
||||
|
||||
|
||||
def test_messages_delta_reducer_assigns_uuid_to_id_less_messages() -> None:
|
||||
"""Messages without IDs get UUIDs assigned, matching add_messages behavior.
|
||||
|
||||
Without UUID assignment, RemoveMessage tombstoning fails on messages that
|
||||
were created without explicit IDs.
|
||||
"""
|
||||
m1 = HumanMessage(content="hi")
|
||||
m2 = AIMessage(content="hello")
|
||||
assert m1.id is None
|
||||
assert m2.id is None
|
||||
|
||||
result = _messages_delta_reducer([], [[m1, m2]])
|
||||
assert len(result) == 2
|
||||
assert result[0].id is not None
|
||||
assert result[1].id is not None
|
||||
|
||||
# RemoveMessage tombstoning must work on the now-assigned IDs.
|
||||
result2 = _messages_delta_reducer(result, [RemoveMessage(id=result[1].id)])
|
||||
assert len(result2) == 1
|
||||
assert result2[0].content == "hi"
|
||||
|
||||
|
||||
def test_delta_channel_checkpoint_returns_missing() -> None:
|
||||
"""checkpoint() always returns MISSING regardless of state.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user