From db0f5ab14df292dcb82b7cd1c8f3bb46a7635d3b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:33:03 +0000 Subject: [PATCH] feat: add HTTP-proxy LangGraph checkpointer Replace direct Postgres checkpointing with HTTP-proxied checkpoint operations through the agentex backend API. Agents no longer need DATABASE_URL or direct DB connections for LangGraph state persistence. - Add HttpCheckpointSaver that proxies through AsyncAgentex client - Add create_checkpointer() factory using the HTTP checkpointer - Replace langgraph-checkpoint-postgres dep with langgraph-checkpoint - Export checkpointer module from adk package Co-Authored-By: Claude Opus 4.6 --- .release-please-manifest.json | 2 +- pyproject.toml | 1 + requirements-dev.lock | 60 ++- requirements.lock | 73 +++- src/agentex/lib/adk/__init__.py | 16 +- .../lib/adk/_modules/_http_checkpointer.py | 388 ++++++++++++++++++ src/agentex/lib/adk/_modules/checkpointer.py | 19 + 7 files changed, 543 insertions(+), 16 deletions(-) create mode 100644 src/agentex/lib/adk/_modules/_http_checkpointer.py create mode 100644 src/agentex/lib/adk/_modules/checkpointer.py diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8cfc016be..7e08ec6aa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { ".": "0.9.2" -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index e1b8a23da..aca54aeec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dependencies = [ "yaspin>=3.1.0", "claude-agent-sdk>=0.1.0", "anthropic>=0.40.0", + "langgraph-checkpoint>=2.0.0", ] requires-python = ">= 3.12,<4" diff --git a/requirements-dev.lock b/requirements-dev.lock index 1078b30de..d4e9e0768 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,12 +16,16 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic +anthropic==0.79.0 + # via agentex-sdk anyio==4.10.0 # via agentex-sdk + # via anthropic + # via claude-agent-sdk # via httpx # via mcp # via openai @@ -51,6 +55,8 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests +claude-agent-sdk==0.1.33 + # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -76,9 +82,12 @@ distlib==0.3.7 # via virtualenv distro==1.9.0 # via agentex-sdk + # via anthropic # via openai # via scale-gp # via scale-gp-beta +docstring-parser==0.17.0 + # via anthropic envier==0.6.1 # via ddtrace execnet==2.1.1 @@ -108,7 +117,9 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk + # via anthropic # via httpx-aiohttp + # via langsmith # via litellm # via mcp # via openai @@ -143,9 +154,14 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 + # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via jsonpatch jsonref==1.1.0 # via agentex-sdk jsonschema==4.25.0 @@ -161,6 +177,12 @@ jupyter-core==5.8.1 # via jupyter-client kubernetes==28.1.0 # via agentex-sdk +langchain-core==1.2.9 + # via langgraph-checkpoint +langgraph-checkpoint==4.0.0 + # via agentex-sdk +langsmith==0.6.9 + # via langchain-core litellm==1.75.5.post1 # via agentex-sdk markdown-it-py==3.0.0 @@ -172,6 +194,7 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk + # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -199,15 +222,21 @@ openai-agents==0.4.2 # via agentex-sdk opentelemetry-api==1.37.0 # via ddtrace +orjson==3.11.7 + # via langsmith +ormsgpack==1.12.2 + # via langgraph-checkpoint packaging==23.2 # via huggingface-hub # via ipykernel + # via langchain-core + # via langsmith # via nox # via pytest -pathspec==0.12.1 - # via mypy parso==0.8.4 # via jedi +pathspec==0.12.1 + # via mypy pexpect==4.9.0 # via ipython platformdirs==3.11.0 @@ -237,7 +266,10 @@ pyasn1-modules==0.4.2 # via google-auth pydantic==2.11.9 # via agentex-sdk + # via anthropic # via fastapi + # via langchain-core + # via langsmith # via litellm # via mcp # via openai @@ -283,6 +315,7 @@ pyyaml==6.0.2 # via agentex-sdk # via huggingface-hub # via kubernetes + # via langchain-core pyzmq==27.0.1 # via ipykernel # via jupyter-client @@ -299,12 +332,16 @@ requests==2.32.4 # via datadog # via huggingface-hub # via kubernetes + # via langsmith # via openai-agents # via python-on-whales # via requests-oauthlib + # via requests-toolbelt # via tiktoken requests-oauthlib==2.0.0 # via kubernetes +requests-toolbelt==1.0.0 + # via langsmith respx==0.22.0 rich==13.9.4 # via agentex-sdk @@ -329,6 +366,7 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via agentex-sdk + # via anthropic # via anyio # via httpx # via openai @@ -343,6 +381,10 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk +tenacity==9.1.4 + # via langchain-core +termcolor==3.3.0 + # via yaspin tiktoken==0.11.0 # via litellm time-machine==2.9.0 @@ -374,9 +416,11 @@ types-urllib3==1.26.25.14 typing-extensions==4.12.2 # via agentex-sdk # via aiosignal + # via anthropic # via anyio # via fastapi # via huggingface-hub + # via langchain-core # via mypy # via nexus-rpc # via openai @@ -392,7 +436,6 @@ typing-extensions==4.12.2 # via temporalio # via typer # via typing-inspection - # via virtualenv typing-inspection==0.4.2 # via pydantic # via pydantic-settings @@ -403,6 +446,9 @@ tzlocal==5.3.1 urllib3==1.26.20 # via kubernetes # via requests +uuid-utils==0.14.0 + # via langchain-core + # via langsmith uvicorn==0.35.0 # via agentex-sdk # via mcp @@ -416,7 +462,13 @@ websocket-client==1.8.0 # via kubernetes wrapt==1.17.3 # via ddtrace +xxhash==3.6.0 + # via langsmith yarl==1.20.0 # via aiohttp +yaspin==3.4.0 + # via agentex-sdk zipp==3.23.0 # via importlib-metadata +zstandard==0.25.0 + # via langsmith diff --git a/requirements.lock b/requirements.lock index 79519671e..24601accb 100644 --- a/requirements.lock +++ b/requirements.lock @@ -16,12 +16,16 @@ aiohttp==3.13.2 # via agentex-sdk # via httpx-aiohttp # via litellm -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic +anthropic==0.79.0 + # via agentex-sdk anyio==4.10.0 # via agentex-sdk + # via anthropic + # via claude-agent-sdk # via httpx # via mcp # via openai @@ -49,6 +53,8 @@ certifi==2023.7.22 # via requests charset-normalizer==3.4.3 # via requests +claude-agent-sdk==0.1.33 + # via agentex-sdk click==8.2.1 # via litellm # via typer @@ -69,9 +75,12 @@ decorator==5.2.1 # via ipython distro==1.8.0 # via agentex-sdk + # via anthropic # via openai # via scale-gp # via scale-gp-beta +docstring-parser==0.17.0 + # via anthropic envier==0.6.1 # via ddtrace executing==2.2.0 @@ -98,7 +107,9 @@ httpcore==1.0.9 # via httpx httpx==0.27.2 # via agentex-sdk + # via anthropic # via httpx-aiohttp + # via langsmith # via litellm # via mcp # via openai @@ -132,9 +143,14 @@ jinja2==3.1.6 # via agentex-sdk # via litellm jiter==0.10.0 + # via anthropic # via openai json-log-formatter==1.1.1 # via agentex-sdk +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via jsonpatch jsonref==1.1.0 # via agentex-sdk jsonschema==4.25.0 @@ -150,6 +166,12 @@ jupyter-core==5.8.1 # via jupyter-client kubernetes==28.1.0 # via agentex-sdk +langchain-core==1.2.9 + # via langgraph-checkpoint +langgraph-checkpoint==4.0.0 + # via agentex-sdk +langsmith==0.6.9 + # via langchain-core litellm==1.75.5.post1 # via agentex-sdk markdown-it-py==4.0.0 @@ -161,6 +183,7 @@ matplotlib-inline==0.1.7 # via ipython mcp==1.12.4 # via agentex-sdk + # via claude-agent-sdk # via openai-agents mdurl==0.1.2 # via markdown-it-py @@ -182,9 +205,15 @@ openai-agents==0.4.2 # via agentex-sdk opentelemetry-api==1.37.0 # via ddtrace +orjson==3.11.7 + # via langsmith +ormsgpack==1.12.2 + # via langgraph-checkpoint packaging==25.0 # via huggingface-hub # via ipykernel + # via langchain-core + # via langsmith # via pytest parso==0.8.4 # via jedi @@ -200,9 +229,6 @@ prompt-toolkit==3.0.51 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.12.5 - # via agentex-sdk -pydantic-core==2.41.5 protobuf==5.29.5 # via ddtrace # via temporalio @@ -217,6 +243,21 @@ pyasn1==0.6.1 # via rsa pyasn1-modules==0.4.2 # via google-auth +pydantic==2.12.5 + # via agentex-sdk + # via anthropic + # via fastapi + # via langchain-core + # via langsmith + # via litellm + # via mcp + # via openai + # via openai-agents + # via pydantic-settings + # via python-on-whales + # via scale-gp + # via scale-gp-beta +pydantic-core==2.41.5 # via pydantic pydantic-settings==2.10.1 # via mcp @@ -247,6 +288,7 @@ pyyaml==6.0.2 # via agentex-sdk # via huggingface-hub # via kubernetes + # via langchain-core pyzmq==27.0.1 # via ipykernel # via jupyter-client @@ -263,12 +305,16 @@ requests==2.32.4 # via datadog # via huggingface-hub # via kubernetes + # via langsmith # via openai-agents # via python-on-whales # via requests-oauthlib + # via requests-toolbelt # via tiktoken requests-oauthlib==2.0.0 # via kubernetes +requests-toolbelt==1.0.0 + # via langsmith rich==13.9.4 # via agentex-sdk # via typer @@ -290,7 +336,8 @@ six==1.17.0 # via python-dateutil sniffio==1.3.0 # via agentex-sdk -typing-extensions==4.15.0 + # via anthropic + # via anyio # via httpx # via openai # via scale-gp @@ -304,6 +351,10 @@ starlette==0.46.2 # via mcp temporalio==1.18.2 # via agentex-sdk +tenacity==9.1.4 + # via langchain-core +termcolor==3.3.0 + # via yaspin tiktoken==0.11.0 # via litellm tokenizers==0.21.4 @@ -331,11 +382,14 @@ types-requests==2.31.0.6 # via openai-agents types-urllib3==1.26.25.14 # via types-requests +typing-extensions==4.15.0 # via agentex-sdk # via aiosignal + # via anthropic # via anyio # via fastapi # via huggingface-hub + # via langchain-core # via nexus-rpc # via openai # via openai-agents @@ -359,6 +413,9 @@ tzlocal==5.3.1 urllib3==1.26.20 # via kubernetes # via requests +uuid-utils==0.14.0 + # via langchain-core + # via langsmith uvicorn==0.35.0 # via agentex-sdk # via mcp @@ -370,7 +427,13 @@ websocket-client==1.8.0 # via kubernetes wrapt==1.17.3 # via ddtrace +xxhash==3.6.0 + # via langsmith yarl==1.20.0 # via aiohttp +yaspin==3.4.0 + # via agentex-sdk zipp==3.23.0 # via importlib-metadata +zstandard==0.25.0 + # via langsmith diff --git a/src/agentex/lib/adk/__init__.py b/src/agentex/lib/adk/__init__.py index cc4e83db4..4ccd7a2a7 100644 --- a/src/agentex/lib/adk/__init__.py +++ b/src/agentex/lib/adk/__init__.py @@ -5,6 +5,7 @@ from agentex.lib.adk._modules.acp import ACPModule from agentex.lib.adk._modules.agents import AgentsModule from agentex.lib.adk._modules.agent_task_tracker import AgentTaskTrackerModule +from agentex.lib.adk._modules.checkpointer import create_checkpointer from agentex.lib.adk._modules.events import EventsModule from agentex.lib.adk._modules.messages import MessagesModule from agentex.lib.adk._modules.state import StateModule @@ -27,16 +28,19 @@ __all__ = [ # Core - "acp", + "acp", "agents", - "tasks", - "messages", - "state", - "streaming", - "tracing", + "tasks", + "messages", + "state", + "streaming", + "tracing", "events", "agent_task_tracker", + # Checkpointing + "create_checkpointer", + # Providers "providers", # Utils diff --git a/src/agentex/lib/adk/_modules/_http_checkpointer.py b/src/agentex/lib/adk/_modules/_http_checkpointer.py new file mode 100644 index 000000000..9744283bb --- /dev/null +++ b/src/agentex/lib/adk/_modules/_http_checkpointer.py @@ -0,0 +1,388 @@ +"""HTTP-proxy LangGraph checkpointer. + +Proxies all checkpoint operations through the agentex backend API +instead of connecting directly to PostgreSQL. The backend handles DB +operations through its own connection pool. +""" + +from __future__ import annotations + +import base64 +import random +from typing import Any, cast, override +from collections.abc import Iterator, Sequence, AsyncIterator + +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, + Checkpoint, + ChannelVersions, + CheckpointTuple, + CheckpointMetadata, + BaseCheckpointSaver, + get_checkpoint_id, + get_serializable_checkpoint_metadata, +) +from langgraph.checkpoint.serde.types import TASKS + +from agentex import AsyncAgentex +from agentex.lib.utils.logging import make_logger + +logger = make_logger(__name__) + + +def _bytes_to_b64(data: bytes | None) -> str | None: + if data is None: + return None + return base64.b64encode(data).decode("ascii") + + +def _b64_to_bytes(data: str | None) -> bytes | None: + if data is None: + return None + return base64.b64decode(data) + + +class HttpCheckpointSaver(BaseCheckpointSaver[str]): + """Checkpoint saver that proxies operations through the agentex HTTP API.""" + + def __init__(self, client: AsyncAgentex) -> None: + super().__init__() + self._client = client + + async def _post(self, path: str, body: dict[str, Any]) -> Any: + """POST JSON to the backend and return parsed response.""" + resp = self._client._client.post( # noqa: SLF001 + f"/checkpoints{path}", + json=body, + ) + response = await resp + response.raise_for_status() + if response.status_code == 204: + return None + return response.json() + + # ── get_next_version (same as BasePostgresSaver) ── + + @override + def get_next_version(self, current: str | None, channel: None) -> str: # type: ignore[override] # noqa: ARG002 + if current is None: + current_v = 0 + elif isinstance(current, int): + current_v = current + else: + current_v = int(current.split(".")[0]) + next_v = current_v + 1 + next_h = random.random() # noqa: S311 + return f"{next_v:032}.{next_h:016}" + + # ── async interface ── + + @override + async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None: + configurable = config["configurable"] # type: ignore[reportTypedDictNotRequiredAccess] + thread_id = configurable["thread_id"] + checkpoint_ns = configurable.get("checkpoint_ns", "") + checkpoint_id = get_checkpoint_id(config) + + data = await self._post( + "/get-tuple", + { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint_id, + }, + ) + + if data is None: + return None + + # Reconstruct channel_values from blobs + inline values + checkpoint = data["checkpoint"] + channel_values: dict[str, Any] = {} + + # Inline primitive values already in the checkpoint + if "channel_values" in checkpoint and checkpoint["channel_values"]: + channel_values.update(checkpoint["channel_values"]) + + # Deserialize blobs + for blob in data.get("blobs", []): + blob_type = blob["type"] + if blob_type == "empty": + continue + blob_bytes = _b64_to_bytes(blob.get("blob")) + channel_values[blob["channel"]] = self.serde.loads_typed((blob_type, blob_bytes)) + + checkpoint["channel_values"] = channel_values + + # Handle pending_sends migration for v < 4 + if checkpoint.get("v", 0) < 4 and data.get("parent_checkpoint_id"): + # The backend already returns all writes; filter for TASKS channel sends + pending_sends_raw = [w for w in data.get("pending_writes", []) if w["channel"] == TASKS] + if pending_sends_raw: + sends = [ + self.serde.loads_typed((w["type"], _b64_to_bytes(w["blob"]))) + for w in pending_sends_raw + if w.get("type") + ] + if sends: + enc, blob_data = self.serde.dumps_typed(sends) + channel_values[TASKS] = self.serde.loads_typed((enc, blob_data)) + if checkpoint.get("channel_versions") is None: + checkpoint["channel_versions"] = {} + checkpoint["channel_versions"][TASKS] = ( + max(checkpoint["channel_versions"].values()) + if checkpoint["channel_versions"] + else self.get_next_version(None, None) + ) + + # Reconstruct pending writes + pending_writes: list[tuple[str, str, Any]] = [] + for w in data.get("pending_writes", []): + w_type = w.get("type") + w_bytes = _b64_to_bytes(w.get("blob")) + pending_writes.append( + ( + w["task_id"], + w["channel"], + self.serde.loads_typed((w_type, w_bytes)) if w_type else w_bytes, + ) + ) + + parent_config: RunnableConfig | None = None + if data.get("parent_checkpoint_id"): + parent_config = { + "configurable": { + "thread_id": data["thread_id"], + "checkpoint_ns": data["checkpoint_ns"], + "checkpoint_id": data["parent_checkpoint_id"], + } + } + + return CheckpointTuple( + config={ + "configurable": { + "thread_id": data["thread_id"], + "checkpoint_ns": data["checkpoint_ns"], + "checkpoint_id": data["checkpoint_id"], + } + }, + checkpoint=checkpoint, + metadata=data["metadata"], + parent_config=parent_config, + pending_writes=pending_writes, + ) + + @override + async def aput( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + configurable = config["configurable"].copy() # type: ignore[reportTypedDictNotRequiredAccess] + thread_id = configurable.pop("thread_id") + checkpoint_ns = configurable.pop("checkpoint_ns") + checkpoint_id = configurable.pop("checkpoint_id", None) + + # Separate inline values from blobs (same logic as AsyncPostgresSaver) + copy = checkpoint.copy() + copy["channel_values"] = copy["channel_values"].copy() + blob_values: dict[str, Any] = {} + for k, v in checkpoint["channel_values"].items(): + if v is None or isinstance(v, (str, int, float, bool)): + pass + else: + blob_values[k] = copy["channel_values"].pop(k) + + # Serialize blob values + blobs: list[dict[str, Any]] = [] + for k, ver in new_versions.items(): + if k in blob_values: + enc, data = self.serde.dumps_typed(blob_values[k]) + blobs.append( + { + "channel": k, + "version": cast(str, ver), + "type": enc, + "blob": _bytes_to_b64(data), + } + ) + else: + blobs.append( + { + "channel": k, + "version": cast(str, ver), + "type": "empty", + "blob": None, + } + ) + + await self._post( + "/put", + { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint["id"], + "parent_checkpoint_id": checkpoint_id, + "checkpoint": copy, + "metadata": get_serializable_checkpoint_metadata(config, metadata), + "blobs": blobs, + }, + ) + + return { + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint["id"], + } + } + + @override + async def aput_writes( + self, + config: RunnableConfig, + writes: Sequence[tuple[str, Any]], + task_id: str, + task_path: str = "", + ) -> None: + configurable = config["configurable"] # type: ignore[reportTypedDictNotRequiredAccess] + thread_id = configurable["thread_id"] + checkpoint_ns = configurable["checkpoint_ns"] + checkpoint_id = configurable["checkpoint_id"] + + upsert = all(w[0] in WRITES_IDX_MAP for w in writes) + + serialized_writes: list[dict[str, Any]] = [] + for idx, (channel, value) in enumerate(writes): + enc, data = self.serde.dumps_typed(value) + serialized_writes.append( + { + "task_id": task_id, + "idx": WRITES_IDX_MAP.get(channel, idx), + "channel": channel, + "type": enc, + "blob": _bytes_to_b64(data), + "task_path": task_path, + } + ) + + await self._post( + "/put-writes", + { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint_id, + "writes": serialized_writes, + "upsert": upsert, + }, + ) + + @override + async def alist( + self, + config: RunnableConfig | None, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> AsyncIterator[CheckpointTuple]: + body: dict[str, Any] = {} + if config: + configurable = config["configurable"] # type: ignore[reportTypedDictNotRequiredAccess] + body["thread_id"] = configurable["thread_id"] + checkpoint_ns = configurable.get("checkpoint_ns") + if checkpoint_ns is not None: + body["checkpoint_ns"] = checkpoint_ns + if filter: + body["filter_metadata"] = filter + if before: + body["before_checkpoint_id"] = get_checkpoint_id(before) + if limit is not None: + body["limit"] = limit + + results = await self._post("/list", body) + + for item in results or []: + # For each listed checkpoint, reconstruct a CheckpointTuple + # with inline channel_values only (blobs not included in list) + checkpoint = item["checkpoint"] + parent_config: RunnableConfig | None = None + if item.get("parent_checkpoint_id"): + parent_config = { + "configurable": { + "thread_id": item["thread_id"], + "checkpoint_ns": item["checkpoint_ns"], + "checkpoint_id": item["parent_checkpoint_id"], + } + } + yield CheckpointTuple( + config={ + "configurable": { + "thread_id": item["thread_id"], + "checkpoint_ns": item["checkpoint_ns"], + "checkpoint_id": item["checkpoint_id"], + } + }, + checkpoint=checkpoint, + metadata=item["metadata"], + parent_config=parent_config, + pending_writes=None, + ) + + @override + async def adelete_thread(self, thread_id: str) -> None: + await self._post("/delete-thread", {"thread_id": thread_id}) + + # ── sync wrappers (required by BaseCheckpointSaver) ── + # These delegate to the async methods via asyncio, matching + # the pattern in AsyncPostgresSaver. + + @override + def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None: + import asyncio + + return asyncio.get_event_loop().run_until_complete(self.aget_tuple(config)) + + @override + def list( + self, + config: RunnableConfig | None, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> Iterator[CheckpointTuple]: + raise NotImplementedError("Synchronous list is not supported. Use alist() instead.") + + @override + def put( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + import asyncio + + return asyncio.get_event_loop().run_until_complete(self.aput(config, checkpoint, metadata, new_versions)) + + @override + def put_writes( + self, + config: RunnableConfig, + writes: Sequence[tuple[str, Any]], + task_id: str, + task_path: str = "", + ) -> None: + import asyncio + + asyncio.get_event_loop().run_until_complete(self.aput_writes(config, writes, task_id, task_path)) + + @override + def delete_thread(self, thread_id: str) -> None: + import asyncio + + asyncio.get_event_loop().run_until_complete(self.adelete_thread(thread_id)) diff --git a/src/agentex/lib/adk/_modules/checkpointer.py b/src/agentex/lib/adk/_modules/checkpointer.py new file mode 100644 index 000000000..544042941 --- /dev/null +++ b/src/agentex/lib/adk/_modules/checkpointer.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentex.lib.adk.utils._modules.client import create_async_agentex_client +from agentex.lib.adk._modules._http_checkpointer import HttpCheckpointSaver + + +async def create_checkpointer() -> HttpCheckpointSaver: + """Create an HTTP-proxy checkpointer for LangGraph. + + Checkpoint operations are proxied through the agentex backend API. + No direct database connection needed — auth is handled via the + agent API key (injected automatically by agentex). + + Usage: + checkpointer = await create_checkpointer() + graph = builder.compile(checkpointer=checkpointer) + """ + client = create_async_agentex_client() + return HttpCheckpointSaver(client=client)