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,90 @@
1
+ """Discover the Unity Catalog AI Gateway chat model services available in a workspace.
2
+
3
+ The AI Gateway exposes Databricks-managed models as Unity Catalog *model services* in the
4
+ ``system.ai`` schema (e.g. ``system.ai.claude-sonnet-4-5``), queryable through an OpenAI-compatible
5
+ endpoint at ``<host>/ai-gateway/mlflow/v1``. This lists the chat-capable ones so the demo UI's model
6
+ picker can offer them; the agent then calls the chosen one with ``use_ai_gateway=True``.
7
+
8
+ Kept framework-neutral (no agent SDK, no ``mlflow``) so it sits with the other neutral runtime
9
+ helpers and can be listed from either template's ``runtime/ui.py``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from typing import TYPE_CHECKING, Any, cast
16
+
17
+ if TYPE_CHECKING:
18
+ from databricks.sdk import WorkspaceClient
19
+
20
+ # The Unity Catalog REST route that lists model services under a schema.
21
+ _MODEL_SERVICES_PATH = "/api/2.1/unity-catalog/model-services"
22
+ # The AI Gateway's Databricks-managed models live in this UC schema.
23
+ _SYSTEM_AI_SCHEMA = "system.ai"
24
+ # The list API rejects page_size above 100 (InvalidParameterValue), so use its maximum.
25
+ _PAGE_SIZE = 100
26
+ # Resource-name prefix the API returns, e.g. "model-services/system.ai.claude-sonnet-4-5".
27
+ _NAME_PREFIX = "model-services/"
28
+ # The list call can intermittently fail; retry a few times so a transient blip doesn't blank the
29
+ # picker. A non-transient error (e.g. no permission) simply fails every attempt, then surfaces.
30
+ _LIST_ATTEMPTS = 3
31
+ _RETRY_DELAY_S = 0.5
32
+
33
+
34
+ def _is_chat_capable(supported_api_types: Any) -> bool:
35
+ """True if a model service speaks a chat/completions API (i.e. not embeddings-only).
36
+
37
+ ``supported_api_types`` looks like ``["openai/v1/chat/completions"]``. Be lenient when it's
38
+ absent (offer the model), but drop services that only advertise embeddings.
39
+ """
40
+ types = [str(t).lower() for t in (supported_api_types or [])]
41
+ if not types:
42
+ return True
43
+ return any(("chat" in t or "completions" in t or "responses" in t) for t in types)
44
+
45
+
46
+ def _list_page(client: WorkspaceClient, query: dict[str, Any]) -> Any:
47
+ """Fetch one page of the model-services list, retrying transient failures."""
48
+ last_error: Exception | None = None
49
+ for attempt in range(_LIST_ATTEMPTS):
50
+ try:
51
+ return client.api_client.do("GET", _MODEL_SERVICES_PATH, query=query)
52
+ except Exception as exc: # noqa: BLE001 - retry any list failure, then surface the last one
53
+ last_error = exc
54
+ if attempt < _LIST_ATTEMPTS - 1:
55
+ time.sleep(_RETRY_DELAY_S)
56
+ raise last_error if last_error else RuntimeError("model-services list returned nothing")
57
+
58
+
59
+ def list_ai_gateway_model_services(client: WorkspaceClient) -> list[str]:
60
+ """Return the names of chat-capable ``system.ai`` AI Gateway model services, sorted.
61
+
62
+ Each name is a Unity Catalog model-service path like ``system.ai.claude-sonnet-4-5`` — exactly
63
+ the string the OpenAI-compatible gateway (``use_ai_gateway=True``) expects as its ``model``.
64
+ Pages through the model-services list API, retrying transient list failures per page.
65
+
66
+ Raises whatever the underlying request raises after retries (e.g. a permission error); callers
67
+ that want a graceful fallback to just the agent's default model should catch it.
68
+ """
69
+ names: list[str] = []
70
+ page_token: str | None = None
71
+ while True:
72
+ query: dict[str, Any] = {"parent": f"schemas/{_SYSTEM_AI_SCHEMA}", "page_size": _PAGE_SIZE}
73
+ if page_token:
74
+ query["page_token"] = page_token
75
+ raw = _list_page(client, query)
76
+ if not isinstance(raw, dict):
77
+ break
78
+ response = cast("dict[str, Any]", raw)
79
+ for service in response.get("model_services") or []:
80
+ if not isinstance(service, dict):
81
+ continue
82
+ name = str(service.get("name") or "")
83
+ if name.startswith(_NAME_PREFIX):
84
+ name = name[len(_NAME_PREFIX) :]
85
+ if name and _is_chat_capable(service.get("supported_api_types")):
86
+ names.append(name)
87
+ page_token = response.get("next_page_token") or None
88
+ if not page_token:
89
+ break
90
+ return sorted(names)
@@ -0,0 +1,273 @@
1
+ """Transport-neutral facade for Agent Bricks invocation execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import copy
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ from databricks_agentkit.runtime.execution import (
11
+ AttemptExecution,
12
+ InvocationExecutor,
13
+ LocalInvocationExecutor,
14
+ copy_json_value,
15
+ )
16
+ from databricks_agentkit.runtime.store import (
17
+ InMemoryRuntimeStore,
18
+ RuntimeStore,
19
+ runtime_store_from_environment,
20
+ )
21
+ from databricks_agentkit.runtime.types import (
22
+ Invocation,
23
+ InvocationEvent,
24
+ InvocationExecutorFn,
25
+ InvocationFailedError,
26
+ InvocationNotFoundError,
27
+ InvocationStatus,
28
+ JsonValue,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
33
+
34
+
35
+ class Runtime:
36
+ """Coordinate one Agent Bricks process's invocation lifecycle.
37
+
38
+ A Runtime Store owns invocation state: requests, status, events, and results. An invocation
39
+ executor turns queued state into agent attempts. This facade accepts idempotent requests,
40
+ delegates scheduling to the executor, and polls the store for foreground results or event replay.
41
+ Use :meth:`local` for process-local execution, :meth:`durable` for a durable store,
42
+ :meth:`from_store` for an explicit store, or :meth:`from_environment` when Agent Bricks configures the
43
+ store through the process environment.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ *,
49
+ runtime_store: RuntimeStore,
50
+ executor: InvocationExecutor,
51
+ poll_seconds: float = 1.0,
52
+ ) -> None:
53
+ if poll_seconds <= 0:
54
+ raise ValueError("poll_seconds must be positive")
55
+ self.runtime_store = runtime_store
56
+ self.executor = executor
57
+ self.poll_seconds = poll_seconds
58
+ self._started = False
59
+
60
+ @classmethod
61
+ def local(
62
+ cls,
63
+ execute_fn: InvocationExecutorFn,
64
+ *,
65
+ runtime_store: RuntimeStore | None = None,
66
+ poll_seconds: float = 1.0,
67
+ ) -> Runtime:
68
+ """Construct a Runtime that schedules work only in the current process.
69
+
70
+ The default :class:`InMemoryRuntimeStore` supports foreground, background, streaming,
71
+ polling, and event replay until the process exits. It does not heartbeat or recover work.
72
+ """
73
+ store = InMemoryRuntimeStore() if runtime_store is None else runtime_store
74
+ from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
75
+
76
+ if isinstance(store, DurableRuntimeStore):
77
+ raise TypeError("Runtime.local requires a non-durable RuntimeStore")
78
+ execution = AttemptExecution(execute_fn, runtime_store=store)
79
+ return cls(
80
+ runtime_store=store,
81
+ executor=LocalInvocationExecutor(execution, runtime_store=store),
82
+ poll_seconds=poll_seconds,
83
+ )
84
+
85
+ @classmethod
86
+ def durable(
87
+ cls,
88
+ execute_fn: InvocationExecutorFn,
89
+ *,
90
+ runtime_store: DurableRuntimeStore,
91
+ recovery_enabled: Callable[[], bool] | None = None,
92
+ heartbeat_seconds: float = 3.0,
93
+ stale_seconds: float = 10.0,
94
+ scan_seconds: float = 3.0,
95
+ poll_seconds: float = 1.0,
96
+ ) -> Runtime:
97
+ """Construct a Runtime that heartbeats and can recover work from a durable store.
98
+
99
+ A durable runtime requires a :class:`DurableRuntimeStore`. Its results and events persist
100
+ across process restarts. ``recovery_enabled`` controls only stale-work recovery; it does not
101
+ change whether normal invocation state is persisted.
102
+ """
103
+ from databricks_agentkit.runtime.durability.execution import DurableInvocationExecutor
104
+ from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
105
+
106
+ if not isinstance(runtime_store, DurableRuntimeStore):
107
+ raise TypeError("Runtime.durable requires a DurableRuntimeStore")
108
+ execution = AttemptExecution(execute_fn, runtime_store=runtime_store)
109
+ return cls(
110
+ runtime_store=runtime_store,
111
+ executor=DurableInvocationExecutor(
112
+ execution,
113
+ runtime_store=runtime_store,
114
+ recovery_enabled=recovery_enabled or (lambda: True),
115
+ heartbeat_seconds=heartbeat_seconds,
116
+ stale_seconds=stale_seconds,
117
+ scan_seconds=scan_seconds,
118
+ ),
119
+ poll_seconds=poll_seconds,
120
+ )
121
+
122
+ @classmethod
123
+ def from_store(
124
+ cls,
125
+ execute_fn: InvocationExecutorFn,
126
+ *,
127
+ runtime_store: RuntimeStore,
128
+ recovery_enabled: Callable[[], bool] | None = None,
129
+ heartbeat_seconds: float = 3.0,
130
+ stale_seconds: float = 10.0,
131
+ scan_seconds: float = 3.0,
132
+ poll_seconds: float = 1.0,
133
+ ) -> Runtime:
134
+ """Construct the local or durable Runtime required by the selected Runtime Store."""
135
+ from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
136
+
137
+ if isinstance(runtime_store, DurableRuntimeStore):
138
+ return cls.durable(
139
+ execute_fn,
140
+ runtime_store=runtime_store,
141
+ recovery_enabled=recovery_enabled,
142
+ heartbeat_seconds=heartbeat_seconds,
143
+ stale_seconds=stale_seconds,
144
+ scan_seconds=scan_seconds,
145
+ poll_seconds=poll_seconds,
146
+ )
147
+ return cls.local(execute_fn, runtime_store=runtime_store, poll_seconds=poll_seconds)
148
+
149
+ @classmethod
150
+ def from_environment(
151
+ cls,
152
+ execute_fn: InvocationExecutorFn,
153
+ *,
154
+ recovery_enabled: Callable[[], bool] | None = None,
155
+ heartbeat_seconds: float = 3.0,
156
+ stale_seconds: float = 10.0,
157
+ scan_seconds: float = 3.0,
158
+ poll_seconds: float = 1.0,
159
+ ) -> Runtime:
160
+ """Construct the Runtime selected by the Agent Bricks process environment."""
161
+ return cls.from_store(
162
+ execute_fn,
163
+ runtime_store=runtime_store_from_environment(),
164
+ recovery_enabled=recovery_enabled,
165
+ heartbeat_seconds=heartbeat_seconds,
166
+ stale_seconds=stale_seconds,
167
+ scan_seconds=scan_seconds,
168
+ poll_seconds=poll_seconds,
169
+ )
170
+
171
+ @property
172
+ def is_durable(self) -> bool:
173
+ """Whether this Runtime has heartbeat and recovery capabilities."""
174
+ from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
175
+
176
+ return isinstance(self.runtime_store, DurableRuntimeStore)
177
+
178
+ async def start(self) -> None:
179
+ """Initialize the Runtime Store and invocation executor."""
180
+ if self._started:
181
+ return
182
+ await self.runtime_store.initialize()
183
+ try:
184
+ await self.executor.start()
185
+ except Exception:
186
+ await self.runtime_store.close()
187
+ raise
188
+ self._started = True
189
+
190
+ async def stop(self) -> None:
191
+ """Stop local work and close the Runtime Store."""
192
+ if not self._started:
193
+ return
194
+ await self.executor.stop()
195
+ self._started = False
196
+ await self.runtime_store.close()
197
+
198
+ async def submit(self, invocation_id: str, request: JsonValue) -> Invocation:
199
+ """Accept an idempotent invocation and schedule it on this worker.
200
+
201
+ The Runtime Store makes ``invocation_id`` the idempotency key. Retrying the same request
202
+ returns its existing state; a different request for the same ID raises
203
+ :class:`InvocationConflictError`.
204
+ """
205
+ self._require_started()
206
+ if not invocation_id:
207
+ raise ValueError("invocation_id must not be empty")
208
+ state = await self.runtime_store.accept(
209
+ invocation_id,
210
+ copy_json_value(request, "request"),
211
+ )
212
+ self.executor.ensure_scheduled(state)
213
+ return state
214
+
215
+ async def invoke(
216
+ self,
217
+ invocation_id: str,
218
+ request: JsonValue,
219
+ *,
220
+ timeout: float | None = None,
221
+ ) -> JsonValue:
222
+ """Accept an invocation and wait for its terminal result."""
223
+ await self.submit(invocation_id, request)
224
+ return await self.wait(invocation_id, timeout=timeout)
225
+
226
+ async def get_invocation(self, invocation_id: str) -> Invocation | None:
227
+ """Return current state and ensure queued first-attempt work is scheduled locally."""
228
+ self._require_started()
229
+ state = await self.runtime_store.get(invocation_id)
230
+ if state is not None:
231
+ self.executor.ensure_scheduled(state)
232
+ return state
233
+
234
+ async def wait(
235
+ self,
236
+ invocation_id: str,
237
+ *,
238
+ timeout: float | None = None,
239
+ ) -> JsonValue:
240
+ """Poll until an invocation completes, fails, or reaches the optional timeout."""
241
+ self._require_started()
242
+
243
+ async def poll() -> JsonValue:
244
+ while True:
245
+ state = await self.get_invocation(invocation_id)
246
+ if state is None:
247
+ raise InvocationNotFoundError(invocation_id)
248
+ if state.status == InvocationStatus.COMPLETED:
249
+ return copy.deepcopy(state.response)
250
+ if state.status == InvocationStatus.FAILED:
251
+ raise InvocationFailedError(invocation_id)
252
+ await asyncio.sleep(self.poll_seconds)
253
+
254
+ if timeout is None:
255
+ return await poll()
256
+ try:
257
+ return await asyncio.wait_for(poll(), timeout=timeout)
258
+ except asyncio.TimeoutError as exc:
259
+ raise TimeoutError from exc
260
+
261
+ async def get_events(
262
+ self,
263
+ invocation_id: str,
264
+ *,
265
+ after_sequence: int | None = None,
266
+ ) -> list[InvocationEvent]:
267
+ """Return persisted invocation events after an optional exclusive replay cursor."""
268
+ self._require_started()
269
+ return await self.runtime_store.events(invocation_id, after_sequence)
270
+
271
+ def _require_started(self) -> None:
272
+ if not self._started:
273
+ raise RuntimeError("Runtime.start() must be called first")
@@ -0,0 +1,120 @@
1
+ """Minimal Databricks Session Store REST client (vendored).
2
+
3
+ Wraps the managed Session Store API (``/api/2.0/agents/session-stores``) — the SDK-agnostic durable
4
+ store for agent session history — with just the calls the LangGraph saver needs: resolve a store,
5
+ get-or-create a session, and append / list / clear its ordered items. Auth/host come from a
6
+ ``databricks.sdk.WorkspaceClient`` (default credentials).
7
+
8
+ This is a trimmed stand-in for the first-party ``databricks_agent_client.SessionStoreClient``; when
9
+ that package is published, import it instead and delete this file. Item ``data`` is opaque JSON, so
10
+ callers store whatever shape they like (the saver stores checkpoint fragments).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Iterator, Optional, Sequence
17
+
18
+ from databricks.sdk import WorkspaceClient
19
+
20
+ from databricks_agentkit.runtime.workspace import workspace_client as default_workspace_client
21
+
22
+ _API_ROOT = "/api/2.0/agents"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Session:
27
+ """A durable interaction within a Session Store, addressed by (store, session_id)."""
28
+
29
+ session_store_name: str
30
+ session_id: str
31
+ actor_id: str
32
+ metadata: dict[str, str] = field(default_factory=dict)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class SessionItem:
37
+ """One opaque, ordered history entry. ``data`` is arbitrary JSON."""
38
+
39
+ item_id: str
40
+ data: Any
41
+
42
+
43
+ class SessionStoreClient:
44
+ """Thin REST client over the managed Session Store API."""
45
+
46
+ def __init__(self, workspace_client: Optional[WorkspaceClient] = None) -> None:
47
+ self._api = (workspace_client or default_workspace_client()).api_client
48
+ self._store_name: Optional[str] = None
49
+
50
+ def set_session_store(self, session_store_name: str) -> "SessionStoreClient":
51
+ """Bind this client to a store; returns self so it doubles as the store handle."""
52
+ if not session_store_name:
53
+ raise ValueError("session_store_name is required")
54
+ self._store_name = session_store_name
55
+ return self
56
+
57
+ def get_session(self, *, session_id: str) -> Session:
58
+ resp: dict[str, Any] = self._api.do("GET", f"{self._sessions_path()}/{session_id}") # type: ignore[assignment]
59
+ return self._session(resp)
60
+
61
+ def create_session(
62
+ self,
63
+ *,
64
+ actor_id: str,
65
+ session_id: Optional[str] = None,
66
+ metadata: Optional[dict[str, str]] = None,
67
+ ) -> Session:
68
+ body: dict[str, Any] = {"actor_id": actor_id}
69
+ if metadata:
70
+ body["metadata"] = metadata
71
+ query = {"session_id": session_id} if session_id else None
72
+ resp: dict[str, Any] = self._api.do("POST", self._sessions_path(), query=query, body=body) # type: ignore[assignment]
73
+ return self._session(resp)
74
+
75
+ def append_items(self, session: Session, *, items: Sequence[Any]) -> None:
76
+ if not items:
77
+ raise ValueError("at least one item is required")
78
+ self._api.do(
79
+ "POST",
80
+ f"{self._items_path(session)}:append",
81
+ body={"items": [{"data": item} for item in items]},
82
+ )
83
+
84
+ def list_items(
85
+ self, session: Session, *, order_by: Optional[str] = None
86
+ ) -> Iterator[SessionItem]:
87
+ """Yield the session's items (paginating transparently)."""
88
+ page_token: Optional[str] = None
89
+ while True:
90
+ query = {k: v for k, v in {"order_by": order_by, "page_token": page_token}.items() if v}
91
+ resp: dict[str, Any] = self._api.do(
92
+ "GET", self._items_path(session), query=query or None
93
+ ) # type: ignore[assignment]
94
+ for item in resp.get("session_items", []):
95
+ if "data" in item:
96
+ yield SessionItem(item_id=item.get("item_id", ""), data=item["data"])
97
+ page_token = resp.get("next_page_token")
98
+ if not page_token:
99
+ return
100
+
101
+ def clear_items(self, session: Session) -> None:
102
+ self._api.do("POST", f"{self._items_path(session)}:clear", body={})
103
+
104
+ # ----- internals ----------------------------------------------------------
105
+
106
+ def _sessions_path(self) -> str:
107
+ if not self._store_name:
108
+ raise ValueError("call set_session_store() before using the client")
109
+ return f"{_API_ROOT}/session-stores/{self._store_name}/sessions"
110
+
111
+ def _items_path(self, session: Session) -> str:
112
+ return f"{self._sessions_path()}/{session.session_id}/items"
113
+
114
+ def _session(self, resp: dict[str, Any]) -> Session:
115
+ return Session(
116
+ session_store_name=resp.get("session_store_name", self._store_name or ""),
117
+ session_id=resp["session_id"],
118
+ actor_id=resp.get("actor_id", ""),
119
+ metadata=dict(resp.get("metadata", {})),
120
+ )