databricks-agentbricks 0.2.0__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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. databricks_agentkit/timefmt.py +85 -0
@@ -0,0 +1,485 @@
1
+ """Conversation session store for the agent.
2
+
3
+ LangGraph persists conversation state through a **checkpointer** keyed by a ``thread_id`` (passed in
4
+ the run config), not through a session object. ``checkpointer()`` returns the checkpointer the agent
5
+ is built with, and ``thread_config(session_id)`` maps a session id onto that thread.
6
+
7
+ Default (no config): an in-memory checkpointer (``InMemorySaver``) — multi-turn history is preserved
8
+ within a single running process, no database. It does NOT survive restarts or span replicas.
9
+
10
+ Durable (``AGENT_SESSION_STORE`` set): a ``DatabricksSessionStoreSaver``. Instead of a database the
11
+ app connects to directly, it serializes each LangGraph checkpoint into ordered **session items** and
12
+ stores them through the managed Session Store REST API. Full graph state — including human-in-the-loop
13
+ pauses (pending writes + interrupts) — is durable across restarts and replicas, over RPCs only (no
14
+ Lakebase/Postgres connection). Setting the env var is the only change; the agent code is identical.
15
+
16
+ The saver is adapted from the first-party ``databricks_agent_client.langgraph`` prototype, over a
17
+ vendored REST client (``session_store_client.py``) so the template needs no unpublished dependency;
18
+ swap both for the published package when it lands.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import builtins
25
+ import hashlib
26
+ import random
27
+ from collections import defaultdict
28
+ from typing import Any, Iterator, Mapping, Optional, Sequence
29
+
30
+ from langchain_core.runnables import RunnableConfig
31
+ from langgraph.checkpoint.base import (
32
+ BaseCheckpointSaver,
33
+ ChannelVersions,
34
+ Checkpoint,
35
+ CheckpointMetadata,
36
+ CheckpointTuple,
37
+ SerializerProtocol,
38
+ get_checkpoint_id,
39
+ get_checkpoint_metadata,
40
+ )
41
+ from langgraph.checkpoint.memory import InMemorySaver
42
+
43
+ from databricks_agentkit.runtime.session_store_client import Session, SessionStoreClient
44
+
45
+ # Discriminators stored inside each session item's `data` JSON.
46
+ _EVENT_CHECKPOINT = "checkpoint"
47
+ _EVENT_CHANNEL_DATA = "channel_data"
48
+ _EVENT_WRITES = "writes"
49
+
50
+ # Sentinel for a channel that has a version bump but no materialized value yet.
51
+ _EMPTY_CHANNEL_VALUE = "__databricks_empty_channel__"
52
+
53
+ # Fetch items oldest-first so replaying them reconstructs state in write order.
54
+ _ORDER_BY = "create_time asc"
55
+
56
+ # Checkpoint metadata kept by generated agents.
57
+ INVOCATION_METADATA_KEY = "databricks_agentkit.invocation_id"
58
+
59
+ # One saver per process, opened lazily on first use and shared thereafter — that's what makes
60
+ # multi-turn work in-process (and, for the durable saver, reuses one client/session cache).
61
+ _saver: BaseCheckpointSaver | None = None
62
+
63
+
64
+ def invocation_metadata(invocation_id: str) -> dict[str, str]:
65
+ """Return checkpoint metadata for the current AgentKit runtime."""
66
+ return {INVOCATION_METADATA_KEY: invocation_id}
67
+
68
+
69
+ def invocation_id_from_metadata(metadata: Mapping[str, Any] | None) -> str | None:
70
+ """Read a checkpoint invocation id from AgentKit metadata."""
71
+ if not metadata:
72
+ return None
73
+ value = metadata.get(INVOCATION_METADATA_KEY)
74
+ return value if isinstance(value, str) else None
75
+
76
+
77
+ def checkpointer(store: str | None = None) -> BaseCheckpointSaver:
78
+ """The checkpointer the agent persists conversation state to (built once, then shared).
79
+
80
+ In-memory by default; a durable ``DatabricksSessionStoreSaver`` when a managed store is
81
+ configured. The store resolves ``store`` arg → ``AGENT_SESSION_STORE`` env → the
82
+ ``[session_store]`` binding in agent.toml (`ab sessions bind`) → none (in-memory).
83
+ """
84
+ from databricks_agentkit.runtime.tool_manifest import resolve_session_store
85
+
86
+ global _saver
87
+ if _saver is None:
88
+ store = resolve_session_store(store)
89
+ _saver = DatabricksSessionStoreSaver(store) if store else InMemorySaver()
90
+ return _saver
91
+
92
+
93
+ def thread_config(session_id: str, actor: str | None = None) -> dict:
94
+ """Run config that anchors this request to ``session_id``'s conversation thread.
95
+
96
+ Includes ``actor_id`` because the durable saver maps it onto the Session's actor; it's ignored by
97
+ the in-memory default. ``actor`` partitions the durable store — the caller supplies it (typically
98
+ the signed-in user), so each user's threads stay separate; it defaults to ``session_id`` (one
99
+ actor per conversation).
100
+ """
101
+ return {"configurable": {"thread_id": session_id, "actor_id": actor or session_id}}
102
+
103
+
104
+ class DatabricksSessionStoreSaver(BaseCheckpointSaver[str]):
105
+ """LangGraph ``BaseCheckpointSaver`` over the Databricks Session Store REST API.
106
+
107
+ A single ``put`` fans out into several session items so large, immutable channel values are stored
108
+ once per version rather than copied into every checkpoint: one ``checkpoint`` item (the checkpoint
109
+ dict minus ``channel_values`` + metadata), one ``channel_data`` item per changed channel, and
110
+ ``writes`` items (via ``put_writes``). Each item's ``data`` is plain JSON with an ``event_type``
111
+ discriminator, because the Session Store treats item ``data`` as opaque JSON. Both ``thread_id``
112
+ and ``actor_id`` are required in the run config's ``configurable`` section.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ session_store_name: str,
118
+ *,
119
+ client: Optional[SessionStoreClient] = None,
120
+ workspace_client: Optional[Any] = None,
121
+ serde: Optional[SerializerProtocol] = None,
122
+ ) -> None:
123
+ super().__init__(serde=serde)
124
+ if not session_store_name:
125
+ raise ValueError("session_store_name is required")
126
+ self._client = (client or SessionStoreClient(workspace_client)).set_session_store(
127
+ session_store_name
128
+ )
129
+ # Cache resolved Session objects per derived session_id to avoid re-resolving every put/get.
130
+ self._sessions: dict[str, Session] = {}
131
+
132
+ # ----- write path ---------------------------------------------------------
133
+
134
+ def put(
135
+ self,
136
+ config: RunnableConfig,
137
+ checkpoint: Checkpoint,
138
+ metadata: CheckpointMetadata,
139
+ new_versions: ChannelVersions,
140
+ ) -> RunnableConfig:
141
+ """Persist a checkpoint and its changed channel values."""
142
+ thread_id, actor_id, checkpoint_ns = _parse_config(config)
143
+ session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
144
+
145
+ checkpoint_copy = dict(checkpoint)
146
+ channel_values = checkpoint_copy.pop("channel_values", {}) or {}
147
+
148
+ items: list[dict[str, Any]] = [
149
+ self._channel_data_item(
150
+ channel, str(version), channel_values.get(channel, _EMPTY_CHANNEL_VALUE)
151
+ )
152
+ for channel, version in new_versions.items()
153
+ ]
154
+ items.append(
155
+ self._checkpoint_item(
156
+ checkpoint_id=checkpoint["id"],
157
+ checkpoint_data=checkpoint_copy,
158
+ metadata=dict(get_checkpoint_metadata(config, metadata)), # type: ignore[arg-type]
159
+ parent_checkpoint_id=get_checkpoint_id(config),
160
+ )
161
+ )
162
+ self._client.append_items(session, items=items)
163
+
164
+ return {
165
+ "configurable": {
166
+ "thread_id": thread_id,
167
+ "actor_id": actor_id,
168
+ "checkpoint_ns": checkpoint_ns,
169
+ "checkpoint_id": checkpoint["id"],
170
+ }
171
+ }
172
+
173
+ def put_writes(
174
+ self,
175
+ config: RunnableConfig,
176
+ writes: Sequence[tuple[str, Any]],
177
+ task_id: str,
178
+ task_path: str = "",
179
+ ) -> None:
180
+ """Persist pending intermediate writes for a checkpoint."""
181
+ thread_id, actor_id, checkpoint_ns = _parse_config(config)
182
+ checkpoint_id = get_checkpoint_id(config)
183
+ if not checkpoint_id:
184
+ raise ValueError("checkpoint_id is required in config for put_writes")
185
+
186
+ session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
187
+ item = {
188
+ "event_type": _EVENT_WRITES,
189
+ "checkpoint_id": checkpoint_id,
190
+ "writes": [
191
+ {
192
+ "task_id": task_id,
193
+ "task_path": task_path,
194
+ "channel": channel,
195
+ "value": self._serialize(value),
196
+ }
197
+ for channel, value in writes
198
+ ],
199
+ }
200
+ self._client.append_items(session, items=[item])
201
+
202
+ def delete_thread(self, thread_id: str, actor_id: str = "") -> None:
203
+ """Remove all checkpoints and writes for a thread (via ClearSessionItems)."""
204
+ session = self._resolve_session(thread_id, actor_id, "")
205
+ self._client.clear_items(session)
206
+
207
+ # ----- read path ----------------------------------------------------------
208
+
209
+ def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
210
+ """Return the checkpoint named in ``config``, or the latest one."""
211
+ thread_id, actor_id, checkpoint_ns = _parse_config(config)
212
+ session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
213
+ checkpoints, writes_by_ckpt, channel_data = self._read(session)
214
+ if not checkpoints:
215
+ return None
216
+
217
+ wanted = get_checkpoint_id(config)
218
+ if wanted:
219
+ event = checkpoints.get(wanted)
220
+ if event is None:
221
+ return None
222
+ else:
223
+ event = checkpoints[max(checkpoints)]
224
+
225
+ return self._build_tuple(
226
+ event,
227
+ writes_by_ckpt.get(event["checkpoint_id"], []),
228
+ channel_data,
229
+ thread_id,
230
+ actor_id,
231
+ checkpoint_ns,
232
+ )
233
+
234
+ def list(
235
+ self,
236
+ config: RunnableConfig | None,
237
+ *,
238
+ filter: Optional[dict[str, Any]] = None,
239
+ before: RunnableConfig | None = None,
240
+ limit: Optional[int] = None,
241
+ ) -> Iterator[CheckpointTuple]:
242
+ """Yield stored checkpoints newest-first, honoring ``before``/``limit``."""
243
+ if config is None:
244
+ raise ValueError("config with a thread_id is required to list checkpoints")
245
+ thread_id, actor_id, checkpoint_ns = _parse_config(config)
246
+ session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
247
+ checkpoints, writes_by_ckpt, channel_data = self._read(session)
248
+
249
+ before_id = get_checkpoint_id(before) if before else None
250
+ count = 0
251
+ for checkpoint_id in sorted(checkpoints, reverse=True):
252
+ if before_id and checkpoint_id >= before_id:
253
+ continue
254
+ if limit is not None and count >= limit:
255
+ break
256
+ yield self._build_tuple(
257
+ checkpoints[checkpoint_id],
258
+ writes_by_ckpt.get(checkpoint_id, []),
259
+ channel_data,
260
+ thread_id,
261
+ actor_id,
262
+ checkpoint_ns,
263
+ )
264
+ count += 1
265
+
266
+ def get_next_version(self, current: Optional[Any], channel: Optional[str] = None) -> str:
267
+ """Monotonic version string, matching LangGraph's default scheme."""
268
+ if current is None:
269
+ current_v = 0
270
+ elif isinstance(current, int):
271
+ current_v = current
272
+ else:
273
+ current_v = int(str(current).split(".")[0])
274
+ return f"{current_v + 1:032}.{random.random():016}"
275
+
276
+ # ----- async wrappers (the Session Store client is synchronous) -----------
277
+
278
+ async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
279
+ return await _run_sync(self.get_tuple, config)
280
+
281
+ async def alist(
282
+ self,
283
+ config: RunnableConfig | None,
284
+ *,
285
+ filter: Optional[dict[str, Any]] = None,
286
+ before: RunnableConfig | None = None,
287
+ limit: Optional[int] = None,
288
+ ):
289
+ for item in await _run_sync(
290
+ lambda: list(self.list(config, filter=filter, before=before, limit=limit))
291
+ ):
292
+ yield item
293
+
294
+ async def aput(
295
+ self,
296
+ config: RunnableConfig,
297
+ checkpoint: Checkpoint,
298
+ metadata: CheckpointMetadata,
299
+ new_versions: ChannelVersions,
300
+ ) -> RunnableConfig:
301
+ return await _run_sync(self.put, config, checkpoint, metadata, new_versions)
302
+
303
+ async def aput_writes(
304
+ self,
305
+ config: RunnableConfig,
306
+ writes: Sequence[tuple[str, Any]],
307
+ task_id: str,
308
+ task_path: str = "",
309
+ ) -> None:
310
+ return await _run_sync(self.put_writes, config, writes, task_id, task_path)
311
+
312
+ async def adelete_thread(self, thread_id: str, actor_id: str = "") -> None:
313
+ await _run_sync(self.delete_thread, thread_id, actor_id)
314
+
315
+ # ----- internals ----------------------------------------------------------
316
+
317
+ def _resolve_session(self, thread_id: str, actor_id: str, checkpoint_ns: str) -> Session:
318
+ session_id = _session_id(thread_id, checkpoint_ns)
319
+ cached = self._sessions.get(session_id)
320
+ if cached is not None:
321
+ return cached
322
+ try:
323
+ session = self._client.get_session(session_id=session_id)
324
+ except tuple(_not_found_errors()) as _: # type: ignore[misc]
325
+ session = self._client.create_session(
326
+ actor_id=actor_id,
327
+ session_id=session_id,
328
+ metadata={"langgraph_thread_id": thread_id, "checkpoint_ns": checkpoint_ns},
329
+ )
330
+ self._sessions[session_id] = session
331
+ return session
332
+
333
+ def _read(
334
+ self, session: Session
335
+ ) -> tuple[
336
+ dict[str, dict[str, Any]],
337
+ dict[str, builtins.list[dict[str, Any]]],
338
+ dict[tuple[str, str], Any],
339
+ ]:
340
+ """List items and group them into checkpoints, writes, and channel data."""
341
+ checkpoints: dict[str, dict[str, Any]] = {}
342
+ writes_by_ckpt: dict[str, list[dict[str, Any]]] = defaultdict(list)
343
+ channel_data: dict[tuple[str, str], Any] = {}
344
+
345
+ for item in self._client.list_items(session, order_by=_ORDER_BY):
346
+ data = item.data
347
+ event_type = data.get("event_type")
348
+ if event_type == _EVENT_CHECKPOINT:
349
+ checkpoints[data["checkpoint_id"]] = data
350
+ elif event_type == _EVENT_WRITES:
351
+ writes_by_ckpt[data["checkpoint_id"]].extend(data["writes"])
352
+ elif event_type == _EVENT_CHANNEL_DATA and data["value"] != _EMPTY_CHANNEL_VALUE:
353
+ channel_data[(data["channel"], data["version"])] = self._deserialize(data["value"])
354
+ return checkpoints, writes_by_ckpt, channel_data
355
+
356
+ def _build_tuple(
357
+ self,
358
+ checkpoint_event: dict[str, Any],
359
+ writes: builtins.list[dict[str, Any]],
360
+ channel_data: dict[tuple[str, str], Any],
361
+ thread_id: str,
362
+ actor_id: str,
363
+ checkpoint_ns: str,
364
+ ) -> CheckpointTuple:
365
+ checkpoint = dict(self._deserialize(checkpoint_event["checkpoint_data"]))
366
+ channel_values: dict[str, Any] = {}
367
+ for channel, version in checkpoint.get("channel_versions", {}).items():
368
+ key = (channel, str(version))
369
+ if key in channel_data:
370
+ channel_values[channel] = channel_data[key]
371
+ checkpoint["channel_values"] = channel_values
372
+
373
+ pending_writes = [
374
+ (w["task_id"], w["channel"], self._deserialize(w["value"])) for w in writes
375
+ ]
376
+
377
+ parent_config = None
378
+ if checkpoint_event.get("parent_checkpoint_id"):
379
+ parent_config = _config(
380
+ thread_id, actor_id, checkpoint_ns, checkpoint_event["parent_checkpoint_id"]
381
+ )
382
+
383
+ return CheckpointTuple(
384
+ config=_config(thread_id, actor_id, checkpoint_ns, checkpoint_event["checkpoint_id"]),
385
+ checkpoint=checkpoint, # type: ignore[arg-type]
386
+ metadata=self._deserialize(checkpoint_event["metadata"]),
387
+ parent_config=parent_config,
388
+ pending_writes=pending_writes,
389
+ )
390
+
391
+ def _checkpoint_item(
392
+ self,
393
+ *,
394
+ checkpoint_id: str,
395
+ checkpoint_data: dict[str, Any],
396
+ metadata: dict[str, Any],
397
+ parent_checkpoint_id: Optional[str],
398
+ ) -> dict[str, Any]:
399
+ return {
400
+ "event_type": _EVENT_CHECKPOINT,
401
+ "checkpoint_id": checkpoint_id,
402
+ "checkpoint_data": self._serialize(checkpoint_data),
403
+ "metadata": self._serialize(metadata),
404
+ "parent_checkpoint_id": parent_checkpoint_id,
405
+ }
406
+
407
+ def _channel_data_item(self, channel: str, version: str, value: Any) -> dict[str, Any]:
408
+ stored = value if value == _EMPTY_CHANNEL_VALUE else self._serialize(value)
409
+ return {
410
+ "event_type": _EVENT_CHANNEL_DATA,
411
+ "channel": channel,
412
+ "version": version,
413
+ "value": stored,
414
+ }
415
+
416
+ def _serialize(self, value: Any) -> dict[str, str]:
417
+ """Serialize via the LangGraph serde into JSON-safe ``{type, data}``."""
418
+ type_tag, blob = self.serde.dumps_typed(value)
419
+ return {"type": type_tag, "data": base64.b64encode(blob).decode("utf-8")}
420
+
421
+ def _deserialize(self, serialized: dict[str, str]) -> Any:
422
+ return self.serde.loads_typed((serialized["type"], base64.b64decode(serialized["data"])))
423
+
424
+
425
+ # ----- module-level helpers --------------------------------------------------
426
+
427
+
428
+ def _parse_config(config: RunnableConfig | dict[str, Any] | None) -> tuple[str, str, str]:
429
+ configurable = (config or {}).get("configurable", {}) if config else {}
430
+ thread_id = configurable.get("thread_id")
431
+ actor_id = configurable.get("actor_id")
432
+ if not thread_id:
433
+ raise ValueError("config must contain configurable.thread_id")
434
+ if not actor_id:
435
+ raise ValueError("config must contain configurable.actor_id")
436
+ return thread_id, actor_id, configurable.get("checkpoint_ns", "") or ""
437
+
438
+
439
+ def _session_id(thread_id: str, checkpoint_ns: str) -> str:
440
+ """Deterministic session id for a (thread_id, checkpoint_ns) pair.
441
+
442
+ With a namespace we hash the pair so the id stays within the store's id constraints and never
443
+ collides with the bare ``thread_id`` used for the default namespace.
444
+ """
445
+ if checkpoint_ns:
446
+ return hashlib.sha256(f"{thread_id}\x1f{checkpoint_ns}".encode()).hexdigest()
447
+ return thread_id
448
+
449
+
450
+ def _config(
451
+ thread_id: str, actor_id: str, checkpoint_ns: str, checkpoint_id: str
452
+ ) -> RunnableConfig:
453
+ return {
454
+ "configurable": {
455
+ "thread_id": thread_id,
456
+ "actor_id": actor_id,
457
+ "checkpoint_ns": checkpoint_ns,
458
+ "checkpoint_id": checkpoint_id,
459
+ }
460
+ }
461
+
462
+
463
+ def _not_found_errors() -> tuple[type, ...]:
464
+ """Exception types that mean 'session does not exist yet'."""
465
+ try:
466
+ from databricks.sdk.errors import NotFound
467
+
468
+ return (NotFound,)
469
+ except ImportError: # pragma: no cover - SDK always present in practice
470
+ return (_SessionNotFound,)
471
+
472
+
473
+ class _SessionNotFound(Exception):
474
+ """Fallback 'not found' used only when databricks.sdk is unavailable."""
475
+
476
+
477
+ async def _run_sync(fn, *args):
478
+ import asyncio
479
+
480
+ loop = asyncio.get_running_loop()
481
+ return (
482
+ await loop.run_in_executor(None, fn, *args)
483
+ if args
484
+ else await loop.run_in_executor(None, fn)
485
+ )