langgraph 1.2.6__tar.gz → 1.2.7__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.6 → langgraph-1.2.7}/PKG-INFO +1 -1
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/binop.py +17 -3
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/delta.py +1 -3
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_checkpoint.py +12 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_loop.py +27 -2
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/types.py +6 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/pyproject.toml +1 -1
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_channels.py +131 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_exit_mode.py +19 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_pregel.py +29 -40
- {langgraph-1.2.6 → langgraph-1.2.7}/uv.lock +264 -201
- {langgraph-1.2.6 → langgraph-1.2.7}/.gitignore +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/LICENSE +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/Makefile +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/README.md +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/__main__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/fanout_to_subgraph.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/pydantic_state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/react_agent.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/sequential.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/serde_allowlist.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/wide_dict.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/bench/wide_state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_cache.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_config.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_constants.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_fields.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_future.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_pydantic.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_queue.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_replay.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_retry.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_runnable.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_scratchpad.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_serde.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_timeout.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/_internal/_typing.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/callbacks.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/any_value.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/base.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/ephemeral_value.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/last_value.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/named_barrier_value.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/topic.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/channels/untracked_value.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/config.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/constants.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/errors.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/func/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/_branch.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/_node.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/message.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/graph/ui.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/managed/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/managed/base.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/managed/is_last_step.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_algo.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_call.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_config.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_draw.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_executor.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_io.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_log.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_messages.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_read.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_remote_run_stream.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_retry.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_runner.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_tools.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_utils.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_validate.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/_write.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/debug.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/main.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/protocol.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/remote.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/pregel/types.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/py.typed +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/runtime.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/_convert.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/_mux.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/_types.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/run_stream.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/stream_channel.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/stream/transformers.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/typing.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/utils/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/utils/config.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/utils/runnable.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/version.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/langgraph/warnings.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/__init__.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/__snapshots__/test_large_cases.ambr +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/__snapshots__/test_pregel.ambr +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/__snapshots__/test_pregel_async.ambr +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/agents.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/any_int.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/any_str.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/compose-postgres.yml +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/compose-redis.yml +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/conftest.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/conftest_checkpointer.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/conftest_store.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/example_app/example_graph.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/example_app/langgraph.json +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/example_app/requirements.txt +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/fake_chat.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/fake_tracer.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/memory_assert.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/messages.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_algo.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_checkpoint_migration.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_config_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_benchmark.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_id_stability.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_migration.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_supersteps_bound.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_delta_channel_update_state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_deprecation.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_graph_callbacks.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_interleave_arrival_order.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_interrupt_migration.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_interruption.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_large_cases.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_large_cases_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_managed_values.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_messages_state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_parent_command.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_parent_command_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_pregel_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_pregel_debug.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_pregel_stream_events_v3.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_pydantic.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_remote_graph.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_remote_graph_v3.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_retry.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_runnable.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_runtime.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_serde_allowlist.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_state.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_before_builtins.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_data_transformers.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_events_v3.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_events_v3_e2e.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_events_v3_kwarg_forwarding.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_lifecycle_transformer.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_messages_transformer.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_stream_subgraph_transformer.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_subgraph_persistence.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_subgraph_persistence_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_time_travel.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_time_travel_async.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_tool_stream_handler.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_tracing_interops.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/tests/test_type_checking.py +0 -0
- {langgraph-1.2.6 → langgraph-1.2.7}/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.7
|
|
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/
|
|
@@ -29,11 +29,25 @@ def _strip_extras(t): # type: ignore[no-untyped-def]
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _get_overwrite(value: Any) -> tuple[bool, Any]:
|
|
32
|
-
"""Inspects the given value and returns (is_overwrite, overwrite_value).
|
|
32
|
+
"""Inspects the given value and returns (is_overwrite, overwrite_value).
|
|
33
|
+
|
|
34
|
+
Recognises three forms:
|
|
35
|
+
|
|
36
|
+
* The typed `Overwrite` dataclass instance.
|
|
37
|
+
* The sentinel-keyed `{"__overwrite__": value}` dict form.
|
|
38
|
+
* The dataclass-erased `{"value": ..., "type": "__overwrite__"}` form that
|
|
39
|
+
results from JSON-serialising an `Overwrite` (e.g. an `orjson`-encoded
|
|
40
|
+
state update routed through the LangGraph API server). This keeps the
|
|
41
|
+
`Overwrite` semantics intact across JSON boundaries that strip dataclass
|
|
42
|
+
types.
|
|
43
|
+
"""
|
|
33
44
|
if isinstance(value, Overwrite):
|
|
34
45
|
return True, value.value
|
|
35
|
-
if isinstance(value, dict)
|
|
36
|
-
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
if len(value) == 1 and OVERWRITE in value:
|
|
48
|
+
return True, value[OVERWRITE]
|
|
49
|
+
if value.get("type") == OVERWRITE and "value" in value:
|
|
50
|
+
return True, value["value"]
|
|
37
51
|
return False, None
|
|
38
52
|
|
|
39
53
|
|
|
@@ -172,13 +172,11 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
|
|
|
172
172
|
overwrite_idx = i
|
|
173
173
|
if overwrite_idx is not None:
|
|
174
174
|
_, overwrite_value = _get_overwrite(values[overwrite_idx])
|
|
175
|
-
|
|
175
|
+
self.value = (
|
|
176
176
|
_copy.copy(overwrite_value)
|
|
177
177
|
if overwrite_value is not None
|
|
178
178
|
else self.typ()
|
|
179
179
|
)
|
|
180
|
-
remaining = [v for i, v in enumerate(values) if i != overwrite_idx]
|
|
181
|
-
self.value = self.reducer(base, remaining) if remaining else base
|
|
182
180
|
return True
|
|
183
181
|
base = self.typ() if self.value is MISSING else self.value
|
|
184
182
|
self.value = self.reducer(base, list(values))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import uuid
|
|
3
4
|
from collections.abc import Callable, Mapping
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from typing import Any, cast
|
|
@@ -34,6 +35,17 @@ def empty_checkpoint() -> Checkpoint:
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def exit_delta_task_id(step: int, task_id: str) -> str:
|
|
39
|
+
"""Synthetic task id for exit-mode DeltaChannel writes.
|
|
40
|
+
|
|
41
|
+
Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
|
|
42
|
+
preserves chronological order while remaining a valid RFC UUID (required by
|
|
43
|
+
Postgres `checkpoint_writes.task_id uuid` columns).
|
|
44
|
+
"""
|
|
45
|
+
parts = str(uuid.UUID(task_id)).split("-")
|
|
46
|
+
return f"{step:08d}-{parts[1]}-{parts[2]}-{parts[3]}-{parts[4]}"
|
|
47
|
+
|
|
48
|
+
|
|
37
49
|
def delta_channels_to_snapshot(
|
|
38
50
|
channels: Mapping[str, BaseChannel],
|
|
39
51
|
counters_since_delta_snapshot: Mapping[str, tuple[int, int]],
|
|
@@ -70,6 +70,7 @@ from langgraph.callbacks import (
|
|
|
70
70
|
GraphResumeEvent,
|
|
71
71
|
)
|
|
72
72
|
from langgraph.channels.base import BaseChannel
|
|
73
|
+
from langgraph.channels.binop import _get_overwrite
|
|
73
74
|
from langgraph.channels.delta import DeltaChannel
|
|
74
75
|
from langgraph.channels.untracked_value import UntrackedValue
|
|
75
76
|
from langgraph.constants import TAG_HIDDEN
|
|
@@ -102,6 +103,7 @@ from langgraph.pregel._checkpoint import (
|
|
|
102
103
|
create_checkpoint,
|
|
103
104
|
delta_channels_to_snapshot,
|
|
104
105
|
empty_checkpoint,
|
|
106
|
+
exit_delta_task_id,
|
|
105
107
|
)
|
|
106
108
|
from langgraph.pregel._executor import (
|
|
107
109
|
AsyncBackgroundExecutor,
|
|
@@ -220,6 +222,11 @@ class PregelLoop:
|
|
|
220
222
|
# under the saver's `ORDER BY task_id, idx` sorting.
|
|
221
223
|
_exit_delta_writes: list[tuple[int, str, str, Any]] | None = None
|
|
222
224
|
|
|
225
|
+
# Delta channels that saw an Overwrite since the last checkpoint. These
|
|
226
|
+
# channels must snapshot after live update applies overwrite semantics so
|
|
227
|
+
# sparse replay starts from the same post-overwrite value.
|
|
228
|
+
_delta_channels_with_overwrite: set[str]
|
|
229
|
+
|
|
223
230
|
# The checkpoint_config that points at the parent loaded at `__enter__`
|
|
224
231
|
# (or the synthetic-empty checkpoint, on first run). We capture it
|
|
225
232
|
# eagerly because every `_put_checkpoint` advances `self.checkpoint_config`
|
|
@@ -676,6 +683,11 @@ class PregelLoop:
|
|
|
676
683
|
def after_tick(self) -> None:
|
|
677
684
|
# finish superstep
|
|
678
685
|
writes = [w for t in self.tasks.values() for w in t.writes]
|
|
686
|
+
self._delta_channels_with_overwrite.update(
|
|
687
|
+
ch
|
|
688
|
+
for ch, v in writes
|
|
689
|
+
if isinstance(self.specs.get(ch), DeltaChannel) and _get_overwrite(v)[0]
|
|
690
|
+
)
|
|
679
691
|
# all tasks have finished
|
|
680
692
|
self.updated_channels = apply_writes(
|
|
681
693
|
self.checkpoint,
|
|
@@ -979,6 +991,11 @@ class PregelLoop:
|
|
|
979
991
|
manager=None,
|
|
980
992
|
updated_channels=updated_channels,
|
|
981
993
|
)
|
|
994
|
+
self._delta_channels_with_overwrite.update(
|
|
995
|
+
c
|
|
996
|
+
for c, v in input_writes
|
|
997
|
+
if isinstance(self.specs.get(c), DeltaChannel) and _get_overwrite(v)[0]
|
|
998
|
+
)
|
|
982
999
|
# apply input writes
|
|
983
1000
|
updated_channels = apply_writes(
|
|
984
1001
|
self.checkpoint,
|
|
@@ -1119,6 +1136,7 @@ class PregelLoop:
|
|
|
1119
1136
|
# create new checkpoint
|
|
1120
1137
|
channels_to_snapshot = (
|
|
1121
1138
|
delta_channels_to_snapshot(self.channels, new_counters)
|
|
1139
|
+
| self._delta_channels_with_overwrite
|
|
1122
1140
|
if do_checkpoint
|
|
1123
1141
|
else set()
|
|
1124
1142
|
)
|
|
@@ -1135,6 +1153,8 @@ class PregelLoop:
|
|
|
1135
1153
|
)
|
|
1136
1154
|
for k in channels_to_snapshot:
|
|
1137
1155
|
new_counters[k] = (0, 0)
|
|
1156
|
+
if do_checkpoint:
|
|
1157
|
+
self._delta_channels_with_overwrite.difference_update(channels_to_snapshot)
|
|
1138
1158
|
non_zero = {k: v for k, v in new_counters.items() if v != (0, 0)}
|
|
1139
1159
|
if non_zero:
|
|
1140
1160
|
self.checkpoint_metadata["counters_since_delta_snapshot"] = non_zero
|
|
@@ -1217,7 +1237,10 @@ class PregelLoop:
|
|
|
1217
1237
|
counters = dict(
|
|
1218
1238
|
self.checkpoint_metadata.get("counters_since_delta_snapshot") or {}
|
|
1219
1239
|
)
|
|
1220
|
-
channels_to_snapshot =
|
|
1240
|
+
channels_to_snapshot = (
|
|
1241
|
+
delta_channels_to_snapshot(self.channels, counters)
|
|
1242
|
+
| self._delta_channels_with_overwrite
|
|
1243
|
+
)
|
|
1221
1244
|
|
|
1222
1245
|
pending = [
|
|
1223
1246
|
(step, tid, ch, v)
|
|
@@ -1272,7 +1295,7 @@ class PregelLoop:
|
|
|
1272
1295
|
},
|
|
1273
1296
|
)
|
|
1274
1297
|
for (step, tid), entries in grouped.items():
|
|
1275
|
-
synth_tid =
|
|
1298
|
+
synth_tid = exit_delta_task_id(step, tid)
|
|
1276
1299
|
if self.checkpointer_put_writes_accepts_task_path:
|
|
1277
1300
|
fut = self.submit(
|
|
1278
1301
|
self.checkpointer_put_writes,
|
|
@@ -1661,6 +1684,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|
|
1661
1684
|
)
|
|
1662
1685
|
self._delta_write_futs = []
|
|
1663
1686
|
self._error_handler_write_futs = []
|
|
1687
|
+
self._delta_channels_with_overwrite = set()
|
|
1664
1688
|
self._exit_delta_writes = (
|
|
1665
1689
|
[] if self.durability == "exit" and self.checkpointer is not None else None
|
|
1666
1690
|
)
|
|
@@ -1918,6 +1942,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|
|
1918
1942
|
)
|
|
1919
1943
|
self._delta_write_futs = []
|
|
1920
1944
|
self._error_handler_write_futs = []
|
|
1945
|
+
self._delta_channels_with_overwrite = set()
|
|
1921
1946
|
self._exit_delta_writes = (
|
|
1922
1947
|
[] if self.durability == "exit" and self.checkpointer is not None else None
|
|
1923
1948
|
)
|
|
@@ -976,3 +976,9 @@ class Overwrite:
|
|
|
976
976
|
|
|
977
977
|
value: Any
|
|
978
978
|
"""The value to write directly to the channel, bypassing any reducer."""
|
|
979
|
+
|
|
980
|
+
type: Literal["__overwrite__"] = "__overwrite__"
|
|
981
|
+
"""Discriminator field. Lets the channel reducer recognise an `Overwrite`
|
|
982
|
+
even after its dataclass form is JSON-serialised and the typed instance
|
|
983
|
+
is lost (e.g. an `orjson`-encoded state update routed through the
|
|
984
|
+
LangGraph API server)."""
|
|
@@ -186,6 +186,50 @@ def test_delta_channel_overwrite() -> None:
|
|
|
186
186
|
assert ch.get()[0].content == "new"
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def test_overwrite_dataclass_form_survives_json_roundtrip() -> None:
|
|
190
|
+
"""`Overwrite` serialised with `orjson` collapses to a plain dict but
|
|
191
|
+
must still be recognised as an overwrite by the channel reducer.
|
|
192
|
+
|
|
193
|
+
Without the `type` discriminator the dataclass-erased shape (`{"value":
|
|
194
|
+
...}`) is indistinguishable from a literal channel value, and downstream
|
|
195
|
+
reducers raise `MESSAGE_COERCION_FAILURE` (or similar) on read.
|
|
196
|
+
"""
|
|
197
|
+
import orjson
|
|
198
|
+
|
|
199
|
+
from langgraph._internal._constants import OVERWRITE
|
|
200
|
+
from langgraph.channels.binop import _get_overwrite
|
|
201
|
+
|
|
202
|
+
ow = Overwrite(value=[HumanMessage(content="new", id="h2")])
|
|
203
|
+
erased = orjson.loads(orjson.dumps(ow, default=lambda o: o.model_dump()))
|
|
204
|
+
|
|
205
|
+
assert erased["type"] == OVERWRITE
|
|
206
|
+
is_overwrite, value = _get_overwrite(erased)
|
|
207
|
+
assert is_overwrite
|
|
208
|
+
assert isinstance(value, list)
|
|
209
|
+
assert value[0]["content"] == "new"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_overwrite_sentinel_dict_still_recognised() -> None:
|
|
213
|
+
"""The pre-existing `{"__overwrite__": value}` dict form continues to be
|
|
214
|
+
recognised. This is the canonical sentinel emitted by producers that do
|
|
215
|
+
not have an `Overwrite` dataclass available."""
|
|
216
|
+
from langgraph._internal._constants import OVERWRITE
|
|
217
|
+
from langgraph.channels.binop import _get_overwrite
|
|
218
|
+
|
|
219
|
+
is_overwrite, value = _get_overwrite({OVERWRITE: ["b"]})
|
|
220
|
+
assert is_overwrite
|
|
221
|
+
assert value == ["b"]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_overwrite_non_matching_dict_not_recognised() -> None:
|
|
225
|
+
"""Dicts that resemble the erased shape but do not carry the
|
|
226
|
+
`__overwrite__` discriminator must not be misclassified as overwrites."""
|
|
227
|
+
from langgraph.channels.binop import _get_overwrite
|
|
228
|
+
|
|
229
|
+
assert _get_overwrite({"value": ["b"]}) == (False, None)
|
|
230
|
+
assert _get_overwrite({"type": "human", "value": "hi"}) == (False, None)
|
|
231
|
+
|
|
232
|
+
|
|
189
233
|
def test_delta_channel_remove_message_and_replay() -> None:
|
|
190
234
|
"""RemoveMessage must round-trip correctly when writes are replayed."""
|
|
191
235
|
spec = DeltaChannel(_messages_delta_reducer, list)
|
|
@@ -397,6 +441,93 @@ def test_delta_channel_inmemory_saver_assembles_writes() -> None:
|
|
|
397
441
|
assert len(state.values["messages"]) == 4 # 2 human + 2 AI
|
|
398
442
|
|
|
399
443
|
|
|
444
|
+
def test_delta_channel_overwrite_superstep_snapshots() -> None:
|
|
445
|
+
def reducer(state: list[str], writes: Sequence[list[str]]) -> list[str]:
|
|
446
|
+
result = list(state)
|
|
447
|
+
for write in writes:
|
|
448
|
+
result.extend(write)
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
class State(TypedDict):
|
|
452
|
+
items: Annotated[
|
|
453
|
+
list[str], DeltaChannel(reducer, list, snapshot_frequency=1000)
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
def node_a(state: State) -> dict:
|
|
457
|
+
return {"items": ["a"]}
|
|
458
|
+
|
|
459
|
+
def node_b(state: State) -> dict:
|
|
460
|
+
return {"items": Overwrite(["b"])}
|
|
461
|
+
|
|
462
|
+
def node_c(state: State) -> dict:
|
|
463
|
+
return {"items": ["c"]}
|
|
464
|
+
|
|
465
|
+
builder = StateGraph(State)
|
|
466
|
+
builder.add_node("node_a", node_a)
|
|
467
|
+
builder.add_node("node_b", node_b)
|
|
468
|
+
builder.add_node("node_c", node_c)
|
|
469
|
+
builder.add_edge(START, "node_a")
|
|
470
|
+
builder.add_edge("node_a", "node_b")
|
|
471
|
+
builder.add_edge("node_a", "node_c")
|
|
472
|
+
|
|
473
|
+
saver = InMemorySaver()
|
|
474
|
+
graph = builder.compile(checkpointer=saver)
|
|
475
|
+
config = {"configurable": {"thread_id": "overwrite-snapshot"}}
|
|
476
|
+
|
|
477
|
+
result = graph.invoke({"items": ["START"]}, config)
|
|
478
|
+
assert result == {"items": ["b"]}
|
|
479
|
+
|
|
480
|
+
saved = saver.get_tuple(config)
|
|
481
|
+
assert saved is not None
|
|
482
|
+
snapshot = saved.checkpoint["channel_values"].get("items")
|
|
483
|
+
assert isinstance(snapshot, _DeltaSnapshot)
|
|
484
|
+
assert snapshot.value == ["b"]
|
|
485
|
+
assert saved.metadata.get("counters_since_delta_snapshot", {}).get("items") is None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def test_delta_channel_replay_after_overwrite_snapshot() -> None:
|
|
489
|
+
def reducer(state: list[str], writes: Sequence[list[str]]) -> list[str]:
|
|
490
|
+
result = list(state)
|
|
491
|
+
for write in writes:
|
|
492
|
+
result.extend(write)
|
|
493
|
+
return result
|
|
494
|
+
|
|
495
|
+
class State(TypedDict):
|
|
496
|
+
items: Annotated[
|
|
497
|
+
list[str], DeltaChannel(reducer, list, snapshot_frequency=1000)
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
calls = 0
|
|
501
|
+
|
|
502
|
+
def node(state: State) -> dict:
|
|
503
|
+
nonlocal calls
|
|
504
|
+
calls += 1
|
|
505
|
+
if calls == 1:
|
|
506
|
+
return {"items": Overwrite(["reset"])}
|
|
507
|
+
return {"items": ["after"]}
|
|
508
|
+
|
|
509
|
+
builder = StateGraph(State)
|
|
510
|
+
builder.add_node("node", node)
|
|
511
|
+
builder.add_edge(START, "node")
|
|
512
|
+
|
|
513
|
+
saver = InMemorySaver()
|
|
514
|
+
graph = builder.compile(checkpointer=saver)
|
|
515
|
+
config = {"configurable": {"thread_id": "overwrite-replay"}}
|
|
516
|
+
|
|
517
|
+
assert graph.invoke({"items": ["before"]}, config) == {"items": ["reset"]}
|
|
518
|
+
first_saved = saver.get_tuple(config)
|
|
519
|
+
assert first_saved is not None
|
|
520
|
+
assert isinstance(
|
|
521
|
+
first_saved.checkpoint["channel_values"].get("items"), _DeltaSnapshot
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
assert graph.invoke({"items": []}, config) == {"items": ["reset", "after"]}
|
|
525
|
+
second_saved = saver.get_tuple(config)
|
|
526
|
+
assert second_saved is not None
|
|
527
|
+
assert "items" not in second_saved.checkpoint["channel_values"]
|
|
528
|
+
assert graph.get_state(config).values == {"items": ["reset", "after"]}
|
|
529
|
+
|
|
530
|
+
|
|
400
531
|
# ---------------------------------------------------------------------------
|
|
401
532
|
# DeltaChannel — dict reducer
|
|
402
533
|
# ---------------------------------------------------------------------------
|
|
@@ -6,6 +6,7 @@ channel), lazy stub creation when no parent exists, and proper read-path
|
|
|
6
6
|
reconstruction via ancestor walks.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import uuid
|
|
9
10
|
from typing import Annotated, Any
|
|
10
11
|
|
|
11
12
|
import pytest
|
|
@@ -17,10 +18,28 @@ from typing_extensions import TypedDict
|
|
|
17
18
|
from langgraph.channels.delta import DeltaChannel
|
|
18
19
|
from langgraph.graph import START, StateGraph
|
|
19
20
|
from langgraph.graph.message import _messages_delta_reducer
|
|
21
|
+
from langgraph.pregel._checkpoint import exit_delta_task_id
|
|
20
22
|
|
|
21
23
|
pytestmark = pytest.mark.anyio
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def test_exit_delta_task_id_is_valid_uuid_and_ordered() -> None:
|
|
27
|
+
"""Exit-mode synthetic task ids must parse as UUID and sort by superstep."""
|
|
28
|
+
tid = "4f7226e4-0270-bf16-1ef8-fb321bef9f3d"
|
|
29
|
+
id1 = exit_delta_task_id(1, tid)
|
|
30
|
+
id7 = exit_delta_task_id(7, tid)
|
|
31
|
+
|
|
32
|
+
uuid.UUID(id1)
|
|
33
|
+
uuid.UUID(id7)
|
|
34
|
+
assert id1 < id7
|
|
35
|
+
assert id1.split("-")[0] == "00000001"
|
|
36
|
+
assert id7.split("-")[0] == "00000007"
|
|
37
|
+
assert id1.endswith("-0270-bf16-1ef8-fb321bef9f3d")
|
|
38
|
+
|
|
39
|
+
with pytest.raises(ValueError):
|
|
40
|
+
uuid.UUID(f"00000001-{tid}")
|
|
41
|
+
|
|
42
|
+
|
|
24
43
|
def _build_graph(
|
|
25
44
|
checkpointer: InMemorySaver,
|
|
26
45
|
*,
|
|
@@ -2755,23 +2755,26 @@ def test_in_one_fan_out_state_graph_waiting_edge_plus_regular(
|
|
|
2755
2755
|
"answer": "doc1,doc2,doc3,doc4",
|
|
2756
2756
|
}
|
|
2757
2757
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
[
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2758
|
+
rewrite = {"rewrite_query": {"query": "query: what is weather in sf"}}
|
|
2759
|
+
analyzer = {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}
|
|
2760
|
+
empty_qa = {"qa": {"answer": ""}}
|
|
2761
|
+
retriever_one_result = {"retriever_one": {"docs": ["doc1", "doc2"]}}
|
|
2762
|
+
retriever_two_result = {"retriever_two": {"docs": ["doc3", "doc4"]}}
|
|
2763
|
+
|
|
2764
|
+
def assert_valid_stream_order(
|
|
2765
|
+
chunks: list[dict[str, Any]], terminal: dict[str, Any]
|
|
2766
|
+
) -> None:
|
|
2767
|
+
assert chunks[0] == rewrite
|
|
2768
|
+
assert chunks[-1] == terminal
|
|
2769
|
+
middle = chunks[1:-1]
|
|
2770
|
+
assert sorted(middle, key=repr) == sorted(
|
|
2771
|
+
[empty_qa, analyzer, retriever_one_result, retriever_two_result], key=repr
|
|
2772
|
+
)
|
|
2773
|
+
assert middle.index(analyzer) < middle.index(retriever_one_result)
|
|
2774
|
+
|
|
2775
|
+
assert_valid_stream_order(
|
|
2776
|
+
[*app.stream({"query": "what is weather in sf"})],
|
|
2777
|
+
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
|
2775
2778
|
)
|
|
2776
2779
|
|
|
2777
2780
|
app_w_interrupt = workflow.compile(
|
|
@@ -2780,25 +2783,9 @@ def test_in_one_fan_out_state_graph_waiting_edge_plus_regular(
|
|
|
2780
2783
|
)
|
|
2781
2784
|
config = {"configurable": {"thread_id": "1"}}
|
|
2782
2785
|
|
|
2783
|
-
|
|
2784
|
-
c for c in app_w_interrupt.stream({"query": "what is weather in sf"}, config)
|
|
2785
|
-
|
|
2786
|
-
[
|
|
2787
|
-
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
|
2788
|
-
{"qa": {"answer": ""}},
|
|
2789
|
-
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
|
2790
|
-
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
|
2791
|
-
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
|
2792
|
-
{"__interrupt__": ()},
|
|
2793
|
-
],
|
|
2794
|
-
[
|
|
2795
|
-
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
|
2796
|
-
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
|
2797
|
-
{"qa": {"answer": ""}},
|
|
2798
|
-
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
|
2799
|
-
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
|
2800
|
-
{"__interrupt__": ()},
|
|
2801
|
-
],
|
|
2786
|
+
assert_valid_stream_order(
|
|
2787
|
+
[c for c in app_w_interrupt.stream({"query": "what is weather in sf"}, config)],
|
|
2788
|
+
{"__interrupt__": ()},
|
|
2802
2789
|
)
|
|
2803
2790
|
|
|
2804
2791
|
assert [c for c in app_w_interrupt.stream(None, config)] == [
|
|
@@ -6291,14 +6278,15 @@ def test_sync_streaming_with_functional_api() -> None:
|
|
|
6291
6278
|
rather than have all the results arrive at once after the graph has completed.
|
|
6292
6279
|
|
|
6293
6280
|
The time of arrival between the two updates corresponding to the two `slow` tasks
|
|
6294
|
-
should be
|
|
6281
|
+
should be roughly the task delay. If results are buffered until graph completion,
|
|
6282
|
+
the two updates arrive back-to-back instead.
|
|
6295
6283
|
"""
|
|
6296
6284
|
|
|
6297
6285
|
time_delay = 0.05
|
|
6298
6286
|
|
|
6299
6287
|
@task()
|
|
6300
6288
|
def slow() -> dict:
|
|
6301
|
-
time.sleep(time_delay)
|
|
6289
|
+
time.sleep(time_delay)
|
|
6302
6290
|
return {"tic": time.monotonic()}
|
|
6303
6291
|
|
|
6304
6292
|
@entrypoint()
|
|
@@ -6316,8 +6304,9 @@ def test_sync_streaming_with_functional_api() -> None:
|
|
|
6316
6304
|
|
|
6317
6305
|
assert len(arrival_times) == 2
|
|
6318
6306
|
delta = arrival_times[1] - arrival_times[0]
|
|
6319
|
-
#
|
|
6320
|
-
|
|
6307
|
+
# Allow a small amount of scheduler jitter while still verifying the chunks
|
|
6308
|
+
# arrived separately rather than back-to-back after graph completion.
|
|
6309
|
+
assert delta > time_delay * 0.8
|
|
6321
6310
|
|
|
6322
6311
|
|
|
6323
6312
|
def test_entrypoint_without_checkpointer() -> None:
|