langgraph 1.2.7__tar.gz → 1.2.9__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 (163) hide show
  1. {langgraph-1.2.7 → langgraph-1.2.9}/PKG-INFO +1 -1
  2. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_checkpoint.py +82 -1
  3. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/main.py +60 -75
  4. {langgraph-1.2.7 → langgraph-1.2.9}/pyproject.toml +1 -1
  5. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_update_state.py +85 -39
  6. {langgraph-1.2.7 → langgraph-1.2.9}/uv.lock +281 -272
  7. {langgraph-1.2.7 → langgraph-1.2.9}/.gitignore +0 -0
  8. {langgraph-1.2.7 → langgraph-1.2.9}/LICENSE +0 -0
  9. {langgraph-1.2.7 → langgraph-1.2.9}/Makefile +0 -0
  10. {langgraph-1.2.7 → langgraph-1.2.9}/README.md +0 -0
  11. {langgraph-1.2.7 → langgraph-1.2.9}/bench/__init__.py +0 -0
  12. {langgraph-1.2.7 → langgraph-1.2.9}/bench/__main__.py +0 -0
  13. {langgraph-1.2.7 → langgraph-1.2.9}/bench/fanout_to_subgraph.py +0 -0
  14. {langgraph-1.2.7 → langgraph-1.2.9}/bench/pydantic_state.py +0 -0
  15. {langgraph-1.2.7 → langgraph-1.2.9}/bench/react_agent.py +0 -0
  16. {langgraph-1.2.7 → langgraph-1.2.9}/bench/sequential.py +0 -0
  17. {langgraph-1.2.7 → langgraph-1.2.9}/bench/serde_allowlist.py +0 -0
  18. {langgraph-1.2.7 → langgraph-1.2.9}/bench/wide_dict.py +0 -0
  19. {langgraph-1.2.7 → langgraph-1.2.9}/bench/wide_state.py +0 -0
  20. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/__init__.py +0 -0
  21. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_cache.py +0 -0
  22. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_config.py +0 -0
  23. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_constants.py +0 -0
  24. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_fields.py +0 -0
  25. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_future.py +0 -0
  26. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_pydantic.py +0 -0
  27. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_queue.py +0 -0
  28. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_replay.py +0 -0
  29. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_retry.py +0 -0
  30. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_runnable.py +0 -0
  31. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_scratchpad.py +0 -0
  32. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_serde.py +0 -0
  33. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_timeout.py +0 -0
  34. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/_internal/_typing.py +0 -0
  35. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/callbacks.py +0 -0
  36. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/__init__.py +0 -0
  37. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/any_value.py +0 -0
  38. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/base.py +0 -0
  39. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/binop.py +0 -0
  40. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/delta.py +0 -0
  41. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/ephemeral_value.py +0 -0
  42. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/last_value.py +0 -0
  43. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/named_barrier_value.py +0 -0
  44. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/topic.py +0 -0
  45. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/channels/untracked_value.py +0 -0
  46. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/config.py +0 -0
  47. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/constants.py +0 -0
  48. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/errors.py +0 -0
  49. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/func/__init__.py +0 -0
  50. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/__init__.py +0 -0
  51. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/_branch.py +0 -0
  52. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/_node.py +0 -0
  53. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/message.py +0 -0
  54. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/state.py +0 -0
  55. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/graph/ui.py +0 -0
  56. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/managed/__init__.py +0 -0
  57. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/managed/base.py +0 -0
  58. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/managed/is_last_step.py +0 -0
  59. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/__init__.py +0 -0
  60. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_algo.py +0 -0
  61. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_call.py +0 -0
  62. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_config.py +0 -0
  63. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_draw.py +0 -0
  64. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_executor.py +0 -0
  65. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_io.py +0 -0
  66. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_log.py +0 -0
  67. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_loop.py +0 -0
  68. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_messages.py +0 -0
  69. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_read.py +0 -0
  70. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_remote_run_stream.py +0 -0
  71. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_retry.py +0 -0
  72. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_runner.py +0 -0
  73. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_tools.py +0 -0
  74. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_utils.py +0 -0
  75. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_validate.py +0 -0
  76. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/_write.py +0 -0
  77. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/debug.py +0 -0
  78. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/protocol.py +0 -0
  79. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/remote.py +0 -0
  80. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/pregel/types.py +0 -0
  81. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/py.typed +0 -0
  82. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/runtime.py +0 -0
  83. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/__init__.py +0 -0
  84. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/_convert.py +0 -0
  85. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/_mux.py +0 -0
  86. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/_types.py +0 -0
  87. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/run_stream.py +0 -0
  88. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/stream_channel.py +0 -0
  89. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/stream/transformers.py +0 -0
  90. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/types.py +0 -0
  91. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/typing.py +0 -0
  92. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/utils/__init__.py +0 -0
  93. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/utils/config.py +0 -0
  94. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/utils/runnable.py +0 -0
  95. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/version.py +0 -0
  96. {langgraph-1.2.7 → langgraph-1.2.9}/langgraph/warnings.py +0 -0
  97. {langgraph-1.2.7 → langgraph-1.2.9}/tests/__init__.py +0 -0
  98. {langgraph-1.2.7 → langgraph-1.2.9}/tests/__snapshots__/test_large_cases.ambr +0 -0
  99. {langgraph-1.2.7 → langgraph-1.2.9}/tests/__snapshots__/test_pregel.ambr +0 -0
  100. {langgraph-1.2.7 → langgraph-1.2.9}/tests/__snapshots__/test_pregel_async.ambr +0 -0
  101. {langgraph-1.2.7 → langgraph-1.2.9}/tests/agents.py +0 -0
  102. {langgraph-1.2.7 → langgraph-1.2.9}/tests/any_int.py +0 -0
  103. {langgraph-1.2.7 → langgraph-1.2.9}/tests/any_str.py +0 -0
  104. {langgraph-1.2.7 → langgraph-1.2.9}/tests/compose-postgres.yml +0 -0
  105. {langgraph-1.2.7 → langgraph-1.2.9}/tests/compose-redis.yml +0 -0
  106. {langgraph-1.2.7 → langgraph-1.2.9}/tests/conftest.py +0 -0
  107. {langgraph-1.2.7 → langgraph-1.2.9}/tests/conftest_checkpointer.py +0 -0
  108. {langgraph-1.2.7 → langgraph-1.2.9}/tests/conftest_store.py +0 -0
  109. {langgraph-1.2.7 → langgraph-1.2.9}/tests/example_app/example_graph.py +0 -0
  110. {langgraph-1.2.7 → langgraph-1.2.9}/tests/example_app/langgraph.json +0 -0
  111. {langgraph-1.2.7 → langgraph-1.2.9}/tests/example_app/requirements.txt +0 -0
  112. {langgraph-1.2.7 → langgraph-1.2.9}/tests/fake_chat.py +0 -0
  113. {langgraph-1.2.7 → langgraph-1.2.9}/tests/fake_tracer.py +0 -0
  114. {langgraph-1.2.7 → langgraph-1.2.9}/tests/memory_assert.py +0 -0
  115. {langgraph-1.2.7 → langgraph-1.2.9}/tests/messages.py +0 -0
  116. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_algo.py +0 -0
  117. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_channels.py +0 -0
  118. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_checkpoint_migration.py +0 -0
  119. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_config_async.py +0 -0
  120. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_benchmark.py +0 -0
  121. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_exit_mode.py +0 -0
  122. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_id_stability.py +0 -0
  123. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_migration.py +0 -0
  124. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_delta_channel_supersteps_bound.py +0 -0
  125. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_deprecation.py +0 -0
  126. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_graph_callbacks.py +0 -0
  127. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_interleave_arrival_order.py +0 -0
  128. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_interrupt_migration.py +0 -0
  129. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_interruption.py +0 -0
  130. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_large_cases.py +0 -0
  131. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_large_cases_async.py +0 -0
  132. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_managed_values.py +0 -0
  133. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_messages_state.py +0 -0
  134. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_parent_command.py +0 -0
  135. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_parent_command_async.py +0 -0
  136. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_pregel.py +0 -0
  137. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_pregel_async.py +0 -0
  138. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_pregel_debug.py +0 -0
  139. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_pregel_stream_events_v3.py +0 -0
  140. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_pydantic.py +0 -0
  141. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_remote_graph.py +0 -0
  142. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_remote_graph_v3.py +0 -0
  143. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_retry.py +0 -0
  144. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_runnable.py +0 -0
  145. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_runtime.py +0 -0
  146. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_serde_allowlist.py +0 -0
  147. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_state.py +0 -0
  148. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_before_builtins.py +0 -0
  149. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_data_transformers.py +0 -0
  150. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_events_v3.py +0 -0
  151. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_events_v3_e2e.py +0 -0
  152. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
  153. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_lifecycle_transformer.py +0 -0
  154. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_messages_transformer.py +0 -0
  155. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_stream_subgraph_transformer.py +0 -0
  156. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_subgraph_persistence.py +0 -0
  157. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_subgraph_persistence_async.py +0 -0
  158. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_time_travel.py +0 -0
  159. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_time_travel_async.py +0 -0
  160. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_tool_stream_handler.py +0 -0
  161. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_tracing_interops.py +0 -0
  162. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_type_checking.py +0 -0
  163. {langgraph-1.2.7 → langgraph-1.2.9}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph
3
- Version: 1.2.7
3
+ Version: 1.2.9
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/
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import uuid
4
- from collections.abc import Callable, Mapping
4
+ from collections.abc import Callable, Iterable, Mapping
5
5
  from datetime import datetime, timezone
6
6
  from typing import Any, cast
7
7
 
@@ -14,6 +14,7 @@ from langgraph.checkpoint.base.id import uuid6
14
14
  from langgraph.checkpoint.serde.types import _DeltaSnapshot
15
15
 
16
16
  from langgraph._internal._config import DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
17
+ from langgraph._internal._constants import PUSH
17
18
  from langgraph._internal._typing import MISSING
18
19
  from langgraph.channels.base import BaseChannel
19
20
  from langgraph.channels.delta import DeltaChannel
@@ -70,6 +71,81 @@ def delta_channels_to_snapshot(
70
71
  return result
71
72
 
72
73
 
74
+ def get_updated_channels_from_tasks(
75
+ run_tasks: Iterable[Any],
76
+ ) -> set[str]:
77
+ """Channel names written by an update_state superstep (excluding PUSH)."""
78
+ return {c for task in run_tasks for c, _ in task.writes if c != PUSH}
79
+
80
+
81
+ def get_delta_channels_from_all_channels(
82
+ channels: Mapping[str, BaseChannel],
83
+ ) -> set[str]:
84
+ """DeltaChannels to snapshot on the first update_state of a fresh thread."""
85
+ return {
86
+ k
87
+ for k, ch in channels.items()
88
+ if isinstance(ch, DeltaChannel) and ch.is_available()
89
+ }
90
+
91
+
92
+ def create_metadata_for_update_state_api(
93
+ channels: Mapping[str, BaseChannel],
94
+ updated_channels: set[str],
95
+ *,
96
+ prev_metadata: Mapping[str, Any] | None,
97
+ ) -> dict[str, tuple[int, int]]:
98
+ """Advance ``counters_since_delta_snapshot`` for update_state on a non-fresh thread.
99
+
100
+ Mirrors the per-superstep counter bump in ``_loop._put_checkpoint``.
101
+ """
102
+ prev_counters = dict(
103
+ (prev_metadata or {}).get("counters_since_delta_snapshot") or {}
104
+ )
105
+ new_counters: dict[str, tuple[int, int]] = {}
106
+ for ch_name, ch in channels.items():
107
+ if not isinstance(ch, DeltaChannel):
108
+ continue
109
+ u, s = prev_counters.get(ch_name, (0, 0))
110
+ s += 1
111
+ if ch_name in updated_channels:
112
+ u += 1
113
+ new_counters[ch_name] = (u, s)
114
+ return new_counters
115
+
116
+
117
+ def create_checkpoint_plan_for_update_state_api(
118
+ channels: Mapping[str, BaseChannel],
119
+ updated_channels: set[str],
120
+ *,
121
+ step: int,
122
+ parents: dict[str, Any],
123
+ saved_metadata: Mapping[str, Any] | None,
124
+ is_fresh_thread: bool,
125
+ ) -> tuple[set[str], dict[str, Any]]:
126
+ """Return ``(channels_to_snapshot, metadata)`` for an update_state head."""
127
+ metadata: dict[str, Any] = {
128
+ "source": "update",
129
+ "step": step,
130
+ "parents": parents,
131
+ }
132
+ if is_fresh_thread:
133
+ return get_delta_channels_from_all_channels(channels), metadata
134
+
135
+ new_counters = create_metadata_for_update_state_api(
136
+ channels,
137
+ updated_channels,
138
+ prev_metadata=saved_metadata,
139
+ )
140
+ channels_to_snapshot = delta_channels_to_snapshot(channels, new_counters)
141
+ for k in channels_to_snapshot:
142
+ new_counters[k] = (0, 0)
143
+ non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
144
+ if non_zero:
145
+ metadata["counters_since_delta_snapshot"] = non_zero
146
+ return channels_to_snapshot, metadata
147
+
148
+
73
149
  def create_checkpoint(
74
150
  checkpoint: Checkpoint,
75
151
  channels: Mapping[str, BaseChannel] | None,
@@ -102,6 +178,11 @@ def create_checkpoint(
102
178
  continue
103
179
  ch = channels[k]
104
180
  if k in channels_to_snapshot:
181
+ # Callers force a full snapshot blob here: exit mode when a
182
+ # delta channel reaches its snapshot cadence, and update_state
183
+ # on a fresh thread (no ancestor to replay writes from). The
184
+ # manual version-bump below only applies to the exit-mode case.
185
+ #
105
186
  # In exit mode, the snapshot decision is deferred to exit
106
187
  # time (intermediate steps have do_checkpoint=False). The
107
188
  # channel's count may have reached snapshot_frequency over
@@ -108,7 +108,6 @@ from langgraph.callbacks import (
108
108
  get_sync_graph_callback_manager_for_config,
109
109
  )
110
110
  from langgraph.channels.base import BaseChannel
111
- from langgraph.channels.delta import DeltaChannel
112
111
  from langgraph.channels.topic import Topic
113
112
  from langgraph.config import get_config
114
113
  from langgraph.constants import END
@@ -133,7 +132,9 @@ from langgraph.pregel._checkpoint import (
133
132
  channels_from_checkpoint,
134
133
  copy_checkpoint,
135
134
  create_checkpoint,
135
+ create_checkpoint_plan_for_update_state_api,
136
136
  empty_checkpoint,
137
+ get_updated_channels_from_tasks,
137
138
  )
138
139
  from langgraph.pregel._draw import draw_graph
139
140
  from langgraph.pregel._io import map_input, read_channels
@@ -1996,35 +1997,14 @@ class Pregel(
1996
1997
  },
1997
1998
  ),
1998
1999
  )
1999
- # save task writes
2000
- has_delta_writes = any(
2001
- isinstance(channels.get(c), DeltaChannel)
2002
- for task in run_tasks
2003
- for c, _ in task.writes
2004
- )
2005
- should_put_writes = saved is not None or has_delta_writes
2006
-
2007
- if saved is None and has_delta_writes:
2008
- # If there is no previous checkpoint, we need to create a stub checkpoint
2009
- # so the first delta writes has a parent to anchor under.
2010
- # This is the model of DeltaChannel.
2011
- stub = empty_checkpoint()
2012
- checkpoint_config = checkpointer.put(
2013
- patch_configurable(
2014
- checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
2015
- ),
2016
- stub,
2017
- {"source": "update", "step": -1, "parents": {}},
2018
- {},
2019
- )
2020
-
2021
- for task_id, task in zip(run_task_ids, run_tasks):
2022
- # channel writes are saved to current checkpoint
2023
- channel_writes = [w for w in task.writes if w[0] != PUSH]
2024
- if should_put_writes and channel_writes:
2025
- checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
2026
-
2027
- # apply to checkpoint and save
2000
+ updated_channels = get_updated_channels_from_tasks(run_tasks)
2001
+ if saved is not None:
2002
+ for task_id, task in zip(run_task_ids, run_tasks):
2003
+ channel_writes = [w for w in task.writes if w[0] != PUSH]
2004
+ if channel_writes:
2005
+ checkpointer.put_writes(
2006
+ checkpoint_config, channel_writes, task_id
2007
+ )
2028
2008
  apply_writes(
2029
2009
  checkpoint,
2030
2010
  channels,
@@ -2032,21 +2012,35 @@ class Pregel(
2032
2012
  checkpointer.get_next_version,
2033
2013
  self.trigger_to_nodes,
2034
2014
  )
2035
- checkpoint = create_checkpoint(checkpoint, channels, step + 1)
2015
+ channels_to_snapshot, checkpoint_metadata = (
2016
+ create_checkpoint_plan_for_update_state_api(
2017
+ channels,
2018
+ updated_channels,
2019
+ step=step + 1,
2020
+ parents=saved.metadata.get("parents", {}) if saved else {},
2021
+ saved_metadata=saved.metadata if saved else None,
2022
+ is_fresh_thread=saved is None,
2023
+ )
2024
+ )
2025
+ checkpoint = create_checkpoint(
2026
+ checkpoint,
2027
+ channels,
2028
+ step + 1,
2029
+ updated_channels=updated_channels if channels_to_snapshot else None,
2030
+ get_next_version=checkpointer.get_next_version
2031
+ if channels_to_snapshot
2032
+ else None,
2033
+ channels_to_snapshot=channels_to_snapshot,
2034
+ )
2036
2035
  next_config = checkpointer.put(
2037
2036
  checkpoint_config,
2038
2037
  checkpoint,
2039
- {
2040
- "source": "update",
2041
- "step": step + 1,
2042
- "parents": saved.metadata.get("parents", {}) if saved else {},
2043
- },
2038
+ checkpoint_metadata,
2044
2039
  get_new_channel_versions(
2045
2040
  checkpoint_previous_versions, checkpoint["channel_versions"]
2046
2041
  ),
2047
2042
  )
2048
2043
  for task_id, task in zip(run_task_ids, run_tasks):
2049
- # save push writes
2050
2044
  if push_writes := [w for w in task.writes if w[0] == PUSH]:
2051
2045
  checkpointer.put_writes(next_config, push_writes, task_id)
2052
2046
 
@@ -2463,36 +2457,14 @@ class Pregel(
2463
2457
  },
2464
2458
  ),
2465
2459
  )
2466
- # save task writes
2467
- has_delta_writes = any(
2468
- isinstance(channels.get(c), DeltaChannel)
2469
- for task in run_tasks
2470
- for c, _ in task.writes
2471
- )
2472
- should_put_writes = saved is not None or has_delta_writes
2473
-
2474
- if saved is None and has_delta_writes:
2475
- # If there is no previous checkpoint, we need to create a stub checkpoint
2476
- # so the first delta writes has a parent to anchor under.
2477
- # This is the model of DeltaChannel.
2478
- stub = empty_checkpoint()
2479
- checkpoint_config = await checkpointer.aput(
2480
- patch_configurable(
2481
- checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
2482
- ),
2483
- stub,
2484
- {"source": "update", "step": -1, "parents": {}},
2485
- {},
2486
- )
2487
-
2488
- for task_id, task in zip(run_task_ids, run_tasks):
2489
- # channel writes are saved to current checkpoint
2490
- channel_writes = [w for w in task.writes if w[0] != PUSH]
2491
- if should_put_writes and channel_writes:
2492
- await checkpointer.aput_writes(
2493
- checkpoint_config, channel_writes, task_id
2494
- )
2495
- # apply to checkpoint and save
2460
+ updated_channels = get_updated_channels_from_tasks(run_tasks)
2461
+ if saved is not None:
2462
+ for task_id, task in zip(run_task_ids, run_tasks):
2463
+ channel_writes = [w for w in task.writes if w[0] != PUSH]
2464
+ if channel_writes:
2465
+ await checkpointer.aput_writes(
2466
+ checkpoint_config, channel_writes, task_id
2467
+ )
2496
2468
  apply_writes(
2497
2469
  checkpoint,
2498
2470
  channels,
@@ -2500,22 +2472,35 @@ class Pregel(
2500
2472
  checkpointer.get_next_version,
2501
2473
  self.trigger_to_nodes,
2502
2474
  )
2503
- checkpoint = create_checkpoint(checkpoint, channels, step + 1)
2504
- # save checkpoint, after applying writes
2475
+ channels_to_snapshot, checkpoint_metadata = (
2476
+ create_checkpoint_plan_for_update_state_api(
2477
+ channels,
2478
+ updated_channels,
2479
+ step=step + 1,
2480
+ parents=saved.metadata.get("parents", {}) if saved else {},
2481
+ saved_metadata=saved.metadata if saved else None,
2482
+ is_fresh_thread=saved is None,
2483
+ )
2484
+ )
2485
+ checkpoint = create_checkpoint(
2486
+ checkpoint,
2487
+ channels,
2488
+ step + 1,
2489
+ updated_channels=updated_channels if channels_to_snapshot else None,
2490
+ get_next_version=checkpointer.get_next_version
2491
+ if channels_to_snapshot
2492
+ else None,
2493
+ channels_to_snapshot=channels_to_snapshot,
2494
+ )
2505
2495
  next_config = await checkpointer.aput(
2506
2496
  checkpoint_config,
2507
2497
  checkpoint,
2508
- {
2509
- "source": "update",
2510
- "step": step + 1,
2511
- "parents": saved.metadata.get("parents", {}) if saved else {},
2512
- },
2498
+ checkpoint_metadata,
2513
2499
  get_new_channel_versions(
2514
2500
  checkpoint_previous_versions, checkpoint["channel_versions"]
2515
2501
  ),
2516
2502
  )
2517
2503
  for task_id, task in zip(run_task_ids, run_tasks):
2518
- # save push writes
2519
2504
  if push_writes := [w for w in task.writes if w[0] == PUSH]:
2520
2505
  await checkpointer.aput_writes(next_config, push_writes, task_id)
2521
2506
  return patch_checkpoint_map(next_config, saved.metadata if saved else None)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph"
7
- version = "1.2.7"
7
+ version = "1.2.9"
8
8
  description = "Building stateful, multi-actor applications with LLMs"
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -1,20 +1,19 @@
1
1
  """Tests for `update_state` / `aupdate_state` against `DeltaChannel`.
2
2
 
3
- Originally a regression suite for deepagents#3774 — `update_state` on a *fresh*
4
- thread silently dropped the first write to a `DeltaChannel`-backed channel
5
- because channel writes were only persisted when a previous checkpoint existed.
6
- Fixed by lazily persisting an empty stub checkpoint on a fresh thread so the
7
- first write has a parent to anchor under (mirrors the exit-mode lazy-stub
8
- pattern in `_loop._put_exit_delta_writes`).
3
+ Regression suite for deepagents#3774 and Postgres read-path compatibility.
4
+
5
+ Fresh-thread ``update_state`` force-snapshots DeltaChannels (1.2.8). Non-fresh
6
+ ``update_state`` persists ``checkpoint_writes`` on the parent, advances
7
+ ``counters_since_delta_snapshot`` on the new head, and snapshots when a
8
+ channel reaches ``snapshot_frequency`` (mirroring normal run cadence).
9
9
 
10
10
  Coverage:
11
11
 
12
- * fresh-thread regression: single `update_state` writes a message and reads back
13
- * non-fresh thread: `update_state` after `invoke`, after another `update_state`,
14
- and `bulk_update_state` with multiple per-superstep updates
15
- * update-by-id end-to-end via `update_state` (DeltaChannel reducer semantics)
16
- * state-history chain shape on a fresh thread (lazy stub + update checkpoint
17
- with correct parent linking)
12
+ * fresh-thread regression: single ``update_state`` writes a message and reads back
13
+ * non-fresh thread: ``update_state`` after ``invoke``, after another ``update_state``,
14
+ and ``bulk_update_state`` with multiple per-superstep updates
15
+ * update-by-id end-to-end via ``update_state`` (DeltaChannel reducer semantics)
16
+ * fresh-thread head is snapshotted; non-fresh heads carry delta replay counters
18
17
  """
19
18
 
20
19
  from typing import Annotated, Any
@@ -22,6 +21,7 @@ from typing import Annotated, Any
22
21
  import pytest
23
22
  from langchain_core.messages import HumanMessage
24
23
  from langgraph.checkpoint.memory import InMemorySaver
24
+ from langgraph.checkpoint.serde.types import _DeltaSnapshot
25
25
  from typing_extensions import TypedDict
26
26
 
27
27
  from langgraph.channels.delta import DeltaChannel
@@ -31,13 +31,20 @@ from langgraph.graph.message import _messages_delta_reducer
31
31
  pytestmark = pytest.mark.anyio
32
32
 
33
33
 
34
- def _build_graph(checkpointer: InMemorySaver, *, two_nodes: bool = False) -> Any:
34
+ def _build_graph(
35
+ checkpointer: InMemorySaver,
36
+ *,
37
+ two_nodes: bool = False,
38
+ snapshot_frequency: int = 1000,
39
+ ) -> Any:
35
40
  """Compile a minimal DeltaChannel-backed `messages` graph.
36
41
 
37
42
  `two_nodes=True` adds a second writer node so `bulk_update_state` can route
38
43
  distinct updates to different `as_node` values within a single superstep.
39
44
  """
40
- channel = DeltaChannel(_messages_delta_reducer)
45
+ channel = DeltaChannel(
46
+ _messages_delta_reducer, snapshot_frequency=snapshot_frequency
47
+ )
41
48
  State = TypedDict("State", {"messages": Annotated[list, channel]}) # type: ignore[call-overload] # noqa: UP013
42
49
 
43
50
  def model(state: dict) -> dict:
@@ -87,14 +94,30 @@ async def test_aupdate_state_fresh_thread_delta_channel() -> None:
87
94
  assert [m.content for m in state.values["messages"]] == ["hello"]
88
95
 
89
96
 
97
+ def test_fresh_update_state_head_snapshots_delta_channel() -> None:
98
+ saver = InMemorySaver()
99
+ graph = _build_graph(saver)
100
+ config = {"configurable": {"thread_id": "fresh-head-snapshot"}}
101
+
102
+ graph.update_state(
103
+ config,
104
+ {"messages": [HumanMessage(content="hello", id="m1")]},
105
+ as_node="model",
106
+ )
107
+
108
+ head = saver.get_tuple(config)
109
+ assert head is not None
110
+ assert isinstance(head.checkpoint["channel_values"].get("messages"), _DeltaSnapshot)
111
+ assert head.metadata is not None
112
+ assert "counters_since_delta_snapshot" not in head.metadata
113
+
114
+
90
115
  # ---------------------------------------------------------------------------
91
116
  # Non-fresh thread: update_state after invoke
92
117
  # ---------------------------------------------------------------------------
93
118
 
94
119
 
95
120
  def test_update_state_after_invoke_delta_channel() -> None:
96
- """The non-fresh-thread path was already working before the fix; pin it
97
- down so the lazy-stub change for fresh threads doesn't regress it."""
98
121
  saver = InMemorySaver()
99
122
  graph = _build_graph(saver)
100
123
  config = {"configurable": {"thread_id": "after-invoke-sync"}}
@@ -110,6 +133,12 @@ def test_update_state_after_invoke_delta_channel() -> None:
110
133
  assert [m.content for m in state.values["messages"]] == ["seed", "appended"]
111
134
  assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
112
135
 
136
+ head = saver.get_tuple(config)
137
+ assert head is not None
138
+ assert "messages" not in head.checkpoint["channel_values"]
139
+ assert head.metadata is not None
140
+ assert head.metadata["counters_since_delta_snapshot"]["messages"] == [2, 4]
141
+
113
142
 
114
143
  async def test_aupdate_state_after_invoke_delta_channel() -> None:
115
144
  saver = InMemorySaver()
@@ -133,9 +162,6 @@ async def test_aupdate_state_after_invoke_delta_channel() -> None:
133
162
 
134
163
 
135
164
  def test_consecutive_update_states_delta_channel() -> None:
136
- """First update_state lazily persists a stub; the second sees a real
137
- parent (`saved is not None`) and takes the original write path. Both
138
- messages must round-trip in chronological order."""
139
165
  saver = InMemorySaver()
140
166
  graph = _build_graph(saver)
141
167
  config = {"configurable": {"thread_id": "consecutive-sync"}}
@@ -155,6 +181,39 @@ def test_consecutive_update_states_delta_channel() -> None:
155
181
  assert [m.content for m in state.values["messages"]] == ["first", "second"]
156
182
  assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
157
183
 
184
+ head = saver.get_tuple(config)
185
+ assert head is not None
186
+ assert "messages" not in head.checkpoint["channel_values"]
187
+ assert head.metadata is not None
188
+ assert head.metadata["counters_since_delta_snapshot"]["messages"] == [1, 1]
189
+
190
+
191
+ def test_update_state_snapshots_at_frequency() -> None:
192
+ """Non-fresh update_state snapshots when counters reach snapshot_frequency."""
193
+ saver = InMemorySaver()
194
+ graph = _build_graph(saver, snapshot_frequency=1)
195
+ config = {"configurable": {"thread_id": "snapshot-at-freq"}}
196
+
197
+ graph.update_state(
198
+ config,
199
+ {"messages": [HumanMessage(content="first", id="m1")]},
200
+ as_node="model",
201
+ )
202
+ graph.update_state(
203
+ config,
204
+ {"messages": [HumanMessage(content="second", id="m2")]},
205
+ as_node="model",
206
+ )
207
+
208
+ state = graph.get_state(config)
209
+ assert [m.content for m in state.values["messages"]] == ["first", "second"]
210
+
211
+ head = saver.get_tuple(config)
212
+ assert head is not None
213
+ assert isinstance(head.checkpoint["channel_values"].get("messages"), _DeltaSnapshot)
214
+ assert head.metadata is not None
215
+ assert "counters_since_delta_snapshot" not in head.metadata
216
+
158
217
 
159
218
  async def test_aconsecutive_update_states_delta_channel() -> None:
160
219
  saver = InMemorySaver()
@@ -252,14 +311,14 @@ def test_bulk_update_state_multi_task_per_superstep_delta_channel() -> None:
252
311
 
253
312
 
254
313
  # ---------------------------------------------------------------------------
255
- # Public-API observation of the lazy-stub mechanism
314
+ # Public-API observation of fresh-thread checkpoint shape
256
315
  # ---------------------------------------------------------------------------
257
316
 
258
317
 
259
318
  def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
260
- """A fresh-thread `update_state` should produce two checkpoints visible
261
- via `get_state_history`: a stub (step=-1, no parent) and the update
262
- (step=0, parent=stub). Both attributed `source='update'`."""
319
+ """A fresh-thread `update_state` should produce a single self-contained
320
+ checkpoint visible via `get_state_history`: step=0, `source='update'`,
321
+ no parent, with the DeltaChannel value snapshotted inline."""
263
322
  saver = InMemorySaver()
264
323
  graph = _build_graph(saver)
265
324
  config = {"configurable": {"thread_id": "history-chain"}}
@@ -270,25 +329,12 @@ def test_state_history_chain_after_fresh_update_state_delta_channel() -> None:
270
329
  as_node="model",
271
330
  )
272
331
 
273
- # Newest first per `get_state_history` ordering.
274
332
  history = list(graph.get_state_history(config))
275
- assert len(history) == 2
276
-
277
- update_snapshot, stub_snapshot = history
333
+ assert len(history) == 1
278
334
 
335
+ (update_snapshot,) = history
279
336
  assert update_snapshot.metadata is not None
280
337
  assert update_snapshot.metadata["source"] == "update"
281
338
  assert update_snapshot.metadata["step"] == 0
339
+ assert update_snapshot.parent_config is None
282
340
  assert [m.content for m in update_snapshot.values["messages"]] == ["hello"]
283
-
284
- assert stub_snapshot.metadata is not None
285
- assert stub_snapshot.metadata["source"] == "update"
286
- assert stub_snapshot.metadata["step"] == -1
287
- assert stub_snapshot.parent_config is None
288
-
289
- # The update checkpoint's parent is the stub.
290
- assert update_snapshot.parent_config is not None
291
- assert (
292
- update_snapshot.parent_config["configurable"]["checkpoint_id"]
293
- == stub_snapshot.config["configurable"]["checkpoint_id"]
294
- )