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,542 @@
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from runtime import ui
4
+
5
+ from databricks_agentkit import DurableAgentServer
6
+ from databricks_agentkit.runtime.store import (
7
+ RUNTIME_STORE_DATABASE_ENV,
8
+ RUNTIME_STORE_LAKEBASE_BRANCH_ENV,
9
+ RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV,
10
+ RUNTIME_STORE_LOCAL_ENV,
11
+ RUNTIME_STORE_SCHEMA_ENV,
12
+ RUNTIME_STORE_USERNAME_ENV,
13
+ InMemoryRuntimeStore,
14
+ )
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _clear_runtime_store_env(monkeypatch):
19
+ for name in (
20
+ RUNTIME_STORE_LOCAL_ENV,
21
+ RUNTIME_STORE_LAKEBASE_BRANCH_ENV,
22
+ RUNTIME_STORE_DATABASE_ENV,
23
+ RUNTIME_STORE_USERNAME_ENV,
24
+ RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV,
25
+ RUNTIME_STORE_SCHEMA_ENV,
26
+ ):
27
+ monkeypatch.delenv(name, raising=False)
28
+
29
+
30
+ class _FakeStateClient:
31
+ def create_memory_entry(self, actor, request, session_id):
32
+ return {
33
+ "name": "memory-stores/store/entries/entry",
34
+ "session_id": session_id,
35
+ "actor_id": actor,
36
+ **request.model_dump(),
37
+ }
38
+
39
+ def list_memory_entries(self, actor, path_prefix=None):
40
+ return {
41
+ "managed_memory_entries": [
42
+ {"path": f"{path_prefix or ''}/profile.md", "actor_id": actor}
43
+ ]
44
+ }
45
+
46
+ def search_memory_entries(self, actor, request):
47
+ return {
48
+ "managed_memory_entries": [
49
+ {"path": "/profile.md", "content": request.query, "actor_id": actor}
50
+ ]
51
+ }
52
+
53
+ def ensure_session(self, actor, session_id):
54
+ return {"session_id": session_id, "actor_id": actor}
55
+
56
+ def get_session(self, session_id):
57
+ return {"session_id": session_id, "actor_id": "alice"}
58
+
59
+ def list_sessions(self, actor):
60
+ return {
61
+ "sessions": [
62
+ {
63
+ "session_id": "s1",
64
+ "actor_id": "alice",
65
+ "last_activity_time": "2026-08-28T12:00:00Z",
66
+ },
67
+ {
68
+ "session_id": "s2",
69
+ "actor_id": "alice",
70
+ "last_activity_time": "2026-08-27T12:00:00Z",
71
+ },
72
+ {
73
+ "session_id": "public-s1",
74
+ "actor_id": "alice",
75
+ "metadata": {"public_session_id": "s1"},
76
+ "last_activity_time": "2026-08-28T12:01:00Z",
77
+ },
78
+ ]
79
+ }
80
+
81
+ def append_session_items(self, session_id, items):
82
+ return {"session_items": [{"item_id": "1", "data": item} for item in items]}
83
+
84
+ def list_session_items(self, session_id):
85
+ return {
86
+ "session_items": [
87
+ {"item_id": "1", "data": {"role": "user", "content": session_id}},
88
+ {
89
+ "item_id": "2",
90
+ "data": {"type": "assistant", "content": "saved reply"},
91
+ },
92
+ {
93
+ "item_id": "3",
94
+ "data": {"event_type": "checkpoint", "checkpoint_id": "checkpoint-1"},
95
+ },
96
+ ]
97
+ }
98
+
99
+
100
+ class _FakeInterrupt:
101
+ def __init__(self, value, id):
102
+ self.value = value
103
+ self.id = id
104
+
105
+
106
+ async def _session_history(session_id, actor):
107
+ return {
108
+ "session_id": session_id,
109
+ "session_items": [
110
+ {"item_id": "1", "data": {"type": "human", "content": session_id}},
111
+ {"item_id": "2", "data": {"type": "ai", "content": "checkpoint reply"}},
112
+ ],
113
+ "interrupts": [],
114
+ }
115
+
116
+
117
+ def _client(monkeypatch, *, configured=False, history=False, session_id="routing-session"):
118
+ if configured:
119
+ monkeypatch.setenv("AGENT_MEMORY_STORE", "store")
120
+ monkeypatch.setenv("AGENT_SESSION_STORE", "sessions")
121
+ monkeypatch.setattr(ui, "_state_client", lambda: _FakeStateClient())
122
+ else:
123
+ monkeypatch.delenv("AGENT_MEMORY_STORE", raising=False)
124
+ monkeypatch.delenv("AGENT_SESSION_STORE", raising=False)
125
+ if history:
126
+ monkeypatch.setattr(ui, "_checkpoint_history", _session_history)
127
+ # Keep model discovery deterministic and offline (no AI Gateway listing call).
128
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
129
+ monkeypatch.setattr(ui, "_discover_chat_models", lambda: ["system.ai.claude-sonnet-4-5"])
130
+
131
+ async def invoke_handler(request, context):
132
+ return {"output": [], "session_id": context.session_id}
133
+
134
+ app = DurableAgentServer(runtime_store=InMemoryRuntimeStore())
135
+ app.invoke(invoke_handler)
136
+ app.recover(invoke_handler)
137
+ ui.install_ui(app)
138
+ client = TestClient(app, base_url="https://testserver")
139
+ client.cookies.set("__Host-databricks-app-router", session_id)
140
+ if configured:
141
+ # The actor is the signed-in user from this forwarded-identity header (ui._request_actor);
142
+ # unconfigured requests have no header and fall back to the "agent" actor.
143
+ client.headers["X-Forwarded-Email"] = "alice"
144
+ return client
145
+
146
+
147
+ def test_demo_ui_routes(monkeypatch):
148
+ client = _client(monkeypatch)
149
+
150
+ index = client.get("/")
151
+ assert index.status_code == 200
152
+ assert index.headers["cache-control"] == "no-store"
153
+ assert 'id="new-session"' in index.text
154
+ assert 'id="session-list"' in index.text
155
+ assert 'id="model-select"' in index.text
156
+ app_script = client.get("/ui-assets/app.js")
157
+ assert app_script.status_code == 200
158
+ assert app_script.headers["cache-control"] == "no-store"
159
+ assert "ab memory bind <store-name>" in app_script.text
160
+ assert "refreshSessionView({ hydrateChat: true })" in app_script.text
161
+ assert "function renderModels(" in app_script.text
162
+ assert 'demoUrl("/api/ui/config")' in app_script.text
163
+ assert 'demoUrl("/api/demo/models")' in app_script.text
164
+ assert 'fetch("/api/session/new"' not in app_script.text
165
+ assert "/api/demo/sessions/${encodeURIComponent(sessionId)}/open" in app_script.text
166
+ assert "session_id: sessionId" in app_script.text
167
+ assert 'fetch("/api/invocations"' in app_script.text
168
+ styles = client.get("/ui-assets/styles.css").text
169
+ assert "@media (min-width: 1181px)" in styles
170
+ assert "scrollbar-gutter: stable" in styles
171
+
172
+ config = client.get("/api/ui/config").json()
173
+ assert config["session_id"] == "routing-session"
174
+ assert config["deployed"] is False
175
+ assert config["models"] == {
176
+ "default": "system.ai.claude-sonnet-4-5",
177
+ "available": ["system.ai.claude-sonnet-4-5"],
178
+ }
179
+ assert config["streaming"]["enabled"] is True
180
+ assert config["background"]["enabled"] is True
181
+ assert config["streaming"]["persistent"] is False
182
+ assert config["streaming"]["mode"] == "In-process Runtime Store"
183
+ assert config["background"]["persistent"] is False
184
+ assert config["background"]["mode"] == "In-process Runtime Store"
185
+ assert config["memory"]["enabled"] is False
186
+ assert config["session"]["managed"] is False
187
+ assert config["session"]["history"] is True
188
+ assert "durability" not in config
189
+ assert "recovery" not in config
190
+ assert client.get("/api/demo/config").status_code == 404
191
+
192
+ assert client.get("/api/demo/models").json() == {
193
+ "default": "system.ai.claude-sonnet-4-5",
194
+ "available": ["system.ai.claude-sonnet-4-5"],
195
+ }
196
+
197
+ sessions = client.get("/api/demo/sessions").json()
198
+ assert sessions == {
199
+ "sessions": [
200
+ {
201
+ "session_id": "routing-session",
202
+ "actor_id": "agent",
203
+ "metadata": {"client": "agentbricks-demo-ui-local"},
204
+ }
205
+ ],
206
+ "current_session_id": "routing-session",
207
+ "managed": False,
208
+ }
209
+
210
+ assert client.post("/api/demo/memory/search", json={"query": "profile"}).status_code == 503
211
+ assert client.post("/api/demo/sessions", json={"session_id": "ignored"}).status_code == 503
212
+
213
+
214
+ @pytest.mark.parametrize(
215
+ ("local", "endpoint", "schema", "persistent"),
216
+ [
217
+ (None, None, None, False),
218
+ ("true", None, None, False),
219
+ ("true", "endpoint", "runtime_schema", False),
220
+ ("TRUE", "endpoint", "runtime_schema", False),
221
+ ("TrUe", "endpoint", "runtime_schema", False),
222
+ (None, "endpoint", "runtime_schema", True),
223
+ ("false", "endpoint", "runtime_schema", True),
224
+ (None, "endpoint", None, False),
225
+ (None, None, "runtime_schema", False),
226
+ (None, "", "runtime_schema", False),
227
+ (None, "endpoint", "", False),
228
+ ],
229
+ )
230
+ def test_demo_config_reports_runtime_store_configuration(
231
+ monkeypatch, local, endpoint, schema, persistent
232
+ ):
233
+ for name, value in (
234
+ (RUNTIME_STORE_LOCAL_ENV, local),
235
+ (RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV, endpoint),
236
+ (RUNTIME_STORE_SCHEMA_ENV, schema),
237
+ ):
238
+ if value is not None:
239
+ monkeypatch.setenv(name, value)
240
+ client = _client(monkeypatch)
241
+
242
+ response = client.get("/api/ui/config")
243
+ assert response.status_code == 200
244
+ config = response.json()
245
+ for capability in ("streaming", "background"):
246
+ assert config[capability]["enabled"] is True
247
+ assert config[capability]["persistent"] is persistent
248
+ assert config[capability]["mode"] == (
249
+ "Runtime Store" if persistent else "In-process Runtime Store"
250
+ )
251
+
252
+
253
+ def test_demo_config_reports_managed_runtime_store(monkeypatch):
254
+ monkeypatch.setenv(RUNTIME_STORE_LAKEBASE_BRANCH_ENV, "projects/p/branches/b")
255
+ monkeypatch.setenv(RUNTIME_STORE_DATABASE_ENV, "runtime-db")
256
+ monkeypatch.setenv(RUNTIME_STORE_USERNAME_ENV, "app-sp")
257
+
258
+ config = _client(monkeypatch).get("/api/ui/config").json()
259
+
260
+ assert config["background"]["persistent"] is True
261
+
262
+
263
+ def test_demo_config_distinguishes_run_local_from_a_deployed_app(monkeypatch):
264
+ monkeypatch.setenv("DATABRICKS_APP_NAME", "app")
265
+ monkeypatch.setenv("DATABRICKS_APP_URL", "http://127.0.0.1:8000")
266
+ assert _client(monkeypatch).get("/api/ui/config").json()["deployed"] is False
267
+
268
+ monkeypatch.setenv("DATABRICKS_APP_URL", "https://agent.example.databricksapps.com")
269
+ assert _client(monkeypatch).get("/api/ui/config").json()["deployed"] is True
270
+
271
+
272
+ def test_demo_config_does_not_wait_for_model_discovery(monkeypatch):
273
+ client = _client(monkeypatch)
274
+ calls = []
275
+ monkeypatch.setattr(
276
+ ui,
277
+ "_discover_chat_models",
278
+ lambda: calls.append(True) or ["system.ai.claude-sonnet-4-5", "system.ai.llama-4-maverick"],
279
+ )
280
+
281
+ assert client.get("/api/ui/config").status_code == 200
282
+ assert calls == []
283
+ assert client.get("/api/demo/models").json()["available"] == [
284
+ "system.ai.claude-sonnet-4-5",
285
+ "system.ai.llama-4-maverick",
286
+ ]
287
+ assert calls == [True]
288
+
289
+
290
+ def test_unmanaged_checkpoint_history_route(monkeypatch):
291
+ client = _client(monkeypatch, history=True, session_id="local-session")
292
+
293
+ config = client.get("/api/ui/config").json()
294
+ assert config["session"]["managed"] is False
295
+ assert config["session"]["history"] is True
296
+
297
+ result = client.get("/api/demo/session/items")
298
+ assert result.status_code == 200
299
+ assert [item["data"]["content"] for item in result.json()["session_items"]] == [
300
+ "local-session",
301
+ "checkpoint reply",
302
+ ]
303
+
304
+
305
+ def test_managed_session_list_is_actor_scoped(monkeypatch):
306
+ monkeypatch.setenv("AGENT_SESSION_STORE", "sessions")
307
+ state_client = object.__new__(ui._ManagedStateClient)
308
+ calls = []
309
+ state_client._do = lambda method, path, **kwargs: calls.append((method, path, kwargs)) or {
310
+ "sessions": []
311
+ }
312
+
313
+ # The actor (a signed-in user) is escaped into the list filter.
314
+ assert state_client.list_sessions('alice "demo"') == {"sessions": []}
315
+ assert calls == [
316
+ (
317
+ "GET",
318
+ "/api/2.0/agents/session-stores/sessions/sessions",
319
+ {
320
+ "query": {
321
+ "filter": 'actor_id = "alice \\"demo\\""',
322
+ "order_by": "last_activity_time desc",
323
+ "page_size": 50,
324
+ }
325
+ },
326
+ )
327
+ ]
328
+
329
+
330
+ def test_chat_session_items_exclude_non_message_items():
331
+ result = ui._chat_session_items(
332
+ {
333
+ "session_items": [
334
+ {"item_id": "1", "data": {"role": "user", "content": "hello"}},
335
+ {"item_id": "2", "data": {"type": "ai", "content": "hi"}},
336
+ {"item_id": "3", "data": {"event_type": "checkpoint"}},
337
+ {"item_id": "5", "data": {"content": "missing role"}},
338
+ ],
339
+ "next_page_token": "next",
340
+ }
341
+ )
342
+
343
+ assert result == {
344
+ "session_items": [
345
+ {"item_id": "1", "data": {"role": "user", "content": "hello"}},
346
+ {"item_id": "2", "data": {"type": "ai", "content": "hi"}},
347
+ ],
348
+ "next_page_token": "next",
349
+ }
350
+
351
+
352
+ def test_discover_chat_models_pins_default_and_dedups(monkeypatch):
353
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
354
+ monkeypatch.setattr(
355
+ ui,
356
+ "list_ai_gateway_model_services",
357
+ lambda _client: [
358
+ "system.ai.llama-4-maverick",
359
+ "system.ai.claude-sonnet-4-5",
360
+ "system.ai.claude-opus-4-8",
361
+ ],
362
+ )
363
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
364
+
365
+ # Default pinned first, the rest alphabetical, and the default not repeated by discovery.
366
+ assert ui._discover_chat_models() == [
367
+ "system.ai.claude-sonnet-4-5",
368
+ "system.ai.claude-opus-4-8",
369
+ "system.ai.llama-4-maverick",
370
+ ]
371
+
372
+
373
+ def test_discover_chat_models_falls_back_to_default_on_error(monkeypatch):
374
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
375
+
376
+ def _boom(_client):
377
+ raise PermissionError("cannot read system.ai")
378
+
379
+ monkeypatch.setattr(ui, "list_ai_gateway_model_services", _boom)
380
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
381
+
382
+ # A workspace that can't list the gateway still gets a working picker.
383
+ assert ui._discover_chat_models() == ["system.ai.claude-sonnet-4-5"]
384
+
385
+
386
+ def test_discover_chat_models_caps_the_picker(monkeypatch):
387
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
388
+ monkeypatch.setattr(
389
+ ui,
390
+ "list_ai_gateway_model_services",
391
+ # More services than the cap allows; synthetic because no real catalog is this long.
392
+ lambda _client: [f"system.ai.test-model-{i:03d}" for i in range(30)],
393
+ )
394
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
395
+
396
+ result = ui._discover_chat_models()
397
+ assert len(result) == ui._MODEL_LIMIT
398
+ assert result[0] == "system.ai.claude-sonnet-4-5" # the default survives truncation
399
+
400
+
401
+ @pytest.mark.asyncio
402
+ async def test_checkpoint_history_reads_messages_and_interrupts(monkeypatch):
403
+ import agent.agent as agent_module
404
+
405
+ class Message:
406
+ id = "message-1"
407
+
408
+ def model_dump(self):
409
+ return {"type": "human", "content": "saved message"}
410
+
411
+ class Snapshot:
412
+ values = {"messages": [Message()]}
413
+ tasks = [
414
+ type(
415
+ "Task",
416
+ (),
417
+ {"interrupts": [_FakeInterrupt({"approval": True}, "int-1")]},
418
+ )()
419
+ ]
420
+
421
+ class FakeAgent:
422
+ async def aget_state(self, config):
423
+ assert config == {
424
+ "configurable": {
425
+ "thread_id": "saved-session",
426
+ "actor_id": "alice",
427
+ }
428
+ }
429
+ return Snapshot()
430
+
431
+ async def fake_create_agent_graph(actor):
432
+ assert actor == "alice"
433
+ return FakeAgent()
434
+
435
+ monkeypatch.setattr(agent_module, "create_agent_graph", fake_create_agent_graph)
436
+ result = await ui._checkpoint_history("saved-session", "alice")
437
+
438
+ assert result == {
439
+ "session_id": "saved-session",
440
+ "session_items": [
441
+ {
442
+ "item_id": "message-1",
443
+ "data": {"type": "human", "content": "saved message"},
444
+ }
445
+ ],
446
+ "interrupts": [{"id": "int-1", "value": {"approval": True}}],
447
+ }
448
+
449
+
450
+ def test_managed_memory_and_session_routes(monkeypatch):
451
+ client = _client(monkeypatch, configured=True, session_id="s1")
452
+
453
+ config = client.get("/api/ui/config").json()
454
+ assert config["memory"] == {
455
+ "enabled": True,
456
+ "store": "memory-stores/store",
457
+ "actor": "alice",
458
+ }
459
+ assert config["session"]["store"] == "sessions"
460
+ assert config["session"]["actor"] == "alice"
461
+ assert config["session"]["history"] is True
462
+
463
+ created = client.post(
464
+ "/api/demo/memory/entries",
465
+ json={"path": "/profile.md", "content": "I work at Databricks"},
466
+ )
467
+ assert created.status_code == 200
468
+ assert created.json()["path"] == "/profile.md"
469
+ assert created.json()["session_id"] == "s1"
470
+ assert client.get("/api/demo/memory/entries", params={"path_prefix": "/"}).status_code == 200
471
+ search = client.post("/api/demo/memory/search", json={"query": "Databricks"})
472
+ assert search.json()["managed_memory_entries"][0]["content"] == "Databricks"
473
+
474
+ # The UI can browse another actor's memories via ?actor= (list) or payload.actor (search);
475
+ # both default to the viewer when omitted.
476
+ listed_default = client.get("/api/demo/memory/entries").json()
477
+ assert listed_default["managed_memory_entries"][0]["actor_id"] == "alice"
478
+ listed_bob = client.get("/api/demo/memory/entries", params={"actor": "bob"}).json()
479
+ assert listed_bob["managed_memory_entries"][0]["actor_id"] == "bob"
480
+ search_bob = client.post("/api/demo/memory/search", json={"query": "x", "actor": "bob"})
481
+ assert search_bob.json()["managed_memory_entries"][0]["actor_id"] == "bob"
482
+
483
+ # Tracing surfaces in config when a destination + experiment are set, with an experiment link.
484
+ monkeypatch.setenv("MLFLOW_TRACKING_URI", "databricks")
485
+ monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "123456")
486
+ monkeypatch.setattr(ui, "_workspace_host", lambda: "https://example.databricks.com")
487
+ tracing = client.get("/api/ui/config").json()["tracing"]
488
+ assert tracing["enabled"] is True
489
+ assert tracing["url"] == "https://example.databricks.com/ml/experiments/123456"
490
+ monkeypatch.delenv("MLFLOW_TRACKING_URI", raising=False)
491
+ monkeypatch.delenv("MLFLOW_EXPERIMENT_ID", raising=False)
492
+ assert client.get("/api/ui/config").json()["tracing"]["enabled"] is False
493
+
494
+ assert (
495
+ client.post("/api/demo/sessions", json={"session_id": "ignored"}).json()["session_id"]
496
+ == "s1"
497
+ )
498
+ listed = client.get("/api/demo/sessions").json()
499
+ assert [session["session_id"] for session in listed["sessions"]] == ["s1", "s2"]
500
+ assert listed["current_session_id"] == "s1"
501
+ assert listed["managed"] is True
502
+ assert client.get("/api/demo/session").json()["session_id"] == "s1"
503
+ appended = client.post(
504
+ "/api/demo/session/items",
505
+ json={"items": [{"role": "user", "content": "hello"}]},
506
+ )
507
+ assert appended.json()["session_items"][0]["data"]["content"] == "hello"
508
+ assert (
509
+ client.get("/api/demo/session/items").json()["session_items"][0]["data"]["content"] == "s1"
510
+ )
511
+ assert [
512
+ item["data"]["content"]
513
+ for item in client.get("/api/demo/session/items").json()["session_items"]
514
+ ] == ["s1", "saved reply"]
515
+
516
+ opened = client.post("/api/demo/sessions/s2/open")
517
+ assert opened.json() == {
518
+ "session_id": "s2",
519
+ "previous_session_id": "s1",
520
+ "managed": True,
521
+ }
522
+ assert client.get("/api/ui/config", params={"session_id": "s2"}).json()["session_id"] == "s2"
523
+ assert (
524
+ client.get("/api/demo/session/items", params={"session_id": "s2"}).json()["session_items"][
525
+ 0
526
+ ]["data"]["content"]
527
+ == "s2"
528
+ )
529
+
530
+
531
+ def test_open_session_rejects_another_actor(monkeypatch):
532
+ client = _client(monkeypatch, configured=True, session_id="s1")
533
+
534
+ class _ForeignActorClient(_FakeStateClient):
535
+ def get_session(self, session_id):
536
+ return {"session_id": session_id, "actor_id": "bob"}
537
+
538
+ monkeypatch.setattr(ui, "_state_client", lambda: _ForeignActorClient())
539
+
540
+ response = client.post("/api/demo/sessions/s2/open")
541
+ assert response.status_code == 403
542
+ assert response.json()["detail"] == "Session belongs to another actor."