langgraph 1.2.0a7__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.0a7 → langgraph-1.2.2}/PKG-INFO +4 -4
  2. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_config.py +3 -0
  3. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/delta.py +15 -3
  4. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/state.py +133 -2
  5. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_checkpoint.py +43 -74
  6. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_loop.py +342 -42
  7. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_messages.py +84 -1
  8. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_runner.py +2 -1
  9. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_mux.py +30 -5
  10. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_types.py +17 -0
  11. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/transformers.py +16 -3
  12. {langgraph-1.2.0a7 → langgraph-1.2.2}/pyproject.toml +4 -4
  13. langgraph-1.2.2/tests/test_delta_channel_exit_mode.py +372 -0
  14. langgraph-1.2.2/tests/test_delta_channel_id_stability.py +140 -0
  15. langgraph-1.2.2/tests/test_delta_channel_supersteps_bound.py +195 -0
  16. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_retry.py +540 -5
  17. langgraph-1.2.2/tests/test_stream_before_builtins.py +296 -0
  18. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_messages_transformer.py +58 -1
  19. {langgraph-1.2.0a7 → langgraph-1.2.2}/uv.lock +26 -26
  20. {langgraph-1.2.0a7 → langgraph-1.2.2}/.gitignore +0 -0
  21. {langgraph-1.2.0a7 → langgraph-1.2.2}/LICENSE +0 -0
  22. {langgraph-1.2.0a7 → langgraph-1.2.2}/Makefile +0 -0
  23. {langgraph-1.2.0a7 → langgraph-1.2.2}/README.md +0 -0
  24. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/__init__.py +0 -0
  25. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/__main__.py +0 -0
  26. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/fanout_to_subgraph.py +0 -0
  27. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/pydantic_state.py +0 -0
  28. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/react_agent.py +0 -0
  29. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/sequential.py +0 -0
  30. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/serde_allowlist.py +0 -0
  31. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/wide_dict.py +0 -0
  32. {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/wide_state.py +0 -0
  33. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/__init__.py +0 -0
  34. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_cache.py +0 -0
  35. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_constants.py +0 -0
  36. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_fields.py +0 -0
  37. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_future.py +0 -0
  38. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_pydantic.py +0 -0
  39. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_queue.py +0 -0
  40. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_replay.py +0 -0
  41. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_retry.py +0 -0
  42. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_runnable.py +0 -0
  43. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_scratchpad.py +0 -0
  44. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_serde.py +0 -0
  45. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_timeout.py +0 -0
  46. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_typing.py +0 -0
  47. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/callbacks.py +0 -0
  48. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/__init__.py +0 -0
  49. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/any_value.py +0 -0
  50. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/base.py +0 -0
  51. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/binop.py +0 -0
  52. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/ephemeral_value.py +0 -0
  53. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/last_value.py +0 -0
  54. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/named_barrier_value.py +0 -0
  55. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/topic.py +0 -0
  56. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/untracked_value.py +0 -0
  57. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/config.py +0 -0
  58. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/constants.py +0 -0
  59. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/errors.py +0 -0
  60. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/func/__init__.py +0 -0
  61. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/__init__.py +0 -0
  62. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/_branch.py +0 -0
  63. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/_node.py +0 -0
  64. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/message.py +0 -0
  65. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/ui.py +0 -0
  66. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/__init__.py +0 -0
  67. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/base.py +0 -0
  68. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/is_last_step.py +0 -0
  69. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/__init__.py +0 -0
  70. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_algo.py +0 -0
  71. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_call.py +0 -0
  72. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_config.py +0 -0
  73. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_draw.py +0 -0
  74. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_executor.py +0 -0
  75. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_io.py +0 -0
  76. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_log.py +0 -0
  77. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_read.py +0 -0
  78. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_retry.py +0 -0
  79. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_tools.py +0 -0
  80. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_utils.py +0 -0
  81. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_validate.py +0 -0
  82. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_write.py +0 -0
  83. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/debug.py +0 -0
  84. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/main.py +0 -0
  85. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/protocol.py +0 -0
  86. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/remote.py +0 -0
  87. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/types.py +0 -0
  88. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/py.typed +0 -0
  89. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/runtime.py +0 -0
  90. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/__init__.py +0 -0
  91. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_convert.py +0 -0
  92. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/run_stream.py +0 -0
  93. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/stream_channel.py +0 -0
  94. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/types.py +0 -0
  95. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/typing.py +0 -0
  96. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/__init__.py +0 -0
  97. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/config.py +0 -0
  98. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/runnable.py +0 -0
  99. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/version.py +0 -0
  100. {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/warnings.py +0 -0
  101. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__init__.py +0 -0
  102. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_large_cases.ambr +0 -0
  103. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_pregel.ambr +0 -0
  104. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_pregel_async.ambr +0 -0
  105. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/agents.py +0 -0
  106. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/any_int.py +0 -0
  107. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/any_str.py +0 -0
  108. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/compose-postgres.yml +0 -0
  109. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/compose-redis.yml +0 -0
  110. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest.py +0 -0
  111. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest_checkpointer.py +0 -0
  112. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest_store.py +0 -0
  113. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/example_graph.py +0 -0
  114. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/langgraph.json +0 -0
  115. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/requirements.txt +0 -0
  116. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/fake_chat.py +0 -0
  117. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/fake_tracer.py +0 -0
  118. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/memory_assert.py +0 -0
  119. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/messages.py +0 -0
  120. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_algo.py +0 -0
  121. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_channels.py +0 -0
  122. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_checkpoint_migration.py +0 -0
  123. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_config_async.py +0 -0
  124. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_delta_channel_benchmark.py +0 -0
  125. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_delta_channel_migration.py +0 -0
  126. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_deprecation.py +0 -0
  127. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_graph_callbacks.py +0 -0
  128. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interleave_arrival_order.py +0 -0
  129. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interrupt_migration.py +0 -0
  130. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interruption.py +0 -0
  131. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_large_cases.py +0 -0
  132. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_large_cases_async.py +0 -0
  133. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_managed_values.py +0 -0
  134. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_messages_state.py +0 -0
  135. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_parent_command.py +0 -0
  136. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_parent_command_async.py +0 -0
  137. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel.py +0 -0
  138. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel_async.py +0 -0
  139. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel_stream_events_v3.py +0 -0
  140. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pydantic.py +0 -0
  141. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_remote_graph.py +0 -0
  142. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_runnable.py +0 -0
  143. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_runtime.py +0 -0
  144. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_serde_allowlist.py +0 -0
  145. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_state.py +0 -0
  146. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_data_transformers.py +0 -0
  147. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3.py +0 -0
  148. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3_e2e.py +0 -0
  149. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
  150. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_lifecycle_transformer.py +0 -0
  151. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_subgraph_transformer.py +0 -0
  152. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_subgraph_persistence.py +0 -0
  153. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_subgraph_persistence_async.py +0 -0
  154. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_time_travel.py +0 -0
  155. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_time_travel_async.py +0 -0
  156. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_tool_stream_handler.py +0 -0
  157. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_tracing_interops.py +0 -0
  158. {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_type_checking.py +0 -0
  159. {langgraph-1.2.0a7 → 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.0a7
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/
@@ -22,9 +22,9 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
23
23
  Classifier: Programming Language :: Python :: Implementation :: PyPy
24
24
  Requires-Python: >=3.10
25
- Requires-Dist: langchain-core<2,>=1.4.0a2
26
- Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a4
27
- Requires-Dist: langgraph-prebuilt<1.2.0,>=1.1.0a2
25
+ Requires-Dist: langchain-core<2,>=1.4.0
26
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0
27
+ Requires-Dist: langgraph-prebuilt<1.2.0,>=1.1.0
28
28
  Requires-Dist: langgraph-sdk<0.4.0,>=0.3.0
29
29
  Requires-Dist: pydantic>=2.7.4
30
30
  Requires-Dist: xxhash>=3.5.0
@@ -29,6 +29,9 @@ from langgraph._internal._constants import (
29
29
  )
30
30
 
31
31
  DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007"))
32
+ DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT = int(
33
+ getenv("LANGGRAPH_DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT", "5000")
34
+ )
32
35
 
33
36
 
34
37
  def recast_checkpoint_ns(ns: str) -> str:
@@ -26,6 +26,15 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
26
26
  """Reducer channel that stores only a sentinel in checkpoint blobs and
27
27
  reconstructs state by replaying ancestor writes through the reducer.
28
28
 
29
+ !!! warning "Beta"
30
+
31
+ `DeltaChannel` is in beta. The API and on-disk representation may
32
+ change in future releases. Threads written with `DeltaChannel` today
33
+ are expected to remain readable, but the surrounding contract
34
+ (`BaseCheckpointSaver.get_delta_channel_history`, the
35
+ `_DeltaSnapshot` blob shape, the `counters_since_delta_snapshot`
36
+ metadata field) is not yet stable.
37
+
29
38
  The reducer receives the current accumulated value and a batch of writes
30
39
  in one call: `reducer(state, [write1, write2, ...]) -> new_state`.
31
40
 
@@ -38,9 +47,12 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
38
47
  This lets LangGraph replay checkpointed writes in larger batches than they
39
48
  were originally produced without changing reconstructed state.
40
49
 
41
- Snapshot cadence is driven by per-channel update count. `create_checkpoint`
42
- writes a full `_DeltaSnapshot` blob every `snapshot_frequency` updates to
43
- this channel, bounding replay depth.
50
+ Snapshot cadence is driven by two counters: per-channel update count and
51
+ total supersteps since last snapshot. `create_checkpoint` writes a full
52
+ `_DeltaSnapshot` blob when EITHER the update count reaches
53
+ `snapshot_frequency` OR the supersteps count reaches the system-wide
54
+ `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT` bound (default 5000), bounding
55
+ replay depth even for channels that stop receiving writes.
44
56
 
45
57
  Parameters:
46
58
  reducer: `(state, list[writes]) -> new_state`. Must be deterministic
@@ -6,7 +6,7 @@ import typing
6
6
  import warnings
7
7
  from collections import defaultdict
8
8
  from collections.abc import Awaitable, Callable, Hashable, Sequence
9
- from dataclasses import is_dataclass
9
+ from dataclasses import dataclass, is_dataclass
10
10
  from datetime import timedelta
11
11
  from functools import partial
12
12
  from inspect import isclass, isfunction, ismethod, signature
@@ -95,6 +95,17 @@ __all__ = ("StateGraph", "CompiledStateGraph")
95
95
  logger = logging.getLogger(__name__)
96
96
 
97
97
  _CHANNEL_BRANCH_TO = "branch:to:{}"
98
+ _DEFAULT_ERROR_HANDLER_NODE = "__default_error_handler__"
99
+
100
+
101
+ @dataclass(slots=True)
102
+ class _NodeDefaults:
103
+ """Default node policies applied to every node at compile time."""
104
+
105
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None
106
+ cache_policy: CachePolicy | None = None
107
+ error_handler: StateNode[Any, Any] | None = None
108
+ timeout: TimeoutPolicy | None = None
98
109
 
99
110
 
100
111
  def _warn_invalid_state_schema(schema: type[Any] | Any) -> None:
@@ -251,10 +262,77 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
251
262
  self.output_schema = cast(type[OutputT], output_schema or state_schema)
252
263
  self.context_schema = context_schema
253
264
 
265
+ self._node_defaults: _NodeDefaults = _NodeDefaults()
266
+
254
267
  self._add_schema(self.state_schema)
255
268
  self._add_schema(self.input_schema, allow_managed=False)
256
269
  self._add_schema(self.output_schema, allow_managed=False)
257
270
 
271
+ def set_node_defaults(
272
+ self,
273
+ *,
274
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
275
+ cache_policy: CachePolicy | None = None,
276
+ error_handler: StateNode[Any, ContextT] | None = None,
277
+ timeout: float | timedelta | TimeoutPolicy | None = None,
278
+ ) -> Self:
279
+ """Set default node policies that apply to every node in this graph.
280
+
281
+ Per-node values passed to `add_node` always take precedence over these
282
+ defaults. Defaults are applied at `compile()` time. Policies set here
283
+ are **not** inherited by subgraphs.
284
+
285
+ `retry_policy` and `timeout` defaults apply to **all** nodes,
286
+ including error-handler nodes. `cache_policy` and `error_handler`
287
+ defaults only apply to regular nodes -- caching error-handler results
288
+ is unsafe, and handlers must never catch themselves.
289
+
290
+ Args:
291
+ retry_policy: Default retry policy for nodes that don't specify
292
+ their own via `add_node(..., retry_policy=...)`. Also applies
293
+ to error-handler nodes.
294
+ cache_policy: Default cache policy for nodes that don't specify
295
+ their own via `add_node(..., cache_policy=...)`. Does **not**
296
+ apply to error-handler nodes.
297
+ error_handler: Default error handler invoked when any regular node
298
+ raises and does not have its own `error_handler` set via
299
+ `add_node`. The handler is **not** invoked when an
300
+ error-handler node itself raises -- handler failures fail the
301
+ run.
302
+ timeout: Default timeout policy for nodes that don't specify their
303
+ own via `add_node(..., timeout=...)`. Also applies to
304
+ error-handler nodes. Accepts a `TimeoutPolicy`, a number of
305
+ seconds (`float`), or a `timedelta`.
306
+
307
+ Returns:
308
+ Self: The builder instance, for chaining.
309
+
310
+ Example:
311
+ ```python
312
+ graph = (
313
+ StateGraph(State)
314
+ .set_node_defaults(
315
+ retry_policy=RetryPolicy(max_attempts=3),
316
+ error_handler=my_fallback_handler,
317
+ )
318
+ .add_node("a", node_a)
319
+ .add_node("b", node_b, retry_policy=custom_retry) # overrides default
320
+ .add_edge(START, "a")
321
+ .compile()
322
+ )
323
+ ```
324
+ """
325
+ defaults = self._node_defaults
326
+ if retry_policy is not None:
327
+ defaults.retry_policy = retry_policy
328
+ if cache_policy is not None:
329
+ defaults.cache_policy = cache_policy
330
+ if error_handler is not None:
331
+ defaults.error_handler = error_handler
332
+ if timeout is not None:
333
+ defaults.timeout = coerce_timeout_policy(timeout)
334
+ return self
335
+
258
336
  @property
259
337
  def _all_edges(self) -> set[tuple[str, str]]:
260
338
  return self.edges | {
@@ -1193,10 +1271,63 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
1193
1271
  key for key, val in self.channels.items() if not is_managed_value(val)
1194
1272
  ]
1195
1273
  )
1274
+ # Apply builder defaults to node specs. Per-node values always win.
1275
+ # Error-handler routing and cache_policy are only assigned to regular
1276
+ # nodes. Retry and timeout defaults also apply to error-handler nodes.
1277
+ defaults = self._node_defaults
1278
+ default_handler_name: str | None = None
1279
+ if defaults.error_handler is not None:
1280
+ if _DEFAULT_ERROR_HANDLER_NODE in self.nodes:
1281
+ raise ValueError(
1282
+ f"Auto-generated default error handler node "
1283
+ f"`{_DEFAULT_ERROR_HANDLER_NODE}` already exists."
1284
+ )
1285
+ default_handler_name = _DEFAULT_ERROR_HANDLER_NODE
1286
+ self.nodes[default_handler_name] = StateNodeSpec[Any, ContextT](
1287
+ coerce_to_runnable(
1288
+ defaults.error_handler, # type: ignore[arg-type]
1289
+ name=default_handler_name,
1290
+ trace=False,
1291
+ ),
1292
+ metadata=None,
1293
+ input_schema=self.state_schema,
1294
+ retry_policy=None,
1295
+ cache_policy=None,
1296
+ is_error_handler=True,
1297
+ )
1298
+
1299
+ # Apply builder defaults to node specs. Per-node values always win.
1300
+ for spec in self.nodes.values():
1301
+ # error_handler: regular nodes only — handlers must never
1302
+ # catch themselves or other handlers.
1303
+ if (
1304
+ not spec.is_error_handler
1305
+ and default_handler_name is not None
1306
+ and spec.error_handler_node is None
1307
+ ):
1308
+ spec.error_handler_node = default_handler_name
1309
+ # retry: all nodes — handlers should be retried on transient
1310
+ # failures just like regular nodes.
1311
+ if defaults.retry_policy is not None and spec.retry_policy is None:
1312
+ spec.retry_policy = defaults.retry_policy
1313
+ # cache: regular nodes only — caching an error-handler result
1314
+ # is unsafe because the input (failed-node state) may differ
1315
+ # across failures even when the cache key matches.
1316
+ if (
1317
+ not spec.is_error_handler
1318
+ and defaults.cache_policy is not None
1319
+ and spec.cache_policy is None
1320
+ ):
1321
+ spec.cache_policy = defaults.cache_policy
1322
+ # timeout: all nodes — a stuck handler should be cancelled the
1323
+ # same way a stuck regular node would be.
1324
+ if defaults.timeout is not None and spec.timeout is None:
1325
+ spec.timeout = defaults.timeout
1326
+
1196
1327
  node_error_handler_map = {
1197
1328
  node_name: spec.error_handler_node
1198
1329
  for node_name, spec in self.nodes.items()
1199
- if spec.error_handler_node is not None
1330
+ if not spec.is_error_handler and spec.error_handler_node is not None
1200
1331
  }
1201
1332
 
1202
1333
  compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
@@ -8,11 +8,11 @@ from langchain_core.runnables import RunnableConfig
8
8
  from langgraph.checkpoint.base import (
9
9
  BaseCheckpointSaver,
10
10
  Checkpoint,
11
- CheckpointMetadata,
12
11
  )
13
12
  from langgraph.checkpoint.base.id import uuid6
14
13
  from langgraph.checkpoint.serde.types import _DeltaSnapshot
15
14
 
15
+ from langgraph._internal._config import DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
16
16
  from langgraph._internal._typing import MISSING
17
17
  from langgraph.channels.base import BaseChannel
18
18
  from langgraph.channels.delta import DeltaChannel
@@ -34,28 +34,28 @@ def empty_checkpoint() -> Checkpoint:
34
34
  )
35
35
 
36
36
 
37
- def _should_snapshot_delta(
38
- name: str,
39
- ch: DeltaChannel,
40
- updates_since_snapshot: Mapping[str, int],
41
- *,
42
- force: bool,
43
- ) -> bool:
44
- """Decide whether `ch` should write a `_DeltaSnapshot` this step.
45
-
46
- Triggers:
47
- * `force` — always snapshot (used by `durability="exit"`).
48
- * Update-count: this channel has accumulated at least
49
- `snapshot_frequency` updates since its last snapshot. The count
50
- is supplied by the caller via `updates_since_snapshot[name]` and
51
- is reset to `0` whenever a snapshot fires.
37
+ def delta_channels_to_snapshot(
38
+ channels: Mapping[str, BaseChannel],
39
+ counters_since_delta_snapshot: Mapping[str, tuple[int, int]],
40
+ ) -> set[str]:
41
+ """Return the set of DeltaChannel names that should snapshot now.
52
42
 
53
- Version-format-independent: works for `int`, `float`, and `str`
54
- versioning schemes alike.
43
+ A channel snapshots when EITHER its accumulated update count reaches
44
+ `snapshot_frequency` OR the total supersteps since its last snapshot
45
+ reaches `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. This is a pure
46
+ predicate — no mutation.
55
47
  """
56
- if force:
57
- return True
58
- return updates_since_snapshot.get(name, 0) >= ch.snapshot_frequency
48
+ result: set[str] = set()
49
+ for name, ch in channels.items():
50
+ if not isinstance(ch, DeltaChannel) or not ch.is_available():
51
+ continue
52
+ updates, supersteps = counters_since_delta_snapshot.get(name, (0, 0))
53
+ if (
54
+ updates >= ch.snapshot_frequency
55
+ or supersteps >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
56
+ ):
57
+ result.add(name)
58
+ return result
59
59
 
60
60
 
61
61
  def create_checkpoint(
@@ -66,34 +66,19 @@ def create_checkpoint(
66
66
  id: str | None = None,
67
67
  updated_channels: set[str] | None = None,
68
68
  get_next_version: GetNextVersion | None = None,
69
- force_delta_snapshot: bool = False,
70
- updates_since_snapshot: Mapping[str, int] | None = None,
71
- new_updates_since_snapshot: dict[str, int] | None = None,
69
+ channels_to_snapshot: set[str] | None = None,
72
70
  ) -> Checkpoint:
73
- """Create a checkpoint for the given channels.
74
-
75
- For each `DeltaChannel`, a `_DeltaSnapshot(value)` blob is written into
76
- `channel_values[k]` when this channel has accumulated at least
77
- `snapshot_frequency` updates since its last snapshot (counter supplied
78
- via `updates_since_snapshot`). Otherwise the channel is omitted from
79
- `channel_values`; its `channel_versions` entry still bumps so that the
80
- saver tracks the channel and the ancestor walk can replay writes.
81
-
82
- Snapshots are eager: even if the channel had no write this step, a
83
- version bump is forced (via `get_next_version`) so `put()` includes
84
- the channel in `new_versions` and stores the blob.
85
-
86
- `force_delta_snapshot` ignores the cadence and always snapshots —
87
- used by `durability="exit"` where intermediate writes are not stored
88
- as ancestor `checkpoint_writes`.
89
-
90
- If `new_updates_since_snapshot` is provided, the function resets the
91
- counter to `0` for any channel that snapshotted this step. Counters
92
- for channels that did not snapshot are left untouched (the caller is
93
- responsible for incrementing them based on `updated_channels`).
71
+ """Build a new Checkpoint from the previous one and live channel state.
72
+
73
+ For each name in `channels_to_snapshot`, a `_DeltaSnapshot(value)` blob
74
+ is written into `channel_values[k]`. Other delta channels are omitted
75
+ from `channel_values` the ancestor walk reconstructs their state
76
+ from `checkpoint_writes`. Callers compute the set via
77
+ `delta_channels_to_snapshot(channels, counters)`; defaults to empty
78
+ (no snapshots) when not provided.
94
79
  """
95
80
  ts = datetime.now(timezone.utc).isoformat()
96
- counts = updates_since_snapshot or {}
81
+ channels_to_snapshot = channels_to_snapshot or set()
97
82
  if channels is None:
98
83
  values = checkpoint["channel_values"]
99
84
  channel_versions = checkpoint["channel_versions"]
@@ -104,25 +89,23 @@ def create_checkpoint(
104
89
  if k not in channel_versions:
105
90
  continue
106
91
  ch = channels[k]
107
- if (
108
- isinstance(ch, DeltaChannel)
109
- and ch.is_available()
110
- and _should_snapshot_delta(
111
- k,
112
- ch,
113
- counts,
114
- force=force_delta_snapshot,
115
- )
116
- ):
117
- # Eager snapshot: bump version if not already written this step
118
- # so put() includes this channel in new_versions and stores blob.
92
+ if k in channels_to_snapshot:
93
+ # In exit mode, the snapshot decision is deferred to exit
94
+ # time (intermediate steps have do_checkpoint=False). The
95
+ # channel's count may have reached snapshot_frequency over
96
+ # several supersteps, but the LAST superstep may not have
97
+ # written to this channel. In that case apply_writes()
98
+ # (in _algo.py) didn't bump this channel's version, so
99
+ # saver.put() wouldn't include it in new_versions and
100
+ # the snapshot blob would be silently dropped. The manual
101
+ # bump below closes the gap. In sync/async durability this
102
+ # branch is effectively dead code (the step that pushes
103
+ # the count to freq always writes the channel).
119
104
  if get_next_version is not None and (
120
105
  updated_channels is None or k not in updated_channels
121
106
  ):
122
107
  channel_versions[k] = get_next_version(channel_versions[k], None)
123
108
  values[k] = _DeltaSnapshot(ch.get())
124
- if new_updates_since_snapshot is not None:
125
- new_updates_since_snapshot[k] = 0
126
109
  else:
127
110
  v = ch.checkpoint()
128
111
  if v is not MISSING:
@@ -253,17 +236,3 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
253
236
  versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
254
237
  updated_channels=checkpoint.get("updated_channels", None),
255
238
  )
256
-
257
-
258
- def read_delta_updates_since_snapshot(
259
- metadata: CheckpointMetadata | None,
260
- ) -> dict[str, int]:
261
- """Read the per-channel update counter from checkpoint metadata.
262
-
263
- Returns an empty dict for missing/None metadata; the dict is
264
- `total=False` on `CheckpointMetadata`, so absence means "no prior
265
- delta-channel activity tracked."
266
- """
267
- if not metadata:
268
- return {}
269
- return dict(metadata.get("delta_updates_since_snapshot", {}) or {})