Use typ() instead of [] throughout DeltaChannel so reducers over dict
(and other non-list types) work correctly. fromCheckpoint(MISSING) now
leaves value as typ() from __init__ instead of overwriting with MISSING.
copy() uses value.copy() to handle dicts. update() initialises base from
typ() when value is MISSING. Add four tests covering the deepagents-style
dict-merge / file-deletion reducer pattern.
- Rename `_steps_since_rehydrate` → `_steps_since_snapshot` in DeltaChannel
for clarity (counts steps since the last snapshot, not since rehydration)
- Pre-seed cycle-detection `visited` set with current checkpoint ID in both
sync and async `_assemble_delta_channels` to prevent self-referential chains
- Add 4 new unit tests:
- `test_delta_channel_snapshot_every_emits_plain_list`: verifies counter
semantics and snapshot/delta transitions
- `test_delta_channel_snapshot_every_end_to_end`: graph-level smoke test
- `test_delta_channel_assembly_fast_path_returns_delta_value`: exercises
chain traversal via get_channel_blob returning DeltaValue then plain list
- `test_delta_channel_assembly_broken_chain_logs_warning`: partial chain
when get_tuple returns None
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add DeltaValue to SAFE_MSGPACK_TYPES so SQLite and other msgpack-based
savers don't emit "Deserializing unregistered type" warnings.
- Rename _is_diff_delta → _is_delta_value (leftover from DiffChannel rename).
- Parametrize benchmark by checkpointer: runs InMemory (fast-path) and
SQLite (get_tuple fallback) in the same table, sharing the _run_turns helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix InMemorySaver.get_channel_blob: use correct storage[thread_id][ns]
nesting and deserialize the checkpoint before extracting channel_versions.
- Pass checkpoint_id to after_checkpoint() in channels_from_checkpoint so
DeltaChannel seeds _last_checkpoint_id correctly on load; without this
every turn broke the chain at its boundary.
- Wire _assemble_delta_channels into _prepare_state_snapshot and
_aprepare_state_snapshot (get_state / get_state_history paths) and into
perform_superstep / aperform_superstep (update_state paths) — previously
only the loop __enter__ path did assembly.
- Fix test_get_channel_blob to use the correct storage structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Periodic full-snapshot checkpoints cap chain depth, trading a small
amount of extra storage for bounded reconstruction time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes a critical deadlock that occurs when _load_diff_chains calls self._cursor()
from within _load_blobs while the outer _load_checkpoint_tuple already holds
self._cursor(). On bare (non-pool) connections, the threading.Lock is not
reentrant, causing a deadlock.
Solution: Pass the cursor as a parameter to _load_diff_chains and _load_blobs
instead of acquiring a new cursor within those methods. Updated _load_checkpoint_tuple
to acquire a cursor once at the top level and pass it through the call chain.
Changes:
- Updated _load_blobs signature to accept optional cur parameter
- Updated _load_diff_chains signature (base and implementations) to accept optional cur parameter
- Modified _load_checkpoint_tuple in PostgresSaver to acquire cursor and pass it
- Modified _load_checkpoint_tuple_async to acquire cursor only when diff_payloads exist
- Removed nested self._cursor() calls in _load_diff_chains and _load_diff_chains_async
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `_load_diff_chains_async` to `AsyncPostgresSaver` and override
`_load_checkpoint_tuple` to inline blob-parsing and diff-chain
resolution via async point-lookup traversal, mirroring the sync
`PostgresSaver._load_diff_chains` implementation. Add integration test
`test_diff_channel_chain_reconstruction` that skips gracefully when
`langgraph` core is not installed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add logger.warning when a mid-chain blob is missing (fixes silent truncation bug)
- Add cycle guard to prevent infinite loops on corrupt blob stores
- Fix type annotation on diff_channels from dict[str, Any] to dict[str, str]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds DiffChannel, a new channel type that stores only per-step write
deltas in checkpoints and reconstructs the full list by replaying the
chain through the operator at load time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace duck-typing check with lazy import inside _is_diff_delta helper
function to avoid module-level circular dependency while using proper
isinstance semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add serde support for DiffDelta by implementing dump/load for the "diff" type tag.
This allows the checkpoint system to efficiently store delta objects by serializing
them as msgpack-encoded dicts with {"d": delta, "p": prev_version} structure.
The implementation uses runtime type checking to avoid circular imports and
leverages the existing msgpack ext hooks for proper deserialization of complex
types like LangChain messages.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
## Summary
- Bumps `langgraph` patch version from `1.1.8` to `1.1.9` in
`libs/langgraph/pyproject.toml`
## Test plan
- [ ] Verify version string is correct in `pyproject.toml`
- [ ] Confirm release workflow triggers on merge
**Description:**
When clients resume an interrupted subgraph with `Command(resume=...)`
plus an explicit `checkpoint_id` in the config (the pattern LangGraph
Studio and the API server emit on every resume), the subgraph restarts
from its first node instead of continuing at the interrupted node.
Fix: gate `ReplayState` propagation on `is_time_traveling` rather than
`is_replaying`, so a resume that happens to carry a head checkpoint_id
behaves the same as one with just a thread_id.
**Verification:** added regression test
`test_subgraph_interrupt_resume_with_explicit_head_checkpoint_id` (fails
on main, passes with fix, across memory/sqlite/sqlite_aes).
Full `test_time_travel.py`, `test_time_travel_async.py`,
`test_interruption.py`, and all pregel subgraph/interrupt/resume/replay
tests still pass.
Co-authored-by: Jessie Ibarra <jessie.ibarra@langgraph.dev>
## Summary
Removes the `add_handler()` overrides on `_GraphCallbackManager` and
`_AsyncGraphCallbackManager` that reject handlers not inheriting from
`GraphCallbackHandler`. This fixes a regression in 1.1.7 where
`opentelemetry-instrumentation-langchain` (and likely other libraries
that patch `BaseCallbackManager.__init__`) crash with `TypeError:
handlers must inherit GraphCallbackHandler` at invocation time.
## Why this is safe
The strict type check is redundant — `_configure_graph_callbacks` and
`_filter_graph_handlers` already filter handlers to
`GraphCallbackHandler` instances at construction time. Non-graph
handlers that enter via external patches (like OTel's monkey-patch) are
harmless because `handle_event("on_interrupt", ...)` /
`handle_event("on_resume", ...)` will simply no-op on handlers that
don't implement those methods.
## What changed
- Deleted `add_handler()` override from `_GraphCallbackManager` (was
lines 248-255)
- Deleted `add_handler()` override from `_AsyncGraphCallbackManager`
(was lines 324-331)
- No other changes — 18 lines removed, 0 added
## Test plan
- [x] All 8 existing `test_graph_callbacks.py` tests pass (`make test
TEST=tests/test_graph_callbacks.py`)
- [x] `make lint` passes
- [x] `make format` passes (no changes needed)
- [x] Verified fix locally: `LangchainInstrumentor().instrument()` +
`create_react_agent()` + `graph.ainvoke()` no longer raises `TypeError`
- [x] Verified the graph lifecycle callbacks (`on_interrupt`,
`on_resume`) still work correctly
Closes#7543
---------
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Resolves https://github.com/langchain-ai/langchain/issues/35585
This would previously raise KeyError:
```python
from typing import Annotated
from langchain_core.tools import tool
from langchain.agents import create_agent
from typing_extensions import NotRequired
from langgraph.prebuilt import InjectedState
from langchain.agents import AgentState
class CustomAgentState(AgentState):
city: NotRequired[str]
@tool
def get_weather(city: Annotated[str | None, InjectedState("city")] = None) -> str:
"""Get weather for a given city."""
if city is None:
city = "Boston"
return f"It's always sunny in {city}!"
agent = create_agent(
model="claude-sonnet-4-6",
tools=[get_weather],
system_prompt="You are a helpful assistant",
state_schema=CustomAgentState,
)
input_message = {
"role": "user",
"content": "What's the weather?",
}
result = agent.invoke({"messages": [input_message]})
for m in result["messages"]:
m.pretty_print()
```
---------
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
# Fix: Create fork checkpoint on subgraph time travel
## Problem
When time-traveling to a subgraph checkpoint that has an interrupt, and
then resuming, the resume would load the **wrong state** — it would pick
up the original execution's latest checkpoint instead of the
time-traveled one.
This happened because replaying from a subgraph checkpoint never created
a new parent checkpoint. If the replay hit an interrupt before
`after_tick()` ran, no checkpoint was written at all, so the parent's
"latest" checkpoint was still the old one from the original execution.
## Fix
When the loop detects a time-travel replay (not an `update_state` fork),
it now **eagerly writes a fork checkpoint** at the start of the tick.
This ensures:
1. The parent thread's latest checkpoint points to the replayed state
2. Subsequent `Command(resume=...)` calls find the correct checkpoint
3. Stale `INTERRUPT` pending writes from the old checkpoint are cleared
(they reference old task IDs)
Additionally, the subgraph replay logic now uses the **parent checkpoint
ID** (from `prev_checkpoint_config`) when resolving subgraph checkpoints
during time-travel, matching the existing behavior for `update_state`
forks.
## Checkpoint flow diagrams
### Before fix: time travel leaves no fork
```
Original execution:
C0 (start) --> C1 (step_a) --> C2 (ask_1 interrupt) --> C3 (resume) --> C4 (ask_2 interrupt) --> C5 (done)
Time travel to C2 (subgraph config):
Replay runs... hits interrupt... no new checkpoint written.
Parent "latest" is still C5.
Command(resume="new_answer"):
Loads C5 (wrong!) instead of the replayed C2 state.
```
### After fix: time travel creates a fork
```
Original execution:
C0 --> C1 --> C2 --> C3 --> C4 --> C5 (done)
Time travel to C2 (subgraph config):
C0 --> C1 --> C2 --> C3 --> C4 --> C5
\
F1 (fork, source="fork") <-- new latest
Command(resume="new_answer"):
Loads F1 (correct!) --> resumes from the right state.
After full resume:
C0 --> C1 --> C2 --> C3 --> C4 --> C5
\
F1 --> F2 (ask_1 result) --> F3 (ask_2 interrupt) --> F4 (done)
```
### Manual fork via `update_state` (unchanged)
```
C0 --> C1 --> C2 --> C3
\
U1 (source="update") <-- created by update_state()
This path already worked. The fix skips update/fork sources
so existing behavior is preserved.
```
## Changes
- **`libs/langgraph/langgraph/pregel/_loop.py`**:
- Extract `is_time_traveling` flag from the existing replay detection
logic for reuse
- Write a fork checkpoint (`source="fork"`) eagerly at the start of a
time-travel tick, before execution begins
- Clear stale `INTERRUPT` pending writes when creating the fork (they
reference old task IDs that won't match the new checkpoint)
- Unify subgraph replay ID resolution: check `source in ("update",
"fork")` instead of a separate `is_time_traveling` condition, since the
new fork checkpoint now has `source="fork"`
- **`libs/langgraph/tests/test_time_travel.py`** and
**`test_time_travel_async.py`**: Added 4 new test cases (sync + async):
- `test_replay_from_before_interrupt_then_resume` — replays from a
checkpoint before an interrupt, resumes with a new answer, and verifies
the full checkpoint history (source, next, values) at each stage
- `test_subgraph_time_travel_resume_from_first_interrupt` — time-travels
to a subgraph's first interrupt, resumes both interrupts with new
answers, and verifies the fork creates a new branch while preserving the
original
- `test_subgraph_time_travel_resume_from_second_interrupt` —
time-travels to a subgraph's second interrupt, resumes with a new
answer, and verifies the first interrupt's original answer is preserved
- `test_subgraph_time_travel_checkpoint_pattern` — verifies the fork
checkpoint branches from the correct replay point and that the full
checkpoint tree is correct after resume
- **`libs/langgraph/tests/test_pregel.py`** /
**`test_pregel_async.py`**: Updated existing
`test_weather_subgraph_state` to account for the new fork checkpoint
appearing in history (history length increases by 1)
Fixes #
<!-- Replace everything above this line with a 1-2 sentence description
of your change. Keep the "Fixes #xx" keyword and update the issue
number. -->
Read the full contributing guidelines:
https://docs.langchain.com/oss/python/contributing/overview
> **All contributions must be in English.** See the [language
policy](https://docs.langchain.com/oss/python/contributing/overview#language-policy).
If you paste a large clearly AI generated description here your PR may
be IGNORED or CLOSED!
Thank you for contributing to LangGraph! Follow these steps to have your
pull request considered as ready for review.
1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION
- feat(langgraph): add multi-tenant support
- Allowed TYPE and SCOPE values:
https://github.com/langchain-ai/langgraph/blob/main/.github/workflows/pr_lint.yml#L19-L43
2. PR description:
- Write 1-2 sentences summarizing the change.
- The `Fixes #xx` line at the top is **required** for external
contributions — update the issue number and keep the keyword. This links
your PR to the approved issue and auto-closes it on merge.
- If there are any breaking changes, please clearly describe them.
- If this PR depends on another PR being merged first, please include
"Depends on #PR_NUMBER" in the description.
3. Run `make format`, `make lint` and `make test` from the root of the
package(s) you've modified.
- We will not consider a PR unless these three are passing in CI.
4. How did you verify your code works?
Additional guidelines:
- All external PRs must link to an issue or discussion where a solution
has been approved by a maintainer, and you must be assigned to that
issue. PRs without prior approval will be closed.
- PRs should not touch more than one package unless absolutely
necessary.
- Do not update the `uv.lock` files or add dependencies to
`pyproject.toml` files (even optional ones) unless you have explicit
permission to do so by a maintainer.
## Social handles (optional)
<!-- If you'd like a shoutout on release, add your socials below -->
Twitter: @
LinkedIn: https://linkedin.com/in/