diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index a608a2a16..020c2c0a0 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -9,6 +9,7 @@ from collections.abc import Awaitable, Callable, Hashable, Mapping, Sequence from dataclasses import dataclass, is_dataclass from datetime import timedelta from functools import partial +from importlib import metadata from inspect import isclass, isfunction, ismethod, signature from types import FunctionType from types import NoneType as NoneType @@ -27,8 +28,9 @@ from typing import ( from langchain_core.runnables import Runnable, RunnableConfig from langgraph.cache.base import BaseCache -from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint +from langgraph.checkpoint.base import Checkpoint from langgraph.store.base import BaseStore +from packaging.version import Version from pydantic import BaseModel, TypeAdapter from typing_extensions import NotRequired, Required, Self, Unpack, is_typeddict @@ -120,32 +122,31 @@ def _warn_invalid_state_schema(schema: type[Any] | Any) -> None: ) -def _warn_if_checkpointer_lacks_delta_support( - channels: Mapping[str, Any], checkpointer: Checkpointer -) -> None: - """Warn when a `DeltaChannel` graph has a checkpointer that can't reconstruct it. +_MIN_DELTA_CHANNEL_API_VERSION = Version("0.9.0") - `DeltaChannel` stores only a sentinel in checkpoint blobs and rebuilds - state by replaying ancestor writes via `BaseCheckpointSaver`'s delta - history API (`get_delta_channel_history`, added in - `langgraph-checkpoint>=4.1.0`). A saver from an older `langgraph-checkpoint` - lacks that method, so reconstruction fails at runtime. Warn at compile - time so users know to upgrade. + +def _check_delta_channel_api_support(channels: Mapping[str, Any]) -> None: + """Raise if a `DeltaChannel` graph runs under an API server too old to support it. + + `DeltaChannel` reconstruction depends on server-side support added in + `langgraph-api>0.9.0`. When running under an older API server, delta + channels fail at runtime, so we raise at compile time with an upgrade + hint. The check is skipped when `langgraph-api` is not installed (e.g. + local execution), since there is no API server to be incompatible. """ - if not isinstance(checkpointer, BaseCheckpointSaver): - return if not any(isinstance(c, DeltaChannel) for c in channels.values()): return - if hasattr(checkpointer, "get_delta_channel_history"): + try: + api_version = metadata.version("langgraph-api") + except metadata.PackageNotFoundError: return - warnings.warn( - f"The configured checkpointer ({type(checkpointer).__name__}) does not " - "support `DeltaChannel` state reconstruction, which requires the " - "`get_delta_channel_history` API added in `langgraph-checkpoint>=4.1.0`. " - "State reconstruction will fail at runtime. Upgrade `langgraph-checkpoint` " - "(e.g. `pip install -U langgraph-checkpoint`) to enable `DeltaChannel` support.", - UserWarning, - stacklevel=4, + if Version(api_version) > _MIN_DELTA_CHANNEL_API_VERSION: + return + raise RuntimeError( + f"`DeltaChannel` requires `langgraph-api>0.9.0`, but the installed " + f"version is {api_version}. Upgrade `langgraph-api` " + "(e.g. `pip install -U langgraph-api`) to use graphs that rely on " + "`DeltaChannel`." ) @@ -1245,7 +1246,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): CompiledStateGraph: The compiled `StateGraph`. """ checkpointer = ensure_valid_checkpointer(checkpointer) - _warn_if_checkpointer_lacks_delta_support(self.channels, checkpointer) + _check_delta_channel_api_support(self.channels) serde_allowlist: set[tuple[str, ...]] | None = None if _serde.STRICT_MSGPACK_ENABLED: diff --git a/libs/langgraph/tests/test_delta_channel_api_support.py b/libs/langgraph/tests/test_delta_channel_api_support.py new file mode 100644 index 000000000..67ccfd969 --- /dev/null +++ b/libs/langgraph/tests/test_delta_channel_api_support.py @@ -0,0 +1,89 @@ +"""Compile-time check that `DeltaChannel` graphs run under a supported API server. + +`DeltaChannel` reconstruction relies on server-side support added in +`langgraph-api>0.9.0`. When running under an older API server, delta channels +fail at runtime, so `StateGraph.compile` raises with an upgrade hint. The check +is skipped when `langgraph-api` is not installed (local execution). +""" + +from __future__ import annotations + +import warnings +from typing import Annotated + +import pytest +from typing_extensions import TypedDict + +from langgraph.channels.delta import DeltaChannel +from langgraph.constants import START +from langgraph.graph import StateGraph +from langgraph.graph import state as state_module +from langgraph.graph.message import _messages_delta_reducer + + +class _DeltaState(TypedDict): + messages: Annotated[ + list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=50) + ] + + +class _PlainState(TypedDict): + values: Annotated[list, lambda a, b: (a or []) + (b or [])] + + +def _delta_graph() -> StateGraph: + return ( + StateGraph(_DeltaState) + .add_node("n", lambda s: {"messages": []}) + .add_edge(START, "n") + ) + + +@pytest.fixture +def api_version(monkeypatch: pytest.MonkeyPatch): + """Override the reported `langgraph-api` version (or simulate absence).""" + + def _set(version: str | None) -> None: + def fake_version(name: str) -> str: + if name == "langgraph-api": + if version is None: + raise state_module.metadata.PackageNotFoundError(name) + return version + return state_module.metadata.version(name) + + monkeypatch.setattr(state_module.metadata, "version", fake_version) + + return _set + + +def test_delta_graph_raises_for_old_api(api_version) -> None: + api_version("0.9.0") + with pytest.raises(RuntimeError, match="requires `langgraph-api>0.9.0`"): + _delta_graph().compile() + + +def test_delta_graph_ok_for_new_api(api_version) -> None: + api_version("0.9.1") + _delta_graph().compile() + + +def test_delta_graph_ok_for_much_newer_api(api_version) -> None: + api_version("0.11.2") + _delta_graph().compile() + + +def test_delta_graph_ok_when_api_not_installed(api_version) -> None: + api_version(None) + _delta_graph().compile() + + +def test_non_delta_graph_never_raises(api_version) -> None: + api_version("0.1.0") + graph = ( + StateGraph(_PlainState) + .add_node("n", lambda s: {"values": []}) + .add_edge(START, "n") + ) + with warnings.catch_warnings(): + warnings.simplefilter("error") + graph.compile() diff --git a/libs/langgraph/tests/test_delta_channel_checkpointer_support.py b/libs/langgraph/tests/test_delta_channel_checkpointer_support.py deleted file mode 100644 index 7e516d425..000000000 --- a/libs/langgraph/tests/test_delta_channel_checkpointer_support.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Compile-time warning when a checkpointer can't reconstruct `DeltaChannel`. - -`DeltaChannel` reconstructs state via `BaseCheckpointSaver`'s delta history -API (`get_delta_channel_history`, added in `langgraph-checkpoint>=4.1.0`). A -saver from an older package lacks that method, so reconstruction fails at -runtime. `StateGraph.compile` warns at compile time so users upgrade. -""" - -from __future__ import annotations - -import warnings -from typing import Annotated - -import pytest -from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.checkpoint.memory import InMemorySaver -from typing_extensions import TypedDict - -from langgraph.channels.delta import DeltaChannel -from langgraph.constants import START -from langgraph.graph import StateGraph -from langgraph.graph.message import _messages_delta_reducer - - -class _DeltaState(TypedDict): - messages: Annotated[ - list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=50) - ] - - -class _PlainState(TypedDict): - values: Annotated[list, lambda a, b: (a or []) + (b or [])] - - -def _delta_graph() -> StateGraph: - return ( - StateGraph(_DeltaState) - .add_node("n", lambda s: {"messages": []}) - .add_edge(START, "n") - ) - - -@pytest.fixture -def saver_without_delta_api(monkeypatch: pytest.MonkeyPatch) -> InMemorySaver: - """A saver whose class tree lacks the delta history API (old package).""" - for cls in (InMemorySaver, BaseCheckpointSaver): - for name in ("get_delta_channel_history", "aget_delta_channel_history"): - if name in cls.__dict__: - monkeypatch.delattr(cls, name) - saver = InMemorySaver() - assert not hasattr(saver, "get_delta_channel_history") - return saver - - -def test_delta_graph_warns_for_unsupported_checkpointer( - saver_without_delta_api: InMemorySaver, -) -> None: - with pytest.warns(UserWarning, match="does not support `DeltaChannel`"): - _delta_graph().compile(checkpointer=saver_without_delta_api) - - -def test_delta_graph_no_warning_for_supported_checkpointer() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - _delta_graph().compile(checkpointer=InMemorySaver()) - - -def test_delta_graph_no_warning_without_checkpointer() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - _delta_graph().compile() - - -def test_non_delta_graph_no_warning_with_unsupported_checkpointer( - saver_without_delta_api: InMemorySaver, -) -> None: - graph = ( - StateGraph(_PlainState) - .add_node("n", lambda s: {"values": []}) - .add_edge(START, "n") - ) - with warnings.catch_warnings(): - warnings.simplefilter("error") - graph.compile(checkpointer=saver_without_delta_api)