databricks-mason 0.1.1.dev0__tar.gz → 0.1.2.dev0__tar.gz

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 (52) hide show
  1. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/PKG-INFO +53 -31
  2. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/README.md +43 -30
  3. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/pyproject.toml +16 -4
  4. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/agent_project.py +13 -2
  5. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/cli.py +5 -0
  6. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/client.py +40 -7
  7. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/deploy.py +65 -16
  8. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/dev.py +44 -10
  9. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/errors.py +21 -0
  10. databricks_mason-0.1.2.dev0/src/databricks_mason/help.py +139 -0
  11. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/init.py +22 -42
  12. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/langgraph/__init__.py +2 -3
  13. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/memory.py +92 -23
  14. databricks_mason-0.1.2.dev0/src/databricks_mason/openai/__init__.py +89 -0
  15. databricks_mason-0.1.2.dev0/src/databricks_mason/openai/mcp.py +94 -0
  16. databricks_mason-0.1.2.dev0/src/databricks_mason/openai/memory.py +63 -0
  17. databricks_mason-0.1.2.dev0/src/databricks_mason/openai/sessions.py +159 -0
  18. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/render.py +13 -0
  19. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/__init__.py +2 -2
  20. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/sessions.py +52 -7
  21. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/store_access.py +50 -21
  22. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/timefmt.py +4 -4
  23. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/tools.py +44 -8
  24. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/tracing.py +1 -1
  25. databricks_mason-0.1.1.dev0/src/databricks_mason/langgraph/long_running.py +0 -59
  26. databricks_mason-0.1.1.dev0/src/databricks_mason/langgraph/recovery.py +0 -247
  27. databricks_mason-0.1.1.dev0/src/databricks_mason/runtime/durability.py +0 -377
  28. databricks_mason-0.1.1.dev0/src/databricks_mason/templates/mcp_runtime_langgraph.py +0 -96
  29. databricks_mason-0.1.1.dev0/src/databricks_mason/templates/tool_manifest_runtime.py +0 -143
  30. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/.gitignore +0 -0
  31. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/NOTICE +0 -0
  32. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/__init__.py +0 -0
  33. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/auth.py +0 -0
  34. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/langgraph/mcp.py +0 -0
  35. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/langgraph/memory.py +0 -0
  36. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/langgraph/session_store.py +0 -0
  37. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/mcp.py +0 -0
  38. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/memory_store_access.py +0 -0
  39. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/models.py +0 -0
  40. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/project_config.py +0 -0
  41. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/py.typed +0 -0
  42. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/background.py +0 -0
  43. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/session_store_client.py +0 -0
  44. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/tool_manifest.py +0 -0
  45. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/tracing.py +0 -0
  46. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/runtime/workspace.py +0 -0
  47. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/sandbox.py +0 -0
  48. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/session_store_access.py +0 -0
  49. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/templates/python_tool_langgraph.py +0 -0
  50. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/templates/python_tool_test.py +0 -0
  51. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/templates/sandbox_mcp.py +0 -0
  52. {databricks_mason-0.1.1.dev0 → databricks_mason-0.1.2.dev0}/src/databricks_mason/templates/sandbox_mcp_langgraph.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: databricks-mason
3
- Version: 0.1.1.dev0
3
+ Version: 0.1.2.dev0
4
4
  Summary: Databricks integration for Mason
5
5
  Author-email: Databricks <agent-feedback@databricks.com>
6
6
  License-File: NOTICE
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: click>=8.1
9
9
  Requires-Dist: databricks-sdk>=0.49
10
+ Requires-Dist: psycopg[binary]>=3.1
10
11
  Requires-Dist: pyyaml>=6.0
11
12
  Requires-Dist: rich>=13.7
12
13
  Requires-Dist: tomli>=2.0
@@ -21,6 +22,14 @@ Requires-Dist: langgraph>=1.1.0; extra == 'runtime'
21
22
  Requires-Dist: mlflow>=3.10.1; extra == 'runtime'
22
23
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.25.0; extra == 'runtime'
23
24
  Requires-Dist: uuid-utils>=0.10.0; extra == 'runtime'
25
+ Provides-Extra: runtime-openai
26
+ Requires-Dist: databricks-agents>=1.9.3; extra == 'runtime-openai'
27
+ Requires-Dist: databricks-openai>=0.13.0; extra == 'runtime-openai'
28
+ Requires-Dist: fastapi>=0.129.0; extra == 'runtime-openai'
29
+ Requires-Dist: mlflow>=3.10.1; extra == 'runtime-openai'
30
+ Requires-Dist: openai-agents>=0.4.1; extra == 'runtime-openai'
31
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.25.0; extra == 'runtime-openai'
32
+ Requires-Dist: uuid-utils>=0.10.0; extra == 'runtime-openai'
24
33
  Provides-Extra: tracing
25
34
  Requires-Dist: mlflow[databricks]>=3.9.0; extra == 'tracing'
26
35
  Description-Content-Type: text/markdown
@@ -102,8 +111,11 @@ accessors (`store.name`) while remaining plain dicts underneath — so `store["n
102
111
  mason [-p <profile>] [-o text|json]
103
112
  login [--profile P]
104
113
  logout
105
- init [--framework openai|langgraph] [--enable-chat-app]
114
+ init [--framework openai|langgraph] [--disable-chat-app]
106
115
  [--profile P] [--repo URL] [--ref REF] [directory]
116
+ dev [--source PATH] [--prepare-environment] [--app-port PORT]
117
+ [--memory/-m N] [--session/-s N]
118
+ [--with-traces C.S] [--no-create-stores]
107
119
  memory
108
120
  stores create | list | get | update | delete
109
121
  entries create | get | list | search | update | delete
@@ -115,19 +127,39 @@ mason [-p <profile>] [-o text|json]
115
127
  list | get | instrument
116
128
  mcp
117
129
  list [--schema CATALOG.SCHEMA]
118
- init [--framework openai|langgraph] [--profile P] [DIRECTORY]
119
130
  tools
120
131
  add sandbox --scope SCOPE [--scope SCOPE ...] [--source PATH]
121
132
  add mcp SERVICE [--name NAME] [--source PATH]
122
133
  add uc-function FUNCTION [--name NAME] [--source PATH]
123
134
  add python NAME [--source PATH]
124
135
  list [--source PATH]
125
- deploy <name> --source PATH [--with-memory-store N]
126
- [--with-session-store N] [--actor-id ID]
127
- [--with-traces C.S] [--create-stores]
136
+ deploy <name> --source PATH [--memory/-m N]
137
+ [--session/-s N] [--actor-id ID]
138
+ [--with-traces C.S] [--no-create-stores]
128
139
  deployments list | get | logs | start | stop | delete
129
140
  ```
130
141
 
142
+ ## Command help
143
+
144
+ Use the conventional help flag at any command level. Every command's help includes runnable
145
+ examples:
146
+
147
+ ```sh
148
+ mason --help
149
+ mason deploy --help
150
+ mason sessions items append --help
151
+ ```
152
+
153
+ For the shortest path from a blank directory to a running and deployed agent:
154
+
155
+ ```sh
156
+ mason login --profile my-workspace
157
+ mason init my-agent
158
+ cd my-agent
159
+ mason dev
160
+ mason deploy my-agent
161
+ ```
162
+
131
163
  ## Agent tools
132
164
 
133
165
  `mason init` writes portable tool intent to `agent.toml` and template provenance to
@@ -168,49 +200,39 @@ arguments controlled by the model.
168
200
 
169
201
  ## Initialize the chat app demo
170
202
 
171
- The chat app is a LangGraph-specific init overlay, not a command that mutates an existing project:
203
+ The chat app is a LangGraph-specific init overlay, not a command that mutates an existing project.
204
+ It is included by default for `--framework langgraph`; pass `--disable-chat-app` to scaffold the
205
+ API-only backend instead.
172
206
 
173
207
  ```sh
174
- mason init --framework langgraph --enable-chat-app \
208
+ mason init --framework langgraph \
175
209
  --profile <profile> \
176
210
  ./my-agent
177
211
  cd ./my-agent
178
212
  uv run start-server
179
213
  ```
180
214
 
181
- `--enable-chat-app` always includes synchronous, SSE streaming, background polling, Session Store,
182
- Memory Store, HITL resume, Start App, Stop App, heartbeat, and recovery UI. There are no separate
183
- stop/crash flags. The base agent owns `agent/mason/durability.py`, `agent/mason/recovery.py`, and
184
- `agent/mason/long_running.py`; the framework-specific overlay only adds `ui/`, `runtime/ui.py`, the
185
- UI-enabled `runtime/main.py`, and UI tests.
215
+ The chat app includes synchronous, SSE streaming, background polling, Session Store, Memory Store,
216
+ and HITL resume UI. The framework-specific overlay adds `ui/`, `runtime/ui.py`, the UI-enabled
217
+ `runtime/main.py`, and UI tests.
186
218
 
187
219
  For the full deployed demo, connect both managed stores:
188
220
 
189
221
  ```sh
190
222
  mason --profile <profile> deploy mason-agent-demo --source . \
191
- --with-session-store mason-demo-sessions \
192
- --with-memory-store mason-demo-memory \
193
- --actor-id alice \
194
- --create-stores
223
+ --session mason-demo-sessions \
224
+ --memory mason-demo-memory \
225
+ --actor-id alice
195
226
  ```
196
227
 
228
+ (Missing stores are created automatically; pass `--no-create-stores` to require they already exist.)
229
+
197
230
  The Databricks Apps `__Host-databricks-app-router` cookie is both the sticky routing key and the
198
231
  application session id. The browser sends it automatically; API clients must reuse it as a cookie.
199
232
  Request bodies never carry `session_id`. A localhost-only `mason-local-session` cookie provides the
200
233
  same behavior outside Databricks Apps. TODO: move to `X-Routing-Key` when Apps supports it.
201
234
 
202
235
  The generated `README.md` documents every request the client makes: config discovery, sync and SSE
203
- invocations, background submission and polling, session transcript loading, HITL resume, memory
204
- entry operations, and stop/start recovery. Capability colors are automatic from `/api/demo/config`;
205
- only the sync/streaming/background transport selector is manual.
206
-
207
- Start App runs `tool_step_1` through `tool_step_4` in a checkpointed sequence. Each completed output
208
- is committed before the next node. Stop App schedules `os._exit(86)`; Databricks Apps restarts the
209
- process, the browser waits for a new instance and a stale heartbeat, and then starts a new attempt
210
- with the same routing cookie. Completed tools are restored and skipped; an interrupted tool whose
211
- output was not committed can run again.
212
-
213
- The ownership log is intentionally demo-grade: Session Store records append-only attempts and
214
- heartbeats, but the claim is last-writer-wins rather than atomic (`atomic_claim: false`). Production
215
- durability also needs transactional ownership, server-side stale scanning, idempotent side effects,
216
- and durable event replay.
236
+ invocations, background submission and polling, session transcript loading, HITL resume, and memory
237
+ entry operations. Capability colors are automatic from `/api/demo/config`; only the
238
+ sync/streaming/background transport selector is manual.
@@ -75,8 +75,11 @@ accessors (`store.name`) while remaining plain dicts underneath — so `store["n
75
75
  mason [-p <profile>] [-o text|json]
76
76
  login [--profile P]
77
77
  logout
78
- init [--framework openai|langgraph] [--enable-chat-app]
78
+ init [--framework openai|langgraph] [--disable-chat-app]
79
79
  [--profile P] [--repo URL] [--ref REF] [directory]
80
+ dev [--source PATH] [--prepare-environment] [--app-port PORT]
81
+ [--memory/-m N] [--session/-s N]
82
+ [--with-traces C.S] [--no-create-stores]
80
83
  memory
81
84
  stores create | list | get | update | delete
82
85
  entries create | get | list | search | update | delete
@@ -88,19 +91,39 @@ mason [-p <profile>] [-o text|json]
88
91
  list | get | instrument
89
92
  mcp
90
93
  list [--schema CATALOG.SCHEMA]
91
- init [--framework openai|langgraph] [--profile P] [DIRECTORY]
92
94
  tools
93
95
  add sandbox --scope SCOPE [--scope SCOPE ...] [--source PATH]
94
96
  add mcp SERVICE [--name NAME] [--source PATH]
95
97
  add uc-function FUNCTION [--name NAME] [--source PATH]
96
98
  add python NAME [--source PATH]
97
99
  list [--source PATH]
98
- deploy <name> --source PATH [--with-memory-store N]
99
- [--with-session-store N] [--actor-id ID]
100
- [--with-traces C.S] [--create-stores]
100
+ deploy <name> --source PATH [--memory/-m N]
101
+ [--session/-s N] [--actor-id ID]
102
+ [--with-traces C.S] [--no-create-stores]
101
103
  deployments list | get | logs | start | stop | delete
102
104
  ```
103
105
 
106
+ ## Command help
107
+
108
+ Use the conventional help flag at any command level. Every command's help includes runnable
109
+ examples:
110
+
111
+ ```sh
112
+ mason --help
113
+ mason deploy --help
114
+ mason sessions items append --help
115
+ ```
116
+
117
+ For the shortest path from a blank directory to a running and deployed agent:
118
+
119
+ ```sh
120
+ mason login --profile my-workspace
121
+ mason init my-agent
122
+ cd my-agent
123
+ mason dev
124
+ mason deploy my-agent
125
+ ```
126
+
104
127
  ## Agent tools
105
128
 
106
129
  `mason init` writes portable tool intent to `agent.toml` and template provenance to
@@ -141,49 +164,39 @@ arguments controlled by the model.
141
164
 
142
165
  ## Initialize the chat app demo
143
166
 
144
- The chat app is a LangGraph-specific init overlay, not a command that mutates an existing project:
167
+ The chat app is a LangGraph-specific init overlay, not a command that mutates an existing project.
168
+ It is included by default for `--framework langgraph`; pass `--disable-chat-app` to scaffold the
169
+ API-only backend instead.
145
170
 
146
171
  ```sh
147
- mason init --framework langgraph --enable-chat-app \
172
+ mason init --framework langgraph \
148
173
  --profile <profile> \
149
174
  ./my-agent
150
175
  cd ./my-agent
151
176
  uv run start-server
152
177
  ```
153
178
 
154
- `--enable-chat-app` always includes synchronous, SSE streaming, background polling, Session Store,
155
- Memory Store, HITL resume, Start App, Stop App, heartbeat, and recovery UI. There are no separate
156
- stop/crash flags. The base agent owns `agent/mason/durability.py`, `agent/mason/recovery.py`, and
157
- `agent/mason/long_running.py`; the framework-specific overlay only adds `ui/`, `runtime/ui.py`, the
158
- UI-enabled `runtime/main.py`, and UI tests.
179
+ The chat app includes synchronous, SSE streaming, background polling, Session Store, Memory Store,
180
+ and HITL resume UI. The framework-specific overlay adds `ui/`, `runtime/ui.py`, the UI-enabled
181
+ `runtime/main.py`, and UI tests.
159
182
 
160
183
  For the full deployed demo, connect both managed stores:
161
184
 
162
185
  ```sh
163
186
  mason --profile <profile> deploy mason-agent-demo --source . \
164
- --with-session-store mason-demo-sessions \
165
- --with-memory-store mason-demo-memory \
166
- --actor-id alice \
167
- --create-stores
187
+ --session mason-demo-sessions \
188
+ --memory mason-demo-memory \
189
+ --actor-id alice
168
190
  ```
169
191
 
192
+ (Missing stores are created automatically; pass `--no-create-stores` to require they already exist.)
193
+
170
194
  The Databricks Apps `__Host-databricks-app-router` cookie is both the sticky routing key and the
171
195
  application session id. The browser sends it automatically; API clients must reuse it as a cookie.
172
196
  Request bodies never carry `session_id`. A localhost-only `mason-local-session` cookie provides the
173
197
  same behavior outside Databricks Apps. TODO: move to `X-Routing-Key` when Apps supports it.
174
198
 
175
199
  The generated `README.md` documents every request the client makes: config discovery, sync and SSE
176
- invocations, background submission and polling, session transcript loading, HITL resume, memory
177
- entry operations, and stop/start recovery. Capability colors are automatic from `/api/demo/config`;
178
- only the sync/streaming/background transport selector is manual.
179
-
180
- Start App runs `tool_step_1` through `tool_step_4` in a checkpointed sequence. Each completed output
181
- is committed before the next node. Stop App schedules `os._exit(86)`; Databricks Apps restarts the
182
- process, the browser waits for a new instance and a stale heartbeat, and then starts a new attempt
183
- with the same routing cookie. Completed tools are restored and skipped; an interrupted tool whose
184
- output was not committed can run again.
185
-
186
- The ownership log is intentionally demo-grade: Session Store records append-only attempts and
187
- heartbeats, but the claim is last-writer-wins rather than atomic (`atomic_claim: false`). Production
188
- durability also needs transactional ownership, server-side stale scanning, idempotent side effects,
189
- and durable event replay.
200
+ invocations, background submission and polling, session transcript loading, HITL resume, and memory
201
+ entry operations. Capability colors are automatic from `/api/demo/config`; only the
202
+ sync/streaming/background transport selector is manual.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "databricks-mason"
3
- version = "0.1.1.dev0"
3
+ version = "0.1.2.dev0"
4
4
  description = "Databricks integration for Mason"
5
5
  authors = [
6
6
  { name="Databricks", email="agent-feedback@databricks.com" },
@@ -10,6 +10,7 @@ requires-python = ">=3.10"
10
10
  dependencies = [
11
11
  "click>=8.1",
12
12
  "databricks-sdk>=0.49",
13
+ "psycopg[binary]>=3.1",
13
14
  "PyYAML>=6.0",
14
15
  "rich>=13.7",
15
16
  "tomli>=2.0",
@@ -20,9 +21,11 @@ dependencies = [
20
21
  tracing = [
21
22
  "mlflow[databricks]>=3.9.0",
22
23
  ]
23
- # The agent-side runtime helpers (databricks_mason.runtime) that a deployed agent imports. Kept as
24
- # an extra so a plain `pip install databricks-mason` (the CLI) stays light; the template depends on
25
- # `databricks-mason[runtime]`.
24
+ # The agent-side runtime helpers a deployed agent imports. Kept as extras so a plain
25
+ # `pip install databricks-mason` (the CLI) stays light; each template depends on the extra for its
26
+ # framework. `runtime` = the LangGraph adapter (databricks_mason.langgraph); `runtime-openai` = the
27
+ # OpenAI Agents SDK adapter (databricks_mason.openai). Both carry the shared framework-neutral stack
28
+ # (databricks_mason.runtime); the framework SDKs differ, so an agent installs only the one it uses.
26
29
  runtime = [
27
30
  "databricks-langchain>=0.17.0",
28
31
  "langgraph>=1.1.0",
@@ -34,6 +37,15 @@ runtime = [
34
37
  "opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
35
38
  "databricks-agents>=1.9.3",
36
39
  ]
40
+ runtime-openai = [
41
+ "openai-agents>=0.4.1",
42
+ "databricks-openai>=0.13.0",
43
+ "fastapi>=0.129.0",
44
+ "mlflow>=3.10.1",
45
+ "uuid-utils>=0.10.0",
46
+ "opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
47
+ "databricks-agents>=1.9.3",
48
+ ]
37
49
 
38
50
  [project.scripts]
39
51
  mason = "databricks_mason.cli:main"
@@ -291,7 +291,8 @@ class AgentProject:
291
291
  except FileNotFoundError as exc:
292
292
  raise AgentCliError(
293
293
  f"Could not find agent.toml in {project_root}.",
294
- hint="Run `mason init` or use a legacy compatibility command.",
294
+ hint="This command needs a Mason project. Run `mason init` to create one, "
295
+ "or point at an existing project with --source <dir>.",
295
296
  ) from exc
296
297
  except (OSError, ParseError) as exc:
297
298
  raise AgentCliError(f"Could not read agent manifest at {path}: {exc}.") from exc
@@ -334,7 +335,17 @@ class AgentProject:
334
335
  continue
335
336
  if existing == spec:
336
337
  return False
337
- raise AgentCliError(f"Tool id {spec.id!r} already exists with different configuration.")
338
+
339
+ def _summary(s: ToolSpec) -> str:
340
+ src = s.source
341
+ return src.service or src.function or src.entrypoint or src.kind
342
+
343
+ raise AgentCliError(
344
+ f"Tool id {spec.id!r} already exists with a different configuration "
345
+ f"(existing: {_summary(existing)}; requested: {_summary(spec)}).",
346
+ hint="Use --name to add it under a different id, or remove the existing "
347
+ "tool from agent.toml first.",
348
+ )
338
349
  raw_tools = self._document.get("tools")
339
350
  if raw_tools is None:
340
351
  raw_tools = tomlkit.aot()
@@ -10,10 +10,12 @@ from typing import Optional
10
10
 
11
11
  import click
12
12
 
13
+ from databricks_mason import errors
13
14
  from databricks_mason.auth import load_default_profile, login, logout
14
15
  from databricks_mason.client import MasonClient
15
16
  from databricks_mason.deploy import deploy, deployments
16
17
  from databricks_mason.dev import dev
18
+ from databricks_mason.help import configure_help
17
19
  from databricks_mason.init import init
18
20
  from databricks_mason.mcp import mcp
19
21
  from databricks_mason.memory import memory
@@ -56,6 +58,8 @@ def mason(ctx: click.Context, profile: Optional[str], output: str) -> None:
56
58
  .databrickscfg profile (pass --profile / -p, run `mason login` to save a default,
57
59
  or rely on the SDK's default resolution).
58
60
  """
61
+ # Let errors render to match the selected output mode (JSON errors for -o json).
62
+ errors.set_output_mode(output)
59
63
  ctx.obj = CliContext(profile=profile or load_default_profile(), output=output)
60
64
 
61
65
 
@@ -70,6 +74,7 @@ mason.add_command(tracing)
70
74
  mason.add_command(deploy)
71
75
  mason.add_command(deployments)
72
76
  mason.add_command(tools)
77
+ configure_help(mason)
73
78
 
74
79
 
75
80
  def main() -> None:
@@ -36,15 +36,39 @@ def _as(cls: type, resp: Any) -> Any:
36
36
 
37
37
 
38
38
  def memory_store_path(name: str) -> str:
39
- """Normalize a store id or name into the `memory-stores/{id}` resource segment."""
40
- name = name.strip().strip("/")
41
- return name if name.startswith("memory-stores/") else f"memory-stores/{name}"
39
+ """Normalize a store id or name into the `memory-stores/{id}` resource segment.
40
+
41
+ Validates client-side so an empty or wrong-typed argument raises a clear error
42
+ instead of building a URL like `memory-stores/` and leaking a raw ENDPOINT_NOT_FOUND.
43
+ """
44
+ raw = (name or "").strip()
45
+ if raw.startswith("memory-stores/"):
46
+ raw = raw[len("memory-stores/") :]
47
+ raw = raw.strip().strip("/")
48
+ if not raw:
49
+ raise AgentCliError("A memory store id or resource name is required.")
50
+ if "/" in raw:
51
+ raise AgentCliError(f"Invalid memory store id or resource name: {name!r}")
52
+ return f"memory-stores/{raw}"
53
+
54
+
55
+ def session_store_path(name: str) -> str:
56
+ """Normalize/validate a session store name into the `session-stores/{name}` segment."""
57
+ raw = (name or "").strip()
58
+ if raw.startswith("session-stores/"):
59
+ raw = raw[len("session-stores/") :]
60
+ raw = raw.strip().strip("/")
61
+ if not raw:
62
+ raise AgentCliError("A session store name is required.")
63
+ return f"session-stores/{raw}"
42
64
 
43
65
 
44
66
  def memory_entry_path(store: str, entry: str) -> str:
45
- entry = entry.strip().strip("/")
67
+ entry = (entry or "").strip().strip("/")
46
68
  if entry.startswith("memory-stores/"):
47
69
  return entry
70
+ if not entry:
71
+ raise AgentCliError("A memory entry id or resource name is required.")
48
72
  return f"{memory_store_path(store)}/entries/{entry}"
49
73
 
50
74
 
@@ -162,6 +186,8 @@ class MasonClient:
162
186
  self, name: str, display_name: Optional[str] = None, description: Optional[str] = None
163
187
  ) -> models.MemoryStore:
164
188
  body = _query(display_name=display_name, description=description)
189
+ if not body:
190
+ raise AgentCliError("No fields to update. Provide a display name and/or description.")
165
191
  mask = ",".join(body.keys())
166
192
  return _as(
167
193
  models.MemoryStore,
@@ -247,6 +273,8 @@ class MasonClient:
247
273
  description: Optional[str] = None,
248
274
  ) -> models.MemoryEntry:
249
275
  body = _query(content=content, description=description)
276
+ if not body:
277
+ raise AgentCliError("No fields to update. Provide content and/or a description.")
250
278
  return _as(
251
279
  models.MemoryEntry,
252
280
  self._do("PATCH", f"{_BASE}/{memory_entry_path(store, entry)}", body=body),
@@ -269,7 +297,7 @@ class MasonClient:
269
297
  )
270
298
 
271
299
  def get_session_store(self, name: str) -> models.SessionStore:
272
- return _as(models.SessionStore, self._do("GET", f"{_BASE}/session-stores/{name}"))
300
+ return _as(models.SessionStore, self._do("GET", f"{_BASE}/{session_store_path(name)}"))
273
301
 
274
302
  def list_session_stores(
275
303
  self, page_size: Optional[int] = None, page_token: Optional[str] = None
@@ -287,16 +315,21 @@ class MasonClient:
287
315
  self, name: str, description: Optional[str] = None, metadata: Optional[dict] = None
288
316
  ) -> models.SessionStore:
289
317
  body = _query(description=description, metadata=metadata)
318
+ if not body:
319
+ raise AgentCliError("No fields to update. Provide a description and/or metadata.")
290
320
  mask = ",".join(body.keys())
291
321
  return _as(
292
322
  models.SessionStore,
293
323
  self._do(
294
- "PATCH", f"{_BASE}/session-stores/{name}", query=_query(update_mask=mask), body=body
324
+ "PATCH",
325
+ f"{_BASE}/{session_store_path(name)}",
326
+ query=_query(update_mask=mask),
327
+ body=body,
295
328
  ),
296
329
  )
297
330
 
298
331
  def delete_session_store(self, name: str) -> dict:
299
- return self._do("DELETE", f"{_BASE}/session-stores/{name}")
332
+ return self._do("DELETE", f"{_BASE}/{session_store_path(name)}")
300
333
 
301
334
  # --- sessions ------------------------------------------------------------
302
335
 
@@ -67,11 +67,35 @@ def _app_compute_state(name: str, profile: Optional[str]) -> Optional[str]:
67
67
  return None
68
68
 
69
69
 
70
+ def _validate_deployment_name(name: str) -> str:
71
+ """Reject an empty or unsafe deployment name before it reaches a URL / workspace path."""
72
+ if (
73
+ not (name or "").strip()
74
+ or name != name.strip()
75
+ or any(token in name for token in ("/", "\\", ".."))
76
+ or any(character.isspace() for character in name)
77
+ ):
78
+ raise AgentCliError(
79
+ f"Invalid deployment name {name!r}.",
80
+ hint="Use a non-empty name of letters, digits, and hyphens "
81
+ "(no slashes, spaces, or '..').",
82
+ )
83
+ return name
84
+
85
+
86
+ def _confirm_destroy(target: str, *, assume_yes: bool) -> None:
87
+ """Prompt before a destructive deployment op; --yes/-y skips it (for scripts)."""
88
+ if assume_yes:
89
+ return
90
+ if not click.confirm(f"{target}? This cannot be undone.", default=False):
91
+ raise click.Abort()
92
+
93
+
70
94
  def _wait_for_running(name: str, profile: Optional[str], timeout_s: int = 300) -> None:
71
- """Block until a just-created app's compute is RUNNING (or raise on timeout).
95
+ """Block until a just-created app's compute is ACTIVE (or raise on timeout).
72
96
 
73
- `apps create` returns before compute is provisioned, but `apps deploy` requires RUNNING — so a
74
- first deploy races without this wait.
97
+ `apps create` returns before compute is provisioned, but `apps deploy` requires the app to be
98
+ ACTIVE — so a first deploy races without this wait.
75
99
  """
76
100
  deadline = time.monotonic() + timeout_s
77
101
  while time.monotonic() < deadline:
@@ -186,9 +210,10 @@ def resolve_store_env(
186
210
  """Resolve store/trace references to the AGENT_*/MLFLOW_* env vars that wire them in.
187
211
 
188
212
  Shared by `mason deploy` and `mason dev` so both wire an agent's stores into app.yaml the same
189
- way. With `create_stores`, missing stores are created (idempotent); otherwise they must already
190
- exist. The memory store resolves to its bare id (the runtime re-adds the `memory-stores/` prefix
191
- when building the entries URL); the session store and trace destination are used verbatim.
213
+ way. With `create_stores` (the default), missing stores are created (idempotent); when it is off
214
+ they must already exist. The memory store resolves to its bare id (the runtime re-adds the
215
+ `memory-stores/` prefix when building the entries URL); the session store and trace destination
216
+ are used verbatim.
192
217
  """
193
218
  env: dict[str, str] = {}
194
219
  if memory_store:
@@ -200,13 +225,24 @@ def resolve_store_env(
200
225
  store = _resolve_memory_store(client, memory_store)
201
226
  if store is None:
202
227
  raise AgentCliError(
203
- f"Memory store '{memory_store}' does not exist (create it with --create-stores)."
228
+ f"Memory store '{memory_store}' does not exist "
229
+ "(drop --no-create-stores to create it)."
204
230
  )
205
231
  store_name = field(store, "name") or memory_store
206
232
  env[_MEMORY_ENV] = store_name.split("/", 1)[-1]
207
233
  if session_store:
208
234
  if create_stores:
209
235
  _ensure_session_store(client, session_store)
236
+ else:
237
+ # Validate existence up front so a typo fails at deploy time, not at runtime.
238
+ try:
239
+ client.get_session_store(session_store)
240
+ except AgentCliError as exc:
241
+ raise AgentCliError(
242
+ f"Session store '{session_store}' does not exist "
243
+ "(drop --no-create-stores to create it).",
244
+ error_code=exc.error_code,
245
+ ) from exc
210
246
  env[_SESSION_ENV] = session_store
211
247
  if traces_destination:
212
248
  env[TRACES_DEST_ENV] = traces_destination
@@ -266,13 +302,15 @@ def _grant_store_access(
266
302
  "current directory.",
267
303
  )
268
304
  @click.option(
269
- "--with-memory-store",
305
+ "--memory",
306
+ "-m",
270
307
  "memory_store",
271
308
  default=None,
272
309
  help="Memory store display name to wire in via AGENT_MEMORY_STORE.",
273
310
  )
274
311
  @click.option(
275
- "--with-session-store",
312
+ "--session",
313
+ "-s",
276
314
  "session_store",
277
315
  default=None,
278
316
  help="Session store name to wire in via AGENT_SESSION_STORE.",
@@ -296,9 +334,10 @@ def _grant_store_access(
296
334
  help="MLflow experiment path to wire in via MLFLOW_EXPERIMENT_NAME.",
297
335
  )
298
336
  @click.option(
299
- "--create-stores",
337
+ "--no-create-stores",
300
338
  is_flag=True,
301
- help="Create the referenced stores if they don't exist (idempotent).",
339
+ help="Require referenced stores to already exist. By default missing stores are created "
340
+ "(idempotent).",
302
341
  )
303
342
  @click.option(
304
343
  "--pip-index-url",
@@ -323,11 +362,12 @@ def deploy(
323
362
  actor_id,
324
363
  traces_destination,
325
364
  traces_experiment,
326
- create_stores,
365
+ no_create_stores,
327
366
  pip_index_url,
328
367
  workspace_path,
329
368
  ) -> None:
330
369
  """Deploy an agent: provision its stores, wire them in, and roll out the deployment."""
370
+ _validate_deployment_name(name)
331
371
  source_dir = pathlib.Path(source)
332
372
  client = obj.client()
333
373
 
@@ -339,7 +379,7 @@ def deploy(
339
379
  session_store=session_store,
340
380
  traces_destination=traces_destination,
341
381
  traces_experiment=traces_experiment,
342
- create_stores=create_stores,
382
+ create_stores=not no_create_stores,
343
383
  )
344
384
  provisioned: dict[str, Any] = {}
345
385
  if _MEMORY_ENV in env_updates:
@@ -411,7 +451,7 @@ def deploy(
411
451
  steps.insert(
412
452
  0,
413
453
  "The app's service principal needs read/write on its store tables; that grant couldn't "
414
- "be applied automatically (it requires store ownership and psql). "
454
+ "be applied automatically (it requires store ownership). "
415
455
  f"Cause: {grant_error}",
416
456
  )
417
457
  if grants_stores and grant_error is None:
@@ -470,6 +510,7 @@ def deployments_list(obj) -> None:
470
510
  @click.pass_obj
471
511
  def deployments_get(obj, name) -> None:
472
512
  """Get an agent deployment's details."""
513
+ _validate_deployment_name(name)
473
514
  result = _databricks(["apps", "get", name, "-o", "json"], obj.profile, capture=True)
474
515
  data = json.loads(result.stdout or "{}")
475
516
  if obj.output == "json":
@@ -495,6 +536,7 @@ def deployments_get(obj, name) -> None:
495
536
  @click.pass_obj
496
537
  def deployments_logs(obj, name) -> None:
497
538
  """Stream a deployment's logs."""
539
+ _validate_deployment_name(name)
498
540
  _databricks(["apps", "logs", name], obj.profile)
499
541
 
500
542
 
@@ -503,6 +545,7 @@ def deployments_logs(obj, name) -> None:
503
545
  @click.pass_obj
504
546
  def deployments_start(obj, name) -> None:
505
547
  """Start a deployment."""
548
+ _validate_deployment_name(name)
506
549
  _databricks(["apps", "start", name], obj.profile)
507
550
  if obj.output == "json":
508
551
  render.emit_json({"started": name})
@@ -512,9 +555,12 @@ def deployments_start(obj, name) -> None:
512
555
 
513
556
  @deployments.command("stop")
514
557
  @click.argument("name")
558
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
515
559
  @click.pass_obj
516
- def deployments_stop(obj, name) -> None:
560
+ def deployments_stop(obj, name, yes) -> None:
517
561
  """Stop a deployment."""
562
+ _validate_deployment_name(name)
563
+ _confirm_destroy(f"Stop deployment '{name}'", assume_yes=yes)
518
564
  _databricks(["apps", "stop", name], obj.profile)
519
565
  if obj.output == "json":
520
566
  render.emit_json({"stopped": name})
@@ -524,9 +570,12 @@ def deployments_stop(obj, name) -> None:
524
570
 
525
571
  @deployments.command("delete")
526
572
  @click.argument("name")
573
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
527
574
  @click.pass_obj
528
- def deployments_delete(obj, name) -> None:
575
+ def deployments_delete(obj, name, yes) -> None:
529
576
  """Delete a deployment."""
577
+ _validate_deployment_name(name)
578
+ _confirm_destroy(f"Delete deployment '{name}'", assume_yes=yes)
530
579
  _databricks(["apps", "delete", name], obj.profile)
531
580
  if obj.output == "json":
532
581
  render.emit_json({"deleted": name})