fix(langgraph): emit valid UUIDs for exit-mode delta task_ids for langgraph-api (#8165)

Fix exit-mode DeltaChannel `PutWrites` task IDs so they remain valid RFC
UUIDs while preserving superstep ordering for `ORDER BY task_id, idx`.

Follow-up to #7730: `_put_exit_delta_writes` used `f"{step:08d}-{tid}"`,
which produces a 6-segment string Postgres rejects for
`checkpoint_writes.task_id uuid` (e.g. `invalid input syntax for type
uuid`) in LangGraph-API.

This new `exit_delta_task_id()` helper embeds the superstep in the first
UUID group and keeps the original task UUID in the remaining segments
(e.g. `00000001-0270-bf16-1ef8-fb321bef9f3d`).

**Breaking changes:** None.

**Verification:**
- `cd libs/langgraph && make format && make lint && make test
TEST="tests/test_delta_channel_exit_mode.py"` (12 passed)
- New unit test asserts synthetic IDs parse as UUID, sort by superstep,
and that the old format is invalid

---------

Signed-off-by: Quanzheng Long <prclqz@gmail.com>
Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
Quanzheng Long
2026-06-23 11:56:11 -07:00
committed by GitHub
co-authored by open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
parent 711b315502
commit 8c9d59c842
3 changed files with 33 additions and 1 deletions
@@ -1,5 +1,6 @@
from __future__ import annotations
import uuid
from collections.abc import Callable, Mapping
from datetime import datetime, timezone
from typing import Any, cast
@@ -34,6 +35,17 @@ def empty_checkpoint() -> Checkpoint:
)
def exit_delta_task_id(step: int, task_id: str) -> str:
"""Synthetic task id for exit-mode DeltaChannel writes.
Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
preserves chronological order while remaining a valid RFC UUID (required by
Postgres `checkpoint_writes.task_id uuid` columns).
"""
parts = str(uuid.UUID(task_id)).split("-")
return f"{step:08d}-{parts[1]}-{parts[2]}-{parts[3]}-{parts[4]}"
def delta_channels_to_snapshot(
channels: Mapping[str, BaseChannel],
counters_since_delta_snapshot: Mapping[str, tuple[int, int]],
+2 -1
View File
@@ -102,6 +102,7 @@ from langgraph.pregel._checkpoint import (
create_checkpoint,
delta_channels_to_snapshot,
empty_checkpoint,
exit_delta_task_id,
)
from langgraph.pregel._executor import (
AsyncBackgroundExecutor,
@@ -1272,7 +1273,7 @@ class PregelLoop:
},
)
for (step, tid), entries in grouped.items():
synth_tid = f"{step:08d}-{tid}"
synth_tid = exit_delta_task_id(step, tid)
if self.checkpointer_put_writes_accepts_task_path:
fut = self.submit(
self.checkpointer_put_writes,
@@ -6,6 +6,7 @@ channel), lazy stub creation when no parent exists, and proper read-path
reconstruction via ancestor walks.
"""
import uuid
from typing import Annotated, Any
import pytest
@@ -17,10 +18,28 @@ from typing_extensions import TypedDict
from langgraph.channels.delta import DeltaChannel
from langgraph.graph import START, StateGraph
from langgraph.graph.message import _messages_delta_reducer
from langgraph.pregel._checkpoint import exit_delta_task_id
pytestmark = pytest.mark.anyio
def test_exit_delta_task_id_is_valid_uuid_and_ordered() -> None:
"""Exit-mode synthetic task ids must parse as UUID and sort by superstep."""
tid = "4f7226e4-0270-bf16-1ef8-fb321bef9f3d"
id1 = exit_delta_task_id(1, tid)
id7 = exit_delta_task_id(7, tid)
uuid.UUID(id1)
uuid.UUID(id7)
assert id1 < id7
assert id1.split("-")[0] == "00000001"
assert id7.split("-")[0] == "00000007"
assert id1.endswith("-0270-bf16-1ef8-fb321bef9f3d")
with pytest.raises(ValueError):
uuid.UUID(f"00000001-{tid}")
def _build_graph(
checkpointer: InMemorySaver,
*,