mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
* chore(checkpoint): bump to checkpoint 4.2.0 and drop the stale saver patch langgraph-checkpoint 4.2.0 fixes the write dropped after a full -> delta migration (upstream langchain-ai/langgraph#8526) and keeps its own InMemorySaver override, so the compatibility patch in checkpoint_patches.py could neither detect the fix nor stand down: it read langgraph's version while InMemorySaver ships in langgraph-checkpoint, and it would have kept shadowing the upstream implementation on 4.2.0. The postgres release 3.1.2 locates plain-value delta seeds (upstream #8535). - floor langgraph-checkpoint>=4.2.0,<5.0 in the harness and raise the postgres extra to >=3.1.2,<3.2; langgraph's own >=4.1.0 would otherwise let a resolver reintroduce the bug now that the patch is gone - delete the patch, its version guard, and the three implementation-assertion tests; test_full_to_delta_migration_replays_on_same_thread stays as the gate and fails on 4.1.1 without the patch (verified by a one-off unpatch run) - langgraph stays 1.2.9 and langgraph-checkpoint-sqlite stays 3.1.1 Verification: make lint; delta checkpointer/cache/worker-resume suites green with the migration gate passing on memory and sqlite (postgres params skip without TEST_POSTGRES_URI). The full make test run before the packaging-pin fix below was 13 failed / 18523 passed; 12 of those failures reproduce on main with the same environment (git stash baseline, identical failure sets), 7 of them because the local config.yaml runs checkpoint_channel_mode: delta. The remaining one, the postgres-extra string pin in tests/test_checkpointer.py, is updated here. Upstream status for the surrounding gaps: cache parity across off/cold/warm states, postgres pagination (langchain-ai/langgraph#8448) and parallel-superstep replay order (#8382) are still open upstream, as is the abandoned-branch fork fix (#8548) that the delta resume linearization in runtime/runs/worker.py works around, so that path stays. * docs(changelog): keep checkpoint upgrade in Unreleased --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
126 lines
5.9 KiB
Python
126 lines
5.9 KiB
Python
"""Compatibility patches for third-party checkpoint machinery.
|
|
|
|
Lives at the top-level package (not ``deerflow.runtime``) so it can be
|
|
imported from ``deerflow.agents.thread_state`` without pulling in the heavy
|
|
``deerflow.runtime`` package __init__ (which eagerly imports the runs
|
|
machinery). Anchored from ``deerflow.agents.thread_state`` so every process
|
|
that builds a DeerFlow graph (gateway, workers, in-process LangGraph
|
|
runtime, tests) runs with the fixes in place.
|
|
|
|
One patch remains: ``BinaryOperatorAggregate`` unwrapping an ``Overwrite``
|
|
first write into an empty (MISSING) channel (#4380). The former
|
|
``InMemorySaver`` delta-history patch was removed: upstream fixed the dropped
|
|
first post-migration write in ``langgraph-checkpoint`` 4.2.0 (upstream #8526)
|
|
while keeping its own override, so a behavioural probe cannot see the fix.
|
|
The harness dependency floor (``langgraph-checkpoint>=4.2.0``) is what keeps
|
|
that bug out, and
|
|
``tests/test_delta_channel_checkpointers.py::test_full_to_delta_migration_replays_on_same_thread``
|
|
is the regression gate that fails when the fix is absent. Do not re-add a
|
|
version-guarded saver patch: the guard that used to live here read
|
|
``langgraph``'s version, but ``InMemorySaver`` ships in the independently
|
|
released ``langgraph-checkpoint`` distribution.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
from langgraph.channels.binop import BinaryOperatorAggregate
|
|
from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
|
|
from langgraph.types import Overwrite
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
_BINOP_PATCH_FLAG = "_deerflow_overwrite_first_write_patched"
|
|
_unpatched_binop_update = BinaryOperatorAggregate.update
|
|
|
|
|
|
def _as_overwrite(value: Any) -> tuple[bool, Any]:
|
|
"""Local stand-in for langgraph's private ``_get_overwrite``.
|
|
|
|
Matches only the public ``Overwrite`` *class* form - the sole form
|
|
DeerFlow's write paths produce for these Union channels (the branch and
|
|
``/state`` routes wrap replace-style writes in ``Overwrite(...)``).
|
|
Avoiding the underscored ``_get_overwrite`` import keeps an upstream
|
|
refactor that drops it - plausibly the very release that fixes the bug -
|
|
from failing this module's import and crashing startup before the probe
|
|
can stand the patch down. The dict sentinel form upstream also accepts is
|
|
an internal serialization detail DeerFlow never emits into these channels.
|
|
"""
|
|
if isinstance(value, Overwrite):
|
|
return True, value.value
|
|
return False, None
|
|
|
|
|
|
def _binop_first_write_stores_overwrite_wrapper() -> bool:
|
|
"""Probe whether upstream still stores an Overwrite first write literally.
|
|
|
|
Uses a Union-typed channel (no constructible default, so it starts
|
|
MISSING) - the same shape as ``ThreadState``'s ``sandbox`` / ``goal`` /
|
|
``todos`` / ``promoted`` channels.
|
|
"""
|
|
channel = BinaryOperatorAggregate(dict | None, lambda existing, new: new)
|
|
channel.key = "deerflow-overwrite-probe"
|
|
channel.update([Overwrite({"probe": True})])
|
|
return isinstance(channel.get(), Overwrite)
|
|
|
|
|
|
def _binop_update_unwrapping_empty_channel(self: Any, values: Sequence[Any]) -> bool:
|
|
"""``BinaryOperatorAggregate.update`` that unwraps an Overwrite first write.
|
|
|
|
Only intercepts the empty-channel + leading-Overwrite case; everything
|
|
else delegates to the upstream implementation. The intercepted case
|
|
mirrors upstream's own post-Overwrite batch semantics: later plain values
|
|
are skipped and a second Overwrite raises ``InvalidUpdateError``.
|
|
"""
|
|
if not self.is_available() and values:
|
|
is_overwrite, overwrite_value = _as_overwrite(values[0])
|
|
if is_overwrite:
|
|
self.value = overwrite_value
|
|
for value in values[1:]:
|
|
if _as_overwrite(value)[0]:
|
|
msg = create_error_message(
|
|
message="Can receive only one Overwrite value per super-step.",
|
|
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
|
|
)
|
|
raise InvalidUpdateError(msg)
|
|
return True
|
|
return _unpatched_binop_update(self, values)
|
|
|
|
|
|
def ensure_binop_overwrite_first_write_patch() -> None:
|
|
"""Fix ``Overwrite`` first writes being stored literally on empty channels.
|
|
|
|
Upstream ``BinaryOperatorAggregate.update`` seeds an empty channel
|
|
(``self.value is MISSING``) with ``values[0]`` verbatim - without the
|
|
Overwrite unwrapping the rest of the method applies. Channels whose type
|
|
is a Union (``SandboxState | None``, ``GoalState | None``, ...) have no
|
|
constructible default, so they start MISSING; a replace-style write into
|
|
a fresh thread (thread branching) or a never-written channel (state
|
|
update) then persists the ``Overwrite`` wrapper itself into the
|
|
checkpoint, and the next consumer crashes with ``TypeError: 'Overwrite'
|
|
object is not subscriptable`` (#4380). ``DeltaChannel.update`` already
|
|
unwraps in the same situation, so this also removes a behavioral
|
|
inconsistency between the two reducer channel types.
|
|
|
|
Idempotent. Guarded by a behavioral probe instead of a version pin: if a
|
|
future LangGraph unwraps the first write itself, the probe reports the
|
|
bug as absent and the patch stands down.
|
|
"""
|
|
if getattr(BinaryOperatorAggregate, _BINOP_PATCH_FLAG, False):
|
|
return
|
|
try:
|
|
if not _binop_first_write_stores_overwrite_wrapper():
|
|
# Upstream unwraps the first write itself: nothing to patch.
|
|
return
|
|
BinaryOperatorAggregate.update = _binop_update_unwrapping_empty_channel # type: ignore[method-assign]
|
|
setattr(BinaryOperatorAggregate, _BINOP_PATCH_FLAG, True)
|
|
except Exception:
|
|
logger.warning("Failed to apply the BinaryOperatorAggregate Overwrite first-write patch; leaving the upstream implementation untouched.", exc_info=True)
|
|
|
|
|
|
ensure_binop_overwrite_first_write_patch()
|