mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-12 02:55:21 +00:00
feat(core)!: auto-assign UUID to BaseMessage.id at creation
Change the `id` field from `default=None` to
`default_factory=lambda: str(uuid.uuid4())` so every message receives a
stable, unique id from birth rather than being id-less until a reducer
or `add_messages` assigns one.
## Motivation
LangGraph's `DeltaChannel` stores pending writes (the raw message
objects) as serialized blobs **before** `update()` is called. Any id
assigned inside the reducer (e.g. a random UUID in
`_messages_delta_reducer`) only exists in the in-memory channel state;
it never reaches the stored write. On checkpoint replay the same
id-less message gets a fresh random UUID, so an eviction/update Command
that references the runtime-assigned id cannot match the replayed
message — both the original and the update land in state as separate
messages.
The root fix is here: if messages already carry a stable id when they
are first constructed (before any serialization boundary), the stored
write and any subsequent Command that updates that message by id will
always agree on the id, making `DeltaChannel` replay fully correct.
## Breaking change
`HumanMessage(content="x") == HumanMessage(content="x")` is now
`False` because auto-assigned UUIDs differ. Previously both had
`id=None` and compared equal. Code that relies on content-based message
equality must be updated (compare `.content` / `.model_dump(exclude={'id'})`
directly, or set an explicit shared `id`).
129 unit tests in langchain-core fail; all are due to this equality
change. Options the team can consider:
- Fix tests to use explicit ids or field-level comparisons
- Override `BaseMessage.__eq__` to exclude `id` (preserves backward
compat for equality; DeltaChannel dedup uses id-keyed dict, not ==)
- Introduce a `__eq__` that only uses `id` when both sides have a
non-None id, otherwise falls back to content comparison
Explicitly passing `id=None` still produces a message with `id=None`
(default_factory is not invoked for explicitly-supplied values).
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any, cast, overload
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
@@ -132,10 +133,22 @@ class BaseMessage(Serializable):
|
||||
|
||||
"""
|
||||
|
||||
id: str | None = Field(default=None, coerce_numbers_to_str=True)
|
||||
"""An optional unique identifier for the message.
|
||||
id: str | None = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
coerce_numbers_to_str=True,
|
||||
)
|
||||
"""A unique identifier for the message.
|
||||
|
||||
This should ideally be provided by the provider/model which created the message.
|
||||
Auto-assigned as a UUID on creation when not provided. The id is used by
|
||||
LangGraph's ``DeltaChannel`` reducer and other components to deduplicate
|
||||
messages: a message written to a channel can be updated in-place on
|
||||
subsequent writes because it carries a stable id from birth. Without an id
|
||||
the deduplication logic cannot match a message to its prior version across
|
||||
checkpoint serialization boundaries, causing duplicates.
|
||||
|
||||
The type is ``str | None`` rather than ``str`` so that existing serialized
|
||||
messages without an ``id`` field deserialize cleanly (they get ``None``
|
||||
back, not an error). New messages created at runtime always receive a UUID.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user