5c18bde0f8
feat(langgraph): DeltaChannel: store sentinel in blobs, reconstruct from checkpoint_writes ( #7586 )
...
# DeltaChannel: sentinel-based checkpoint blobs + write-replay
reconstruction
## Summary
`DeltaChannel` is a new fold-reducer channel that stores only a
zero-byte sentinel in checkpoint blobs instead of the full accumulated
value. On restore, the runtime replays ancestor writes through the
reducer to reconstruct state. For long-running threads with large
accumulating state (e.g. message histories), this delivers dramatically
smaller checkpoint blobs with configurable read-depth bounds.
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph.message import _messages_delta_reducer
class State(TypedDict):
# blob per step: ~60 bytes (sentinel) instead of growing full list
messages: Annotated[list, DeltaChannel(_messages_delta_reducer)]
# bound read depth to 10 steps via periodic snapshots
messages_bounded: Annotated[list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=10)]
```
---
## Storage benchmarks (InMemory, ~400 char/msg)
**Messages blob storage** (`checkpoint_blobs` bytes for the messages
channel):
| turns | add\_messages | delta(inf) | delta(freq=50) | delta(freq=10) |
delta(freq=5) |
|------:|-------------:|-----------:|---------------:|---------------:|--------------:|
| 10 | 91.0 KB | 60 B (1517x) | 60 B (1517x) | 14.4 KB (6x) | 32.6 KB
(3x) |
| 50 | 2.20 MB | 300 B (7347x) | 67.1 KB (33x) | 423 KB (5x) | 864 KB
(3x) |
| 100 | 8.78 MB | 600 B (14636x) | 310 KB (28x) | 1.72 MB (5x) | 3.48 MB
(3x) |
| 250 | 54.80 MB | 1.5 KB (36536x) | 2.09 MB (26x) | 10.87 MB (5x) |
21.84 MB (3x) |
| 500 | 219.19 MB | 3.0 KB (73063x) | 8.56 MB (26x) | 43.67 MB (5x) |
87.50 MB (3x) |
**Total checkpoint storage** (blobs + writes + metadata):
| turns | add\_messages | delta(inf) | delta(freq=50) | delta(freq=10) |
delta(freq=5) |
|------:|-------------:|-----------:|---------------:|---------------:|--------------:|
| 10 | 129.7 KB | 38.7 KB (3.4x) | 38.7 KB (3.4x) | 53.1 KB (2.4x) |
71.2 KB (1.8x) |
| 50 | 2.40 MB | 196 KB (12x) | 263 KB (9x) | 620 KB (3.9x) | 1.06 MB
(2.3x) |
| 100 | 9.18 MB | 394 KB (23x) | 703 KB (13x) | 2.12 MB (4.3x) | 3.87 MB
(2.4x) |
| 250 | 55.79 MB | 987 KB (57x) | 3.07 MB (18x) | 11.86 MB (4.7x) |
22.82 MB (2.4x) |
| 500 | 221.16 MB | 1.98 MB (112x) | 10.53 MB (21x) | 45.64 MB (4.9x) |
89.48 MB (2.5x) |
**Write-phase peak heap**:
| turns | add\_messages | delta(inf) | delta(freq=50) | delta(freq=10) |
delta(freq=5) |
|------:|-------------:|-----------:|---------------:|---------------:|--------------:|
| 10 | 456 KB | 199 KB (2.3x) | 199 KB (2.3x) | 212 KB (2.2x) | 232 KB
(2.0x) |
| 50 | 3.04 MB | 742 KB (4.1x) | 805 KB (3.8x) | 1.21 MB (2.5x) | 1.67
MB (1.8x) |
| 100 | 10.70 MB | 1.41 MB (7.6x) | 1.82 MB (5.9x) | 3.42 MB (3.1x) |
5.25 MB (2.0x) |
| 250 | 60.44 MB | 3.36 MB (18x) | 5.67 MB (11x) | 14.87 MB (4.1x) |
26.31 MB (2.3x) |
**Read-phase avg `get_state` latency** (5 calls, InMemory):
| turns | add\_messages | delta(inf) | delta(freq=50) | delta(freq=10) |
delta(freq=5) |
|------:|-------------:|-----------:|---------------:|---------------:|--------------:|
| 10 | 0.7 ms | 1.1 ms (0.6x) | 1.1 ms (0.6x) | 0.8 ms (0.9x) | 0.6 ms
(1.1x) |
| 50 | 2.7 ms | 5.3 ms (0.5x) | 3.5 ms (0.8x) | 2.7 ms (1.0x) | 2.7 ms
(1.0x) |
| 100 | 5.5 ms | 11.1 ms (0.5x) | 6.0 ms (0.9x) | 5.2 ms (1.1x) | 5.4 ms
(1.0x) |
| 250 | 12.9 ms | 27.2 ms (0.5x) | 13.6 ms (0.9x) | 12.9 ms (1.0x) |
13.0 ms (1.0x) |
**Postgres `get_tuple` read latency** (~100 tok/msg per step):
| steps | full-list | delta(inf) | delta(freq=50) | delta(freq=10) |
delta(freq=5) |
|------:|----------:|-----------:|---------------:|---------------:|--------------:|
| 10 | 0.29 ms | 0.21 ms (1.4x) | 0.19 ms (1.6x) | 0.19 ms (1.5x) | 0.19
ms (1.6x) |
| 50 | 0.19 ms | 0.15 ms (1.3x) | 0.19 ms (1.0x) | 0.22 ms (0.8x) | 0.29
ms (0.7x) |
| 100 | 0.27 ms | 0.17 ms (1.6x) | 0.22 ms (1.2x) | 0.23 ms (1.2x) |
0.21 ms (1.3x) |
| 500 | 0.60 ms | 0.30 ms (2.0x) | 0.66 ms (0.9x) | 0.56 ms (1.1x) |
0.69 ms (0.9x) |
**Takeaway:** `snapshot_frequency=10` matches full-list read latency
while still saving 5x on blob storage and ~4x on total storage.
---
## How it works
### Checkpoint blobs
`checkpoint()` always returns `DELTA_SENTINEL` (a zero-byte msgpack ext
marker) instead of the accumulated value. On restore, the saver's
`_get_channel_writes_history` walks the ancestor chain collecting
`checkpoint_writes` entries and replays them through the reducer:
```python
# blob stored per step: ~1 byte (sentinel)
# vs. full list growing O(N) every step with BinaryOperatorAggregate
```
### Reducer interface
`DeltaChannel` takes a **batch reducer** `(state, list[writes]) ->
state` — all writes for a step arrive in one call, enabling single-pass
implementations:
```python
# ❌ Don't use add_messages directly — it's a binary operator, not a batch reducer
messages: Annotated[list, DeltaChannel(add_messages)] # wrong
# ✅ Use _messages_delta_reducer — single pass, dedup by ID, RemoveMessage support
messages: Annotated[list, DeltaChannel(_messages_delta_reducer)]
# ✅ Or write your own batch reducer for custom types
def my_dict_reducer(state: dict, writes: list[dict]) -> dict:
result = dict(state)
for w in writes:
result.update(w)
return result
files: Annotated[dict, DeltaChannel(my_dict_reducer)]
```
### Snapshot frequency
`snapshot_frequency=N` writes a full `_DeltaSnapshot` blob every N
pregel steps, bounding replay depth regardless of thread length.
Snapshots are eager — written even if the channel had no update that
step, so the depth bound always holds:
```python
# Replay walks at most 10 ancestors before hitting a snapshot
messages: Annotated[list, DeltaChannel(_messages_delta_reducer, snapshot_frequency=10)]
```
### Migration from `BinaryOperatorAggregate`
Pre-existing threads written under `BinaryOperatorAggregate` work
transparently after swapping the annotation — the saver detects a
plain-value ancestor blob and uses it as the reconstruction seed:
```python
# Before: BinaryOperatorAggregate stores full list every step
items: Annotated[list, add_messages]
# After: DeltaChannel — existing checkpoints still readable, new steps use sentinel
items: Annotated[list, DeltaChannel(_messages_delta_reducer)]
```
### Async write-ordering safety
In `durability="async"` mode (default), `put_writes` calls are
fire-and-forget. `AsyncPregelLoop` tracks in-flight `aput_writes`
futures for DeltaChannel channels in `_delta_write_futs` and drains them
via `await asyncio.gather()` in `_checkpointer_put_after_previous`
before `aput()` — ensuring `checkpoint_writes` are durable before the
sentinel blob is committed.
---
## What's in scope
- **`libs/langgraph/langgraph/channels/delta.py`** — `DeltaChannel`
implementation
- **`libs/langgraph/langgraph/graph/message.py`** —
`_messages_delta_reducer` (experimental)
- **`libs/checkpoint/`** — `_get_channel_writes_history` ancestor-walk
API on `BaseCheckpointSaver`, `InMemorySaver` optimized override
- **`libs/checkpoint-postgres/`** — `PostgresSaver` /
`AsyncPostgresSaver` single-roundtrip UNION ALL override
- **`libs/langgraph/langgraph/pregel/`** — `channels_from_checkpoint` /
`create_checkpoint` wiring, async write-ordering safety
---
## Follow-ups
- **Batch reconstruction**: each DeltaChannel field issues its own
`_get_channel_writes_history` call; a single walk collecting all
sentinel channels would reduce roundtrips proportionally to the number
of DeltaChannel fields.
- **Sync write ordering**: `BackgroundExecutor.__exit__` guarantees
completion before `invoke()` returns, but within a run there's no
explicit ordering between `put_writes` and `put`. Two-phase commit for
sync would close this gap.
- **`ShallowPostgresSaver` compatibility**: shallow savers keep only the
latest checkpoint and have no parent chain to walk; DeltaChannel is
currently incompatible and should raise or warn at compile time.
- Updating the writes table w/ delta epoch ids for more efficient reads
- follow up w/ LSD checkpointer implementations to support delta
channel! and update prune
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com >
Co-authored-by: ccurme <chester.curme@gmail.com >
Co-authored-by: Will Fu-Hinthorn <will@langchain.dev >
2026-04-29 17:26:17 -04:00
168674dd2a
chore: update x links to langchain_oss ( #7645 )
...
## Description
Updates the X/Twitter social links to point to the new `@langchain_oss`
account across README badges and Python package metadata.
## Test Plan
- [ ] Verify README badges and package metadata point to
`@langchain_oss` on X
_Opened collaboratively by Mason Daugherty and open-swe._
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com >
Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com >
Co-authored-by: Mason Daugherty <mason@langchain.dev >
2026-04-29 14:02:51 -04:00
William FH and GitHub
f393a5415e
release(checkpoint-postgres): 3.0.5 ( #7221 )
2026-03-18 14:17:57 -07:00
John Kennedy and GitHub
666c224c2d
chore: bump orjson ( #6852 )
...
The orjson.dumps function in orjson thru 3.11.4 does not limit recursion
for deeply nested JSON documents.
2026-02-17 10:30:58 -08:00
193e128c20
chore: Omit lock when using connection pool ( #6734 )
...
Co-authored-by: Conrad Ludgate <conradludgate@gmail.com >
2026-01-31 00:30:34 +00:00
Mason Daugherty and GitHub
089cdd0ffb
chore: update twitter URLs ( #6683 )
2026-01-13 09:33:56 -05:00
Sydney Runkle and GitHub
f91d79d0c8
fix: flip default on base cache ( #6677 )
...
rebasing https://github.com/langchain-ai/langgraph/pull/6562 w/ current
main
2026-01-12 15:26:37 -05:00
William FH and GitHub
4d01e69b82
release(checkpoint-postgres): 3.0.1 ( #6568 )
2025-12-09 23:05:49 +00:00
Mason Daugherty and GitHub
4ec92f9fb1
chore: add pyproject.toml links ( #6364 )
2025-11-07 07:43:51 -05:00
Josh Rogers and GitHub
9fd3dfc542
chore(checkpoint-postgres): bump to 3.0.1 ( #6402 )
...
**Description:** Bumping the checkpoint-postgres package to version
3.0.1 to release an update to migrations
(https://github.com/langchain-ai/langgraph/pull/6400 ).
**Issue:** N/A
**Dependencies:** N/A
**Twitter handle:** N/A
2025-11-06 11:14:02 -05:00
William FH and GitHub
fca3e4513c
release: Checkpointers 3.0 ( #6313 )
...
In this PR:
- Bump `langgraph-checkpoint` to 3.0
- Bump `langgraph-checkpoint-sqlite` to 3.0; Update
`langgraph-checkpoint` deps to >=3,<4
- Bump `langgraph-checkpoint-postgres` to 3.0; Update
`langgraph-checkpoint` max to <4 (keep prior min since the deprecated
functionality wasn't explicitly used)
- Bump `langgraph` to 1.0.1; update `langgraph-checkpoint` max bound to
4
- Bump `prebuilt` to 1.0.1; update `langgraph-checkpoint` max bound to 4
2025-10-20 11:31:55 -07:00
Sydney Runkle and GitHub
2c3e380a35
feat: adding cursory Python 3.14 support ( #6298 )
...
* catching error thrown by asyncio
* using 2nd check for annotations given Pydantic 2.12 changes
* skipping tests for remote graph bc langgraph-api is dependent on
`jsonschema-rs`
* skipping tests w/ pydantic v1 models
```bash
hint: This usually indicates a problem with the package or the build environment.
help: `jsonschema-rs` (v0.29.1) was included because `langgraph:dev` (v1.0.0rc1) depends on `langgraph-cli[inmem]` which
depends on `langgraph-api` (v0.4.29) which depends on `jsonschema-rs`
```
not yet testing for free threaded python, that'll be much more involved!
ended up separating lint / testing deps during this process bc I was
getting a ton of not required deps while testing that were complicating
things :/
2025-10-17 08:26:52 -04:00
Sydney Runkle and GitHub
2d3121a17c
chore: drop Python 3.9 (and syntax) ( #6289 )
...
* `strict=False` is the default, pyupgrade to min version 3.10 adds this
to be explicit w/ behavior
2025-10-16 20:17:46 -04:00
Caspar Broekhuizen and GitHub
7d166bfb9f
chore(checkpoint): bump patch version ( #6244 )
...
- Bump `langgraph-checkpoint` to 2.1.2
- Bump `langgraph-checkpoint-postgres` to 2.0.25 and raise
`langgraph-checkpoint` dep lower bound to 2.1.2
2025-10-07 10:41:24 -07:00
Isaac Francisco and GitHub
9d1bb9d86c
chore(checkpoint-postgres): bump version ( #6222 )
2025-09-30 07:41:59 -07:00
Nuno Campos
a5eb6a75bf
checkpoint-postgres 2.0.23
2025-07-16 11:58:07 +02:00
William FH and GitHub
9b9bf88aee
fix(checkpoint-postgres): Remove python invalid escape warning ( #5441 )
2025-07-10 22:38:05 +00:00
Sydney Runkle and GitHub
000f5c3043
fix[deps]: update lockfiles / deps bounds for internal tools ( #5301 )
...
update lockfiles / deps bounds
2025-07-02 10:30:55 -04:00
Sydney Runkle and GitHub
5e7566f4a3
lint: use pep 604 union syntax and pep 585 generic syntax ( #4963 )
...
* new union syntax
* fix test
* second round of conversions by injecting future annotations
* format + add top level makefile
2025-06-04 21:50:16 -04:00
Sydney Runkle and GitHub
228a08b966
ci: migrate to uv! ( #4698 )
...
* Migrate to `uv`
* Format `pyproject.toml` files properly
* Remove upper bounds on dependencies, and bounds on dev dependencies
(we should be using latest)
* Move to hatch for packaing
In the future we should:
* Set up dependabot / automate lockfile updates and tests
* Add tests for min compatible versions (I'll do this right after merge)
* Use dynamic versioning
* Bump `pydantic` to v2.11.4 in the lockfile, we have some tests failing
2025-05-15 17:39:14 -07:00
Sydney Runkle
5e9bea5134
no upper bounds for python
2025-04-25 17:45:54 -07:00
ba7f9975fa
Fix text fields naming ( #4345 )
...
The configuration expects the key "fields", not "text_fields": I had
failed to update across all implementations in the original PR
Thank you to Vincent Min for the fix!
---------
Co-authored-by: Vincent Min <93780551+VMinB12@users.noreply.github.com >
2025-04-18 08:21:46 -07:00
Vadym Barda and GitHub
3da5c73a04
checkpoint-postgres: release 2.0.20 ( #4335 )
2025-04-17 17:08:18 -04:00
William FH and GitHub
992b05a196
langgraph-checkpoint-postgres 2.0.19 ( #3945 )
2025-03-20 07:25:41 -07:00
William Fu-Hinthorn
893a9646d3
langgraph-checkpoint-postgres 2.0.19
2025-03-20 07:25:14 -07:00
William Fu-Hinthorn
daee8d88bb
Increase pg->checkpoint minbound
2025-03-20 07:24:15 -07:00
William Fu-Hinthorn
424f24720a
Make expires_at idempotent
2025-03-17 13:25:22 -07:00
William Fu-Hinthorn
18ed044c27
Bump patch version
2025-03-14 14:03:30 -07:00
William FH and GitHub
e4c7db180e
Release checkpoint-postgres ( #3745 )
2025-03-07 13:51:36 -08:00
Nuno Campos
9786be1ff7
Update requirement
2025-02-14 19:01:50 -08:00
Nuno Campos
d4b22ac1d4
Fix postgres tests
2025-02-14 18:40:30 -08:00
Nuno Campos
9706211aca
Exclude complex values from checkpoint metadata
2025-02-14 17:35:14 -08:00
Vadym Barda and GitHub
d45253cee8
checkpoint: release libraries ( #3412 )
2025-02-12 22:44:44 -05:00
Vadym Barda and GitHub
89b0be3a7d
checkpoint-postgres: release 2.0.13 ( #3063 )
2025-01-16 09:17:43 -05:00
Nuno Campos
fc887f7a5d
checkpoint-postgres/sqlite 2.0.12/2.0.3
2025-01-15 11:26:51 -08:00
Vadym Barda and GitHub
74adbb744b
bump checkpoint sqlite, postgres ( #3021 )
2025-01-14 14:41:22 -05:00
William FH and GitHub
e5b5f9510b
Fix empty migration ( #2978 )
2025-01-09 23:14:02 +00:00
Vadym Barda and GitHub
a3c5b8fc37
checkpoint-postgres: release 2.0.9 ( #2849 )
2024-12-20 17:47:20 -05:00
William FH and GitHub
93e4c8cc1f
Create index concurrently ( #2659 )
2024-12-05 15:56:39 -08:00
William FH and GitHub
0361554fcf
Bump Checkpoint Postgres ( #2601 )
2024-12-02 17:56:23 -08:00
Vadym Barda and GitHub
784821705b
checkpoint-postgres: pin psycopg >= 3.2.0 ( #2580 )
2024-11-29 11:30:19 -05:00
William FH and GitHub
12486d977a
Update postgres-checkpoint min bounds ( #2564 )
2024-11-27 22:31:16 -08:00
William FH and GitHub
d767af421b
feat: Add vector search ( #2535 )
...
- Initializing the store with an 'embedding config' -> this contains the
'dims' (used to create the table) and the encoder object (rn langchain
embeddings object, though that is ......)
- Call setup() -> creates the vector table.
Each document has 1 or more vectors associated with it for each json
path in the embedding config.
Would welcome critique and requests!
Leaving the params as the defaults for pgvector but open to feedback if
you think it's important to be able to more transparently configure that
in setup()
```python
from typing import TypedDict, List, Dict, Any, Optional
from langchain_openai import OpenAIEmbeddings
from langgraph.graph import StateGraph
from langgraph.store.postgres import PostgresStore
emb_config = {
"dims": 1536, # OpenAI embedding dimensions
"embed": OpenAIEmbeddings(model="text-embedding-3-small"),
"distance_type": "cosine",
}
with PostgresStore.from_conn_string(
"postgres://postgres:postgres@localhost:5441",
embedding=emb_config,
) as store:
store.setup()
# Define the state type for our graph
class State(TypedDict):
query: str
results: Optional[List[Dict[str, Any]]]
def put_stuff(state: State) -> State:
docs = [
("doc1", {"text": "red apple in kitchen"}),
("doc2", {"text": "blue car in garage"}),
("doc3", {"text": "green apple on table"}),
]
for key, value in docs:
store.put(("docs",), key, value)
def search_stuff(state: State) -> State:
"""Search for documents using vector similarity."""
results = store.search(("docs",), query=state["query"])
return {"results": results}
builder = StateGraph(State)
builder.add_node(put_stuff)
builder.add_node(search_stuff)
builder.add_edge("__start__", "put_stuff")
builder.add_edge("put_stuff", "search_stuff")
# Compile
with PostgresStore.from_conn_string(
"postgres://postgres:postgres@localhost:5441",
embedding=emb_config,
) as store:
chain = builder.compile(store=store)
result = chain.invoke({"query": "sour apple"})
# Print results
for doc in result["results"]:
print(doc.key)
print(doc.value)
print(doc.response_metadata)
```
2024-11-28 04:40:12 +00:00
William FH and GitHub
8f649abd0a
Release PG Checkpointer ( #2536 )
2024-11-26 01:44:45 +00:00
Vadym Barda and GitHub
016a9c1936
checkpoint-postgres: release 2.0.3 ( #2455 )
2024-11-18 16:55:54 -05:00
Vadym Barda and GitHub
39d9cdbef0
checkpoint-postgres: release 2.0.2 ( #2183 )
2024-10-24 17:06:36 -04:00
William FH and GitHub
254b12a62d
Use AsyncBatch for postgres store ( #2020 )
2024-10-08 06:58:26 +00:00
Vadym Barda and GitHub
4a45f6c99a
langgraph, sqlite, postgres: update to use langgraph-checkpoint==2.0.0 ( #1946 )
2024-10-01 15:28:06 -04:00
William FH and GitHub
29f58fd9e5
Custom loads support in postgres checkpointer ( #1930 )
2024-09-30 16:42:23 -07:00
William FH and GitHub
97f79fc66b
Add Postgres Store Implementation ( #1906 )
2024-09-30 21:18:58 +00:00