This commit is contained in:
Sydney Runkle
2026-04-22 17:30:22 -04:00
parent 325cb42f19
commit 2e7edb2b60
7 changed files with 88 additions and 62 deletions
@@ -4,9 +4,10 @@ import copy
import logging
import threading
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, Sequence
from typing import (
from typing import ( # noqa: UP035
Any,
Generic,
List,
Literal,
NamedTuple,
TypedDict,
@@ -20,13 +21,17 @@ from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import (
DELTA_SENTINEL,
DELTA_SENTINEL as DELTA_SENTINEL,
)
from langgraph.checkpoint.serde.types import (
ERROR,
INTERRUPT,
RESUME,
SCHEDULED,
ChannelProtocol,
DeltaChannelWrites,
)
from langgraph.checkpoint.serde.types import (
DeltaChannelWrites as DeltaChannelWrites,
)
V = TypeVar("V", int, float, str)
@@ -34,6 +39,7 @@ PendingWrite = tuple[str, str, Any]
_DELTA_RECONSTRUCTION: threading.local = threading.local()
logger = logging.getLogger(__name__)
@@ -463,7 +469,7 @@ class BaseCheckpointSaver(Generic[V]):
"""
raise NotImplementedError
def get_channel_writes(self, config: RunnableConfig, channel: str) -> list[Any]:
def get_channel_writes(self, config: RunnableConfig, channel: str) -> List[Any]: # noqa: UP006
"""Collect writes for `channel` across this checkpoint's ancestry, oldest→newest.
Scans newest→oldest and stops at the first `Overwrite` (either from
@@ -471,18 +477,18 @@ class BaseCheckpointSaver(Generic[V]):
Default implementation walks the full thread history via `list()`; savers
can override with a more efficient query (InMemorySaver and PostgresSaver
do this).
"""
try:
from langgraph.types import Overwrite # type: ignore[import-not-found]
except ImportError:
Overwrite = None # type: ignore[assignment]
`List` is used instead of `list` to avoid mypy confusing it with the
saver's own `list` method.
"""
# Guard against re-entrant calls: when list() triggers reconstruction
# which calls list() again, the inner call returns tuples with
# DELTA_SENTINEL in channel_values (which get_channel_writes ignores —
# it only reads pending_writes). This breaks the recursion safely.
if getattr(_DELTA_RECONSTRUCTION, "active", False):
return []
from langgraph.types import Overwrite # type: ignore[import-untyped]
_DELTA_RECONSTRUCTION.active = True
try:
collected: list[Any] = [] # newest first
@@ -498,7 +504,7 @@ class BaseCheckpointSaver(Generic[V]):
if ch != channel:
continue
collected.append(value)
if Overwrite is not None and isinstance(value, Overwrite):
if isinstance(value, Overwrite):
collected.reverse()
return collected
collected.reverse()
@@ -508,12 +514,9 @@ class BaseCheckpointSaver(Generic[V]):
async def aget_channel_writes(
self, config: RunnableConfig, channel: str
) -> list[Any]:
) -> List[Any]: # noqa: UP006
"""Async version of get_channel_writes."""
try:
from langgraph.types import Overwrite # type: ignore[import-not-found]
except ImportError:
Overwrite = None # type: ignore[assignment]
from langgraph.types import Overwrite # type: ignore[import-untyped]
if getattr(_DELTA_RECONSTRUCTION, "active", False):
return []
@@ -530,7 +533,7 @@ class BaseCheckpointSaver(Generic[V]):
if ch != channel:
continue
collected.append(value)
if Overwrite is not None and isinstance(value, Overwrite):
if isinstance(value, Overwrite):
collected.reverse()
return collected
collected.reverse()
@@ -14,13 +14,13 @@ from typing import Any, cast
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
DELTA_SENTINEL,
WRITES_IDX_MAP,
BaseCheckpointSaver,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
DELTA_SENTINEL,
DeltaChannelWrites,
SerializerProtocol,
get_checkpoint_id,
@@ -154,13 +154,6 @@ class InMemorySaver(
)
def get_channel_writes(self, config: RunnableConfig, channel: str) -> list[Any]:
# Lazy import: avoids a hard dep on `langgraph` at module load time
# (mirrors the Send import pattern in the serializer).
try:
from langgraph.types import Overwrite # type: ignore[import-not-found]
except ImportError:
Overwrite = None # type: ignore[assignment]
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = config["configurable"].get("checkpoint_id", "")
@@ -175,6 +168,8 @@ class InMemorySaver(
chain.append(current)
_, _, parent = entry
current = parent
from langgraph.types import Overwrite # type: ignore[import-untyped]
# Scan writes newest→oldest. Stop at the first `Overwrite` — it
# dominates all older history. Either from `snapshot_every` or from
# user code: the bound applies the same way.
@@ -190,7 +185,7 @@ class InMemorySaver(
continue
val = self.serde.loads_typed(serialized)
collected.append(val)
if Overwrite is not None and isinstance(val, Overwrite):
if isinstance(val, Overwrite):
collected.reverse()
return collected
collected.reverse()
@@ -43,6 +43,7 @@ class DeltaChannelWrites:
writes: list[Any]
Value = TypeVar("Value", covariant=True)
Update = TypeVar("Update", contravariant=True)
C = TypeVar("C")