diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 78222205f..5e61b50ed 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -469,7 +469,7 @@ class PostgresSaver(BasePostgresSaver): thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"].get("checkpoint_ns", "") checkpoint_id = get_checkpoint_id(config) - if checkpoint_id is None: + if not checkpoint_id: target = self.get_tuple(config) if target is None: return {ch: {"writes": []} for ch in channels} diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 2b813b391..5a00c99b1 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -417,7 +417,7 @@ class AsyncPostgresSaver(BasePostgresSaver): thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"].get("checkpoint_ns", "") checkpoint_id = get_checkpoint_id(config) - if checkpoint_id is None: + if not checkpoint_id: target = await self.aget_tuple(config) if target is None: return {ch: {"writes": []} for ch in channels} diff --git a/libs/checkpoint-postgres/tests/test_delta_pagination.py b/libs/checkpoint-postgres/tests/test_delta_pagination.py index 0c7407068..f9b483fc3 100644 --- a/libs/checkpoint-postgres/tests/test_delta_pagination.py +++ b/libs/checkpoint-postgres/tests/test_delta_pagination.py @@ -164,6 +164,40 @@ def test_sync_walk_reads_nothing_newer_than_the_target( assert read == _ids_from_target_down(configs) +def _empty_checkpoint_id(config: dict) -> dict: + return {"configurable": {**config["configurable"], "checkpoint_id": ""}} + + +async def test_async_empty_checkpoint_id_reads_from_the_latest_checkpoint() -> None: + async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver: + await saver.setup() + configs = await _abuild_chain(saver) + latest = await saver.aget_delta_channel_history( + config=configs[-1], channels=[CHANNEL] + ) + + result = await saver.aget_delta_channel_history( + config=_empty_checkpoint_id(configs[-1]), channels=[CHANNEL] + ) + + assert latest[CHANNEL]["writes"] + assert result == latest + + +def test_sync_empty_checkpoint_id_reads_from_the_latest_checkpoint() -> None: + with PostgresSaver.from_conn_string(DEFAULT_URI) as saver: + saver.setup() + configs = _build_chain(saver) + latest = saver.get_delta_channel_history(config=configs[-1], channels=[CHANNEL]) + + result = saver.get_delta_channel_history( + config=_empty_checkpoint_id(configs[-1]), channels=[CHANNEL] + ) + + assert latest[CHANNEL]["writes"] + assert result == latest + + async def test_root_target_has_no_history_and_still_terminates( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index d7369921d..ba77deb98 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -539,7 +539,7 @@ class SqliteSaver(BaseCheckpointSaver[str]): thread_id = str(config["configurable"]["thread_id"]) checkpoint_ns = config["configurable"].get("checkpoint_ns", "") checkpoint_id = get_checkpoint_id(config) - if checkpoint_id is None: + if not checkpoint_id: target = self.get_tuple(config) if target is None: return {ch: {"writes": []} for ch in channels} diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index b26e6ac9c..a62a35473 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -653,7 +653,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]): thread_id = str(config["configurable"]["thread_id"]) checkpoint_ns = config["configurable"].get("checkpoint_ns", "") checkpoint_id = get_checkpoint_id(config) - if checkpoint_id is None: + if not checkpoint_id: target = await self.aget_tuple(config) if target is None: return {ch: {"writes": []} for ch in channels} diff --git a/libs/checkpoint-sqlite/tests/test_delta_parent_walk.py b/libs/checkpoint-sqlite/tests/test_delta_parent_walk.py index 849752a5d..d8fc82698 100644 --- a/libs/checkpoint-sqlite/tests/test_delta_parent_walk.py +++ b/libs/checkpoint-sqlite/tests/test_delta_parent_walk.py @@ -65,6 +65,39 @@ async def test_async_walk_reaches_parent_whatever_the_id_order( assert got[CHANNEL] == EXPECTED +EMPTY_CHECKPOINT_ID: dict[str, Any] = { + "configurable": {**CONFIG["configurable"], "checkpoint_id": ""} +} + + +def test_sync_empty_checkpoint_id_reads_from_the_latest_checkpoint() -> None: + with SqliteSaver.from_conn_string(":memory:") as saver: + root = saver.put(CONFIG, _checkpoint("a-older", {CHANNEL: "seed"}), {}, {}) + saver.put_writes(root, [(CHANNEL, "write-root")], "task") + saver.put(root, _checkpoint("z-newer", {}), {}, {}) + + got = saver.get_delta_channel_history( + config=EMPTY_CHECKPOINT_ID, channels=[CHANNEL] + ) + + assert got[CHANNEL] == EXPECTED + + +async def test_async_empty_checkpoint_id_reads_from_the_latest_checkpoint() -> None: + async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: + root = await saver.aput( + CONFIG, _checkpoint("a-older", {CHANNEL: "seed"}), {}, {} + ) + await saver.aput_writes(root, [(CHANNEL, "write-root")], "task") + await saver.aput(root, _checkpoint("z-newer", {}), {}, {}) + + got = await saver.aget_delta_channel_history( + config=EMPTY_CHECKPOINT_ID, channels=[CHANNEL] + ) + + assert got[CHANNEL] == EXPECTED + + def test_walk_reaches_root_of_long_chain_with_descending_ids() -> None: steps = 40 with SqliteSaver.from_conn_string(":memory:") as saver: