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,673 @@
1
+ """Private transport for the managed agent store APIs.
2
+
3
+ The public SDK is the resource-oriented :class:`databricks_agentkit.AgentKitClient`.
4
+ This module temporarily owns the one-method-per-endpoint transport used by that
5
+ wrapper and the CLI. It can be replaced by the generated ``WorkspaceClient.mason``
6
+ service without changing the public resource surface.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import configparser
12
+ import os
13
+ import pathlib
14
+ import time
15
+ from typing import TYPE_CHECKING, Any, Optional
16
+ from urllib.parse import quote
17
+
18
+ from databricks_agentbricks.errors import TRANSIENT_ERROR_CODES, AgentCliError, wrap_api_error
19
+ from databricks_agentkit import models
20
+
21
+ if TYPE_CHECKING:
22
+ from databricks.sdk import WorkspaceClient
23
+
24
+ _BASE = "/api/2.0/agents"
25
+ _MCP_SERVICES_PATH = "/api/2.1/unity-catalog/mcp-services"
26
+
27
+ # Transient backend failures (e.g. a CANCELLED RPC) usually clear on a retry, so retry safe
28
+ # requests once before surfacing them. Mutating requests must opt in explicitly: their first
29
+ # attempt may have committed even when its response was lost.
30
+ _MAX_ATTEMPTS = 2
31
+ _RETRY_BASE_DELAY_S = 0.2
32
+
33
+ # Bound the SDK's own retry budget (default 300s). The SDK retries every HTTP 429/503 as platform
34
+ # throttling, so an interactive command otherwise hangs up to 5 minutes on a sustained 429 before
35
+ # surfacing anything; cap it so throttling fails fast with a clear error.
36
+ _CLI_RETRY_TIMEOUT_S = 60
37
+
38
+
39
+ def _query(**kwargs: Any) -> dict[str, Any]:
40
+ """Build a query dict, dropping None and empty values."""
41
+ return {k: v for k, v in kwargs.items() if v is not None and v != ""}
42
+
43
+
44
+ def _body(**kwargs: Any) -> dict[str, Any]:
45
+ """Build a request body while retaining meaningful empty values."""
46
+ return {k: v for k, v in kwargs.items() if v is not None}
47
+
48
+
49
+ def _as(cls: type, resp: Any) -> Any:
50
+ """Wrap a JSON response in a typed model, passing non-dicts through unchanged."""
51
+ return cls(resp) if isinstance(resp, dict) else resp
52
+
53
+
54
+ def memory_store_path(name: str) -> str:
55
+ """Normalize a store id or name into the `memory-stores/{id}` resource segment.
56
+
57
+ Validate locally so malformed resource names do not produce misleading endpoint errors.
58
+ """
59
+ raw = (name or "").strip()
60
+ if raw.startswith("memory-stores/"):
61
+ raw = raw[len("memory-stores/") :]
62
+ raw = raw.strip().strip("/")
63
+ if not raw:
64
+ raise AgentCliError("A memory store id or resource name is required.")
65
+ if "/" in raw:
66
+ raise AgentCliError(f"Invalid memory store id or resource name: {name!r}")
67
+ return f"memory-stores/{raw}"
68
+
69
+
70
+ def session_store_path(name: str) -> str:
71
+ """Normalize a session store name into the `session-stores/{name}` resource segment."""
72
+ raw = (name or "").strip()
73
+ if raw.startswith("session-stores/"):
74
+ raw = raw[len("session-stores/") :]
75
+ raw = raw.strip().strip("/")
76
+ if not raw:
77
+ raise AgentCliError("A session store name is required.")
78
+ return f"session-stores/{raw}"
79
+
80
+
81
+ def memory_entry_path(store: str, entry: str) -> str:
82
+ entry = (entry or "").strip().strip("/")
83
+ if entry.startswith("memory-stores/"):
84
+ return entry
85
+ if not entry:
86
+ raise AgentCliError("A memory entry id or resource name is required.")
87
+ return f"{memory_store_path(store)}/entries/{entry}"
88
+
89
+
90
+ def _profile_host(profile: str) -> Optional[str]:
91
+ config_path = pathlib.Path(
92
+ os.getenv("DATABRICKS_CONFIG_FILE", pathlib.Path.home() / ".databrickscfg")
93
+ )
94
+ parser = configparser.ConfigParser()
95
+ try:
96
+ parser.read(config_path)
97
+ except (OSError, configparser.Error):
98
+ return None
99
+ return parser.get(profile, "host", fallback=None)
100
+
101
+
102
+ def _bound_retry_timeout(client: WorkspaceClient) -> WorkspaceClient:
103
+ # Shorten the SDK's default 300s retry budget so 429/503 throttling fails in ~1 min, not ~5.
104
+ try:
105
+ client.api_client._api_client._retry_timeout_seconds = _CLI_RETRY_TIMEOUT_S
106
+ except AttributeError:
107
+ pass
108
+ return client
109
+
110
+
111
+ def _workspace_client(profile: Optional[str]) -> WorkspaceClient:
112
+ # Imported here (not at module top) so the ~0.7s databricks.sdk import is paid only when a
113
+ # command actually builds a client, not on every CLI invocation.
114
+ from databricks.sdk import WorkspaceClient
115
+
116
+ client = _bound_retry_timeout(WorkspaceClient(profile=profile))
117
+ if not profile or not client.config.workspace_id:
118
+ return client
119
+
120
+ configured_host = _profile_host(profile)
121
+ resolved_host = client.config.host
122
+ if not configured_host or configured_host.rstrip("/") == (resolved_host or "").rstrip("/"):
123
+ return client
124
+
125
+ workspace_id = str(client.config.workspace_id)
126
+ return _bound_retry_timeout(
127
+ WorkspaceClient(
128
+ profile=profile,
129
+ host=configured_host,
130
+ custom_headers={"X-Databricks-Org-Id": workspace_id},
131
+ )
132
+ )
133
+
134
+
135
+ class _AgentBricksApiClient:
136
+ """Private transport for the 2.0 agents API until the generated SDK is available."""
137
+
138
+ def __init__(
139
+ self,
140
+ profile: Optional[str] = None,
141
+ *,
142
+ workspace_client: Optional[WorkspaceClient] = None,
143
+ ) -> None:
144
+ if profile is not None and workspace_client is not None:
145
+ raise ValueError("profile and workspace_client are mutually exclusive")
146
+ try:
147
+ self._w = workspace_client or _workspace_client(profile)
148
+ except Exception as exc: # noqa: BLE001 - surfaced as a clean CLI error
149
+ raise AgentCliError(
150
+ f"Could not initialize Databricks auth: {exc}",
151
+ hint="Select an existing profile with `ab --profile <name> <command>` "
152
+ "or authenticate and save it with `ab login --profile <name>`.",
153
+ ) from exc
154
+
155
+ @property
156
+ def host(self) -> str:
157
+ return self._w.config.host or "unknown"
158
+
159
+ @property
160
+ def current_user(self) -> str:
161
+ """The authenticated user's name (used to derive the app source workspace path)."""
162
+ return str(self._w.current_user.me().user_name or "unknown")
163
+
164
+ def ensure_workspace_dir(self, path: str) -> None:
165
+ """Create a workspace directory (and parents), idempotently.
166
+
167
+ MLflow's create_experiment won't create the intermediate folder for a nested experiment
168
+ path, so callers make the parent dir first.
169
+ """
170
+ self._w.workspace.mkdirs(path)
171
+
172
+ def create_runtime_store(
173
+ self,
174
+ runtime_store_id: str,
175
+ app_service_principal_id: str,
176
+ *,
177
+ app_name: str,
178
+ retry_transient: bool = False,
179
+ ) -> models.RuntimeStore:
180
+ """Create the deployment's Runtime Store through Conversation Store."""
181
+ return _as(
182
+ models.RuntimeStore,
183
+ self._do(
184
+ "POST",
185
+ f"{_BASE}/runtime-stores",
186
+ query={"runtime_store_id": runtime_store_id},
187
+ body={
188
+ "owner": {
189
+ "app": {
190
+ "name": app_name,
191
+ "service_principal_id": app_service_principal_id,
192
+ }
193
+ }
194
+ },
195
+ safe_to_retry=retry_transient,
196
+ ),
197
+ )
198
+
199
+ def get_runtime_store(self, runtime_store_id: str) -> models.RuntimeStore:
200
+ """Resolve the service-managed backend and app owner before reusing a store."""
201
+ return _as(
202
+ models.RuntimeStore,
203
+ self._do("GET", f"{_BASE}/runtime-stores/{quote(runtime_store_id, safe='')}"),
204
+ )
205
+
206
+ def delete_runtime_store(self, runtime_store_id: str) -> dict:
207
+ """Delete a deployment's Runtime Store and its dedicated database."""
208
+ return self._do(
209
+ "DELETE",
210
+ f"{_BASE}/runtime-stores/{quote(runtime_store_id, safe='')}",
211
+ safe_to_retry=True,
212
+ )
213
+
214
+ def _do(
215
+ self,
216
+ method: str,
217
+ path: str,
218
+ *,
219
+ query: Optional[dict] = None,
220
+ body: Optional[dict] = None,
221
+ safe_to_retry: bool = False,
222
+ ) -> Any:
223
+ retry_allowed = method == "GET" or safe_to_retry
224
+ delay = _RETRY_BASE_DELAY_S
225
+ for attempt in range(1, _MAX_ATTEMPTS + 1):
226
+ try:
227
+ return self._w.api_client.do(method, path, query=query, body=body)
228
+ except Exception as exc: # noqa: BLE001 - normalized to AgentCliError
229
+ retryable = (
230
+ retry_allowed and getattr(exc, "error_code", None) in TRANSIENT_ERROR_CODES
231
+ )
232
+ if not retryable or attempt == _MAX_ATTEMPTS:
233
+ raise wrap_api_error(exc) from exc
234
+ time.sleep(delay)
235
+ delay *= 2
236
+
237
+ # --- Unity Catalog MCP Services -----------------------------------------
238
+
239
+ def get_mcp_service(self, service: str) -> dict:
240
+ """Look up a managed MCP service visible to the authenticated user."""
241
+ return self._do("GET", f"{_MCP_SERVICES_PATH}/{quote(service, safe='')}")
242
+
243
+ def list_mcp_services(
244
+ self, schema: str = "system.ai", page_token: Optional[str] = None
245
+ ) -> dict:
246
+ """List MCP Services visible to the user in a Unity Catalog schema."""
247
+ return self._do(
248
+ "GET",
249
+ _MCP_SERVICES_PATH,
250
+ query=_query(parent=f"schemas/{schema}", page_token=page_token),
251
+ )
252
+
253
+ # --- memory stores -------------------------------------------------------
254
+
255
+ def create_memory_store(
256
+ self,
257
+ display_name: str,
258
+ description: Optional[str] = None,
259
+ *,
260
+ retry_transient: bool = False,
261
+ ) -> models.MemoryStore:
262
+ body = _body(display_name=display_name, description=description)
263
+ return _as(
264
+ models.MemoryStore,
265
+ self._do(
266
+ "POST",
267
+ f"{_BASE}/memory-stores",
268
+ query={"managed_memory_store_id": display_name},
269
+ body=body,
270
+ safe_to_retry=retry_transient,
271
+ ),
272
+ )
273
+
274
+ def get_memory_store(self, name: str) -> models.MemoryStore:
275
+ return _as(models.MemoryStore, self._do("GET", f"{_BASE}/{memory_store_path(name)}"))
276
+
277
+ def list_memory_stores(
278
+ self, page_size: Optional[int] = None, page_token: Optional[str] = None
279
+ ) -> models.MemoryStoreList:
280
+ return _as(
281
+ models.MemoryStoreList,
282
+ self._do(
283
+ "GET",
284
+ f"{_BASE}/memory-stores",
285
+ query=_query(page_size=page_size, page_token=page_token),
286
+ ),
287
+ )
288
+
289
+ def update_memory_store(
290
+ self, name: str, display_name: Optional[str] = None, description: Optional[str] = None
291
+ ) -> models.MemoryStore:
292
+ body = _body(display_name=display_name, description=description)
293
+ if not body:
294
+ raise AgentCliError("No fields to update. Provide a display name and/or description.")
295
+ mask = ",".join(body.keys())
296
+ return _as(
297
+ models.MemoryStore,
298
+ self._do(
299
+ "PATCH",
300
+ f"{_BASE}/{memory_store_path(name)}",
301
+ query=_query(update_mask=mask),
302
+ body=body,
303
+ ),
304
+ )
305
+
306
+ def delete_memory_store(self, name: str) -> dict:
307
+ return self._do("DELETE", f"{_BASE}/{memory_store_path(name)}")
308
+
309
+ # --- memory entries ------------------------------------------------------
310
+
311
+ def create_memory_entry(
312
+ self,
313
+ store: str,
314
+ actor_id: str,
315
+ path: str,
316
+ content: Optional[str] = None,
317
+ description: Optional[str] = None,
318
+ session_id: Optional[str] = None,
319
+ source_type: Optional[str] = None,
320
+ write_mode: Optional[str] = None,
321
+ ) -> models.MemoryEntry:
322
+ body = _body(
323
+ actor_id=actor_id,
324
+ path=path,
325
+ content=content,
326
+ description=description,
327
+ session_id=session_id,
328
+ source_type=source_type,
329
+ write_mode=write_mode,
330
+ )
331
+ return _as(
332
+ models.MemoryEntry,
333
+ self._do("POST", f"{_BASE}/{memory_store_path(store)}/entries", body=body),
334
+ )
335
+
336
+ def get_memory_entry(
337
+ self, store: str, entry: str, read_mask: Optional[str] = None
338
+ ) -> models.MemoryEntry:
339
+ return _as(
340
+ models.MemoryEntry,
341
+ self._do(
342
+ "GET",
343
+ f"{_BASE}/{memory_entry_path(store, entry)}",
344
+ query=_query(read_mask=read_mask),
345
+ ),
346
+ )
347
+
348
+ def list_memory_entries(
349
+ self,
350
+ store: str,
351
+ actor_id: str,
352
+ path_prefix: Optional[str] = None,
353
+ session_id: Optional[str] = None,
354
+ page_size: Optional[int] = None,
355
+ page_token: Optional[str] = None,
356
+ read_mask: Optional[str] = None,
357
+ ) -> models.MemoryEntryList:
358
+ return _as(
359
+ models.MemoryEntryList,
360
+ self._do(
361
+ "GET",
362
+ f"{_BASE}/{memory_store_path(store)}/entries",
363
+ query=_query(
364
+ actor_id=actor_id,
365
+ path_prefix=path_prefix,
366
+ session_id=session_id,
367
+ page_size=page_size,
368
+ page_token=page_token,
369
+ read_mask=read_mask,
370
+ ),
371
+ ),
372
+ )
373
+
374
+ def search_memory_entries(
375
+ self,
376
+ store: str,
377
+ actor_id: str,
378
+ query: str,
379
+ limit: Optional[int] = None,
380
+ page_size: Optional[int] = None,
381
+ path_prefix: Optional[str] = None,
382
+ session_id: Optional[str] = None,
383
+ read_mask: Optional[str] = None,
384
+ ) -> models.MemorySearchResult:
385
+ body = _body(
386
+ actor_id=actor_id,
387
+ query=query,
388
+ limit=limit,
389
+ page_size=page_size,
390
+ path_prefix=path_prefix,
391
+ session_id=session_id,
392
+ read_mask=read_mask,
393
+ )
394
+ return _as(
395
+ models.MemorySearchResult,
396
+ self._do(
397
+ "POST",
398
+ f"{_BASE}/{memory_store_path(store)}/entries:search",
399
+ body=body,
400
+ safe_to_retry=True,
401
+ ),
402
+ )
403
+
404
+ def update_memory_entry(
405
+ self,
406
+ store: str,
407
+ entry: str,
408
+ content: Optional[str] = None,
409
+ description: Optional[str] = None,
410
+ ) -> models.MemoryEntry:
411
+ body = _body(content=content, description=description)
412
+ if not body:
413
+ raise AgentCliError("No fields to update. Provide content and/or a description.")
414
+ mask = ",".join(body.keys())
415
+ return _as(
416
+ models.MemoryEntry,
417
+ self._do(
418
+ "PATCH",
419
+ f"{_BASE}/{memory_entry_path(store, entry)}",
420
+ query=_query(update_mask=mask),
421
+ body=body,
422
+ ),
423
+ )
424
+
425
+ def delete_memory_entry(self, store: str, entry: str) -> dict:
426
+ return self._do("DELETE", f"{_BASE}/{memory_entry_path(store, entry)}")
427
+
428
+ # --- session stores ------------------------------------------------------
429
+
430
+ def create_session_store(
431
+ self,
432
+ name: str,
433
+ description: Optional[str] = None,
434
+ metadata: Optional[dict] = None,
435
+ *,
436
+ retry_transient: bool = False,
437
+ ) -> models.SessionStore:
438
+ body = _body(description=description, metadata=metadata)
439
+ return _as(
440
+ models.SessionStore,
441
+ self._do(
442
+ "POST",
443
+ f"{_BASE}/session-stores",
444
+ query={"session_store_name": name, "session_store_id": name},
445
+ body=body,
446
+ safe_to_retry=retry_transient,
447
+ ),
448
+ )
449
+
450
+ def get_session_store(self, name: str) -> models.SessionStore:
451
+ return _as(models.SessionStore, self._do("GET", f"{_BASE}/{session_store_path(name)}"))
452
+
453
+ def list_session_stores(
454
+ self, page_size: Optional[int] = None, page_token: Optional[str] = None
455
+ ) -> models.SessionStoreList:
456
+ return _as(
457
+ models.SessionStoreList,
458
+ self._do(
459
+ "GET",
460
+ f"{_BASE}/session-stores",
461
+ query=_query(page_size=page_size, page_token=page_token),
462
+ ),
463
+ )
464
+
465
+ def update_session_store(
466
+ self, name: str, description: Optional[str] = None, metadata: Optional[dict] = None
467
+ ) -> models.SessionStore:
468
+ body = _body(description=description, metadata=metadata)
469
+ if not body:
470
+ raise AgentCliError("No fields to update. Provide a description and/or metadata.")
471
+ mask = ",".join(body.keys())
472
+ return _as(
473
+ models.SessionStore,
474
+ self._do(
475
+ "PATCH",
476
+ f"{_BASE}/{session_store_path(name)}",
477
+ query=_query(update_mask=mask),
478
+ body=body,
479
+ ),
480
+ )
481
+
482
+ def delete_session_store(self, name: str) -> dict:
483
+ return self._do("DELETE", f"{_BASE}/{session_store_path(name)}")
484
+
485
+ # --- store permission grants --------------------------------------------
486
+
487
+ def grant_session_store_permission(
488
+ self, name: str, principal_client_id: str, permission: str = "WRITE"
489
+ ) -> dict:
490
+ """Grant a service principal READ/WRITE on a session store.
491
+
492
+ The store service performs the underlying Lakebase role provisioning and GRANTs itself, so
493
+ this succeeds without the caller owning the store or holding MANAGE on its Lakebase.
494
+ """
495
+ return self._do(
496
+ "POST",
497
+ f"{_BASE}/{session_store_path(name)}/permissions:grant",
498
+ body={
499
+ "principal": {"type": "SERVICE_PRINCIPAL", "name": principal_client_id},
500
+ "permission": permission,
501
+ },
502
+ safe_to_retry=True,
503
+ )
504
+
505
+ def grant_memory_store_permission(
506
+ self, name: str, principal_client_id: str, permission: str = "WRITE"
507
+ ) -> dict:
508
+ """Grant a service principal READ/WRITE on a memory store (``name`` is its resource id)."""
509
+ return self._do(
510
+ "POST",
511
+ f"{_BASE}/{memory_store_path(name)}/permissions:grant",
512
+ body={
513
+ "principal": {"type": "SERVICE_PRINCIPAL", "name": principal_client_id},
514
+ "permission": permission,
515
+ },
516
+ safe_to_retry=True,
517
+ )
518
+
519
+ # --- sessions ------------------------------------------------------------
520
+
521
+ def create_session(
522
+ self,
523
+ store: str,
524
+ actor_id: str,
525
+ session_id: Optional[str] = None,
526
+ parent_session_id: Optional[str] = None,
527
+ metadata: Optional[dict] = None,
528
+ ) -> models.Session:
529
+ body = _body(actor_id=actor_id, parent_session_id=parent_session_id, metadata=metadata)
530
+ return _as(
531
+ models.Session,
532
+ self._do(
533
+ "POST",
534
+ f"{_BASE}/session-stores/{store}/sessions",
535
+ query=_query(session_id=session_id),
536
+ body=body,
537
+ ),
538
+ )
539
+
540
+ def list_sessions(
541
+ self,
542
+ store: str,
543
+ filter: Optional[str] = None,
544
+ order_by: Optional[str] = None,
545
+ page_size: Optional[int] = None,
546
+ page_token: Optional[str] = None,
547
+ ) -> models.SessionList:
548
+ return _as(
549
+ models.SessionList,
550
+ self._do(
551
+ "GET",
552
+ f"{_BASE}/session-stores/{store}/sessions",
553
+ query=_query(
554
+ filter=filter, order_by=order_by, page_size=page_size, page_token=page_token
555
+ ),
556
+ ),
557
+ )
558
+
559
+ def get_session(self, session_id: str, store: str) -> models.Session:
560
+ path = f"{_BASE}/session-stores/{store}/sessions/{session_id}"
561
+ return _as(models.Session, self._do("GET", path))
562
+
563
+ def update_session(self, store: str, session_id: str, metadata: dict) -> models.Session:
564
+ return _as(
565
+ models.Session,
566
+ self._do(
567
+ "PATCH",
568
+ f"{_BASE}/session-stores/{store}/sessions/{session_id}",
569
+ query={"update_mask": "metadata"},
570
+ body=_body(metadata=metadata),
571
+ ),
572
+ )
573
+
574
+ def delete_session(self, store: str, session_id: str, force: bool = False) -> dict:
575
+ return self._do(
576
+ "DELETE",
577
+ f"{_BASE}/session-stores/{store}/sessions/{session_id}",
578
+ query={"force": True} if force else None,
579
+ )
580
+
581
+ def fork_session(
582
+ self,
583
+ store: str,
584
+ source_session_id: str,
585
+ actor_id: str,
586
+ up_to_item_id: Optional[str] = None,
587
+ session_id: Optional[str] = None,
588
+ metadata: Optional[dict] = None,
589
+ ) -> models.Session:
590
+ body = _body(
591
+ source_session_id=source_session_id,
592
+ actor_id=actor_id,
593
+ up_to_item_id=up_to_item_id,
594
+ session_id=session_id,
595
+ metadata=metadata,
596
+ )
597
+ return _as(
598
+ models.Session,
599
+ self._do("POST", f"{_BASE}/session-stores/{store}/sessions:fork", body=body),
600
+ )
601
+
602
+ # --- session items -------------------------------------------------------
603
+
604
+ def list_session_items(
605
+ self,
606
+ store: str,
607
+ session_id: str,
608
+ order_by: Optional[str] = None,
609
+ page_size: Optional[int] = None,
610
+ page_token: Optional[str] = None,
611
+ ) -> models.SessionItemList:
612
+ return _as(
613
+ models.SessionItemList,
614
+ self._do(
615
+ "GET",
616
+ f"{_BASE}/session-stores/{store}/sessions/{session_id}/items",
617
+ query=_query(order_by=order_by, page_size=page_size, page_token=page_token),
618
+ ),
619
+ )
620
+
621
+ def append_session_items(
622
+ self, store: str, session_id: str, items: list[dict]
623
+ ) -> models.SessionItemList:
624
+ body = {"items": [{"data": item} for item in items]}
625
+ return _as(
626
+ models.SessionItemList,
627
+ self._do(
628
+ "POST",
629
+ f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:append",
630
+ body=body,
631
+ ),
632
+ )
633
+
634
+ def pop_session_item(self, store: str, session_id: str) -> models.PoppedSessionItem:
635
+ return _as(
636
+ models.PoppedSessionItem,
637
+ self._do(
638
+ "POST", f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:pop", body={}
639
+ ),
640
+ )
641
+
642
+ def clear_session_items(self, store: str, session_id: str) -> dict:
643
+ return self._do(
644
+ "POST", f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:clear", body={}
645
+ )
646
+
647
+ def extract_memories(
648
+ self,
649
+ store: str,
650
+ session_id: str,
651
+ memory_store: str,
652
+ *,
653
+ instructions: Optional[str] = None,
654
+ dry_run: bool = False,
655
+ ) -> models.ExtractMemoriesResponse:
656
+ """Synchronously distill a session's transcript into memory entries.
657
+
658
+ Writes the entries to `memory_store` and returns them; pass `dry_run=True` to return the
659
+ extracted entries without persisting them.
660
+ """
661
+ body = _body(
662
+ memory_store=memory_store_path(memory_store),
663
+ instructions=instructions,
664
+ dry_run=dry_run or None,
665
+ )
666
+ return _as(
667
+ models.ExtractMemoriesResponse,
668
+ self._do(
669
+ "POST",
670
+ f"{_BASE}/session-stores/{store}/sessions/{session_id}/extractions",
671
+ body=body,
672
+ ),
673
+ )
@@ -0,0 +1,25 @@
1
+ """Shared pagination validation for typed Agent Bricks resources."""
2
+
3
+ from typing import Optional
4
+
5
+ _MAX_PAGE_SIZE = 100
6
+
7
+
8
+ def _validate_bound(value: Optional[int], name: str) -> None:
9
+ if value is None:
10
+ return
11
+ # Reject non-integers (and bool, which is an int subclass) up front: without this a str
12
+ # raised a cryptic ``'<=' not supported between 'int' and 'str'`` TypeError, and a bool/float
13
+ # slipped through the range check (True == 1, 1.5 in [1, 100]) to reach the API as garbage.
14
+ if not isinstance(value, int) or isinstance(value, bool):
15
+ raise ValueError(f"{name} must be an integer between 1 and {_MAX_PAGE_SIZE}")
16
+ if not 1 <= value <= _MAX_PAGE_SIZE:
17
+ raise ValueError(f"{name} must be between 1 and {_MAX_PAGE_SIZE}")
18
+
19
+
20
+ def validate_page_size(page_size: Optional[int]) -> None:
21
+ _validate_bound(page_size, "page_size")
22
+
23
+
24
+ def validate_limit(limit: Optional[int]) -> None:
25
+ _validate_bound(limit, "limit")