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.
Files changed (159) hide show
  1. {langgraph-1.2.1 → langgraph-1.2.2}/PKG-INFO +1 -1
  2. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_loop.py +9 -0
  3. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_messages.py +54 -0
  4. {langgraph-1.2.1 → langgraph-1.2.2}/pyproject.toml +1 -1
  5. langgraph-1.2.2/tests/test_delta_channel_id_stability.py +140 -0
  6. {langgraph-1.2.1 → langgraph-1.2.2}/uv.lock +2 -2
  7. {langgraph-1.2.1 → langgraph-1.2.2}/.gitignore +0 -0
  8. {langgraph-1.2.1 → langgraph-1.2.2}/LICENSE +0 -0
  9. {langgraph-1.2.1 → langgraph-1.2.2}/Makefile +0 -0
  10. {langgraph-1.2.1 → langgraph-1.2.2}/README.md +0 -0
  11. {langgraph-1.2.1 → langgraph-1.2.2}/bench/__init__.py +0 -0
  12. {langgraph-1.2.1 → langgraph-1.2.2}/bench/__main__.py +0 -0
  13. {langgraph-1.2.1 → langgraph-1.2.2}/bench/fanout_to_subgraph.py +0 -0
  14. {langgraph-1.2.1 → langgraph-1.2.2}/bench/pydantic_state.py +0 -0
  15. {langgraph-1.2.1 → langgraph-1.2.2}/bench/react_agent.py +0 -0
  16. {langgraph-1.2.1 → langgraph-1.2.2}/bench/sequential.py +0 -0
  17. {langgraph-1.2.1 → langgraph-1.2.2}/bench/serde_allowlist.py +0 -0
  18. {langgraph-1.2.1 → langgraph-1.2.2}/bench/wide_dict.py +0 -0
  19. {langgraph-1.2.1 → langgraph-1.2.2}/bench/wide_state.py +0 -0
  20. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/__init__.py +0 -0
  21. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_cache.py +0 -0
  22. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_config.py +0 -0
  23. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_constants.py +0 -0
  24. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_fields.py +0 -0
  25. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_future.py +0 -0
  26. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_pydantic.py +0 -0
  27. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_queue.py +0 -0
  28. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_replay.py +0 -0
  29. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_retry.py +0 -0
  30. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_runnable.py +0 -0
  31. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_scratchpad.py +0 -0
  32. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_serde.py +0 -0
  33. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_timeout.py +0 -0
  34. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/_internal/_typing.py +0 -0
  35. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/callbacks.py +0 -0
  36. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/__init__.py +0 -0
  37. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/any_value.py +0 -0
  38. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/base.py +0 -0
  39. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/binop.py +0 -0
  40. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/delta.py +0 -0
  41. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/ephemeral_value.py +0 -0
  42. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/last_value.py +0 -0
  43. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/named_barrier_value.py +0 -0
  44. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/topic.py +0 -0
  45. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/channels/untracked_value.py +0 -0
  46. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/config.py +0 -0
  47. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/constants.py +0 -0
  48. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/errors.py +0 -0
  49. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/func/__init__.py +0 -0
  50. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/__init__.py +0 -0
  51. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/_branch.py +0 -0
  52. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/_node.py +0 -0
  53. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/message.py +0 -0
  54. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/state.py +0 -0
  55. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/graph/ui.py +0 -0
  56. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/__init__.py +0 -0
  57. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/base.py +0 -0
  58. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/managed/is_last_step.py +0 -0
  59. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/__init__.py +0 -0
  60. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_algo.py +0 -0
  61. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_call.py +0 -0
  62. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_checkpoint.py +0 -0
  63. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_config.py +0 -0
  64. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_draw.py +0 -0
  65. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_executor.py +0 -0
  66. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_io.py +0 -0
  67. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_log.py +0 -0
  68. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_read.py +0 -0
  69. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_retry.py +0 -0
  70. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_runner.py +0 -0
  71. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_tools.py +0 -0
  72. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_utils.py +0 -0
  73. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_validate.py +0 -0
  74. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/_write.py +0 -0
  75. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/debug.py +0 -0
  76. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/main.py +0 -0
  77. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/protocol.py +0 -0
  78. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/remote.py +0 -0
  79. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/pregel/types.py +0 -0
  80. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/py.typed +0 -0
  81. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/runtime.py +0 -0
  82. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/__init__.py +0 -0
  83. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_convert.py +0 -0
  84. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_mux.py +0 -0
  85. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/_types.py +0 -0
  86. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/run_stream.py +0 -0
  87. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/stream_channel.py +0 -0
  88. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/stream/transformers.py +0 -0
  89. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/types.py +0 -0
  90. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/typing.py +0 -0
  91. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/__init__.py +0 -0
  92. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/config.py +0 -0
  93. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/utils/runnable.py +0 -0
  94. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/version.py +0 -0
  95. {langgraph-1.2.1 → langgraph-1.2.2}/langgraph/warnings.py +0 -0
  96. {langgraph-1.2.1 → langgraph-1.2.2}/tests/__init__.py +0 -0
  97. {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_large_cases.ambr +0 -0
  98. {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_pregel.ambr +0 -0
  99. {langgraph-1.2.1 → langgraph-1.2.2}/tests/__snapshots__/test_pregel_async.ambr +0 -0
  100. {langgraph-1.2.1 → langgraph-1.2.2}/tests/agents.py +0 -0
  101. {langgraph-1.2.1 → langgraph-1.2.2}/tests/any_int.py +0 -0
  102. {langgraph-1.2.1 → langgraph-1.2.2}/tests/any_str.py +0 -0
  103. {langgraph-1.2.1 → langgraph-1.2.2}/tests/compose-postgres.yml +0 -0
  104. {langgraph-1.2.1 → langgraph-1.2.2}/tests/compose-redis.yml +0 -0
  105. {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest.py +0 -0
  106. {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest_checkpointer.py +0 -0
  107. {langgraph-1.2.1 → langgraph-1.2.2}/tests/conftest_store.py +0 -0
  108. {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/example_graph.py +0 -0
  109. {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/langgraph.json +0 -0
  110. {langgraph-1.2.1 → langgraph-1.2.2}/tests/example_app/requirements.txt +0 -0
  111. {langgraph-1.2.1 → langgraph-1.2.2}/tests/fake_chat.py +0 -0
  112. {langgraph-1.2.1 → langgraph-1.2.2}/tests/fake_tracer.py +0 -0
  113. {langgraph-1.2.1 → langgraph-1.2.2}/tests/memory_assert.py +0 -0
  114. {langgraph-1.2.1 → langgraph-1.2.2}/tests/messages.py +0 -0
  115. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_algo.py +0 -0
  116. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_channels.py +0 -0
  117. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_checkpoint_migration.py +0 -0
  118. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_config_async.py +0 -0
  119. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_benchmark.py +0 -0
  120. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_exit_mode.py +0 -0
  121. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_migration.py +0 -0
  122. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_delta_channel_supersteps_bound.py +0 -0
  123. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_deprecation.py +0 -0
  124. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_graph_callbacks.py +0 -0
  125. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interleave_arrival_order.py +0 -0
  126. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interrupt_migration.py +0 -0
  127. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_interruption.py +0 -0
  128. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_large_cases.py +0 -0
  129. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_large_cases_async.py +0 -0
  130. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_managed_values.py +0 -0
  131. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_messages_state.py +0 -0
  132. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_parent_command.py +0 -0
  133. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_parent_command_async.py +0 -0
  134. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel.py +0 -0
  135. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel_async.py +0 -0
  136. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pregel_stream_events_v3.py +0 -0
  137. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_pydantic.py +0 -0
  138. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_remote_graph.py +0 -0
  139. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_retry.py +0 -0
  140. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_runnable.py +0 -0
  141. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_runtime.py +0 -0
  142. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_serde_allowlist.py +0 -0
  143. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_state.py +0 -0
  144. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_before_builtins.py +0 -0
  145. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_data_transformers.py +0 -0
  146. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3.py +0 -0
  147. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3_e2e.py +0 -0
  148. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
  149. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_lifecycle_transformer.py +0 -0
  150. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_messages_transformer.py +0 -0
  151. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_stream_subgraph_transformer.py +0 -0
  152. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_subgraph_persistence.py +0 -0
  153. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_subgraph_persistence_async.py +0 -0
  154. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_time_travel.py +0 -0
  155. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_time_travel_async.py +0 -0
  156. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_tool_stream_handler.py +0 -0
  157. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_tracing_interops.py +0 -0
  158. {langgraph-1.2.1 → langgraph-1.2.2}/tests/test_type_checking.py +0 -0
  159. {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.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph"
7
- version = "1.2.1"
7
+ version = "1.2.2"
8
8
  description = "Building stateful, multi-actor applications with LLMs"
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -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.1"
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.0"
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