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