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.
- {langgraph-1.2.0a7 → langgraph-1.2.2}/PKG-INFO +4 -4
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_config.py +3 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/delta.py +15 -3
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/state.py +133 -2
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_checkpoint.py +43 -74
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_loop.py +342 -42
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_messages.py +84 -1
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_runner.py +2 -1
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_mux.py +30 -5
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_types.py +17 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/transformers.py +16 -3
- {langgraph-1.2.0a7 → langgraph-1.2.2}/pyproject.toml +4 -4
- langgraph-1.2.2/tests/test_delta_channel_exit_mode.py +372 -0
- langgraph-1.2.2/tests/test_delta_channel_id_stability.py +140 -0
- langgraph-1.2.2/tests/test_delta_channel_supersteps_bound.py +195 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_retry.py +540 -5
- langgraph-1.2.2/tests/test_stream_before_builtins.py +296 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_messages_transformer.py +58 -1
- {langgraph-1.2.0a7 → langgraph-1.2.2}/uv.lock +26 -26
- {langgraph-1.2.0a7 → langgraph-1.2.2}/.gitignore +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/LICENSE +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/Makefile +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/README.md +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/__main__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/fanout_to_subgraph.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/pydantic_state.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/react_agent.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/sequential.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/serde_allowlist.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/wide_dict.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/bench/wide_state.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_cache.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_constants.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_fields.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_future.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_pydantic.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_queue.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_replay.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_retry.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_runnable.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_scratchpad.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_serde.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_timeout.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/_internal/_typing.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/callbacks.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/any_value.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/base.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/binop.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/ephemeral_value.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/last_value.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/named_barrier_value.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/topic.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/channels/untracked_value.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/config.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/constants.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/errors.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/func/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/_branch.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/_node.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/message.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/graph/ui.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/base.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/managed/is_last_step.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_algo.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_call.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_config.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_draw.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_executor.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_io.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_log.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_read.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_retry.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_tools.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_utils.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_validate.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/_write.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/debug.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/main.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/protocol.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/remote.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/pregel/types.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/py.typed +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/runtime.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/_convert.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/run_stream.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/stream/stream_channel.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/types.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/typing.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/config.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/utils/runnable.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/version.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/langgraph/warnings.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__init__.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_large_cases.ambr +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_pregel.ambr +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/__snapshots__/test_pregel_async.ambr +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/agents.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/any_int.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/any_str.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/compose-postgres.yml +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/compose-redis.yml +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest_checkpointer.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/conftest_store.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/example_graph.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/langgraph.json +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/example_app/requirements.txt +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/fake_chat.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/fake_tracer.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/memory_assert.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/messages.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_algo.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_channels.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_checkpoint_migration.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_config_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_delta_channel_benchmark.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_delta_channel_migration.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_deprecation.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_graph_callbacks.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interleave_arrival_order.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interrupt_migration.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_interruption.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_large_cases.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_large_cases_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_managed_values.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_messages_state.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_parent_command.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_parent_command_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pregel_stream_events_v3.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_pydantic.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_remote_graph.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_runnable.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_runtime.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_serde_allowlist.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_state.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_data_transformers.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3_e2e.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_lifecycle_transformer.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_stream_subgraph_transformer.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_subgraph_persistence.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_subgraph_persistence_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_time_travel.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_time_travel_async.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_tool_stream_handler.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_tracing_interops.py +0 -0
- {langgraph-1.2.0a7 → langgraph-1.2.2}/tests/test_type_checking.py +0 -0
- {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.
|
|
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.
|
|
26
|
-
Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.
|
|
27
|
-
Requires-Dist: langgraph-prebuilt<1.2.0,>=1.1.
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
74
|
-
|
|
75
|
-
For each `
|
|
76
|
-
`channel_values[k]
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
#
|
|
118
|
-
#
|
|
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 {})
|