langgraph 1.2.4__tar.gz → 1.2.5__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.4 → langgraph-1.2.5}/Makefile +2 -5
  2. {langgraph-1.2.4 → langgraph-1.2.5}/PKG-INFO +2 -2
  3. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_config.py +46 -13
  4. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/main.py +46 -2
  5. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/types.py +1 -1
  6. {langgraph-1.2.4 → langgraph-1.2.5}/pyproject.toml +21 -12
  7. langgraph-1.2.5/tests/test_delta_channel_update_state.py +294 -0
  8. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_large_cases.py +7 -0
  9. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_large_cases_async.py +7 -0
  10. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_pregel.py +2 -0
  11. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_pregel_async.py +111 -272
  12. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_events_v3.py +1 -1
  13. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_utils.py +154 -0
  14. {langgraph-1.2.4 → langgraph-1.2.5}/uv.lock +396 -524
  15. {langgraph-1.2.4 → langgraph-1.2.5}/.gitignore +0 -0
  16. {langgraph-1.2.4 → langgraph-1.2.5}/LICENSE +0 -0
  17. {langgraph-1.2.4 → langgraph-1.2.5}/README.md +0 -0
  18. {langgraph-1.2.4 → langgraph-1.2.5}/bench/__init__.py +0 -0
  19. {langgraph-1.2.4 → langgraph-1.2.5}/bench/__main__.py +0 -0
  20. {langgraph-1.2.4 → langgraph-1.2.5}/bench/fanout_to_subgraph.py +0 -0
  21. {langgraph-1.2.4 → langgraph-1.2.5}/bench/pydantic_state.py +0 -0
  22. {langgraph-1.2.4 → langgraph-1.2.5}/bench/react_agent.py +0 -0
  23. {langgraph-1.2.4 → langgraph-1.2.5}/bench/sequential.py +0 -0
  24. {langgraph-1.2.4 → langgraph-1.2.5}/bench/serde_allowlist.py +0 -0
  25. {langgraph-1.2.4 → langgraph-1.2.5}/bench/wide_dict.py +0 -0
  26. {langgraph-1.2.4 → langgraph-1.2.5}/bench/wide_state.py +0 -0
  27. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/__init__.py +0 -0
  28. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_cache.py +0 -0
  29. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_constants.py +0 -0
  30. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_fields.py +0 -0
  31. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_future.py +0 -0
  32. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_pydantic.py +0 -0
  33. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_queue.py +0 -0
  34. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_replay.py +0 -0
  35. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_retry.py +0 -0
  36. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_runnable.py +0 -0
  37. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_scratchpad.py +0 -0
  38. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_serde.py +0 -0
  39. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_timeout.py +0 -0
  40. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/_internal/_typing.py +0 -0
  41. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/callbacks.py +0 -0
  42. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/__init__.py +0 -0
  43. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/any_value.py +0 -0
  44. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/base.py +0 -0
  45. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/binop.py +0 -0
  46. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/delta.py +0 -0
  47. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/ephemeral_value.py +0 -0
  48. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/last_value.py +0 -0
  49. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/named_barrier_value.py +0 -0
  50. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/topic.py +0 -0
  51. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/channels/untracked_value.py +0 -0
  52. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/config.py +0 -0
  53. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/constants.py +0 -0
  54. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/errors.py +0 -0
  55. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/func/__init__.py +0 -0
  56. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/__init__.py +0 -0
  57. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/_branch.py +0 -0
  58. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/_node.py +0 -0
  59. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/message.py +0 -0
  60. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/state.py +0 -0
  61. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/graph/ui.py +0 -0
  62. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/managed/__init__.py +0 -0
  63. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/managed/base.py +0 -0
  64. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/managed/is_last_step.py +0 -0
  65. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/__init__.py +0 -0
  66. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_algo.py +0 -0
  67. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_call.py +0 -0
  68. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_checkpoint.py +0 -0
  69. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_config.py +0 -0
  70. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_draw.py +0 -0
  71. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_executor.py +0 -0
  72. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_io.py +0 -0
  73. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_log.py +0 -0
  74. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_loop.py +0 -0
  75. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_messages.py +0 -0
  76. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_read.py +0 -0
  77. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_remote_run_stream.py +0 -0
  78. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_retry.py +0 -0
  79. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_runner.py +0 -0
  80. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_tools.py +0 -0
  81. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_utils.py +0 -0
  82. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_validate.py +0 -0
  83. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/_write.py +0 -0
  84. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/debug.py +0 -0
  85. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/protocol.py +0 -0
  86. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/remote.py +0 -0
  87. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/pregel/types.py +0 -0
  88. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/py.typed +0 -0
  89. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/runtime.py +0 -0
  90. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/__init__.py +0 -0
  91. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/_convert.py +0 -0
  92. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/_mux.py +0 -0
  93. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/_types.py +0 -0
  94. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/run_stream.py +0 -0
  95. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/stream_channel.py +0 -0
  96. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/stream/transformers.py +0 -0
  97. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/typing.py +0 -0
  98. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/utils/__init__.py +0 -0
  99. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/utils/config.py +0 -0
  100. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/utils/runnable.py +0 -0
  101. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/version.py +0 -0
  102. {langgraph-1.2.4 → langgraph-1.2.5}/langgraph/warnings.py +0 -0
  103. {langgraph-1.2.4 → langgraph-1.2.5}/tests/__init__.py +0 -0
  104. {langgraph-1.2.4 → langgraph-1.2.5}/tests/__snapshots__/test_large_cases.ambr +0 -0
  105. {langgraph-1.2.4 → langgraph-1.2.5}/tests/__snapshots__/test_pregel.ambr +0 -0
  106. {langgraph-1.2.4 → langgraph-1.2.5}/tests/__snapshots__/test_pregel_async.ambr +0 -0
  107. {langgraph-1.2.4 → langgraph-1.2.5}/tests/agents.py +0 -0
  108. {langgraph-1.2.4 → langgraph-1.2.5}/tests/any_int.py +0 -0
  109. {langgraph-1.2.4 → langgraph-1.2.5}/tests/any_str.py +0 -0
  110. {langgraph-1.2.4 → langgraph-1.2.5}/tests/compose-postgres.yml +0 -0
  111. {langgraph-1.2.4 → langgraph-1.2.5}/tests/compose-redis.yml +0 -0
  112. {langgraph-1.2.4 → langgraph-1.2.5}/tests/conftest.py +0 -0
  113. {langgraph-1.2.4 → langgraph-1.2.5}/tests/conftest_checkpointer.py +0 -0
  114. {langgraph-1.2.4 → langgraph-1.2.5}/tests/conftest_store.py +0 -0
  115. {langgraph-1.2.4 → langgraph-1.2.5}/tests/example_app/example_graph.py +0 -0
  116. {langgraph-1.2.4 → langgraph-1.2.5}/tests/example_app/langgraph.json +0 -0
  117. {langgraph-1.2.4 → langgraph-1.2.5}/tests/example_app/requirements.txt +0 -0
  118. {langgraph-1.2.4 → langgraph-1.2.5}/tests/fake_chat.py +0 -0
  119. {langgraph-1.2.4 → langgraph-1.2.5}/tests/fake_tracer.py +0 -0
  120. {langgraph-1.2.4 → langgraph-1.2.5}/tests/memory_assert.py +0 -0
  121. {langgraph-1.2.4 → langgraph-1.2.5}/tests/messages.py +0 -0
  122. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_algo.py +0 -0
  123. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_channels.py +0 -0
  124. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_checkpoint_migration.py +0 -0
  125. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_config_async.py +0 -0
  126. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_delta_channel_benchmark.py +0 -0
  127. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_delta_channel_exit_mode.py +0 -0
  128. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_delta_channel_id_stability.py +0 -0
  129. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_delta_channel_migration.py +0 -0
  130. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_delta_channel_supersteps_bound.py +0 -0
  131. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_deprecation.py +0 -0
  132. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_graph_callbacks.py +0 -0
  133. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_interleave_arrival_order.py +0 -0
  134. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_interrupt_migration.py +0 -0
  135. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_interruption.py +0 -0
  136. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_managed_values.py +0 -0
  137. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_messages_state.py +0 -0
  138. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_parent_command.py +0 -0
  139. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_parent_command_async.py +0 -0
  140. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_pregel_debug.py +0 -0
  141. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_pregel_stream_events_v3.py +0 -0
  142. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_pydantic.py +0 -0
  143. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_remote_graph.py +0 -0
  144. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_remote_graph_v3.py +0 -0
  145. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_retry.py +0 -0
  146. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_runnable.py +0 -0
  147. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_runtime.py +0 -0
  148. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_serde_allowlist.py +0 -0
  149. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_state.py +0 -0
  150. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_before_builtins.py +0 -0
  151. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_data_transformers.py +0 -0
  152. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_events_v3_e2e.py +0 -0
  153. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
  154. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_lifecycle_transformer.py +0 -0
  155. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_messages_transformer.py +0 -0
  156. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_stream_subgraph_transformer.py +0 -0
  157. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_subgraph_persistence.py +0 -0
  158. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_subgraph_persistence_async.py +0 -0
  159. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_time_travel.py +0 -0
  160. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_time_travel_async.py +0 -0
  161. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_tool_stream_handler.py +0 -0
  162. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_tracing_interops.py +0 -0
  163. {langgraph-1.2.4 → langgraph-1.2.5}/tests/test_type_checking.py +0 -0
@@ -111,22 +111,19 @@ test_watch_all:
111
111
 
112
112
  # Define a variable for Python and notebook files.
113
113
  PYTHON_FILES=.
114
- MYPY_CACHE=.mypy_cache
115
114
  lint format: PYTHON_FILES=.
116
115
  lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E r'\.py$$|\.ipynb$$')
117
116
  lint_package: PYTHON_FILES=langgraph
118
117
  lint_tests: PYTHON_FILES=tests
119
- lint_tests: MYPY_CACHE=.mypy_cache_test
120
118
 
121
119
  lint lint_diff lint_package lint_tests:
122
120
  uv run ruff check .
123
121
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
124
122
  [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
125
- [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
126
- [ "$(PYTHON_FILES)" = "" ] || uv run mypy langgraph --cache-dir $(MYPY_CACHE)
123
+ [ "$(PYTHON_FILES)" = "" ] || uv run ty check langgraph
127
124
 
128
125
  type:
129
- mkdir -p $(MYPY_CACHE) && uv run mypy langgraph --cache-dir $(MYPY_CACHE)
126
+ uv run ty check langgraph
130
127
 
131
128
  format format_diff:
132
129
  uv run ruff format $(PYTHON_FILES)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph
3
- Version: 1.2.4
3
+ Version: 1.2.5
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,7 +22,7 @@ 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.0
25
+ Requires-Dist: langchain-core<2,>=1.4.7
26
26
  Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0
27
27
  Requires-Dist: langgraph-prebuilt<1.2.0,>=1.1.0
28
28
  Requires-Dist: langgraph-sdk<0.5.0,>=0.4.2
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import ChainMap
4
- from collections.abc import Sequence
4
+ from collections.abc import Mapping, Sequence
5
5
  from os import getenv
6
6
  from typing import Any, cast
7
7
 
@@ -79,6 +79,42 @@ def patch_checkpoint_map(
79
79
  return config
80
80
 
81
81
 
82
+ def _copy_mapping_value(value: Any) -> Any:
83
+ return dict(value) if isinstance(value, Mapping) else value
84
+
85
+
86
+ def _merge_metadata(
87
+ base: Mapping[str, Any] | None, new: Mapping[str, Any] | None
88
+ ) -> dict[str, Any]:
89
+ """Merge metadata without mutating inputs.
90
+
91
+ Top-level keys merge with newer values winning. `lc_versions` is the only
92
+ mapping-valued key that merges one level deeper, so independent LangChain
93
+ packages can contribute package versions without changing generic metadata
94
+ semantics. Mapping values are copied one level, so deeper nested objects
95
+ remain shared. `None` inputs are treated as empty metadata.
96
+
97
+ Mirrors `langchain_core.runnables.config._merge_metadata_dicts` so configs
98
+ that pass through LangGraph's merge keep the same `lc_versions` semantics;
99
+ keep the two in sync. Unlike lc-core, this copies mapping values one level
100
+ (intentionally more defensive — do not "simplify" back to a shared ref).
101
+ """
102
+ merged = {key: _copy_mapping_value(value) for key, value in (base or {}).items()}
103
+ for key, value in (new or {}).items():
104
+ if (
105
+ key == "lc_versions"
106
+ and isinstance(merged.get(key), Mapping)
107
+ and isinstance(value, Mapping)
108
+ ):
109
+ merged[key] = {
110
+ **cast(Mapping[str, Any], merged[key]),
111
+ **value,
112
+ }
113
+ else:
114
+ merged[key] = _copy_mapping_value(value)
115
+ return merged
116
+
117
+
82
118
  def _merge_callbacks(base: Callbacks, new: Callbacks) -> Callbacks:
83
119
  """Merge two callbacks values (None / list / BaseCallbackManager).
84
120
 
@@ -126,10 +162,10 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
126
162
  if not value:
127
163
  continue
128
164
  if key == "metadata":
129
- if base_value := base.get(key):
130
- base[key] = {**base_value, **value} # type: ignore
131
- else:
132
- base[key] = value # type: ignore[literal-required]
165
+ base[key] = _merge_metadata(
166
+ cast(Mapping[str, Any] | None, base.get(key)),
167
+ cast(Mapping[str, Any], value),
168
+ )
133
169
  elif key == "tags":
134
170
  if base_value := base.get(key):
135
171
  base[key] = [*base_value, *value] # type: ignore
@@ -327,14 +363,11 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
327
363
  empty.get("callbacks"), cast(Callbacks, v)
328
364
  )
329
365
  elif k == "metadata":
330
- # Shallow-merge metadata dicts across configs so values
331
- # bound via with_config(...) (e.g. user_id) are preserved
332
- # when later configs supply other metadata keys.
333
- existing = empty.get("metadata")
334
- empty["metadata"] = (
335
- {**cast(dict, existing), **cast(dict, v)}
336
- if existing
337
- else cast(dict, v).copy()
366
+ # Matches merge_configs: top-level metadata keys merge, and
367
+ # only `lc_versions` merges one level deeper.
368
+ empty["metadata"] = _merge_metadata(
369
+ cast(Mapping[str, Any] | None, empty.get("metadata")),
370
+ cast(Mapping[str, Any], v),
338
371
  )
339
372
  elif k == "tags":
340
373
  # Concatenate tags across configs so values bound via
@@ -108,6 +108,7 @@ 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
111
112
  from langgraph.channels.topic import Topic
112
113
  from langgraph.config import get_config
113
114
  from langgraph.constants import END
@@ -1996,11 +1997,33 @@ class Pregel(
1996
1997
  ),
1997
1998
  )
1998
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
+
1999
2021
  for task_id, task in zip(run_task_ids, run_tasks):
2000
2022
  # channel writes are saved to current checkpoint
2001
2023
  channel_writes = [w for w in task.writes if w[0] != PUSH]
2002
- if saved and channel_writes:
2024
+ if should_put_writes and channel_writes:
2003
2025
  checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
2026
+
2004
2027
  # apply to checkpoint and save
2005
2028
  apply_writes(
2006
2029
  checkpoint,
@@ -2441,10 +2464,31 @@ class Pregel(
2441
2464
  ),
2442
2465
  )
2443
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
+
2444
2488
  for task_id, task in zip(run_task_ids, run_tasks):
2445
2489
  # channel writes are saved to current checkpoint
2446
2490
  channel_writes = [w for w in task.writes if w[0] != PUSH]
2447
- if saved and channel_writes:
2491
+ if should_put_writes and channel_writes:
2448
2492
  await checkpointer.aput_writes(
2449
2493
  checkpoint_config, channel_writes, task_id
2450
2494
  )
@@ -32,7 +32,7 @@ from langgraph.warnings import LangGraphDeprecatedSinceV10, LangGraphDeprecatedS
32
32
 
33
33
  # Local TypeVars for generic stream TypedDicts.
34
34
  # We use separate TypeVars here (rather than importing from langgraph.typing)
35
- # because the typing module TypeVars have defaults that cause mypy issues
35
+ # because the typing module TypeVars have defaults that cause type checker issues
36
36
  # when used in standalone type aliases.
37
37
  StateT = TypeVar("StateT")
38
38
  OutputT = TypeVar("OutputT")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph"
7
- version = "1.2.4"
7
+ version = "1.2.5"
8
8
  description = "Building stateful, multi-actor applications with LLMs"
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -24,7 +24,7 @@ classifiers = [
24
24
  'Programming Language :: Python :: 3.13',
25
25
  ]
26
26
  dependencies = [
27
- "langchain-core>=1.4.0,<2",
27
+ "langchain-core>=1.4.7,<2",
28
28
  "langgraph-checkpoint>=4.1.0,<5.0.0",
29
29
  "langgraph-sdk>=0.4.2,<0.5.0",
30
30
  "langgraph-prebuilt>=1.1.0,<1.2.0",
@@ -69,8 +69,8 @@ test = [
69
69
  "redis",
70
70
  ]
71
71
  lint = [
72
- "mypy",
73
72
  "ruff",
73
+ "ty",
74
74
  "types-requests",
75
75
  ]
76
76
  dev = [
@@ -99,15 +99,24 @@ target-version = "py310"
99
99
  [tool.ruff.lint.flake8-tidy-imports.banned-api]
100
100
  "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
101
101
 
102
- [tool.mypy]
103
- # https://mypy.readthedocs.io/en/stable/config_file.html
104
- disallow_untyped_defs = "True"
105
- explicit_package_bases = "True"
106
- warn_no_return = "False"
107
- warn_unused_ignores = "True"
108
- warn_redundant_casts = "True"
109
- allow_redefinition = "True"
110
- disable_error_code = "typeddict-item, return-value, override, has-type"
102
+ [tool.ty.rules]
103
+ call-non-callable = "ignore"
104
+ deprecated = "ignore"
105
+ invalid-argument-type = "ignore"
106
+ invalid-assignment = "ignore"
107
+ invalid-key = "ignore"
108
+ invalid-method-override = "ignore"
109
+ invalid-paramspec = "ignore"
110
+ invalid-return-type = "ignore"
111
+ invalid-type-form = "ignore"
112
+ invalid-yield = "ignore"
113
+ missing-typed-dict-key = "ignore"
114
+ no-matching-overload = "ignore"
115
+ not-iterable = "ignore"
116
+ unused-type-ignore-comment = "ignore"
117
+ unresolved-attribute = "ignore"
118
+ unsupported-base = "ignore"
119
+ unsupported-operator = "ignore"
111
120
 
112
121
  [tool.coverage.run]
113
122
  omit = ["tests/*"]
@@ -0,0 +1,294 @@
1
+ """Tests for `update_state` / `aupdate_state` against `DeltaChannel`.
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`).
9
+
10
+ Coverage:
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)
18
+ """
19
+
20
+ from typing import Annotated, Any
21
+
22
+ import pytest
23
+ from langchain_core.messages import HumanMessage
24
+ from langgraph.checkpoint.memory import InMemorySaver
25
+ from typing_extensions import TypedDict
26
+
27
+ from langgraph.channels.delta import DeltaChannel
28
+ from langgraph.graph import START, StateGraph
29
+ from langgraph.graph.message import _messages_delta_reducer
30
+
31
+ pytestmark = pytest.mark.anyio
32
+
33
+
34
+ def _build_graph(checkpointer: InMemorySaver, *, two_nodes: bool = False) -> Any:
35
+ """Compile a minimal DeltaChannel-backed `messages` graph.
36
+
37
+ `two_nodes=True` adds a second writer node so `bulk_update_state` can route
38
+ distinct updates to different `as_node` values within a single superstep.
39
+ """
40
+ channel = DeltaChannel(_messages_delta_reducer)
41
+ State = TypedDict("State", {"messages": Annotated[list, channel]}) # type: ignore[call-overload] # noqa: UP013
42
+
43
+ def model(state: dict) -> dict:
44
+ return {}
45
+
46
+ def assistant(state: dict) -> dict:
47
+ return {}
48
+
49
+ builder = StateGraph(State)
50
+ builder.add_node("model", model)
51
+ builder.add_edge(START, "model")
52
+ if two_nodes:
53
+ builder.add_node("assistant", assistant)
54
+ builder.add_edge("model", "assistant")
55
+ builder.set_finish_point("assistant")
56
+ else:
57
+ builder.set_finish_point("model")
58
+ return builder.compile(checkpointer=checkpointer)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Fresh-thread regression (deepagents#3774)
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def test_update_state_fresh_thread_delta_channel() -> None:
67
+ saver = InMemorySaver()
68
+ graph = _build_graph(saver)
69
+ config = {"configurable": {"thread_id": "fresh-sync"}}
70
+ message = HumanMessage(content="hello", id="m1")
71
+
72
+ graph.update_state(config, {"messages": [message]}, as_node="model")
73
+
74
+ state = graph.get_state(config)
75
+ assert [m.content for m in state.values["messages"]] == ["hello"]
76
+
77
+
78
+ async def test_aupdate_state_fresh_thread_delta_channel() -> None:
79
+ saver = InMemorySaver()
80
+ graph = _build_graph(saver)
81
+ config = {"configurable": {"thread_id": "fresh-async"}}
82
+ message = HumanMessage(content="hello", id="m1")
83
+
84
+ await graph.aupdate_state(config, {"messages": [message]}, as_node="model")
85
+
86
+ state = await graph.aget_state(config)
87
+ assert [m.content for m in state.values["messages"]] == ["hello"]
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Non-fresh thread: update_state after invoke
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ 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
+ saver = InMemorySaver()
99
+ graph = _build_graph(saver)
100
+ config = {"configurable": {"thread_id": "after-invoke-sync"}}
101
+
102
+ graph.invoke({"messages": [HumanMessage(content="seed", id="m1")]}, config)
103
+ graph.update_state(
104
+ config,
105
+ {"messages": [HumanMessage(content="appended", id="m2")]},
106
+ as_node="model",
107
+ )
108
+
109
+ state = graph.get_state(config)
110
+ assert [m.content for m in state.values["messages"]] == ["seed", "appended"]
111
+ assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
112
+
113
+
114
+ async def test_aupdate_state_after_invoke_delta_channel() -> None:
115
+ saver = InMemorySaver()
116
+ graph = _build_graph(saver)
117
+ config = {"configurable": {"thread_id": "after-invoke-async"}}
118
+
119
+ await graph.ainvoke({"messages": [HumanMessage(content="seed", id="m1")]}, config)
120
+ await graph.aupdate_state(
121
+ config,
122
+ {"messages": [HumanMessage(content="appended", id="m2")]},
123
+ as_node="model",
124
+ )
125
+
126
+ state = await graph.aget_state(config)
127
+ assert [m.content for m in state.values["messages"]] == ["seed", "appended"]
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Non-fresh thread: consecutive update_state calls
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ 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
+ saver = InMemorySaver()
140
+ graph = _build_graph(saver)
141
+ config = {"configurable": {"thread_id": "consecutive-sync"}}
142
+
143
+ graph.update_state(
144
+ config,
145
+ {"messages": [HumanMessage(content="first", id="m1")]},
146
+ as_node="model",
147
+ )
148
+ graph.update_state(
149
+ config,
150
+ {"messages": [HumanMessage(content="second", id="m2")]},
151
+ as_node="model",
152
+ )
153
+
154
+ state = graph.get_state(config)
155
+ assert [m.content for m in state.values["messages"]] == ["first", "second"]
156
+ assert [m.id for m in state.values["messages"]] == ["m1", "m2"]
157
+
158
+
159
+ async def test_aconsecutive_update_states_delta_channel() -> None:
160
+ saver = InMemorySaver()
161
+ graph = _build_graph(saver)
162
+ config = {"configurable": {"thread_id": "consecutive-async"}}
163
+
164
+ await graph.aupdate_state(
165
+ config,
166
+ {"messages": [HumanMessage(content="first", id="m1")]},
167
+ as_node="model",
168
+ )
169
+ await graph.aupdate_state(
170
+ config,
171
+ {"messages": [HumanMessage(content="second", id="m2")]},
172
+ as_node="model",
173
+ )
174
+
175
+ state = await graph.aget_state(config)
176
+ assert [m.content for m in state.values["messages"]] == ["first", "second"]
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Update-by-id semantics through the update_state path
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def test_update_state_replaces_message_by_id_delta_channel() -> None:
185
+ """`_messages_delta_reducer` dedups by `id` — re-issuing a write with the
186
+ same id replaces the existing entry rather than appending. Verify this
187
+ works through the `update_state` path (not just `invoke`)."""
188
+ saver = InMemorySaver()
189
+ graph = _build_graph(saver)
190
+ config = {"configurable": {"thread_id": "update-by-id"}}
191
+
192
+ graph.invoke({"messages": [HumanMessage(content="original", id="h1")]}, config)
193
+ graph.update_state(
194
+ config,
195
+ {"messages": [HumanMessage(content="updated", id="h1")]},
196
+ as_node="model",
197
+ )
198
+
199
+ state = graph.get_state(config)
200
+ msgs = state.values["messages"]
201
+ assert len(msgs) == 1
202
+ assert msgs[0].id == "h1"
203
+ assert msgs[0].content == "updated"
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # bulk_update_state with multiple updates per superstep
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ def test_bulk_update_state_multi_task_per_superstep_delta_channel() -> None:
212
+ """`bulk_update_state` with N updates in one superstep produces N tasks
213
+ that each call `put_writes`. Guards the regression where moving
214
+ `put_writes` outside the per-task loop would persist only the last
215
+ task's writes.
216
+
217
+ Explicit `task_id`s are required to disambiguate writes belonging to
218
+ different `StateUpdate`s targeting the same node — otherwise both share
219
+ the deterministic interrupt-derived id and collide in the saver.
220
+ """
221
+ from langgraph.types import StateUpdate
222
+
223
+ saver = InMemorySaver()
224
+ graph = _build_graph(saver)
225
+ config = {"configurable": {"thread_id": "bulk-multi-task"}}
226
+
227
+ graph.bulk_update_state(
228
+ config,
229
+ [
230
+ [
231
+ StateUpdate(
232
+ values={"messages": [HumanMessage(content="first", id="m1")]},
233
+ as_node="model",
234
+ task_id="task-1",
235
+ ),
236
+ StateUpdate(
237
+ values={"messages": [HumanMessage(content="second", id="m2")]},
238
+ as_node="model",
239
+ task_id="task-2",
240
+ ),
241
+ ]
242
+ ],
243
+ )
244
+
245
+ state = graph.get_state(config)
246
+ contents = [m.content for m in state.values["messages"]]
247
+ ids = [m.id for m in state.values["messages"]]
248
+ assert sorted(contents) == ["first", "second"], (
249
+ f"both updates' writes must persist; got {contents}"
250
+ )
251
+ assert sorted(ids) == ["m1", "m2"]
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Public-API observation of the lazy-stub mechanism
256
+ # ---------------------------------------------------------------------------
257
+
258
+
259
+ 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'`."""
263
+ saver = InMemorySaver()
264
+ graph = _build_graph(saver)
265
+ config = {"configurable": {"thread_id": "history-chain"}}
266
+
267
+ graph.update_state(
268
+ config,
269
+ {"messages": [HumanMessage(content="hello", id="m1")]},
270
+ as_node="model",
271
+ )
272
+
273
+ # Newest first per `get_state_history` ordering.
274
+ history = list(graph.get_state_history(config))
275
+ assert len(history) == 2
276
+
277
+ update_snapshot, stub_snapshot = history
278
+
279
+ assert update_snapshot.metadata is not None
280
+ assert update_snapshot.metadata["source"] == "update"
281
+ assert update_snapshot.metadata["step"] == 0
282
+ 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
+ )
@@ -9,6 +9,7 @@ import pytest
9
9
  from langchain_core.messages import AIMessage, AnyMessage, ToolCall
10
10
  from langchain_core.runnables import RunnableConfig, RunnableMap, RunnablePick
11
11
  from langchain_core.tools import tool
12
+ from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
12
13
  from langgraph.checkpoint.base import BaseCheckpointSaver
13
14
  from langgraph.checkpoint.memory import InMemorySaver
14
15
  from langgraph.prebuilt.chat_agent_executor import create_react_agent
@@ -1399,6 +1400,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
1399
1400
  "ls_provider": "fakechatmodel",
1400
1401
  "ls_model_type": "chat",
1401
1402
  "ls_integration": "langchain_chat_model",
1403
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
1402
1404
  },
1403
1405
  ),
1404
1406
  (
@@ -1461,6 +1463,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
1461
1463
  "ls_provider": "fakechatmodel",
1462
1464
  "ls_model_type": "chat",
1463
1465
  "ls_integration": "langchain_chat_model",
1466
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
1464
1467
  },
1465
1468
  ),
1466
1469
  ]
@@ -1513,6 +1516,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
1513
1516
  "ls_provider": "fakechatmodel",
1514
1517
  "ls_model_type": "chat",
1515
1518
  "ls_integration": "langchain_chat_model",
1519
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
1516
1520
  },
1517
1521
  ),
1518
1522
  ]
@@ -6884,6 +6888,7 @@ def test_weather_subgraph(
6884
6888
  "ls_provider": "fakemessageslistchatmodel",
6885
6889
  "ls_model_type": "chat",
6886
6890
  "ls_integration": "langchain_chat_model",
6891
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
6887
6892
  },
6888
6893
  ),
6889
6894
  ),
@@ -6911,6 +6916,7 @@ def test_weather_subgraph(
6911
6916
  "ls_provider": "fakemessageslistchatmodel",
6912
6917
  "ls_model_type": "chat",
6913
6918
  "ls_integration": "langchain_chat_model",
6919
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
6914
6920
  },
6915
6921
  ),
6916
6922
  ),
@@ -6947,6 +6953,7 @@ def test_weather_subgraph(
6947
6953
  "ls_provider": "fakemessageslistchatmodel",
6948
6954
  "ls_model_type": "chat",
6949
6955
  "ls_integration": "langchain_chat_model",
6956
+ "lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
6950
6957
  },
6951
6958
  ),
6952
6959
  ]