langgraph-api 0.12.0.dev8__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 +1 -1
- langgraph_api/_checkpointer/_adapter.py +26 -1
- langgraph_api/event_streaming/service.py +49 -8
- langgraph_api/event_streaming/state_normalizers.py +8 -0
- langgraph_api/feature_flags.py +10 -0
- langgraph_api/grpc/servicers/checkpointer.py +14 -0
- {langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/METADATA +2 -2
- {langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/RECORD +20 -20
- langgraph_grpc_common/checkpointer.py +115 -21
- langgraph_grpc_common/conversion/checkpoint.py +95 -6
- langgraph_grpc_common/conversion/struct.py +34 -1
- langgraph_grpc_common/conversion/value.py +11 -1
- langgraph_grpc_common/proto/checkpointer_pb2.py +13 -3
- langgraph_grpc_common/proto/checkpointer_pb2.pyi +113 -0
- langgraph_grpc_common/proto/checkpointer_pb2_grpc.py +47 -0
- langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi +24 -0
- langgraph_grpc_common/serde.py +36 -4
- {langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/WHEEL +0 -0
- {langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.12.0.
|
|
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(
|
|
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
|
|
597
|
-
#
|
|
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
|
|
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.
|
|
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
|
-
(
|
|
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.
|
|
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"]
|
langgraph_api/feature_flags.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
167
|
-
langgraph_grpc_common/serde.py,sha256=
|
|
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=
|
|
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=
|
|
174
|
-
langgraph_grpc_common/conversion/value.py,sha256=
|
|
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=
|
|
177
|
-
langgraph_grpc_common/proto/checkpointer_pb2.pyi,sha256=
|
|
178
|
-
langgraph_grpc_common/proto/checkpointer_pb2_grpc.py,sha256=
|
|
179
|
-
langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi,sha256=
|
|
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.
|
|
233
|
-
langgraph_api-0.12.0.
|
|
234
|
-
langgraph_api-0.12.0.
|
|
235
|
-
langgraph_api-0.12.0.
|
|
236
|
-
langgraph_api-0.12.0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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\
|
|
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['
|
|
68
|
-
_globals['
|
|
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: ...
|
langgraph_grpc_common/serde.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
File without changes
|
{langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{langgraph_api-0.12.0.dev8.dist-info → langgraph_api-0.12.0.dev10.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|