langgraph-api 0.12.0.dev9__py3-none-any.whl → 0.12.0.dev10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.12.0.dev9"
1
+ __version__ = "0.12.0.dev10"
@@ -24,6 +24,14 @@ from langgraph_grpc_common.checkpointer import GrpcCheckpointer
24
24
  from langgraph_api import config, timing
25
25
  from langgraph_api.asyncio import as_asynccontextmanager
26
26
  from langgraph_api.grpc.client import get_shared_client
27
+
28
+ # Used as the per-instance serializer for ``GrpcCheckpointer`` below so
29
+ # checkpoint payloads honour the same ``USE_PICKLE_FALLBACK`` /
30
+ # encryption / ``allowed_json_modules`` policy as the in-process Python
31
+ # ``Checkpointer``. Scoped to checkpointer RPCs only (via the contextvar
32
+ # in ``langgraph_grpc_common.serde``) so non-checkpointer gRPC ops keep
33
+ # using the safer ``JsonPlusSerializer`` default.
34
+ from langgraph_api.serde import Serializer as _ApiSerializer
27
35
  from langgraph_api.timing import profiled_import
28
36
  from langgraph_api.utils.config import run_in_executor
29
37
 
@@ -320,7 +328,24 @@ async def get_checkpointer(
320
328
  ):
321
329
  return cast(
322
330
  "FullCheckpointerProtocol",
323
- GrpcCheckpointer(get_stub=_get_shared_checkpointer_stub),
331
+ GrpcCheckpointer(
332
+ get_stub=_get_shared_checkpointer_stub,
333
+ serializer=_ApiSerializer(),
334
+ ),
335
+ )
336
+
337
+ from langgraph_api.feature_flags import ( # noqa: PLC0415
338
+ IS_POSTGRES_BACKEND,
339
+ PREFER_GRPC_CHECKPOINTER,
340
+ )
341
+
342
+ if IS_POSTGRES_BACKEND and PREFER_GRPC_CHECKPOINTER:
343
+ return cast(
344
+ "FullCheckpointerProtocol",
345
+ GrpcCheckpointer(
346
+ get_stub=_get_shared_checkpointer_stub,
347
+ serializer=_ApiSerializer(),
348
+ ),
324
349
  )
325
350
 
326
351
  from langgraph_runtime.checkpoint import Checkpointer # noqa: PLC0415
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import asyncio
22
22
  import contextlib
23
23
  import inspect
24
+ import time
24
25
  from collections.abc import Awaitable, Callable
25
26
  from typing import Any, cast, get_args
26
27
  from uuid import UUID, uuid4
@@ -124,6 +125,12 @@ def _multitask_strategy_from_run_start(params: dict[str, Any]) -> str:
124
125
  EventSink = Callable[[dict[str, Any]], Awaitable[None] | None]
125
126
 
126
127
 
128
+ # ``set_joint_status`` commits interrupts after the stream event; gRPC can lag.
129
+ _IN_FLIGHT_THREAD_STATUS = "busy"
130
+ _INTERRUPT_SETTLE_TIMEOUT_SECONDS = 5.0
131
+ _INTERRUPT_SETTLE_POLL_INTERVAL_SECONDS = 0.05
132
+
133
+
127
134
  # Protocol v2 commands this server implements. Anything outside this
128
135
  # set returns ``unknown_command`` up front from ``handle_command`` so
129
136
  # pre-session dispatch does not mislabel typos (or removed methods
@@ -593,13 +600,11 @@ class ThreadRunManager:
593
600
  # this HTTP request (the stateless ``POST /commands`` transport) its
594
601
  # source task hasn't had a chance to emit ``input.requested`` yet —
595
602
  # ``_pending_interrupts`` is still empty. In that case, fall back to
596
- # the thread-state check so we don't reject legitimate resumes with a
597
- # fresh handle. The WebSocket path (long-lived session) hits the
598
- # in-memory check and skips the DB round-trip.
603
+ # the thread-state check (``_collect_settled_interrupt_ids``) so we
604
+ # don't reject legitimate resumes with a fresh handle.
599
605
  #
600
606
  # The thread-state fallback is fetched at most once per batch and
601
- # cached in ``thread_state_ids``: a batch of N entries that all miss
602
- # the session would otherwise issue N identical ``State.get`` calls.
607
+ # cached in ``thread_state_ids``.
603
608
  thread_state_ids: set[str] | None = None
604
609
  thread_state_fetched = False
605
610
  for interrupt_id, claimed_namespace, _ in entries:
@@ -623,7 +628,7 @@ class ThreadRunManager:
623
628
  # only, so trust the client-claimed namespace and validate by
624
629
  # existence (the id is a namespace hash, so that's sufficient).
625
630
  if not thread_state_fetched:
626
- thread_state_ids = await self._collect_persisted_interrupt_ids()
631
+ thread_state_ids = await self._collect_settled_interrupt_ids()
627
632
  thread_state_fetched = True
628
633
  # Fail open when the lookup is unavailable (``None``): only reject
629
634
  # when a definitive id set was read and lacks the target. The
@@ -669,6 +674,7 @@ class ThreadRunManager:
669
674
  {
670
675
  "assistant_id": assistant_id,
671
676
  "input": resume_input,
677
+ "force_resume": True,
672
678
  "update": params.get("update"),
673
679
  "goto": params.get("goto"),
674
680
  "config": params.get("config"),
@@ -732,7 +738,8 @@ class ThreadRunManager:
732
738
  has_interrupts = await self._has_pending_interrupts()
733
739
 
734
740
  is_resume = params.get("input") is not None and (
735
- (current_run is not None and current_status == "interrupted")
741
+ params.get("force_resume")
742
+ or (current_run is not None and current_status == "interrupted")
736
743
  or has_interrupts
737
744
  )
738
745
 
@@ -1108,6 +1115,40 @@ class ThreadRunManager:
1108
1115
  found.add(entry_id)
1109
1116
  return found
1110
1117
 
1118
+ async def _fetch_thread_status(self) -> str | None:
1119
+ """Return durable thread status, or ``None`` if unreadable."""
1120
+ from langgraph_runtime.database import connect # noqa: PLC0415
1121
+
1122
+ try:
1123
+ async with connect() as conn:
1124
+ result = await self._threads.get(conn, self._thread_id)
1125
+ thread = await anext(result) if hasattr(result, "__anext__") else result
1126
+ except Exception:
1127
+ return None
1128
+ status = thread.get("status") if _is_record(thread) else None
1129
+ return status if isinstance(status, str) else None
1130
+
1131
+ async def _collect_settled_interrupt_ids(self) -> set[str] | None:
1132
+ """Persisted interrupt ids, polling while the thread is still ``busy``."""
1133
+ ids = await self._collect_persisted_interrupt_ids()
1134
+ if ids is None:
1135
+ return None
1136
+ if ids:
1137
+ return ids
1138
+ deadline = time.monotonic() + _INTERRUPT_SETTLE_TIMEOUT_SECONDS
1139
+ while True:
1140
+ status = await self._fetch_thread_status()
1141
+ if status != _IN_FLIGHT_THREAD_STATUS:
1142
+ return await self._collect_persisted_interrupt_ids()
1143
+ if time.monotonic() >= deadline:
1144
+ return set()
1145
+ await asyncio.sleep(_INTERRUPT_SETTLE_POLL_INTERVAL_SECONDS)
1146
+ ids = await self._collect_persisted_interrupt_ids()
1147
+ if ids is None:
1148
+ return None
1149
+ if ids:
1150
+ return ids
1151
+
1111
1152
  async def _has_pending_interrupts(self) -> bool:
1112
1153
  # Prefer the session's in-memory pending interrupts (populated the
1113
1154
  # instant ``input.requested`` surfaces) to skip a DB round-trip;
@@ -1116,7 +1157,7 @@ class ThreadRunManager:
1116
1157
  # the ``current_status == "interrupted"`` signal for resume-vs-start.
1117
1158
  if self._session is not None and self._session._pending_interrupts:
1118
1159
  return True
1119
- found = await self._collect_persisted_interrupt_ids()
1160
+ found = await self._collect_settled_interrupt_ids()
1120
1161
  return bool(found)
1121
1162
 
1122
1163
  # ------------------------------------------------------------------
@@ -320,6 +320,14 @@ def _normalize_state_message(value: dict[str, Any]) -> dict[str, Any]:
320
320
  if msg_type in ("ai", "human") and isinstance(value.get("example"), bool):
321
321
  message["example"] = value["example"]
322
322
 
323
+ # Preserve ``response_metadata`` when present. It is a first-class protocol
324
+ # message field, and HITL flows rely on it: an interrupt's card is carried
325
+ # on ``AIMessage.response_metadata`` (e.g. ``{"cards": ...}``) and the
326
+ # frontend pushes that message into state via ``respond(decision, update)``.
327
+ # We only forward a non-empty record to keep the common (empty) case minimal.
328
+ if _is_record(value.get("response_metadata")) and value["response_metadata"]:
329
+ message["response_metadata"] = value["response_metadata"]
330
+
323
331
  if msg_type == "tool":
324
332
  if isinstance(value.get("tool_call_id"), str):
325
333
  message["tool_call_id"] = value["tool_call_id"]
@@ -35,6 +35,16 @@ FF_V2_EVENT_STREAMING = os.getenv("FF_V2_EVENT_STREAMING", "true").lower() in (
35
35
  "yes",
36
36
  )
37
37
 
38
+ # When true on the postgres runtime, use ``GrpcCheckpointer``.
39
+ # Inmem edition is unaffected (``IS_POSTGRES_BACKEND`` is false there).
40
+ # Custom checkpointer (``backend=custom`` in ``LANGGRAPH_CHECKPOINTER``) and
41
+ # Mongo (``backend=mongo``) is also unaffected.
42
+ PREFER_GRPC_CHECKPOINTER = os.getenv("PREFER_GRPC_CHECKPOINTER", "false").lower() in (
43
+ "true",
44
+ "1",
45
+ "yes",
46
+ )
47
+
38
48
  # In langgraph <= 1.0.3, we automatically subscribed to updates stream events to surface interrupts. In langgraph 1.0.4 we include interrupts in values events (which we are automatically subscribed to), so we no longer need to implicitly subscribe to updates stream events
39
49
  # Strip prerelease suffixes (e.g. "0a5" -> 0) so versions like 1.2.0a5 still
40
50
  # parse correctly; fall back to (0, 0, 0) only if no leading digits at all.
@@ -242,3 +242,17 @@ class CheckpointerServicerImpl(CheckpointerServicer):
242
242
  context.set_code(grpc.StatusCode.INTERNAL)
243
243
  context.set_details(f"Checkpointer Prune failed: {e}")
244
244
  raise
245
+
246
+ async def GetDeltaChannelHistory(
247
+ self,
248
+ request: checkpointer_pb2.GetDeltaChannelHistoryRequest,
249
+ context: grpc_aio.ServicerContext,
250
+ ) -> checkpointer_pb2.GetDeltaChannelHistoryResponse:
251
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
252
+ context.set_details(
253
+ "GetDeltaChannelHistory is not implemented by the Python "
254
+ "checkpointer servicer"
255
+ )
256
+ raise NotImplementedError(
257
+ "GetDeltaChannelHistory not implemented by the Python checkpointer servicer"
258
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.12.0.dev9
3
+ Version: 0.12.0.dev10
4
4
  Author-email: Will Fu-Hinthorn <will@langchain.dev>, Josh Rogers <josh@langchain.dev>, Parker Rule <parker@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -16,7 +16,7 @@ Requires-Dist: jsonschema-rs<0.45,>=0.20.0
16
16
  Requires-Dist: langchain-core>=0.3.64
17
17
  Requires-Dist: langchain-protocol<0.1,>=0.0.18
18
18
  Requires-Dist: langgraph-checkpoint<5,>=3.0.1
19
- Requires-Dist: langgraph-runtime-inmem<0.32.0.dev0,>=0.31.0.dev0
19
+ Requires-Dist: langgraph-runtime-inmem<0.33.0.dev0,>=0.32.0.dev0
20
20
  Requires-Dist: langgraph-sdk>=0.3.5
21
21
  Requires-Dist: langgraph<2,>=0.4.10
22
22
  Requires-Dist: langsmith[otel]>=0.6.3
@@ -1,4 +1,4 @@
1
- langgraph_api/__init__.py,sha256=TQcm_pAAhX_K7rWzStt5FmHrPCh8MmiBLpm5tEiShdE,28
1
+ langgraph_api/__init__.py,sha256=ET9JJ1BldzP5SKSEuzw5ZRYUjqRmfoJF5DqqnNc_Taw,29
2
2
  langgraph_api/_factory_utils.py,sha256=5JsiJbg_YocVSryN2jwoZTg03-eyymlWMK6sKCmXwz0,5756
3
3
  langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
4
4
  langgraph_api/asyncio.py,sha256=c-YE-14N7_AP1GzifsbP14XnhLsmxT2P916KXruerpI,10573
@@ -7,7 +7,7 @@ langgraph_api/cli.py,sha256=ATtS9s9Cx7QNiGPJceKnMCko29A25ZA-xz39fdxmgfg,22389
7
7
  langgraph_api/command.py,sha256=d-k8h6H4ix1n7fSZ-Zb01NbSkEyqrD6cMKfDFXEIYEw,821
8
8
  langgraph_api/cron_scheduler.py,sha256=OwFzCwD86pfMNpfm9Z8bBS_WY9bBfDxGnq8_7wXurdA,6016
9
9
  langgraph_api/errors.py,sha256=zlMW99wAzNkz2xfik-HMkl_wMqmRFvs1j8V-_DZbAUc,2553
10
- langgraph_api/feature_flags.py,sha256=IcSPG_EfPyE-PP4rxsNRSIRRCTt83KOHKK7IqHjh178,2242
10
+ langgraph_api/feature_flags.py,sha256=P7yQ9jXMHO3dPWv51iWGAVrPg3LbR9m4aCT_7t1UtZw,2624
11
11
  langgraph_api/graph.py,sha256=fKlnNv4Wn1ThYiRsYgT7A4EVvJssymWtI12gnNJQIVU,37734
12
12
  langgraph_api/http.py,sha256=7hPxKbj-xoAKcm7iucBpT5nM_hXOgGVCPbBsCD693Cw,6977
13
13
  langgraph_api/http_metrics.py,sha256=etxbZNmYxdb58DVLNkHP7S-N6njXPTiQh2OWKMaIZi8,5336
@@ -36,7 +36,7 @@ langgraph_api/validation.py,sha256=XyeKyt7jAICmIlT_b0J0mv2YbwIbNoe4m6zEmfk9gOA,1
36
36
  langgraph_api/webhook.py,sha256=qXEtkE6orek2POeOQmPRsEarJgXIYp-LBrZB-OwITxc,9572
37
37
  langgraph_api/worker.py,sha256=HGirbQ1i1hxaayORoihsQ6JMVAq3VsrC8CUxAHwY9t0,21151
38
38
  langgraph_api/_checkpointer/__init__.py,sha256=ofJTJLGy7Hsuzhj-2dpfDvrDloM0BzlhTzvZOdR9K8U,2223
39
- langgraph_api/_checkpointer/_adapter.py,sha256=1Mdb3B_bg6EQ_3uJfk6ImiobIiAPdaMQVaruDBzVkE8,20833
39
+ langgraph_api/_checkpointer/_adapter.py,sha256=1jJi0vXoNKeCwNP26W8bmYrwe-fOWFqepnovYFt14W0,21782
40
40
  langgraph_api/_checkpointer/protocol.py,sha256=udgYKMNtKWG_eLDwYkHXV3b2bZLZg8Rsfm3fjkhU-rU,3635
41
41
  langgraph_api/api/__init__.py,sha256=Zu1ew3dxYZu7cLRAjn-6HcYmtuQBdihlVFMKMJ77Y3c,9269
42
42
  langgraph_api/api/a2a.py,sha256=VPllgqfoLUQD6Eqob3RjcegjtKgLhphNGTrTqbNLoIY,95135
@@ -78,9 +78,9 @@ langgraph_api/event_streaming/capabilities.py,sha256=qjVbhCjl1VEQPGeiDxeJAhYGI_7
78
78
  langgraph_api/event_streaming/constants.py,sha256=eGsm-NvOlqV3gNxDO5vlr00FdngmgEQf59zuHSi_74E,1378
79
79
  langgraph_api/event_streaming/event_normalizers.py,sha256=5bVSqGPW-Uh7WX91qgTfwpK433pCSv_wchpbzW8TLi4,2794
80
80
  langgraph_api/event_streaming/namespace.py,sha256=aJDFt45Or2_bQdRpKJgdFhBgTDj6PYl7Coz5GzfbM84,1509
81
- langgraph_api/event_streaming/service.py,sha256=Z41ioDEDxXqDzXCGqo0L5-Qm_1Y8x_yp8yxu4gQ8nz8,48468
81
+ langgraph_api/event_streaming/service.py,sha256=axQ2sDawAtsE96ZeJcBB8q8DbzS2VN-OqsdCM1rDF58,50063
82
82
  langgraph_api/event_streaming/session.py,sha256=8kx5nxJvJlfIu5fFzQCxEl-8UBJEtxftBLhCYgXMNwo,75947
83
- langgraph_api/event_streaming/state_normalizers.py,sha256=fEjb9AWihhZeqkGOlTBYmph06XAP3AvyAkNmNdquqAU,13113
83
+ langgraph_api/event_streaming/state_normalizers.py,sha256=vgT4O4tJPr9VDBMn1EP994ieDGDYP43sROnOyLjkEAE,13659
84
84
  langgraph_api/event_streaming/types.py,sha256=RyZqfqgH-jmmmmAFQj5f6nH9M1rGK93zVG7nlmvqZgc,3647
85
85
  langgraph_api/grpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  langgraph_api/grpc/client.py,sha256=ypSgZWclK7OCKLGW8gD_LZwwgsgQEahJqc8mBWF8Rug,15578
@@ -93,7 +93,7 @@ langgraph_api/grpc/ops/crons.py,sha256=oGPW9qW-J4H9baX7Jsee1opsFnXfThHLHh_N47mDf
93
93
  langgraph_api/grpc/ops/runs.py,sha256=dD2Rne9EBU7z4lGF3OicpIMZNU4MBNbbli1Hw5gz2tw,41479
94
94
  langgraph_api/grpc/ops/threads.py,sha256=SJraV838nZ0m6wRrCia7sLYh8KK1WfY7x5xmcspZLek,48140
95
95
  langgraph_api/grpc/servicers/__init__.py,sha256=l8yqTA_gMbIj0xHw5-RZYo0JbjL-EpFD8RxPMgW_-bA,422
96
- langgraph_api/grpc/servicers/checkpointer.py,sha256=1gGTQcJrMSXc0sqP69YW9gexvBAeLNPvBQnsvxviI3M,9927
96
+ langgraph_api/grpc/servicers/checkpointer.py,sha256=aeB2hzsDxbb9i-ildx-kylpDxaYtpAUSG6rphOzbDOs,10482
97
97
  langgraph_api/grpc/servicers/encryption.py,sha256=_qpcrjURpnm7PvdWOronNt3FtmqR1mjwxUb5bWMj32I,12962
98
98
  langgraph_api/js/.gitignore,sha256=l5yI6G_V6F1600I1IjiUKn87f4uYIrBAYU1MOyBBhg4,59
99
99
  langgraph_api/js/.prettierrc,sha256=0es3ovvyNIqIw81rPQsdt1zCQcOdBqyR_DMbFE4Ifms,19
@@ -163,20 +163,20 @@ hatch_build.py,sha256=Nn9N-EFfVqmH5q3iCUrhqX6zc7qT1jLI3gWSBN2xgYQ,1738
163
163
  logging.json,sha256=sBy8HDDPuucpLbLUD6e8t5fsiQjJoklZsbVHi2qQ7aQ,367
164
164
  openapi.json,sha256=0N-D4j0_3ruH6nMF4k5lnqmpV-aoB5Se-WWheCQuQQs,219552
165
165
  langgraph_grpc_common/__init__.py,sha256=sO7jFHekQxt6zZPhTPZAWLYLB_S8w7-VuJ7NyWFlFNo,97
166
- langgraph_grpc_common/checkpointer.py,sha256=WLDShg7rqC3dkxFudLNVElH_1yxe9LlqEdZYh0k1Azs,9876
167
- langgraph_grpc_common/serde.py,sha256=ejr7EKWK5ZZBVolilgrBDViixnPEvg9brPKiVVEst9Y,4996
166
+ langgraph_grpc_common/checkpointer.py,sha256=u3ERFgqnUCpCYpOSxKR_6Jvo_tR4yDvq7qU-EL61bSg,14022
167
+ langgraph_grpc_common/serde.py,sha256=pxIhgxtrXtwWVhAlLE2LAeSAuKk2ZHtnvAP3DRDT2Qw,6184
168
168
  langgraph_grpc_common/conversion/__init__.py,sha256=pLsOrRtmF9YkgmCPhcfym6lYOB7PCJTRItyo1YK_AT8,208
169
169
  langgraph_grpc_common/conversion/_compat.py,sha256=MIEo69I7tfaPskunPGwBCKheksLwxpw-z-4O3hvO9og,4010
170
- langgraph_grpc_common/conversion/checkpoint.py,sha256=ZxNhUOSdf9O-bHer5FXNvha9hiYF1UT7HRQ8qVV_pCY,8747
170
+ langgraph_grpc_common/conversion/checkpoint.py,sha256=Ty81cijafeal7blzzXbYFjWpu9iXVb_spFLBZ2FvmgI,12045
171
171
  langgraph_grpc_common/conversion/config.py,sha256=HjlHRX-_GpKa1m0RWVL1PEZQCvA3w_8wKVYrtaT06do,13015
172
172
  langgraph_grpc_common/conversion/durability.py,sha256=FWhqjzVN4JrrsAL9AwsJkvIorQGNRl14w97Va-WZRk0,991
173
- langgraph_grpc_common/conversion/struct.py,sha256=TgpDYnOUt3TjBcOi0h56Dn92OnCFjQAVi-XpLVvumgw,770
174
- langgraph_grpc_common/conversion/value.py,sha256=ILTEgKcc590EFP2fXvGGSPm-3MHMV4L4l4sqDIiKJuk,7564
173
+ langgraph_grpc_common/conversion/struct.py,sha256=yk4jpPdywzn8UosAohun21lU5D_IIglOY9rkBhiG-gA,2072
174
+ langgraph_grpc_common/conversion/value.py,sha256=eZ9fDqKje9tKsO8A_6f0BPSI7lt94Vzuu3NNEEYLFBM,8343
175
175
  langgraph_grpc_common/proto/__init__.py,sha256=cFnJ18b62FnqKcZHz9wjKES0QetC_5twG47G1W0A2mM,1283
176
- langgraph_grpc_common/proto/checkpointer_pb2.py,sha256=YaKpQpn8kBpZt5mcyCfCY1XsTUCvKtTiwcMfpF4Eu7Q,6346
177
- langgraph_grpc_common/proto/checkpointer_pb2.pyi,sha256=R91LHQBWBOUOlQUFBKI81OEdK6FSzmQoXOKKcT2yuRk,15664
178
- langgraph_grpc_common/proto/checkpointer_pb2_grpc.py,sha256=PAYyUXuskG2vdfRPe5OHuo1_pLzrZZIbR6UWi13DcMo,18090
179
- langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi,sha256=vbZf41Y3pcnNJeopd4cRJEVdZVa0hgDLpfms5ExcVx4,8781
176
+ langgraph_grpc_common/proto/checkpointer_pb2.py,sha256=c-gaVwNHm7sUzThJDzumME2M_nP4B1RjEki4ZG7-kjM,7915
177
+ langgraph_grpc_common/proto/checkpointer_pb2.pyi,sha256=rGUxQyNFifmu8OPy0S418sCWSBQY_CVckobPXtFfZfs,20763
178
+ langgraph_grpc_common/proto/checkpointer_pb2_grpc.py,sha256=MLhZ7M4g9gjfXZqWenk93oO2uIZSybbFoaeU8bauFPA,20176
179
+ langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi,sha256=8fPL90WjHnMaREG7l9qYt5fQePeT8YFWQ0qVy56QtYw,10210
180
180
  langgraph_grpc_common/proto/core_api_pb2.py,sha256=xbzqAKhwvgc_-J5BlqS7krhOlgKsRgzrb7uhGax1jbo,50813
181
181
  langgraph_grpc_common/proto/core_api_pb2.pyi,sha256=lN5P83sM9_gvlPGOkDQIUEq9QDiiffmY2MIQOZtdV7s,206457
182
182
  langgraph_grpc_common/proto/core_api_pb2_grpc.py,sha256=zIfWY664xF4r9c48Ys1OcYN4dEmtIWAwYiCzP7gsyLI,81342
@@ -229,8 +229,8 @@ langgraph_grpc_common/proto/errors_pb2.py,sha256=JI6x-vBK1AE7DHZ5DQwN1mZWF6C4xTR
229
229
  langgraph_grpc_common/proto/errors_pb2.pyi,sha256=rd3-BYUH8V-aO66taL7OOblaLgdrDtf1Vcd38GUoVVM,2181
230
230
  langgraph_grpc_common/proto/errors_pb2_grpc.py,sha256=2-LwQ0OPGo-NtC0269q7Fw6GPBxnTLYWq3xP5Eq0_YA,886
231
231
  langgraph_grpc_common/proto/errors_pb2_grpc.pyi,sha256=uC9Wnq6uyg488QiONpJ0ba1s_iouQCOYsjd_FDd1XUM,495
232
- langgraph_api-0.12.0.dev9.dist-info/METADATA,sha256=CxpUdl-vbUCBqXAauoGgp16E_eGWROwJhkyjv58fLPo,4631
233
- langgraph_api-0.12.0.dev9.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
234
- langgraph_api-0.12.0.dev9.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
235
- langgraph_api-0.12.0.dev9.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
236
- langgraph_api-0.12.0.dev9.dist-info/RECORD,,
232
+ langgraph_api-0.12.0.dev10.dist-info/METADATA,sha256=WhXiyRWSggW7mhNlI8pkHYcnABwggJkuXPXR-jLpak0,4632
233
+ langgraph_api-0.12.0.dev10.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
234
+ langgraph_api-0.12.0.dev10.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
235
+ langgraph_api-0.12.0.dev10.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
236
+ langgraph_api-0.12.0.dev10.dist-info/RECORD,,
@@ -8,10 +8,14 @@ from collections.abc import (
8
8
  Callable,
9
9
  Coroutine,
10
10
  Iterator,
11
+ Mapping,
11
12
  Sequence,
12
13
  )
14
+ from contextlib import AbstractContextManager, nullcontext
13
15
  from typing import TYPE_CHECKING, Any, TypeVar, cast
14
16
 
17
+ import grpc
18
+ import grpc.aio
15
19
  from langgraph.checkpoint.base import (
16
20
  BaseCheckpointSaver,
17
21
  ChannelVersions,
@@ -20,6 +24,7 @@ from langgraph.checkpoint.base import (
20
24
  CheckpointTuple,
21
25
  )
22
26
 
27
+ from langgraph_grpc_common import serde as _grpc_serde
23
28
  from langgraph_grpc_common.conversion import checkpoint as ckpt_conv
24
29
  from langgraph_grpc_common.conversion.config import (
25
30
  config_from_proto,
@@ -30,6 +35,7 @@ from langgraph_grpc_common.proto import checkpointer_pb2
30
35
 
31
36
  if TYPE_CHECKING:
32
37
  from langchain_core.runnables import RunnableConfig
38
+ from langgraph.checkpoint.serde.base import SerializerProtocol
33
39
 
34
40
  from langgraph_grpc_common.proto.checkpointer_pb2_grpc import CheckpointerStub
35
41
 
@@ -49,17 +55,43 @@ class GrpcCheckpointer(BaseCheckpointSaver):
49
55
  get_stub: StubProvider,
50
56
  retry: RetryFn | None = None,
51
57
  retry_context_prefix: str | None = None,
58
+ serializer: SerializerProtocol | None = None,
52
59
  ) -> None:
60
+ """Construct a gRPC checkpointer client.
61
+
62
+ Args:
63
+ get_stub: Async factory returning the ``CheckpointerStub`` to
64
+ use for the next RPC. Called per-call so the caller can
65
+ pool / refresh connections.
66
+ retry: Optional retry wrapper. When provided, every RPC is
67
+ dispatched through ``retry(func, context)``.
68
+ retry_context_prefix: Label included in ``retry``'s context
69
+ string. Defaults to the concrete class name.
70
+ serializer: Optional ``SerializerProtocol`` to use for
71
+ payload (de)serialization on *this* checkpointer
72
+ instance only. Scoped via a :class:`~contextvars.ContextVar`
73
+ so unrelated gRPC ops sharing
74
+ ``langgraph_grpc_common.conversion.*`` keep using the
75
+ process-wide default
76
+ """
53
77
  super().__init__(serde=None)
54
78
  self._get_stub = get_stub
55
79
  self._retry = retry
56
80
  self._retry_context_prefix = retry_context_prefix or type(self).__name__
81
+ self._serializer = serializer
57
82
  self.latest_iter = None
58
83
  try:
59
84
  self._loop: asyncio.AbstractEventLoop | None = asyncio.get_running_loop()
60
85
  except RuntimeError:
61
86
  self._loop = None
62
87
 
88
+ def _scoped(self) -> AbstractContextManager[None]:
89
+ """Bind ``self._serializer`` for the duration of a conversion call.
90
+ """
91
+ if self._serializer is None:
92
+ return nullcontext()
93
+ return _grpc_serde.use_serializer(self._serializer)
94
+
63
95
  @property
64
96
  def loop(self) -> asyncio.AbstractEventLoop:
65
97
  if self._loop is None:
@@ -94,13 +126,15 @@ class GrpcCheckpointer(BaseCheckpointSaver):
94
126
  return None
95
127
 
96
128
  async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
97
- request = checkpointer_pb2.GetTupleRequest(config=config_to_proto(config))
129
+ with self._scoped():
130
+ request = checkpointer_pb2.GetTupleRequest(config=config_to_proto(config))
98
131
 
99
132
  async def _request() -> CheckpointTuple | None:
100
133
  response = await (await self._stub()).GetTuple(request)
101
134
  if not response.HasField("checkpoint_tuple"):
102
135
  return None
103
- return ckpt_conv.checkpoint_tuple_from_proto(response.checkpoint_tuple)
136
+ with self._scoped():
137
+ return ckpt_conv.checkpoint_tuple_from_proto(response.checkpoint_tuple)
104
138
 
105
139
  return await self._call("aget_tuple", _request)
106
140
 
@@ -128,16 +162,18 @@ class GrpcCheckpointer(BaseCheckpointSaver):
128
162
  metadata: CheckpointMetadata,
129
163
  new_versions: ChannelVersions,
130
164
  ) -> RunnableConfig:
131
- request = checkpointer_pb2.PutRequest(
132
- config=config_to_proto(config),
133
- checkpoint=ckpt_conv.checkpoint_to_proto(checkpoint),
134
- metadata=ckpt_conv.checkpoint_metadata_to_proto(metadata),
135
- new_versions={k: str(v) for k, v in new_versions.items()},
136
- )
165
+ with self._scoped():
166
+ request = checkpointer_pb2.PutRequest(
167
+ config=config_to_proto(config),
168
+ checkpoint=ckpt_conv.checkpoint_to_proto(checkpoint),
169
+ metadata=ckpt_conv.checkpoint_metadata_to_proto(metadata),
170
+ new_versions={k: str(v) for k, v in new_versions.items()},
171
+ )
137
172
 
138
173
  async def _request() -> RunnableConfig:
139
174
  response = await (await self._stub()).Put(request)
140
- next_config = config_from_proto(response.next_config)
175
+ with self._scoped():
176
+ next_config = config_from_proto(response.next_config)
141
177
  if next_config is None:
142
178
  raise ValueError("Unexpected None value for next_config")
143
179
  return next_config
@@ -160,12 +196,13 @@ class GrpcCheckpointer(BaseCheckpointSaver):
160
196
  task_id: str,
161
197
  task_path: str = "",
162
198
  ) -> None:
163
- request = checkpointer_pb2.PutWritesRequest(
164
- config=config_to_proto(config),
165
- writes=ckpt_conv.writes_to_proto(writes),
166
- task_id=task_id,
167
- task_path=task_path,
168
- )
199
+ with self._scoped():
200
+ request = checkpointer_pb2.PutWritesRequest(
201
+ config=config_to_proto(config),
202
+ writes=ckpt_conv.writes_to_proto(writes),
203
+ task_id=task_id,
204
+ task_path=task_path,
205
+ )
169
206
 
170
207
  async def _request() -> None:
171
208
  await (await self._stub()).PutWrites(request)
@@ -212,11 +249,12 @@ class GrpcCheckpointer(BaseCheckpointSaver):
212
249
  before: RunnableConfig | None = None,
213
250
  limit: int | None = None,
214
251
  ) -> AsyncIterator[CheckpointTuple]:
215
- request = checkpointer_pb2.ListRequest(
216
- config=config_to_proto(config) if config is not None else None,
217
- filter_json=convert_dict_to_json_bytes(filter) or b"",
218
- before=config_to_proto(before) if before is not None else None,
219
- )
252
+ with self._scoped():
253
+ request = checkpointer_pb2.ListRequest(
254
+ config=config_to_proto(config) if config is not None else None,
255
+ filter_json=convert_dict_to_json_bytes(filter) or b"",
256
+ before=config_to_proto(before) if before is not None else None,
257
+ )
220
258
  if limit is not None:
221
259
  request.limit = limit
222
260
 
@@ -225,7 +263,11 @@ class GrpcCheckpointer(BaseCheckpointSaver):
225
263
 
226
264
  response = await self._call("alist", _request)
227
265
  for proto_tuple in response.checkpoint_tuples:
228
- if (tup := ckpt_conv.checkpoint_tuple_from_proto(proto_tuple)) is not None:
266
+ # Bind inside the loop body so the override does not leak
267
+ # across ``yield`` into the consumer's frame.
268
+ with self._scoped():
269
+ tup = ckpt_conv.checkpoint_tuple_from_proto(proto_tuple)
270
+ if tup is not None:
229
271
  yield tup
230
272
 
231
273
  def delete_thread(self, thread_id: str) -> None:
@@ -298,3 +340,55 @@ class GrpcCheckpointer(BaseCheckpointSaver):
298
340
  next_v = current_v + 1
299
341
  next_h = random.random()
300
342
  return f"{next_v:032}.{next_h:.16f}"
343
+
344
+ def get_delta_channel_history(
345
+ self,
346
+ *,
347
+ config: RunnableConfig,
348
+ channels: Sequence[str],
349
+ ) -> Mapping[str, dict[str, Any]]:
350
+ """Synchronous wrapper for ``aget_delta_channel_history``.
351
+
352
+ langgraph >= 1.2 calls the async variant from the loop, but the
353
+ BaseCheckpointSaver protocol expects sync wrappers to exist.
354
+ """
355
+ return self._run_sync(
356
+ self.aget_delta_channel_history(config=config, channels=channels)
357
+ )
358
+
359
+ async def aget_delta_channel_history(
360
+ self,
361
+ *,
362
+ config: RunnableConfig,
363
+ channels: Sequence[str],
364
+ ) -> Mapping[str, dict[str, Any]]:
365
+ if not channels:
366
+ return {}
367
+ configurable = config.get("configurable", {}) if config else {}
368
+ request = checkpointer_pb2.GetDeltaChannelHistoryRequest(
369
+ thread_id=configurable.get("thread_id", "") or "",
370
+ checkpoint_ns=configurable.get("checkpoint_ns", "") or "",
371
+ # Empty checkpoint_id => server resolves the latest checkpoint.
372
+ checkpoint_id=configurable.get("checkpoint_id", "") or "",
373
+ channels=list(channels),
374
+ )
375
+
376
+ async def _request() -> checkpointer_pb2.GetDeltaChannelHistoryResponse:
377
+ return await (await self._stub()).GetDeltaChannelHistory(request)
378
+
379
+ try:
380
+ response = await self._call("aget_delta_channel_history", _request)
381
+ except grpc.aio.AioRpcError as e:
382
+ if e.code() == grpc.StatusCode.UNIMPLEMENTED:
383
+ # The backend has no native fast path for delta-channel
384
+ # reconstruction (e.g. the Go mongo/sqlite checkpointers).
385
+ # Fall back to BaseCheckpointSaver's parent-chain walk, which
386
+ # drives aget_tuple (-> gRPC GetTuple) per ancestor. langgraph
387
+ # calls this method unguarded, so we must degrade gracefully
388
+ # rather than raise.
389
+ return await super().aget_delta_channel_history(
390
+ config=config, channels=channels
391
+ )
392
+ raise
393
+ with self._scoped():
394
+ return ckpt_conv.delta_channel_history_from_proto(response.entries)
@@ -1,3 +1,4 @@
1
+ from collections.abc import Mapping
1
2
  from collections.abc import Sequence as SequenceType
2
3
  from typing import Any, Literal, cast
3
4
 
@@ -16,7 +17,7 @@ from langgraph_grpc_common.conversion.config import (
16
17
  )
17
18
  from langgraph_grpc_common.conversion.struct import dict_from_raw_map, raw_map_from_dict
18
19
  from langgraph_grpc_common.conversion.value import (
19
- any_to_serialized_value,
20
+ base_value_to_proto,
20
21
  send_to_proto,
21
22
  value_from_proto,
22
23
  value_to_proto,
@@ -86,11 +87,7 @@ def checkpoint_to_proto(checkpoint: Checkpoint) -> engine_common_pb2.Checkpoint:
86
87
  )
87
88
  )
88
89
  else:
89
- checkpoint_proto.channel_values[k].CopyFrom(
90
- engine_common_pb2.ChannelValue(
91
- serialized_value=any_to_serialized_value(v)
92
- )
93
- )
90
+ checkpoint_proto.channel_values[k].CopyFrom(base_value_to_proto(v))
94
91
 
95
92
  return checkpoint_proto
96
93
 
@@ -241,3 +238,95 @@ def prune_strategy_to_proto(
241
238
  case "delete_all":
242
239
  return checkpointer_pb2.PruneRequest.PruneStrategy.DELETE_ALL
243
240
  raise ValueError("Unknown prune strategy: " + strategy)
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # DeltaChannelHistory conversion
245
+ # ---------------------------------------------------------------------------
246
+ #
247
+ # ``DeltaChannelHistory`` is a TypedDict from langgraph >= 1.2:
248
+ #
249
+ # class DeltaChannelHistory(TypedDict, total=False):
250
+ # seed: Any # optional snapshot value
251
+ # writes: list[PendingWrite] # (task_id, channel, value) tuples
252
+ #
253
+ # These helpers serialize/deserialize that shape to/from the wire-format
254
+ # ``DeltaChannelHistoryEntry`` proto.
255
+ #
256
+ # We accept and return plain ``dict[str, dict]`` rather than importing
257
+ # the ``DeltaChannelHistory`` TypedDict — langgraph treats the TypedDict
258
+ # structurally, so any dict with the right keys is accepted by the
259
+ # consumer (langgraph's ``DeltaChannel.replay_writes``).
260
+
261
+
262
+ def delta_channel_history_entry_to_proto(
263
+ entry: Mapping[str, Any],
264
+ *,
265
+ channel: str,
266
+ ) -> checkpointer_pb2.DeltaChannelHistoryEntry:
267
+ """Encode one channel's ``DeltaChannelHistory`` dict as proto.
268
+
269
+ Args:
270
+ entry: Mapping with optional ``seed`` and optional ``writes``.
271
+ ``writes`` is a sequence of ``(task_id, channel, value)`` tuples
272
+ (langgraph's ``PendingWrite``).
273
+ channel: The channel name this entry corresponds to. We pass it to
274
+ ``value_to_proto`` so the TASKS channel special-cases the
275
+ seed/write conversion if it ever appears here.
276
+ """
277
+ pb_entry = checkpointer_pb2.DeltaChannelHistoryEntry()
278
+ if "seed" in entry:
279
+ pb_entry.seed.CopyFrom(value_to_proto(channel, entry["seed"]))
280
+ for write in entry.get("writes", ()) or ():
281
+ task_id, write_channel, write_value = write
282
+ pb_entry.writes.append(
283
+ engine_common_pb2.PendingWrite(
284
+ task_id=str(task_id),
285
+ channel=str(write_channel),
286
+ value=value_to_proto(write_channel, write_value),
287
+ )
288
+ )
289
+ return pb_entry
290
+
291
+
292
+ def delta_channel_history_to_proto(
293
+ history: Mapping[str, Mapping[str, Any]],
294
+ ) -> dict[str, checkpointer_pb2.DeltaChannelHistoryEntry]:
295
+ """Encode the full per-channel mapping for the response proto."""
296
+ return {
297
+ ch: delta_channel_history_entry_to_proto(entry, channel=ch)
298
+ for ch, entry in history.items()
299
+ }
300
+
301
+
302
+ def delta_channel_history_entry_from_proto(
303
+ entry: checkpointer_pb2.DeltaChannelHistoryEntry,
304
+ ) -> dict[str, Any]:
305
+ """Decode one channel's proto entry to a ``DeltaChannelHistory`` dict.
306
+
307
+ The ``seed`` key is omitted from the result when the proto has no
308
+ seed set — matches the Python source's "absence means MISSING"
309
+ contract (see ``_assemble_delta_history`` in
310
+ ``storage_postgres/langgraph_runtime_postgres/checkpoint.py``).
311
+ """
312
+ out: dict[str, Any] = {
313
+ "writes": [
314
+ (write.task_id, write.channel, value_from_proto(write.value))
315
+ for write in entry.writes
316
+ ],
317
+ }
318
+ if entry.HasField("seed"):
319
+ out["seed"] = value_from_proto(entry.seed)
320
+ return out
321
+
322
+
323
+ def delta_channel_history_from_proto(
324
+ entries: Mapping[str, checkpointer_pb2.DeltaChannelHistoryEntry],
325
+ ) -> dict[str, dict[str, Any]]:
326
+ """Decode the full per-channel response proto map."""
327
+ return {
328
+ ch: delta_channel_history_entry_from_proto(entry)
329
+ for ch, entry in entries.items()
330
+ }
331
+
332
+
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from collections.abc import Mapping
2
3
  from typing import Any
3
4
 
@@ -19,8 +20,40 @@ def _default_serializer(obj: Any) -> Any:
19
20
  raise TypeError(f"Type is not JSON serializable: {type(obj).__name__}")
20
21
 
21
22
 
23
+ _SURROGATE_RE = re.compile(r"[\ud800-\udfff]")
24
+
25
+
26
+ def _replace_surrogates(o: Any) -> Any:
27
+ """Recursively replace lone-surrogate codepoints (orjson rejects them).
28
+
29
+ Mirrors ``_sanitise`` in ``langgraph_grpc_common.serde`` so metadata
30
+ encode falls back gracefully instead of raising. Without this,
31
+ fixtures that intentionally embed surrogates (e.g.
32
+ ``test_thread_copy``'s ``"surrogate"`` field on
33
+ ``langgraph==0.4.10`` which mirrors input into metadata) crash the
34
+ whole put.
35
+ """
36
+ if isinstance(o, str):
37
+ return _SURROGATE_RE.sub("?", o) if _SURROGATE_RE.search(o) else o
38
+ if isinstance(o, Mapping):
39
+ return {_replace_surrogates(k): _replace_surrogates(v) for k, v in o.items()}
40
+ if isinstance(o, (list, tuple, set)):
41
+ return type(o)(_replace_surrogates(x) for x in o)
42
+ return o
43
+
44
+
22
45
  def raw_map_from_dict(d: Mapping[str, Any]) -> Mapping[str, bytes]:
23
- return {k: orjson.dumps(v, default=_default_serializer) for k, v in d.items()}
46
+ out: dict[str, bytes] = {}
47
+ for k, v in d.items():
48
+ try:
49
+ out[k] = orjson.dumps(v, default=_default_serializer)
50
+ except (TypeError, ValueError):
51
+ # orjson rejects e.g. lone surrogates and a few other oddities
52
+ # that the in-process Python ``Checkpointer.aput`` happily
53
+ # stores via its own serde. Sanitize and retry rather than
54
+ # failing the entire put.
55
+ out[k] = orjson.dumps(_replace_surrogates(v), default=_default_serializer)
56
+ return out
24
57
 
25
58
 
26
59
  def dict_from_raw_map(m: Mapping[str, bytes]) -> dict[str, Any]:
@@ -18,7 +18,17 @@ def serialized_value_from_proto(value: engine_common_pb2.SerializedValue) -> Any
18
18
 
19
19
 
20
20
  def any_to_serialized_value(value: Any) -> engine_common_pb2.SerializedValue:
21
- if isinstance(value, tuple):
21
+ # Wrap *bare* tuples (e.g. ``(task_id, channel, payload)``-style writes)
22
+ # in a list so the downstream msgpack consumer sees the expected
23
+ # list-of-tuples shape. NamedTuples (detected by ``_fields``) MUST NOT be
24
+ # wrapped: they have registered serde handlers — notably
25
+ # ``_DeltaSnapshot`` from langgraph >= 1.2, which serializes via the
26
+ # ``EXT_DELTA_SNAPSHOT`` msgpack ext code — and wrapping them in a list
27
+ # buries the NamedTuple inside a generic sequence, defeating the handler
28
+ # and causing the snapshot value to round-trip as ``[inner_list]`` instead
29
+ # of the original ``_DeltaSnapshot(inner_list)``. That manifested as
30
+ # ``DeltaChannel`` reconstruction reading back ``[[seed], delta1, ...]``.
31
+ if isinstance(value, tuple) and not hasattr(value, "_fields"):
22
32
  value = [value]
23
33
  encoding, ser_val = serde.get_serializer().dumps_typed(value)
24
34
  return engine_common_pb2.SerializedValue(encoding=encoding, value=bytes(ser_val))
@@ -26,7 +26,7 @@ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
26
26
  from . import engine_common_pb2 as engine__common__pb2
27
27
 
28
28
 
29
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63heckpointer.proto\x12\x0c\x63heckpointer\x1a\x1bgoogle/protobuf/empty.proto\x1a\x13\x65ngine-common.proto\"\x97\x02\n\nPutRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12,\n\ncheckpoint\x18\x02 \x01(\x0b\x32\x18.engineCommon.Checkpoint\x12\x32\n\x08metadata\x18\x03 \x01(\x0b\x32 .engineCommon.CheckpointMetadata\x12?\n\x0cnew_versions\x18\x04 \x03(\x0b\x32).checkpointer.PutRequest.NewVersionsEntry\x1a\x32\n\x10NewVersionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8f\x01\n\x10PutWritesRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12#\n\x06writes\x18\x02 \x03(\x0b\x32\x13.engineCommon.Write\x12\x0f\n\x07task_id\x18\x03 \x01(\t\x12\x11\n\ttask_path\x18\x04 \x01(\t\"\x86\x01\n\x0c\x43\x61pabilities\x12\x1e\n\x16supports_delete_thread\x18\x01 \x01(\x08\x12\x16\n\x0esupports_prune\x18\x02 \x01(\x08\x12 \n\x18supports_delete_for_runs\x18\x03 \x01(\x08\x12\x1c\n\x14supports_copy_thread\x18\x04 \x01(\x08\"\xa8\x01\n\x0bListRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12\x13\n\x0b\x66ilter_json\x18\x02 \x01(\x0c\x12\x32\n\x06\x62\x65\x66ore\x18\x03 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12\x12\n\x05limit\x18\x04 \x01(\x03H\x00\x88\x01\x01\x42\x08\n\x06_limit\"(\n\x13\x44\x65leteThreadRequest\x12\x11\n\tthread_id\x18\x01 \x01(\t\"\'\n\x14\x44\x65leteForRunsRequest\x12\x0f\n\x07run_ids\x18\x01 \x03(\t\"A\n\x11\x43opyThreadRequest\x12\x16\n\x0e\x66rom_thread_id\x18\x01 \x01(\t\x12\x14\n\x0cto_thread_id\x18\x02 \x01(\t\"\xa1\x01\n\x0cPruneRequest\x12\x12\n\nthread_ids\x18\x01 \x03(\t\x12:\n\x08strategy\x18\x02 \x01(\x0e\x32(.checkpointer.PruneRequest.PruneStrategy\"A\n\rPruneStrategy\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0f\n\x0bKEEP_LATEST\x10\x01\x12\x0e\n\nDELETE_ALL\x10\x02\"E\n\x0fGetTupleRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\"F\n\x0bPutResponse\x12\x37\n\x0bnext_config\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\"H\n\x0cListResponse\x12\x38\n\x11\x63heckpoint_tuples\x18\x01 \x03(\x0b\x32\x1d.engineCommon.CheckpointTuple\"e\n\x10GetTupleResponse\x12<\n\x10\x63heckpoint_tuple\x18\x01 \x01(\x0b\x32\x1d.engineCommon.CheckpointTupleH\x00\x88\x01\x01\x42\x13\n\x11_checkpoint_tuple2\xfc\x04\n\x0c\x43heckpointer\x12:\n\x03Put\x12\x18.checkpointer.PutRequest\x1a\x19.checkpointer.PutResponse\x12\x43\n\tPutWrites\x12\x1e.checkpointer.PutWritesRequest\x1a\x16.google.protobuf.Empty\x12\x45\n\x0fGetCapabilities\x12\x16.google.protobuf.Empty\x1a\x1a.checkpointer.Capabilities\x12=\n\x04List\x12\x19.checkpointer.ListRequest\x1a\x1a.checkpointer.ListResponse\x12I\n\x08GetTuple\x12\x1d.checkpointer.GetTupleRequest\x1a\x1e.checkpointer.GetTupleResponse\x12I\n\x0c\x44\x65leteThread\x12!.checkpointer.DeleteThreadRequest\x1a\x16.google.protobuf.Empty\x12K\n\rDeleteForRuns\x12\".checkpointer.DeleteForRunsRequest\x1a\x16.google.protobuf.Empty\x12\x45\n\nCopyThread\x12\x1f.checkpointer.CopyThreadRequest\x1a\x16.google.protobuf.Empty\x12;\n\x05Prune\x12\x1a.checkpointer.PruneRequest\x1a\x16.google.protobuf.EmptyB?Z=github.com/langchain-ai/langgraph-api/core/internal/engine/pbb\x06proto3')
29
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63heckpointer.proto\x12\x0c\x63heckpointer\x1a\x1bgoogle/protobuf/empty.proto\x1a\x13\x65ngine-common.proto\"\x97\x02\n\nPutRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12,\n\ncheckpoint\x18\x02 \x01(\x0b\x32\x18.engineCommon.Checkpoint\x12\x32\n\x08metadata\x18\x03 \x01(\x0b\x32 .engineCommon.CheckpointMetadata\x12?\n\x0cnew_versions\x18\x04 \x03(\x0b\x32).checkpointer.PutRequest.NewVersionsEntry\x1a\x32\n\x10NewVersionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8f\x01\n\x10PutWritesRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12#\n\x06writes\x18\x02 \x03(\x0b\x32\x13.engineCommon.Write\x12\x0f\n\x07task_id\x18\x03 \x01(\t\x12\x11\n\ttask_path\x18\x04 \x01(\t\"\x86\x01\n\x0c\x43\x61pabilities\x12\x1e\n\x16supports_delete_thread\x18\x01 \x01(\x08\x12\x16\n\x0esupports_prune\x18\x02 \x01(\x08\x12 \n\x18supports_delete_for_runs\x18\x03 \x01(\x08\x12\x1c\n\x14supports_copy_thread\x18\x04 \x01(\x08\"\xa8\x01\n\x0bListRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12\x13\n\x0b\x66ilter_json\x18\x02 \x01(\x0c\x12\x32\n\x06\x62\x65\x66ore\x18\x03 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\x12\x12\n\x05limit\x18\x04 \x01(\x03H\x00\x88\x01\x01\x42\x08\n\x06_limit\"(\n\x13\x44\x65leteThreadRequest\x12\x11\n\tthread_id\x18\x01 \x01(\t\"\'\n\x14\x44\x65leteForRunsRequest\x12\x0f\n\x07run_ids\x18\x01 \x03(\t\"A\n\x11\x43opyThreadRequest\x12\x16\n\x0e\x66rom_thread_id\x18\x01 \x01(\t\x12\x14\n\x0cto_thread_id\x18\x02 \x01(\t\"\xa1\x01\n\x0cPruneRequest\x12\x12\n\nthread_ids\x18\x01 \x03(\t\x12:\n\x08strategy\x18\x02 \x01(\x0e\x32(.checkpointer.PruneRequest.PruneStrategy\"A\n\rPruneStrategy\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0f\n\x0bKEEP_LATEST\x10\x01\x12\x0e\n\nDELETE_ALL\x10\x02\"E\n\x0fGetTupleRequest\x12\x32\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\"F\n\x0bPutResponse\x12\x37\n\x0bnext_config\x18\x01 \x01(\x0b\x32\".engineCommon.EngineRunnableConfig\"H\n\x0cListResponse\x12\x38\n\x11\x63heckpoint_tuples\x18\x01 \x03(\x0b\x32\x1d.engineCommon.CheckpointTuple\"e\n\x10GetTupleResponse\x12<\n\x10\x63heckpoint_tuple\x18\x01 \x01(\x0b\x32\x1d.engineCommon.CheckpointTupleH\x00\x88\x01\x01\x42\x13\n\x11_checkpoint_tuple\"~\n\x18\x44\x65ltaChannelHistoryEntry\x12-\n\x04seed\x18\x01 \x01(\x0b\x32\x1a.engineCommon.ChannelValueH\x00\x88\x01\x01\x12*\n\x06writes\x18\x02 \x03(\x0b\x32\x1a.engineCommon.PendingWriteB\x07\n\x05_seed\"r\n\x1dGetDeltaChannelHistoryRequest\x12\x11\n\tthread_id\x18\x01 \x01(\t\x12\x15\n\rcheckpoint_ns\x18\x02 \x01(\t\x12\x15\n\rcheckpoint_id\x18\x03 \x01(\t\x12\x10\n\x08\x63hannels\x18\x04 \x03(\t\"\xc4\x01\n\x1eGetDeltaChannelHistoryResponse\x12J\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x39.checkpointer.GetDeltaChannelHistoryResponse.EntriesEntry\x1aV\n\x0c\x45ntriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.checkpointer.DeltaChannelHistoryEntry:\x02\x38\x01\x32\xf1\x05\n\x0c\x43heckpointer\x12:\n\x03Put\x12\x18.checkpointer.PutRequest\x1a\x19.checkpointer.PutResponse\x12\x43\n\tPutWrites\x12\x1e.checkpointer.PutWritesRequest\x1a\x16.google.protobuf.Empty\x12\x45\n\x0fGetCapabilities\x12\x16.google.protobuf.Empty\x1a\x1a.checkpointer.Capabilities\x12=\n\x04List\x12\x19.checkpointer.ListRequest\x1a\x1a.checkpointer.ListResponse\x12I\n\x08GetTuple\x12\x1d.checkpointer.GetTupleRequest\x1a\x1e.checkpointer.GetTupleResponse\x12I\n\x0c\x44\x65leteThread\x12!.checkpointer.DeleteThreadRequest\x1a\x16.google.protobuf.Empty\x12K\n\rDeleteForRuns\x12\".checkpointer.DeleteForRunsRequest\x1a\x16.google.protobuf.Empty\x12\x45\n\nCopyThread\x12\x1f.checkpointer.CopyThreadRequest\x1a\x16.google.protobuf.Empty\x12;\n\x05Prune\x12\x1a.checkpointer.PruneRequest\x1a\x16.google.protobuf.Empty\x12s\n\x16GetDeltaChannelHistory\x12+.checkpointer.GetDeltaChannelHistoryRequest\x1a,.checkpointer.GetDeltaChannelHistoryResponseB?Z=github.com/langchain-ai/langgraph-api/core/internal/engine/pbb\x06proto3')
30
30
 
31
31
  _globals = globals()
32
32
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -36,6 +36,8 @@ if not _descriptor._USE_C_DESCRIPTORS:
36
36
  _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/langchain-ai/langgraph-api/core/internal/engine/pb'
37
37
  _globals['_PUTREQUEST_NEWVERSIONSENTRY']._loaded_options = None
38
38
  _globals['_PUTREQUEST_NEWVERSIONSENTRY']._serialized_options = b'8\001'
39
+ _globals['_GETDELTACHANNELHISTORYRESPONSE_ENTRIESENTRY']._loaded_options = None
40
+ _globals['_GETDELTACHANNELHISTORYRESPONSE_ENTRIESENTRY']._serialized_options = b'8\001'
39
41
  _globals['_PUTREQUEST']._serialized_start=87
40
42
  _globals['_PUTREQUEST']._serialized_end=366
41
43
  _globals['_PUTREQUEST_NEWVERSIONSENTRY']._serialized_start=316
@@ -64,6 +66,14 @@ if not _descriptor._USE_C_DESCRIPTORS:
64
66
  _globals['_LISTRESPONSE']._serialized_end=1351
65
67
  _globals['_GETTUPLERESPONSE']._serialized_start=1353
66
68
  _globals['_GETTUPLERESPONSE']._serialized_end=1454
67
- _globals['_CHECKPOINTER']._serialized_start=1457
68
- _globals['_CHECKPOINTER']._serialized_end=2093
69
+ _globals['_DELTACHANNELHISTORYENTRY']._serialized_start=1456
70
+ _globals['_DELTACHANNELHISTORYENTRY']._serialized_end=1582
71
+ _globals['_GETDELTACHANNELHISTORYREQUEST']._serialized_start=1584
72
+ _globals['_GETDELTACHANNELHISTORYREQUEST']._serialized_end=1698
73
+ _globals['_GETDELTACHANNELHISTORYRESPONSE']._serialized_start=1701
74
+ _globals['_GETDELTACHANNELHISTORYRESPONSE']._serialized_end=1897
75
+ _globals['_GETDELTACHANNELHISTORYRESPONSE_ENTRIESENTRY']._serialized_start=1811
76
+ _globals['_GETDELTACHANNELHISTORYRESPONSE_ENTRIESENTRY']._serialized_end=1897
77
+ _globals['_CHECKPOINTER']._serialized_start=1900
78
+ _globals['_CHECKPOINTER']._serialized_end=2653
69
79
  # @@protoc_insertion_point(module_scope)
@@ -353,3 +353,116 @@ class GetTupleResponse(_message.Message):
353
353
  def WhichOneof(self, oneof_group: _WhichOneofArgType__checkpoint_tuple) -> _WhichOneofReturnType__checkpoint_tuple | None: ...
354
354
 
355
355
  Global___GetTupleResponse: _TypeAlias = GetTupleResponse # noqa: Y015
356
+
357
+ @_typing.final
358
+ class DeltaChannelHistoryEntry(_message.Message):
359
+ """DeltaChannelHistoryEntry is the per-channel reconstruction result for
360
+ DeltaChannel state replay (langgraph >= 1.2).
361
+
362
+ - `seed` is the last snapshot, or absent when no snapshot was found
363
+ - `writes` is ordered oldest-to-newest writes between the last snapshot
364
+ and the target checkpoint
365
+ """
366
+
367
+ DESCRIPTOR: _descriptor.Descriptor
368
+
369
+ SEED_FIELD_NUMBER: _builtins.int
370
+ WRITES_FIELD_NUMBER: _builtins.int
371
+ @_builtins.property
372
+ def seed(self) -> _engine_common_pb2.ChannelValue: ...
373
+ @_builtins.property
374
+ def writes(self) -> _containers.RepeatedCompositeFieldContainer[_engine_common_pb2.PendingWrite]: ...
375
+ def __init__(
376
+ self,
377
+ *,
378
+ seed: _engine_common_pb2.ChannelValue | None = ...,
379
+ writes: _abc.Iterable[_engine_common_pb2.PendingWrite] | None = ...,
380
+ ) -> None: ...
381
+ _HasFieldArgType: _TypeAlias = _typing.Literal["_seed", b"_seed", "seed", b"seed"] # noqa: Y015
382
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
383
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["_seed", b"_seed", "seed", b"seed", "writes", b"writes"] # noqa: Y015
384
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
385
+ _WhichOneofReturnType__seed: _TypeAlias = _typing.Literal["seed"] # noqa: Y015
386
+ _WhichOneofArgType__seed: _TypeAlias = _typing.Literal["_seed", b"_seed"] # noqa: Y015
387
+ def WhichOneof(self, oneof_group: _WhichOneofArgType__seed) -> _WhichOneofReturnType__seed | None: ...
388
+
389
+ Global___DeltaChannelHistoryEntry: _TypeAlias = DeltaChannelHistoryEntry # noqa: Y015
390
+
391
+ @_typing.final
392
+ class GetDeltaChannelHistoryRequest(_message.Message):
393
+ DESCRIPTOR: _descriptor.Descriptor
394
+
395
+ THREAD_ID_FIELD_NUMBER: _builtins.int
396
+ CHECKPOINT_NS_FIELD_NUMBER: _builtins.int
397
+ CHECKPOINT_ID_FIELD_NUMBER: _builtins.int
398
+ CHANNELS_FIELD_NUMBER: _builtins.int
399
+ thread_id: _builtins.str
400
+ """Thread whose delta-channel state to reconstruct."""
401
+ checkpoint_ns: _builtins.str
402
+ """Checkpoint namespace (empty for root graph, non-empty for subgraphs)."""
403
+ checkpoint_id: _builtins.str
404
+ """Target checkpoint to reconstruct at. Empty resolves to the latest
405
+ checkpoint for (thread_id, checkpoint_ns).
406
+ """
407
+ @_builtins.property
408
+ def channels(self) -> _containers.RepeatedScalarFieldContainer[_builtins.str]:
409
+ """Channels to reconstruct."""
410
+
411
+ def __init__(
412
+ self,
413
+ *,
414
+ thread_id: _builtins.str = ...,
415
+ checkpoint_ns: _builtins.str = ...,
416
+ checkpoint_id: _builtins.str = ...,
417
+ channels: _abc.Iterable[_builtins.str] | None = ...,
418
+ ) -> None: ...
419
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
420
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
421
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["channels", b"channels", "checkpoint_id", b"checkpoint_id", "checkpoint_ns", b"checkpoint_ns", "thread_id", b"thread_id"] # noqa: Y015
422
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
423
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
424
+
425
+ Global___GetDeltaChannelHistoryRequest: _TypeAlias = GetDeltaChannelHistoryRequest # noqa: Y015
426
+
427
+ @_typing.final
428
+ class GetDeltaChannelHistoryResponse(_message.Message):
429
+ DESCRIPTOR: _descriptor.Descriptor
430
+
431
+ @_typing.final
432
+ class EntriesEntry(_message.Message):
433
+ DESCRIPTOR: _descriptor.Descriptor
434
+
435
+ KEY_FIELD_NUMBER: _builtins.int
436
+ VALUE_FIELD_NUMBER: _builtins.int
437
+ key: _builtins.str
438
+ @_builtins.property
439
+ def value(self) -> Global___DeltaChannelHistoryEntry: ...
440
+ def __init__(
441
+ self,
442
+ *,
443
+ key: _builtins.str = ...,
444
+ value: Global___DeltaChannelHistoryEntry | None = ...,
445
+ ) -> None: ...
446
+ _HasFieldArgType: _TypeAlias = _typing.Literal["value", b"value"] # noqa: Y015
447
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
448
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["key", b"key", "value", b"value"] # noqa: Y015
449
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
450
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
451
+
452
+ ENTRIES_FIELD_NUMBER: _builtins.int
453
+ @_builtins.property
454
+ def entries(self) -> _containers.MessageMap[_builtins.str, Global___DeltaChannelHistoryEntry]:
455
+ """Per-channel reconstruction. Keys are channel names"""
456
+
457
+ def __init__(
458
+ self,
459
+ *,
460
+ entries: _abc.Mapping[_builtins.str, Global___DeltaChannelHistoryEntry] | None = ...,
461
+ ) -> None: ...
462
+ _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
463
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
464
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["entries", b"entries"] # noqa: Y015
465
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
466
+ def WhichOneof(self, oneof_group: _Never) -> None: ...
467
+
468
+ Global___GetDeltaChannelHistoryResponse: _TypeAlias = GetDeltaChannelHistoryResponse # noqa: Y015
@@ -83,6 +83,11 @@ class CheckpointerStub(object):
83
83
  request_serializer=checkpointer__pb2.PruneRequest.SerializeToString,
84
84
  response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
85
85
  _registered_method=True)
86
+ self.GetDeltaChannelHistory = channel.unary_unary(
87
+ '/checkpointer.Checkpointer/GetDeltaChannelHistory',
88
+ request_serializer=checkpointer__pb2.GetDeltaChannelHistoryRequest.SerializeToString,
89
+ response_deserializer=checkpointer__pb2.GetDeltaChannelHistoryResponse.FromString,
90
+ _registered_method=True)
86
91
 
87
92
 
88
93
  class CheckpointerServicer(object):
@@ -156,6 +161,16 @@ class CheckpointerServicer(object):
156
161
  context.set_details('Method not implemented!')
157
162
  raise NotImplementedError('Method not implemented!')
158
163
 
164
+ def GetDeltaChannelHistory(self, request, context):
165
+ """GetDeltaChannelHistory reconstructs DeltaChannel state for one
166
+ target checkpoint by walking the parent chain to find the most
167
+ recent seed snapshot and collecting all writes from that snapshot
168
+ forward, per requested channel.
169
+ """
170
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
171
+ context.set_details('Method not implemented!')
172
+ raise NotImplementedError('Method not implemented!')
173
+
159
174
 
160
175
  def add_CheckpointerServicer_to_server(servicer, server):
161
176
  rpc_method_handlers = {
@@ -204,6 +219,11 @@ def add_CheckpointerServicer_to_server(servicer, server):
204
219
  request_deserializer=checkpointer__pb2.PruneRequest.FromString,
205
220
  response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
206
221
  ),
222
+ 'GetDeltaChannelHistory': grpc.unary_unary_rpc_method_handler(
223
+ servicer.GetDeltaChannelHistory,
224
+ request_deserializer=checkpointer__pb2.GetDeltaChannelHistoryRequest.FromString,
225
+ response_serializer=checkpointer__pb2.GetDeltaChannelHistoryResponse.SerializeToString,
226
+ ),
207
227
  }
208
228
  generic_handler = grpc.method_handlers_generic_handler(
209
229
  'checkpointer.Checkpointer', rpc_method_handlers)
@@ -460,3 +480,30 @@ class Checkpointer(object):
460
480
  timeout,
461
481
  metadata,
462
482
  _registered_method=True)
483
+
484
+ @staticmethod
485
+ def GetDeltaChannelHistory(request,
486
+ target,
487
+ options=(),
488
+ channel_credentials=None,
489
+ call_credentials=None,
490
+ insecure=False,
491
+ compression=None,
492
+ wait_for_ready=None,
493
+ timeout=None,
494
+ metadata=None):
495
+ return grpc.experimental.unary_unary(
496
+ request,
497
+ target,
498
+ '/checkpointer.Checkpointer/GetDeltaChannelHistory',
499
+ checkpointer__pb2.GetDeltaChannelHistoryRequest.SerializeToString,
500
+ checkpointer__pb2.GetDeltaChannelHistoryResponse.FromString,
501
+ options,
502
+ channel_credentials,
503
+ insecure,
504
+ call_credentials,
505
+ compression,
506
+ wait_for_ready,
507
+ timeout,
508
+ metadata,
509
+ _registered_method=True)
@@ -59,6 +59,12 @@ class CheckpointerStub:
59
59
  """CopyThread copies checkpoint data from one thread to another."""
60
60
  Prune: _grpc.UnaryUnaryMultiCallable[_checkpointer_pb2.PruneRequest, _empty_pb2.Empty]
61
61
  """Prune deletes checkpoints and related data for a set of threads."""
62
+ GetDeltaChannelHistory: _grpc.UnaryUnaryMultiCallable[_checkpointer_pb2.GetDeltaChannelHistoryRequest, _checkpointer_pb2.GetDeltaChannelHistoryResponse]
63
+ """GetDeltaChannelHistory reconstructs DeltaChannel state for one
64
+ target checkpoint by walking the parent chain to find the most
65
+ recent seed snapshot and collecting all writes from that snapshot
66
+ forward, per requested channel.
67
+ """
62
68
 
63
69
  @_typing.type_check_only
64
70
  class CheckpointerAsyncStub(CheckpointerStub):
@@ -90,6 +96,12 @@ class CheckpointerAsyncStub(CheckpointerStub):
90
96
  """CopyThread copies checkpoint data from one thread to another."""
91
97
  Prune: _aio.UnaryUnaryMultiCallable[_checkpointer_pb2.PruneRequest, _empty_pb2.Empty] # type: ignore[assignment]
92
98
  """Prune deletes checkpoints and related data for a set of threads."""
99
+ GetDeltaChannelHistory: _aio.UnaryUnaryMultiCallable[_checkpointer_pb2.GetDeltaChannelHistoryRequest, _checkpointer_pb2.GetDeltaChannelHistoryResponse] # type: ignore[assignment]
100
+ """GetDeltaChannelHistory reconstructs DeltaChannel state for one
101
+ target checkpoint by walking the parent chain to find the most
102
+ recent seed snapshot and collecting all writes from that snapshot
103
+ forward, per requested channel.
104
+ """
93
105
 
94
106
  class CheckpointerServicer(metaclass=_abc_1.ABCMeta):
95
107
  """Checkpoint persistence.
@@ -173,4 +185,16 @@ class CheckpointerServicer(metaclass=_abc_1.ABCMeta):
173
185
  ) -> _typing.Union[_empty_pb2.Empty, _abc.Awaitable[_empty_pb2.Empty]]:
174
186
  """Prune deletes checkpoints and related data for a set of threads."""
175
187
 
188
+ @_abc_1.abstractmethod
189
+ def GetDeltaChannelHistory(
190
+ self,
191
+ request: _checkpointer_pb2.GetDeltaChannelHistoryRequest,
192
+ context: _ServicerContext,
193
+ ) -> _typing.Union[_checkpointer_pb2.GetDeltaChannelHistoryResponse, _abc.Awaitable[_checkpointer_pb2.GetDeltaChannelHistoryResponse]]:
194
+ """GetDeltaChannelHistory reconstructs DeltaChannel state for one
195
+ target checkpoint by walking the parent chain to find the most
196
+ recent seed snapshot and collecting all writes from that snapshot
197
+ forward, per requested channel.
198
+ """
199
+
176
200
  def add_CheckpointerServicer_to_server(servicer: CheckpointerServicer, server: _typing.Union[_grpc.Server, _aio.Server]) -> None: ...
@@ -2,7 +2,9 @@ import re
2
2
  import uuid
3
3
  from base64 import b64encode
4
4
  from collections import deque
5
- from collections.abc import Mapping
5
+ from collections.abc import Iterator, Mapping
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
6
8
  from datetime import timedelta, timezone
7
9
  from decimal import Decimal
8
10
  from ipaddress import (
@@ -22,8 +24,16 @@ import orjson
22
24
  from langgraph.checkpoint.serde.base import SerializerProtocol
23
25
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
24
26
 
27
+ # Process-wide default serializer.
25
28
  SERIALIZER: SerializerProtocol = JsonPlusSerializer()
26
29
 
30
+ # Task-local override and falls back to ``SERIALIZER`` when unset.
31
+ # (e.g. ``GrpcCheckpointer``) scope a custom serializer to its own RPC
32
+ # bodies without leaking into unrelated gRPC ops
33
+ _SCOPED_SERIALIZER: ContextVar[SerializerProtocol | None] = ContextVar(
34
+ "_grpc_scoped_serializer", default=None
35
+ )
36
+
27
37
 
28
38
  class Fragment(NamedTuple):
29
39
  buf: bytes
@@ -154,9 +164,31 @@ def json_dumpb(obj) -> bytes:
154
164
 
155
165
 
156
166
  def set_serializer(serializer: SerializerProtocol) -> None:
157
- global SERIALIZER
158
- SERIALIZER = serializer
167
+ """Replace the process-wide default serializer.
159
168
 
169
+ This only affects every consumer of ``langgraph_grpc_common.conversion.*``.
170
+ """
171
+ global SERIALIZER
172
+ SERIALIZER = serializer
160
173
 
161
174
  def get_serializer() -> SerializerProtocol:
162
- return SERIALIZER
175
+ """Return the serializer to use for gRPC payload (de)serialization.
176
+
177
+ Resolution order:
178
+
179
+ 1. A task-local override bound via :func:`use_serializer`, if any.
180
+ 2. The process-wide default set by :func:`set_serializer` (or the
181
+ ``JsonPlusSerializer()`` baseline if never replaced).
182
+ """
183
+ return _SCOPED_SERIALIZER.get() or SERIALIZER
184
+
185
+
186
+ @contextmanager
187
+ def use_serializer(serializer: SerializerProtocol) -> Iterator[None]:
188
+ """Bind ``serializer`` for the current async task / sync context.
189
+ """
190
+ token = _SCOPED_SERIALIZER.set(serializer)
191
+ try:
192
+ yield
193
+ finally:
194
+ _SCOPED_SERIALIZER.reset(token)