refactor(delta-channel): drop snapshot_every and saver Overwrite terminator

snapshot_every was a knob for bounding reconstruction cost on deep threads.
Benchmarks (notes/add_messages_replay_problem.md + scratch work on
sr/add-messages-replay-bench) showed the add_messages fast-path
(optimize/add-messages-fast-path) closes the quadratic replay cost for
threads under ~1000 turns, where the crossover to snapshots makes sense.
For deeper threads we'll ship a first-class compaction primitive instead.

Removals:

* DeltaChannel: snapshot_every ctor param, _writes_since_snapshot counter,
  should_snapshot() / snapshot_write() methods, counter threading through
  _apply_write / update / from_checkpoint / copy.
* Pregel loop: post-checkpoint snapshot-injection block and
  SNAPSHOT_TASK_ID import + constant.
* Checkpoint base: _overwrite_types() helper and the ancestor-walk
  short-circuit on user-emitted Overwrite in sync + async
  get_channel_writes.
* InMemory + Postgres savers: same walk-terminator shortcut. The
  pre-delta blob terminator (seed-from-ancestor-blob) stays — it's
  required for migration correctness, not a snapshot optimization.
* Tests for all of the above.

Preserved:

* Channel-level Overwrite semantics in DeltaChannel / BinOpAggregate:
  Overwrite still resets the value at reducer level; same-super-step
  dedup and InvalidUpdateError on multiple Overwrites still enforced.
* Pre-delta migration seeding.
This commit is contained in:
Sydney Runkle
2026-04-23 09:54:12 -04:00
parent d120f127ca
commit 31ef0e942a
9 changed files with 65 additions and 418 deletions
@@ -13,7 +13,6 @@ from langgraph.checkpoint.base import (
BaseCheckpointSaver,
ChannelVersions,
DeltaChannelWrites,
_overwrite_types,
get_checkpoint_id,
)
from langgraph.checkpoint.serde.types import TASKS
@@ -238,19 +237,14 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
(`AsyncPostgresSaver`); both paths run the queries themselves and
feed the rows here.
Walk is newest → oldest from the target's parent. Stops at the first
terminator:
* a user-emitted `Overwrite` in `checkpoint_writes` — replaces
prior history;
* a non-sentinel blob in `checkpoint_blobs` — a pre-delta snapshot;
bound as `DeltaChannelWrites.seed` so replay starts from it.
Walk is newest → oldest from the target's parent. A non-sentinel
blob in `checkpoint_blobs` (a pre-delta snapshot) terminates the
walk and is bound as `DeltaChannelWrites.seed` so replay starts
from it.
Writes stored at `target_id` itself are pending writes for the next
step and are excluded — the walk begins at the target's parent.
"""
overwrite_types = _overwrite_types()
parent_of: dict[str, str | None] = {}
ver_of: dict[str, str | None] = {}
for r in parents_rows:
@@ -298,15 +292,9 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
seed = blob_value
found_seed = True
break
terminated = False
for type_tag, write_blob, _task_id, _idx in writes_by_cid.get(cid, []):
val = self.serde.loads_typed((type_tag, write_blob))
collected.append(val)
if isinstance(val, overwrite_types):
terminated = True
break
if terminated:
break
collected.reverse() # oldest → newest
if found_seed:
@@ -48,21 +48,6 @@ _DELTA_RECONSTRUCTION: contextvars.ContextVar[bool] = contextvars.ContextVar(
)
def _overwrite_types() -> tuple[type, ...]:
"""Return `(Overwrite,)` if `langgraph` is installed, else `()`.
`Overwrite` lives in `langgraph.types`, which this library does not depend
on; importing eagerly would also be circular. An empty tuple makes
`isinstance(x, overwrite_types)` safely return `False` when `langgraph` is
not installed — no `Overwrite` values can exist in that environment.
"""
try:
from langgraph.types import Overwrite # type: ignore[import-untyped]
except ImportError:
return ()
return (Overwrite,)
def _split_list_config(
config: RunnableConfig,
) -> tuple[RunnableConfig, RunnableConfig | None]:
@@ -527,8 +512,8 @@ class BaseCheckpointSaver(Generic[V]):
Default `SEED_UNSET` means no seed.
Walks the **parent chain** (not `list(before=...)`): for a thread with
forks, only on-path ancestors contribute. Scans newest→oldest and
stops at the first `Overwrite`, so reconstruction cost is bounded.
forks, only on-path ancestors contribute. Writes are returned
oldest→newest.
Writes stored at the target `checkpoint_id` itself are pending writes
for the next step and are excluded — pregel applies them separately
@@ -546,7 +531,6 @@ class BaseCheckpointSaver(Generic[V]):
# method ignores — it only reads pending_writes).
if _DELTA_RECONSTRUCTION.get():
return DeltaChannelWrites(writes=[])
overwrite_types = _overwrite_types()
token = _DELTA_RECONSTRUCTION.set(True)
try:
@@ -566,9 +550,6 @@ class BaseCheckpointSaver(Generic[V]):
if ch != channel:
continue
collected.append(value)
if isinstance(value, overwrite_types):
collected.reverse()
return DeltaChannelWrites(writes=collected)
cursor_config = tup.parent_config
collected.reverse()
return DeltaChannelWrites(writes=collected)
@@ -581,7 +562,6 @@ class BaseCheckpointSaver(Generic[V]):
"""Async version of `get_channel_writes`. See docstring there."""
if _DELTA_RECONSTRUCTION.get():
return DeltaChannelWrites(writes=[])
overwrite_types = _overwrite_types()
token = _DELTA_RECONSTRUCTION.set(True)
try:
@@ -599,9 +579,6 @@ class BaseCheckpointSaver(Generic[V]):
if ch != channel:
continue
collected.append(value)
if isinstance(value, overwrite_types):
collected.reverse()
return DeltaChannelWrites(writes=collected)
cursor_config = tup.parent_config
collected.reverse()
return DeltaChannelWrites(writes=collected)
@@ -23,7 +23,6 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
DeltaChannelWrites,
SerializerProtocol,
_overwrite_types,
get_checkpoint_id,
get_checkpoint_metadata,
)
@@ -173,14 +172,11 @@ class InMemorySaver(
chain.append(current)
_, _, parent = entry
current = parent
overwrite_types = _overwrite_types()
# Scan newest→oldest. Two terminators stop the walk:
# 1. a user-emitted `Overwrite` in writes — replaces prior history;
# 2. a pre-delta blob on an ancestor — bind it as `seed`.
# Without (2), a thread migrated from pre-delta storage would replay
# ancestor writes all the way to the root AND miss any value that
# lived only in the old blob (e.g. from `update_state`).
# Scan newest→oldest. A pre-delta blob on an ancestor terminates the
# walk and is bound as `seed`; without this, a thread migrated from
# pre-delta storage would replay ancestor writes all the way to the
# root AND miss any value that lived only in the old blob (e.g. from
# `update_state`).
#
# At each ancestor, check the blob BEFORE processing its pending
# writes: a pre-delta blob represents the state AT that ancestor,
@@ -215,9 +211,6 @@ class InMemorySaver(
continue
val = self.serde.loads_typed(serialized)
collected.append(val)
if isinstance(val, overwrite_types):
collected.reverse()
return DeltaChannelWrites(writes=collected)
collected.reverse()
return DeltaChannelWrites(writes=collected)
-37
View File
@@ -555,43 +555,6 @@ class TestBaseFallbackGetChannelWrites:
assert results[0] == expected
assert results[1] == expected
def test_fallback_stops_at_first_overwrite(self) -> None:
"""An `Overwrite` dominates older history: scan newest→oldest stops at
the first one (so `snapshot_every` / user Overwrites bound replay cost).
"""
langgraph_types = pytest.importorskip(
"langgraph.types", reason="langgraph core not installed"
)
Overwrite = langgraph_types.Overwrite
saver, thread_id, ns = self._build_saver_with_chain()
serde = JsonPlusSerializer()
cp1_id = "00000000000000000000000000000002.0000000000000000"
# Replace cp1's write with an Overwrite — cp0's write must be dropped.
saver.writes[(thread_id, ns, cp1_id)][("task2", 0)] = (
"task2",
"messages",
serde.dumps_typed(Overwrite([{"content": "reset"}])),
"",
)
target_id = "00000000000000000000000000000003.0000000000000000"
config: RunnableConfig = {
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": ns,
"checkpoint_id": target_id,
}
}
result = saver.get_channel_writes(config, "messages")
assert len(result.writes) == 1
assert isinstance(result.writes[0], Overwrite)
assert result.writes[0].value == [{"content": "reset"}]
assert result.seed is SEED_UNSET
class TestPreDeltaBlobTerminator:
"""Verify the pre-delta blob terminator: when the ancestor walk hits a
checkpoint whose blob for the channel is a real value (not
@@ -80,8 +80,6 @@ CONF = cast(Literal["configurable"], sys.intern("configurable"))
# key for the configurable dict in RunnableConfig
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
# the task_id to use for writes that are not associated with a task
SNAPSHOT_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000001")
# the task_id to use for framework-injected DeltaChannel snapshot writes
OVERWRITE = sys.intern("__overwrite__")
# dict key for the overwrite value, used as `{'__overwrite__': value}`
+15 -64
View File
@@ -11,7 +11,6 @@ from langgraph._internal._typing import MISSING
from langgraph.channels.base import BaseChannel, Value
from langgraph.channels.binop import _get_overwrite
from langgraph.errors import EmptyChannelError
from langgraph.types import Overwrite
__all__ = ("DeltaChannel",)
@@ -31,41 +30,28 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
to eliminate O(N²) blob growth — storage is O(N) using the writes table that
every checkpointer already maintains.
``snapshot_every`` bounds reconstruction cost. When set, every N effective
writes an `Overwrite(full_value)` is injected into `checkpoint_writes`; on
reload the saver scans writes newest→oldest and stops at the first
`Overwrite`, so replay work is bounded regardless of thread age. Any
user-written `Overwrite` on the channel provides the same benefit for free.
Reconstruction replays every ancestor write through the operator, so
per-get cost scales with thread depth. Compaction for deep threads is
a follow-up — today, use this on threads of a few hundred turns.
Usage::
class State(TypedDict):
messages: Annotated[list[AnyMessage], DeltaChannel(add_messages)]
# With periodic snapshotting for long threads:
messages: Annotated[
list[AnyMessage],
DeltaChannel(add_messages, snapshot_every=100),
]
"""
__slots__ = (
"value",
"operator",
"snapshot_every",
"_writes_since_snapshot",
)
def __init__(
self,
operator: Callable[[Any, Any], Any],
*,
snapshot_every: int | None = None,
) -> None:
super().__init__(list)
self.operator = operator
self.value: Any = []
self.snapshot_every = snapshot_every
self._writes_since_snapshot = 0
def __eq__(self, other: object) -> bool:
if not isinstance(other, DeltaChannel):
@@ -86,62 +72,50 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
return self.typ
def copy(self) -> Self:
new: DeltaChannel[Value] = DeltaChannel(
self.operator, snapshot_every=self.snapshot_every
)
new: DeltaChannel[Value] = DeltaChannel(self.operator)
new.typ = self.typ
new.key = self.key
new.value = self.value if self.value is MISSING else _copy.copy(self.value)
new._writes_since_snapshot = self._writes_since_snapshot
return new
def _apply_write(self, value: Any, write: Any, counter: int) -> tuple[Any, int]:
"""Apply one write to `value`; return (new_value, new_counter).
def _apply_write(self, value: Any, write: Any) -> Any:
"""Apply one write to `value` and return the new value.
An `Overwrite` resets the counter to 0; any other write increments it.
Centralizes the Overwrite/reducer branching used by both `update` (live
super-step) and `from_checkpoint` (ancestor replay).
An `Overwrite` replaces the value; any other write is folded through
the operator. Centralizes the Overwrite/reducer branching used by both
`update` (live super-step) and `from_checkpoint` (ancestor replay).
"""
is_overwrite, overwrite_value = _get_overwrite(write)
if is_overwrite:
new_value = (
return (
_copy.copy(overwrite_value)
if overwrite_value is not None
else _empty(self.typ)
)
return new_value, 0
base = _empty(self.typ) if value is MISSING else value
return self.operator(base, write), counter + 1
return self.operator(base, write)
def from_checkpoint(self, checkpoint: Any) -> Self:
new: DeltaChannel[Value] = DeltaChannel(
self.operator, snapshot_every=self.snapshot_every
)
new: DeltaChannel[Value] = DeltaChannel(self.operator)
new.typ = self.typ
new.key = self.key
if checkpoint is MISSING:
new.value = _empty(new.typ)
new._writes_since_snapshot = 0
elif isinstance(checkpoint, DeltaChannelWrites):
# Saver reconstructed per-step writes; replay through the operator.
# `seed` (if set) is a pre-delta accumulated value that terminates
# the ancestor walk on the saver side: replay starts from it
# instead of the channel's empty value. Counter tracks writes
# since the last Overwrite so snapshot cadence stays accurate
# across reloads.
# instead of the channel's empty value.
value: Any = (
_empty(new.typ) if checkpoint.seed is SEED_UNSET else checkpoint.seed
)
counter = 0
for write in checkpoint.writes:
value, counter = new._apply_write(value, write, counter)
value = new._apply_write(value, write)
new.value = value
new._writes_since_snapshot = counter
else:
# Backward compat: a pre-DeltaChannel thread stored the accumulated
# value directly (no saver-side reconstruction happened). Trust it.
new.value = checkpoint
new._writes_since_snapshot = 0
return new
def update(self, values: Sequence[Any]) -> bool:
@@ -167,9 +141,7 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
elif seen_overwrite:
# Post-Overwrite writes within the same super-step are dropped.
continue
self.value, self._writes_since_snapshot = self._apply_write(
self.value, value, self._writes_since_snapshot
)
self.value = self._apply_write(self.value, value)
return True
def get(self) -> Any:
@@ -182,24 +154,3 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
def checkpoint(self) -> Any:
return DELTA_SENTINEL
def should_snapshot(self) -> bool:
"""True if enough writes have accumulated to justify a snapshot.
Pregel checks this after a checkpoint is saved; if true, it injects
`snapshot_write()` into `checkpoint_writes`.
"""
return (
self.snapshot_every is not None
and self._writes_since_snapshot >= self.snapshot_every
)
def snapshot_write(self) -> Overwrite:
"""Return the write to persist and reset the counter.
The write is an `Overwrite(current_value)`; on replay the operator's
`Overwrite` handling resets state to this value, and the saver's
ancestor walk can stop here.
"""
self._writes_since_snapshot = 0
return Overwrite(_copy.copy(self.value))
-28
View File
@@ -57,7 +57,6 @@ from langgraph._internal._constants import (
NULL_TASK_ID,
PUSH,
RESUME,
SNAPSHOT_TASK_ID,
TASKS,
)
from langgraph._internal._replay import ReplayState
@@ -69,7 +68,6 @@ from langgraph.callbacks import (
GraphResumeEvent,
)
from langgraph.channels.base import BaseChannel
from langgraph.channels.delta import DeltaChannel
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import TAG_HIDDEN
from langgraph.errors import (
@@ -947,32 +945,6 @@ class PregelLoop:
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
},
}
# DeltaChannel snapshot injection: after the checkpoint save is
# queued, any DeltaChannel that has crossed its `snapshot_every`
# threshold emits an Overwrite write stored at the just-saved
# checkpoint_id. On any descendant load, the saver's ancestor walk
# encounters this Overwrite and stops, bounding replay cost.
if self.checkpointer_put_writes is not None and self.channels:
snapshot_writes: list[tuple[str, Any]] = []
for ch_name, channel in self.channels.items():
if isinstance(channel, DeltaChannel) and channel.should_snapshot():
snapshot_writes.append((ch_name, channel.snapshot_write()))
if snapshot_writes:
if self.checkpointer_put_writes_accepts_task_path:
self.submit(
self.checkpointer_put_writes,
self.checkpoint_config,
snapshot_writes,
SNAPSHOT_TASK_ID,
"",
)
else:
self.submit(
self.checkpointer_put_writes,
self.checkpoint_config,
snapshot_writes,
SNAPSHOT_TASK_ID,
)
if not exiting:
# increment step
self.step += 1
+1 -198
View File
@@ -460,30 +460,6 @@ def test_delta_channel_dict_reducer_overwrite_in_writes_replay() -> None:
assert ch.get() == {"x": 10, "y": 20, "z": 30}
def test_delta_channel_dict_reducer_snapshot_write_preserves_shape() -> None:
"""snapshot_write() on a dict channel must emit Overwrite(dict), not Overwrite(list)."""
from langgraph.channels.delta import DeltaChannel
from langgraph.types import Overwrite
def merge_dicts(left: dict, right: dict) -> dict:
return {**left, **right}
spec = _delta_channel_with_type(merge_dicts, dict)
# Force snapshot_every by reaching into the spec instance.
assert isinstance(spec, DeltaChannel)
spec.snapshot_every = 2
ch = spec.from_checkpoint(MISSING)
ch.update([{"a": 1}])
ch.update([{"b": 2}])
assert ch.should_snapshot()
w = ch.snapshot_write()
assert isinstance(w, Overwrite)
assert w.value == {"a": 1, "b": 2}
assert isinstance(w.value, dict)
def test_delta_channel_dict_reducer_with_notrequired_annotation() -> None:
"""DeltaChannel infers dict type through `Annotated[NotRequired[dict[...]], ch]`.
@@ -605,69 +581,10 @@ def test_delta_channel_dict_reducer_backwards_compat() -> None:
# ---------------------------------------------------------------------------
# snapshot_every
# seed / pre-delta migration
# ---------------------------------------------------------------------------
def test_delta_channel_snapshot_counter_triggers() -> None:
"""Counter hits threshold → should_snapshot() true; snapshot_write() resets it."""
from langchain_core.messages import HumanMessage
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import add_messages
from langgraph.types import Overwrite
ch = DeltaChannel(add_messages, snapshot_every=3).from_checkpoint(MISSING)
assert not ch.should_snapshot()
ch.update([HumanMessage(content="a", id="1")])
assert not ch.should_snapshot()
ch.update([HumanMessage(content="b", id="2")])
assert not ch.should_snapshot()
ch.update([HumanMessage(content="c", id="3")])
assert ch.should_snapshot()
w = ch.snapshot_write()
assert isinstance(w, Overwrite)
assert len(w.value) == 3
# counter reset
assert not ch.should_snapshot()
def test_delta_channel_snapshot_default_disabled() -> None:
"""No snapshot_every → should_snapshot() is never true."""
from langchain_core.messages import HumanMessage
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import add_messages
ch = DeltaChannel(add_messages).from_checkpoint(MISSING)
for i in range(50):
ch.update([HumanMessage(content=str(i), id=str(i))])
assert not ch.should_snapshot()
def test_delta_channel_user_overwrite_resets_counter() -> None:
"""An Overwrite (user or framework) resets the snapshot counter."""
from langchain_core.messages import HumanMessage
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import add_messages
from langgraph.types import Overwrite
ch = DeltaChannel(add_messages, snapshot_every=3).from_checkpoint(MISSING)
ch.update([HumanMessage(content="a", id="1")])
ch.update([HumanMessage(content="b", id="2")])
# Right before threshold — user Overwrite should reset.
ch.update([Overwrite([HumanMessage(content="new", id="new")])])
assert not ch.should_snapshot()
# Need 3 more writes to trigger.
ch.update([HumanMessage(content="c", id="c")])
ch.update([HumanMessage(content="d", id="d")])
ch.update([HumanMessage(content="e", id="e")])
assert ch.should_snapshot()
def test_delta_channel_from_checkpoint_honors_seed() -> None:
"""DeltaChannelWrites(seed=...) starts replay from that snapshot.
@@ -714,117 +631,3 @@ def test_delta_channel_from_checkpoint_seed_none_is_distinct_from_unset() -> Non
assert unset.seed is SEED_UNSET
def test_delta_channel_replay_tracks_counter_across_overwrite() -> None:
"""Counter reloaded from writes reflects writes-since-last-Overwrite."""
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.base import DeltaChannelWrites
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import add_messages
from langgraph.types import Overwrite
spec = DeltaChannel(add_messages, snapshot_every=3)
writes = DeltaChannelWrites(
[
HumanMessage(content="a", id="1"),
HumanMessage(content="b", id="2"),
HumanMessage(content="c", id="3"),
Overwrite([HumanMessage(content="reset", id="r")]),
HumanMessage(content="d", id="4"),
]
)
ch = spec.from_checkpoint(writes)
# Post-snapshot: one write since reset.
assert ch._writes_since_snapshot == 1
assert not ch.should_snapshot()
assert len(ch.get()) == 2 # reset → [r], +d → [r, d]
def test_delta_channel_snapshot_end_to_end_inmemory() -> None:
"""Full graph: snapshot is injected, descendants short-circuit at it."""
from typing import Annotated
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.types import Overwrite
class State(TypedDict):
messages: Annotated[list, DeltaChannel(add_messages, snapshot_every=2)]
n = {"v": 0}
def respond(state: State) -> dict:
n["v"] += 1
return {"messages": [AIMessage(content=f"r{n['v']}", id=f"ai{n['v']}")]}
builder = StateGraph(State)
builder.add_node("respond", respond)
builder.add_edge(START, "respond")
saver = InMemorySaver()
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "snap"}}
# 3 turns × (HumanMessage input + AIMessage reply) — plenty to trigger
# multiple snapshots at snapshot_every=2.
for i in range(3):
graph.invoke({"messages": [HumanMessage(content=f"q{i}", id=f"h{i}")]}, config)
# Final state has 6 messages (3 H + 3 AI) regardless of snapshots.
state = graph.get_state(config)
assert len(state.values["messages"]) == 6
# At least one Overwrite snapshot write was injected into checkpoint_writes.
all_writes = [
v
for wdict in saver.writes.values()
for (_task, _idx), (_, ch, serialized, _) in wdict.items()
if ch == "messages"
for v in [saver.serde.loads_typed(serialized)]
]
overwrites = [w for w in all_writes if isinstance(w, Overwrite)]
assert len(overwrites) >= 1, (
f"expected at least one snapshot Overwrite, got writes: {all_writes}"
)
def test_delta_channel_snapshot_preserves_time_travel() -> None:
"""Time-travel to a checkpoint created before a snapshot still replays correctly."""
from typing import Annotated
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, DeltaChannel(add_messages, snapshot_every=2)]
def respond(state: State) -> dict:
n = len(state["messages"])
return {"messages": [AIMessage(content=f"r{n}", id=f"ai{n}")]}
builder = StateGraph(State)
builder.add_node("respond", respond)
builder.add_edge(START, "respond")
saver = InMemorySaver()
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "snap-tt"}}
for i in range(4):
graph.invoke({"messages": [HumanMessage(content=f"q{i}", id=f"h{i}")]}, config)
# Walk history; each snapshot has the expected message count at that point.
history = list(graph.get_state_history(config))
# Reverse to chronological order.
history = list(reversed(history))
# At each point the visible message count should monotonically grow.
counts = [len(h.values.get("messages", [])) for h in history]
assert counts == sorted(counts), f"message counts not monotonic: {counts}"
@@ -126,15 +126,6 @@ class DeltaState(TypedDict):
messages: Annotated[list, DeltaChannel(add_messages)]
_SNAPSHOT_EVERY = 25
class DeltaSnapshotState(TypedDict):
messages: Annotated[
list, DeltaChannel(add_messages, snapshot_every=_SNAPSHOT_EVERY)
]
# ---------------------------------------------------------------------------
# Graph factory
# ---------------------------------------------------------------------------
@@ -232,9 +223,9 @@ def _approx_tokens(n_turns: int) -> str:
# Extrapolation: 5,000 turns × ~200 tokens/turn ≈ 1M tokens (Claude's full context window).
TURN_COUNTS = [10, 25, 50, 100, 500]
# Snapshot-only counts: pure delta replay becomes painful past 500 turns and
# add_messages storage balloons past 1 GB, so only the snapshot variant runs here.
SNAPSHOT_ONLY_TURN_COUNTS = [1000]
# Deep-thread counts where add_messages blob storage would exceed 1 GB;
# only DeltaChannel runs here.
DELTA_ONLY_TURN_COUNTS = [1000]
def _checkpointer_factories() -> list[tuple[str, Any]]:
@@ -294,24 +285,22 @@ def _run_benchmark_for_checkpointer(cp_hint: Any) -> None:
rows: list[tuple[int, Any, Any, Any, Any, Any, Any]] = []
for turns in TURN_COUNTS:
with _make_saver() as saver:
_, b_rt, b_bytes = _run_turns(turns, BinaryState, saver)
b_wt, b_rt, b_bytes = _run_turns(turns, BinaryState, saver)
with _make_saver() as saver:
_, d_rt, d_bytes = _run_turns(turns, DeltaState, saver)
d_wt, d_rt, d_bytes = _run_turns(turns, DeltaState, saver)
rows.append((turns, b_bytes, d_bytes, b_rt, d_rt, b_wt, d_wt))
for turns in DELTA_ONLY_TURN_COUNTS:
with _make_saver() as saver:
_, s_rt, s_bytes = _run_turns(turns, DeltaSnapshotState, saver)
rows.append((turns, b_bytes, d_bytes, s_bytes, b_rt, d_rt, s_rt))
for turns in SNAPSHOT_ONLY_TURN_COUNTS:
with _make_saver() as saver:
_, s_rt, s_bytes = _run_turns(turns, DeltaSnapshotState, saver)
rows.append((turns, None, None, s_bytes, None, None, s_rt))
d_wt, d_rt, d_bytes = _run_turns(turns, DeltaState, saver)
rows.append((turns, None, d_bytes, None, d_rt, None, d_wt))
# ── Table 1: Storage ─────────────────────────────────────────────────────
W = 78
W = 64
print("Storage (checkpoint blob bytes)")
print("=" * W)
print(
f"{'turns':>6} {'ctx size':>10} {'add_msgs':>12} {'delta':>12} "
f"{'delta+snap':>12} {'savings':>8}"
f"{'savings':>8}"
)
print("-" * W)
@@ -325,7 +314,7 @@ def _run_benchmark_for_checkpointer(cp_hint: Any) -> None:
def _ms_or_na(v: Any) -> str:
return "n/a" if v is None else f"{v * 1000:.1f}ms"
for turns, b_bytes, d_bytes, s_bytes, b_rt, d_rt, s_rt in rows:
for turns, b_bytes, d_bytes, b_rt, d_rt, b_wt, d_wt in rows:
if b_bytes is None or b_bytes < 0 or d_bytes is None or d_bytes < 0:
ratio_str = "n/a"
else:
@@ -334,35 +323,48 @@ def _run_benchmark_for_checkpointer(cp_hint: Any) -> None:
print(
f"{turns:>6} {_approx_tokens(turns):>10} "
f"{_bytes_or_na(b_bytes):>12} {_bytes_or_na(d_bytes):>12} "
f"{_bytes_or_na(s_bytes):>12} {ratio_str:>8}"
f"{ratio_str:>8}"
)
print("=" * W)
print()
# ── Table 2: Read latency ─────────────────────────────────────────────────
print("Read latency (avg of 5 get_state calls = cost per invoke)")
print("Read latency (avg of 5 get_state calls)")
print("=" * W)
print(
f"{'turns':>6} {'ctx size':>10} {'add_msgs':>12} {'delta':>12} "
f"{'delta+snap':>12}"
f"{'turns':>6} {'ctx size':>10} {'add_msgs':>12} {'delta':>12}"
)
print("-" * W)
for turns, b_bytes, d_bytes, s_bytes, b_rt, d_rt, s_rt in rows:
for turns, b_bytes, d_bytes, b_rt, d_rt, b_wt, d_wt in rows:
print(
f"{turns:>6} {_approx_tokens(turns):>10} "
f"{_ms_or_na(b_rt):>12} {_ms_or_na(d_rt):>12} "
f"{_ms_or_na(s_rt):>12}"
f"{_ms_or_na(b_rt):>12} {_ms_or_na(d_rt):>12}"
)
print("=" * W)
print()
# ── Table 3: Per-invoke latency (total write_elapsed / turns) ─────────────
print("Per-invoke latency (total graph.invoke time / turns)")
print("=" * W)
print(
f"{'turns':>6} {'ctx size':>10} {'add_msgs':>12} {'delta':>12}"
)
print("-" * W)
for turns, b_bytes, d_bytes, b_rt, d_rt, b_wt, d_wt in rows:
def _per(wt: Any) -> str:
if wt is None:
return "n/a"
return f"{(wt / turns) * 1000:.1f}ms"
print(
f"{turns:>6} {_approx_tokens(turns):>10} "
f"{_per(b_wt):>12} {_per(d_wt):>12}"
)
print("=" * W)
print()
print("Legend:")
print(" add_msgs = Annotated[list, add_messages] — O(N²) storage")
print(" delta = DeltaChannel(add_messages) — O(N) storage")
print(
f" delta+snap = DeltaChannel(add_messages, snapshot_every={_SNAPSHOT_EVERY})"
" — O(N) storage, bounded read"
)
print(" add_msgs = Annotated[list, add_messages] — O(N²) storage")
print(" delta = Annotated[list, DeltaChannel(add_messages)] — O(N) storage")
print()