langgraph 1.2.1__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langgraph-1.2.1 → langgraph-1.2.2}/PKG-INFO +1 -1
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_loop.py +9 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_messages.py +54 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/pyproject.toml +1 -1
- langgraph-1.2.2/tests/test_delta_channel_id_stability.py +140 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/uv.lock +2 -2
- {langgraph-1.2.1 → langgraph-1.2.2}/.gitignore +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/LICENSE +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/Makefile +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/README.md +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/__main__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/fanout_to_subgraph.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/pydantic_state.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/react_agent.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/sequential.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/serde_allowlist.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/wide_dict.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/bench/wide_state.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_cache.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_config.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_constants.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_fields.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_future.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_pydantic.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_queue.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_replay.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_retry.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_runnable.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_scratchpad.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_serde.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_timeout.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_typing.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/callbacks.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/any_value.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/base.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/binop.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/delta.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/ephemeral_value.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/last_value.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/named_barrier_value.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/topic.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/untracked_value.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/config.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/constants.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/errors.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/func/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/_branch.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/_node.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/message.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/state.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/ui.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/base.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/is_last_step.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_algo.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_call.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_checkpoint.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_config.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_draw.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_executor.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_io.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_log.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_read.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_retry.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_runner.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_tools.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_utils.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_validate.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_write.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/debug.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/main.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/protocol.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/remote.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/types.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/py.typed +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/runtime.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_convert.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_mux.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_types.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/run_stream.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/stream_channel.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/transformers.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/types.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/typing.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/config.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/runnable.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/version.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/warnings.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/__init__.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_large_cases.ambr +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_pregel.ambr +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_pregel_async.ambr +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/agents.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/any_int.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/any_str.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/compose-postgres.yml +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/compose-redis.yml +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest_checkpointer.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest_store.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/example_graph.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/langgraph.json +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/requirements.txt +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/fake_chat.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/fake_tracer.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/memory_assert.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/messages.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_algo.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_channels.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_checkpoint_migration.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_config_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_benchmark.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_exit_mode.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_migration.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_supersteps_bound.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_deprecation.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_graph_callbacks.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interleave_arrival_order.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interrupt_migration.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interruption.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_large_cases.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_large_cases_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_managed_values.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_messages_state.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_parent_command.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_parent_command_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel_stream_events_v3.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pydantic.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_remote_graph.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_retry.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_runnable.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_runtime.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_serde_allowlist.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_state.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_before_builtins.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_data_transformers.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3_e2e.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_lifecycle_transformer.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_messages_transformer.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_subgraph_transformer.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_subgraph_persistence.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_subgraph_persistence_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_time_travel.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_time_travel_async.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_tool_stream_handler.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_tracing_interops.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_type_checking.py +0 -0
- {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Building stateful, multi-actor applications with LLMs
|
|
5
5
|
Project-URL: Homepage, https://docs.langchain.com/oss/python/langgraph/overview
|
|
6
6
|
Project-URL: Documentation, https://reference.langchain.com/python/langgraph/
|
|
@@ -115,6 +115,7 @@ from langgraph.pregel._io import (
|
|
|
115
115
|
map_output_values,
|
|
116
116
|
read_channels,
|
|
117
117
|
)
|
|
118
|
+
from langgraph.pregel._messages import ensure_message_ids
|
|
118
119
|
from langgraph.pregel._read import PregelNode
|
|
119
120
|
from langgraph.pregel._utils import get_new_channel_versions, is_xxh3_128_hexdigest
|
|
120
121
|
from langgraph.pregel.debug import (
|
|
@@ -447,6 +448,14 @@ class PregelLoop:
|
|
|
447
448
|
|
|
448
449
|
# save writes
|
|
449
450
|
self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
|
|
451
|
+
# Assign stable IDs to any id=None BaseMessages in DeltaChannel writes
|
|
452
|
+
# before the background thread serialises them. Without this, reducers
|
|
453
|
+
# that assign IDs inside apply_writes() race with serialisation and
|
|
454
|
+
# store id=None, causing get_state() replays to produce a different UUID
|
|
455
|
+
# on every call.
|
|
456
|
+
for c, v in writes_to_save:
|
|
457
|
+
if isinstance(self.specs.get(c), DeltaChannel):
|
|
458
|
+
ensure_message_ids(v)
|
|
450
459
|
if self.durability != "exit" and self.checkpointer_put_writes is not None:
|
|
451
460
|
config = patch_configurable(
|
|
452
461
|
self.checkpoint_config,
|
|
@@ -11,6 +11,7 @@ from uuid import UUID, uuid4
|
|
|
11
11
|
|
|
12
12
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
13
13
|
from langchain_core.messages import BaseMessage, ToolMessage
|
|
14
|
+
from langchain_core.messages.utils import convert_to_messages
|
|
14
15
|
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
|
|
15
16
|
from pydantic import BaseModel
|
|
16
17
|
|
|
@@ -405,3 +406,56 @@ class StreamMessagesHandlerV2(StreamMessagesHandler, _V2StreamingCallbackHandler
|
|
|
405
406
|
self.seen.add(msg_id)
|
|
406
407
|
v2_meta = {**meta[1], "run_id": str(run_id)}
|
|
407
408
|
self.stream((meta[0], "messages", (event, v2_meta)))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# Known role values (OpenAI-style) and type values (LangChain serialisation)
|
|
412
|
+
# that identify a dict as a message. Checked before coercing to BaseMessage so
|
|
413
|
+
# we don't accidentally touch unrelated dicts that happen to have a "role" key.
|
|
414
|
+
_MESSAGE_ROLES: frozenset[str] = frozenset(
|
|
415
|
+
{"user", "human", "assistant", "ai", "tool", "system", "function"}
|
|
416
|
+
)
|
|
417
|
+
_MESSAGE_TYPES: frozenset[str] = frozenset(
|
|
418
|
+
{"human", "ai", "tool", "system", "function", "remove"}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _is_message_dict(item: dict) -> bool:
|
|
423
|
+
return item.get("role") in _MESSAGE_ROLES or item.get("type") in _MESSAGE_TYPES
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def ensure_message_ids(value: Any) -> None:
|
|
427
|
+
"""Coerce message-like write values to typed BaseMessages with stable IDs.
|
|
428
|
+
|
|
429
|
+
Called in put_writes() before DeltaChannel writes are submitted to the
|
|
430
|
+
checkpointer. Without this the checkpoint may store raw dicts or id=None
|
|
431
|
+
BaseMessages; every get_state() replay then produces a different UUID and
|
|
432
|
+
the same message appears with a different ID in each LangSmith trace.
|
|
433
|
+
|
|
434
|
+
Handles three input shapes:
|
|
435
|
+
- BaseMessage objects: assign a UUID if id is None.
|
|
436
|
+
- Dicts with a known "role" (OpenAI-style) or "type" (LangChain format) at
|
|
437
|
+
the root level: stamp "id" into the dict in-place. The reducer's
|
|
438
|
+
convert_to_messages call will forward the id to the resulting BaseMessage.
|
|
439
|
+
- Lists of the above: apply the same logic to each element, replacing dict
|
|
440
|
+
items with coerced BaseMessages so the shared list reference seen by
|
|
441
|
+
checkpoint_pending_writes and the background thread both get typed messages.
|
|
442
|
+
|
|
443
|
+
Mutating synchronously here (before the background thread is submitted) is
|
|
444
|
+
safe: the serialised bytes always reflect the post-coercion state.
|
|
445
|
+
"""
|
|
446
|
+
if isinstance(value, BaseMessage):
|
|
447
|
+
if value.id is None:
|
|
448
|
+
value.id = str(uuid4())
|
|
449
|
+
elif isinstance(value, dict) and _is_message_dict(value):
|
|
450
|
+
if not value.get("id"):
|
|
451
|
+
value["id"] = str(uuid4())
|
|
452
|
+
elif isinstance(value, list):
|
|
453
|
+
for i, item in enumerate(value):
|
|
454
|
+
if isinstance(item, BaseMessage):
|
|
455
|
+
if item.id is None:
|
|
456
|
+
item.id = str(uuid4())
|
|
457
|
+
elif isinstance(item, dict) and _is_message_dict(item):
|
|
458
|
+
msg = convert_to_messages([item])[0]
|
|
459
|
+
if msg.id is None:
|
|
460
|
+
msg.id = str(uuid4())
|
|
461
|
+
value[i] = msg
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""ensure_message_ids() assigns stable UUIDs to id=None BaseMessages
|
|
2
|
+
before DeltaChannel writes are serialised to the checkpoint.
|
|
3
|
+
|
|
4
|
+
Without this, the checkpoint stores id=None and every get_state() replay
|
|
5
|
+
produces a different UUID — the same HumanMessage appears with a different
|
|
6
|
+
ID in each LangSmith trace / on every resumed invocation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Annotated, Any
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
|
|
15
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
16
|
+
from typing_extensions import TypedDict
|
|
17
|
+
|
|
18
|
+
from langgraph.channels.delta import DeltaChannel
|
|
19
|
+
from langgraph.graph import END, START, StateGraph
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.anyio
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _append_reducer(
|
|
25
|
+
state: list[AnyMessage], writes: list[list[AnyMessage]]
|
|
26
|
+
) -> list[AnyMessage]:
|
|
27
|
+
"""Simple append — no ID assignment. IDs come from ensure_message_ids()."""
|
|
28
|
+
result = list(state)
|
|
29
|
+
for w in writes:
|
|
30
|
+
if isinstance(w, list):
|
|
31
|
+
result.extend(w)
|
|
32
|
+
else:
|
|
33
|
+
result.append(w) # type: ignore[arg-type]
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_graph(checkpointer: Any) -> Any:
|
|
38
|
+
State = TypedDict( # noqa: UP013
|
|
39
|
+
"State",
|
|
40
|
+
{
|
|
41
|
+
"messages": Annotated[
|
|
42
|
+
list, DeltaChannel(_append_reducer, snapshot_frequency=50)
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
) # type: ignore[call-overload]
|
|
46
|
+
|
|
47
|
+
def agent(state: dict) -> dict:
|
|
48
|
+
return {"messages": [AIMessage(content="reply", id="ai-1")]}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
StateGraph(State)
|
|
52
|
+
.add_node("agent", agent)
|
|
53
|
+
.add_edge(START, "agent")
|
|
54
|
+
.add_edge("agent", END)
|
|
55
|
+
.compile(checkpointer=checkpointer)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_delta_channel_message_gets_id_and_stays_stable() -> None:
|
|
60
|
+
"""Messages written with id=None must receive a stable UUID.
|
|
61
|
+
|
|
62
|
+
ensure_message_ids() is called in put_writes() before the background
|
|
63
|
+
thread serialises DeltaChannel writes. The checkpoint stores the
|
|
64
|
+
assigned UUID, so every get_state() replay sees the same ID.
|
|
65
|
+
"""
|
|
66
|
+
saver = InMemorySaver()
|
|
67
|
+
graph = _build_graph(saver)
|
|
68
|
+
config = {"configurable": {"thread_id": "id-stability"}}
|
|
69
|
+
|
|
70
|
+
graph.invoke({"messages": [HumanMessage(content="hello")]}, config)
|
|
71
|
+
|
|
72
|
+
ids = [
|
|
73
|
+
next(
|
|
74
|
+
m.id
|
|
75
|
+
for m in graph.get_state(config).values["messages"]
|
|
76
|
+
if isinstance(m, HumanMessage)
|
|
77
|
+
)
|
|
78
|
+
for _ in range(3)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
assert ids[0] is not None, "ensure_message_ids should have assigned a UUID"
|
|
82
|
+
assert len(set(ids)) == 1, (
|
|
83
|
+
f"HumanMessage id must be stable across get_state() calls; "
|
|
84
|
+
f"got {ids}. The checkpoint is storing id=None."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def test_delta_channel_message_gets_id_and_stays_stable_async() -> None:
|
|
89
|
+
"""Same check via ainvoke (AsyncPregelLoop path)."""
|
|
90
|
+
saver = InMemorySaver()
|
|
91
|
+
graph = _build_graph(saver)
|
|
92
|
+
config = {"configurable": {"thread_id": "id-stability-async"}}
|
|
93
|
+
|
|
94
|
+
await graph.ainvoke({"messages": [HumanMessage(content="hello")]}, config)
|
|
95
|
+
|
|
96
|
+
ids = [
|
|
97
|
+
next(
|
|
98
|
+
m.id
|
|
99
|
+
for m in (await graph.aget_state(config)).values["messages"]
|
|
100
|
+
if isinstance(m, HumanMessage)
|
|
101
|
+
)
|
|
102
|
+
for _ in range(3)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
assert ids[0] is not None, "ensure_message_ids should have assigned a UUID"
|
|
106
|
+
assert len(set(ids)) == 1, (
|
|
107
|
+
f"Async path: HumanMessage id unstable across aget_state() calls: {ids}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_delta_channel_dict_style_message_gets_stable_id() -> None:
|
|
112
|
+
"""Dict-style inputs (API / over-the-wire format) must also get stable IDs.
|
|
113
|
+
|
|
114
|
+
When the graph is invoked via the LangGraph API the input arrives as a raw
|
|
115
|
+
dict {"role": "user", "content": "..."} rather than a BaseMessage object.
|
|
116
|
+
ensure_message_ids() must coerce those dicts to typed BaseMessages and
|
|
117
|
+
stamp a UUID so the checkpoint never stores an id-less message.
|
|
118
|
+
"""
|
|
119
|
+
saver = InMemorySaver()
|
|
120
|
+
graph = _build_graph(saver)
|
|
121
|
+
config = {"configurable": {"thread_id": "dict-id-stability"}}
|
|
122
|
+
|
|
123
|
+
# Invoke with a raw dict (the format LangGraph API sends)
|
|
124
|
+
graph.invoke({"messages": [{"role": "user", "content": "hello"}]}, config)
|
|
125
|
+
|
|
126
|
+
ids = [
|
|
127
|
+
next(
|
|
128
|
+
m.id
|
|
129
|
+
for m in graph.get_state(config).values["messages"]
|
|
130
|
+
if isinstance(m, HumanMessage)
|
|
131
|
+
)
|
|
132
|
+
for _ in range(3)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
assert ids[0] is not None, (
|
|
136
|
+
"dict-style message should have been coerced and assigned a UUID"
|
|
137
|
+
)
|
|
138
|
+
assert len(set(ids)) == 1, (
|
|
139
|
+
f"dict-style HumanMessage id must be stable across get_state() calls; got {ids}"
|
|
140
|
+
)
|
|
@@ -1382,7 +1382,7 @@ wheels = [
|
|
|
1382
1382
|
|
|
1383
1383
|
[[package]]
|
|
1384
1384
|
name = "langgraph"
|
|
1385
|
-
version = "1.2.
|
|
1385
|
+
version = "1.2.2"
|
|
1386
1386
|
source = { editable = "." }
|
|
1387
1387
|
dependencies = [
|
|
1388
1388
|
{ name = "langchain-core" },
|
|
@@ -1563,7 +1563,7 @@ wheels = [
|
|
|
1563
1563
|
|
|
1564
1564
|
[[package]]
|
|
1565
1565
|
name = "langgraph-checkpoint"
|
|
1566
|
-
version = "4.1.
|
|
1566
|
+
version = "4.1.1"
|
|
1567
1567
|
source = { editable = "../checkpoint" }
|
|
1568
1568
|
dependencies = [
|
|
1569
1569
|
{ name = "langchain-core" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|