databricks-mason 0.1.2.dev0__tar.gz → 0.1.3.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 (51) hide show
  1. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/PKG-INFO +24 -11
  2. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/README.md +22 -9
  3. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/pyproject.toml +2 -2
  4. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/agent_project.py +60 -1
  5. databricks_mason-0.1.3.dev0/src/databricks_mason/auth.py +139 -0
  6. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/client.py +61 -13
  7. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/deploy.py +56 -25
  8. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/dev.py +16 -2
  9. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/errors.py +30 -2
  10. databricks_mason-0.1.3.dev0/src/databricks_mason/help.py +282 -0
  11. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/init.py +5 -5
  12. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/__init__.py +1 -5
  13. databricks_mason-0.1.3.dev0/src/databricks_mason/langgraph/memory.py +67 -0
  14. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/session_store.py +12 -12
  15. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/memory.py +86 -3
  16. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/__init__.py +1 -5
  17. databricks_mason-0.1.3.dev0/src/databricks_mason/openai/memory.py +67 -0
  18. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/sessions.py +12 -14
  19. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/render.py +51 -5
  20. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/tool_manifest.py +47 -0
  21. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/sessions.py +91 -5
  22. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/tools.py +46 -0
  23. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/tracing.py +19 -6
  24. databricks_mason-0.1.2.dev0/src/databricks_mason/auth.py +0 -83
  25. databricks_mason-0.1.2.dev0/src/databricks_mason/help.py +0 -139
  26. databricks_mason-0.1.2.dev0/src/databricks_mason/langgraph/memory.py +0 -63
  27. databricks_mason-0.1.2.dev0/src/databricks_mason/openai/memory.py +0 -63
  28. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/.gitignore +0 -0
  29. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/NOTICE +0 -0
  30. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/__init__.py +0 -0
  31. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/cli.py +0 -0
  32. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/mcp.py +0 -0
  33. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/mcp.py +0 -0
  34. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/memory_store_access.py +0 -0
  35. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/models.py +0 -0
  36. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/mcp.py +0 -0
  37. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/project_config.py +0 -0
  38. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/py.typed +0 -0
  39. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/__init__.py +0 -0
  40. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/background.py +0 -0
  41. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/session_store_client.py +0 -0
  42. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/tracing.py +0 -0
  43. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/workspace.py +0 -0
  44. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/sandbox.py +0 -0
  45. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/session_store_access.py +0 -0
  46. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/store_access.py +0 -0
  47. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/python_tool_langgraph.py +0 -0
  48. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/python_tool_test.py +0 -0
  49. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/sandbox_mcp.py +0 -0
  50. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/sandbox_mcp_langgraph.py +0 -0
  51. {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/timefmt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: databricks-mason
3
- Version: 0.1.2.dev0
3
+ Version: 0.1.3.dev0
4
4
  Summary: Databricks integration for Mason
5
5
  Author-email: Databricks <agent-feedback@databricks.com>
6
6
  License-File: NOTICE
@@ -27,7 +27,7 @@ Requires-Dist: databricks-agents>=1.9.3; extra == 'runtime-openai'
27
27
  Requires-Dist: databricks-openai>=0.13.0; extra == 'runtime-openai'
28
28
  Requires-Dist: fastapi>=0.129.0; extra == 'runtime-openai'
29
29
  Requires-Dist: mlflow>=3.10.1; extra == 'runtime-openai'
30
- Requires-Dist: openai-agents>=0.4.1; extra == 'runtime-openai'
30
+ Requires-Dist: openai-agents>=0.7.0; extra == 'runtime-openai'
31
31
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.25.0; extra == 'runtime-openai'
32
32
  Requires-Dist: uuid-utils>=0.10.0; extra == 'runtime-openai'
33
33
  Provides-Extra: tracing
@@ -62,23 +62,31 @@ For tracing commands, install Mason with tracing extras:
62
62
  pip install 'databricks-mason[tracing]'
63
63
  ```
64
64
 
65
+ ## Shell completion
66
+ Add this to `~/.zshrc`:
67
+ ```sh
68
+ eval "$(_MASON_COMPLETE=zsh_source mason)"
69
+ ```
70
+
65
71
  ## Authentication
66
72
 
67
73
  Mason uses [Databricks authentication](https://docs.databricks.com/aws/en/dev-tools/cli/authentication).
68
- If you do not already have credentials, authenticate a named profile first. You can
69
- then ask Mason to validate and remember that profile:
74
+ Ask Mason to authenticate and remember a named profile:
70
75
 
71
76
  ```sh
72
- databricks auth login --profile <profile>
73
77
  mason login --profile <profile>
74
78
  mason sessions stores list
75
79
  ```
76
80
 
77
- `mason login` does not create credentials; it stores the selected profile in
78
- `~/.mason/config.json`. `mason logout` forgets that selection without revoking the
79
- underlying credentials. If Databricks SDK default authentication is already configured,
80
- you can skip `mason login`. You can also pass `--profile/-p` for an individual command.
81
- Use `--output json` for scripting.
81
+ `mason login` validates existing credentials first. If credentials are missing or rejected in
82
+ an interactive terminal, Mason runs `databricks auth login --profile <profile>`, revalidates the
83
+ profile, and stores the selection in `~/.mason/config.json`. This browser-based setup requires
84
+ the Databricks CLI. In non-interactive environments, authenticate the profile before running
85
+ Mason. `mason logout` forgets the saved selection without revoking the underlying credentials.
86
+
87
+ If Databricks SDK default authentication is already configured, you can skip `mason login`.
88
+ You can also pass the global `--profile/-p` option before an individual command, for example
89
+ `mason --profile <profile> mcp list`. Use `--output json` for scripting.
82
90
 
83
91
  ## Python SDK
84
92
 
@@ -153,7 +161,7 @@ mason sessions items append --help
153
161
  For the shortest path from a blank directory to a running and deployed agent:
154
162
 
155
163
  ```sh
156
- mason login --profile my-workspace
164
+ mason login --profile <profile>
157
165
  mason init my-agent
158
166
  cd my-agent
159
167
  mason dev
@@ -176,9 +184,14 @@ mason tools add sandbox --scope table:samples.nyctaxi.trips
176
184
  mason tools add mcp system.ai.web_search
177
185
  mason tools add uc-function catalog.schema.lookup_ticket
178
186
  mason tools add python lookup-ticket
187
+ mason tools remove mcp system.ai.web_search
179
188
  mason tools list
180
189
  ```
181
190
 
191
+ For MCP services, the remove command accepts the same service name as the add command. You can also
192
+ remove any binding by the ID shown in `mason tools list`, for example `mason tools remove
193
+ web_search`. Removal updates only `agent.toml`; Python source and test files remain user-owned.
194
+
182
195
  Discover the MCP Services available to your user before adding one. By default Mason lists the
183
196
  Databricks-managed services in `system.ai`; pass `--schema catalog.schema` for another Unity Catalog
184
197
  schema. Text output includes a copyable add command, while `--output json` returns normalized service
@@ -26,23 +26,31 @@ For tracing commands, install Mason with tracing extras:
26
26
  pip install 'databricks-mason[tracing]'
27
27
  ```
28
28
 
29
+ ## Shell completion
30
+ Add this to `~/.zshrc`:
31
+ ```sh
32
+ eval "$(_MASON_COMPLETE=zsh_source mason)"
33
+ ```
34
+
29
35
  ## Authentication
30
36
 
31
37
  Mason uses [Databricks authentication](https://docs.databricks.com/aws/en/dev-tools/cli/authentication).
32
- If you do not already have credentials, authenticate a named profile first. You can
33
- then ask Mason to validate and remember that profile:
38
+ Ask Mason to authenticate and remember a named profile:
34
39
 
35
40
  ```sh
36
- databricks auth login --profile <profile>
37
41
  mason login --profile <profile>
38
42
  mason sessions stores list
39
43
  ```
40
44
 
41
- `mason login` does not create credentials; it stores the selected profile in
42
- `~/.mason/config.json`. `mason logout` forgets that selection without revoking the
43
- underlying credentials. If Databricks SDK default authentication is already configured,
44
- you can skip `mason login`. You can also pass `--profile/-p` for an individual command.
45
- Use `--output json` for scripting.
45
+ `mason login` validates existing credentials first. If credentials are missing or rejected in
46
+ an interactive terminal, Mason runs `databricks auth login --profile <profile>`, revalidates the
47
+ profile, and stores the selection in `~/.mason/config.json`. This browser-based setup requires
48
+ the Databricks CLI. In non-interactive environments, authenticate the profile before running
49
+ Mason. `mason logout` forgets the saved selection without revoking the underlying credentials.
50
+
51
+ If Databricks SDK default authentication is already configured, you can skip `mason login`.
52
+ You can also pass the global `--profile/-p` option before an individual command, for example
53
+ `mason --profile <profile> mcp list`. Use `--output json` for scripting.
46
54
 
47
55
  ## Python SDK
48
56
 
@@ -117,7 +125,7 @@ mason sessions items append --help
117
125
  For the shortest path from a blank directory to a running and deployed agent:
118
126
 
119
127
  ```sh
120
- mason login --profile my-workspace
128
+ mason login --profile <profile>
121
129
  mason init my-agent
122
130
  cd my-agent
123
131
  mason dev
@@ -140,9 +148,14 @@ mason tools add sandbox --scope table:samples.nyctaxi.trips
140
148
  mason tools add mcp system.ai.web_search
141
149
  mason tools add uc-function catalog.schema.lookup_ticket
142
150
  mason tools add python lookup-ticket
151
+ mason tools remove mcp system.ai.web_search
143
152
  mason tools list
144
153
  ```
145
154
 
155
+ For MCP services, the remove command accepts the same service name as the add command. You can also
156
+ remove any binding by the ID shown in `mason tools list`, for example `mason tools remove
157
+ web_search`. Removal updates only `agent.toml`; Python source and test files remain user-owned.
158
+
146
159
  Discover the MCP Services available to your user before adding one. By default Mason lists the
147
160
  Databricks-managed services in `system.ai`; pass `--schema catalog.schema` for another Unity Catalog
148
161
  schema. Text output includes a copyable add command, while `--output json` returns normalized service
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "databricks-mason"
3
- version = "0.1.2.dev0"
3
+ version = "0.1.3.dev0"
4
4
  description = "Databricks integration for Mason"
5
5
  authors = [
6
6
  { name="Databricks", email="agent-feedback@databricks.com" },
@@ -38,7 +38,7 @@ runtime = [
38
38
  "databricks-agents>=1.9.3",
39
39
  ]
40
40
  runtime-openai = [
41
- "openai-agents>=0.4.1",
41
+ "openai-agents>=0.7.0",
42
42
  "databricks-openai>=0.13.0",
43
43
  "fastapi>=0.129.0",
44
44
  "mlflow>=3.10.1",
@@ -15,6 +15,7 @@ from tomlkit import TOMLDocument
15
15
  from tomlkit.exceptions import ParseError
16
16
 
17
17
  from databricks_mason.errors import AgentCliError
18
+ from databricks_mason.runtime.tool_manifest import MEMORY_STORE_TABLE, SESSION_STORE_TABLE
18
19
 
19
20
  _SCHEMA_VERSION = 1
20
21
  _SUPPORTED_FRAMEWORKS = {"langgraph", "openai"}
@@ -191,6 +192,15 @@ def _required_string(value: object, description: str) -> str:
191
192
  return value
192
193
 
193
194
 
195
+ def _store_name_from_manifest(value: object, table: str) -> str | None:
196
+ """Read the ``name`` from a ``[memory_store]`` / ``[session_store]`` table, or None if absent."""
197
+ if value is None:
198
+ return None
199
+ if not isinstance(value, Mapping):
200
+ raise AgentCliError(f"agent.toml [{table}] must be a table.")
201
+ return _required_string(cast(Mapping[str, Any], value).get("name"), f"[{table}] name")
202
+
203
+
194
204
  def _scope_from_manifest(value: object) -> Scope:
195
205
  if not isinstance(value, Mapping):
196
206
  raise AgentCliError("Sandbox downscope entries must be TOML tables.")
@@ -275,12 +285,17 @@ class AgentProject:
275
285
  document: TOMLDocument,
276
286
  framework: str,
277
287
  tools: list[ToolSpec],
288
+ memory_store: str | None = None,
289
+ session_store: str | None = None,
278
290
  ) -> None:
279
291
  self.root = root
280
292
  self.path = root / "agent.toml"
281
293
  self._document = document
282
294
  self.framework = framework
283
295
  self.tools = tools
296
+ # Managed store bindings declared in agent.toml; None = unbound.
297
+ self.memory_store = memory_store
298
+ self.session_store = session_store
284
299
 
285
300
  @classmethod
286
301
  def load(cls, root: pathlib.Path | str) -> "AgentProject":
@@ -314,7 +329,13 @@ class AgentProject:
314
329
  ids = [tool.id for tool in tools]
315
330
  if len(ids) != len(set(ids)):
316
331
  raise AgentCliError("agent.toml tool ids must be unique.")
317
- return cls(project_root, document, framework, tools)
332
+ memory_store = _store_name_from_manifest(
333
+ document.get(MEMORY_STORE_TABLE), MEMORY_STORE_TABLE
334
+ )
335
+ session_store = _store_name_from_manifest(
336
+ document.get(SESSION_STORE_TABLE), SESSION_STORE_TABLE
337
+ )
338
+ return cls(project_root, document, framework, tools, memory_store, session_store)
318
339
 
319
340
  @classmethod
320
341
  def create(cls, root: pathlib.Path | str, *, framework: str) -> "AgentProject":
@@ -367,6 +388,44 @@ class AgentProject:
367
388
  del self.tools[index]
368
389
  return True
369
390
 
391
+ def bind_memory_store(self, name: str) -> bool:
392
+ """Declare the memory store binding in agent.toml. Returns True if it changed."""
393
+ return self._set_store(MEMORY_STORE_TABLE, name)
394
+
395
+ def bind_session_store(self, name: str) -> bool:
396
+ """Declare the session store binding in agent.toml. Returns True if it changed."""
397
+ return self._set_store(SESSION_STORE_TABLE, name)
398
+
399
+ def unbind_memory_store(self) -> bool:
400
+ """Remove the memory store binding from agent.toml. Returns True if it was present."""
401
+ return self._clear_store(MEMORY_STORE_TABLE)
402
+
403
+ def unbind_session_store(self) -> bool:
404
+ """Remove the session store binding from agent.toml. Returns True if it was present."""
405
+ return self._clear_store(SESSION_STORE_TABLE)
406
+
407
+ def _set_store(self, table: str, name: str) -> bool:
408
+ name = _required_string(name, f"[{table}] name")
409
+ if getattr(self, table) == name:
410
+ return False
411
+ existing = self._document.get(table)
412
+ if isinstance(existing, Mapping):
413
+ existing["name"] = name
414
+ else:
415
+ store_table = tomlkit.table()
416
+ store_table.add("name", name)
417
+ self._document.append(table, store_table)
418
+ setattr(self, table, name)
419
+ return True
420
+
421
+ def _clear_store(self, table: str) -> bool:
422
+ if getattr(self, table) is None:
423
+ return False
424
+ if table in self._document:
425
+ del self._document[table]
426
+ setattr(self, table, None)
427
+ return True
428
+
370
429
  def write(self) -> pathlib.Path:
371
430
  self.path.parent.mkdir(parents=True, exist_ok=True)
372
431
  temporary: pathlib.Path | None = None
@@ -0,0 +1,139 @@
1
+ """`mason login` / `logout` — remember an optional Databricks profile.
2
+
3
+ `login` validates a named profile and persists the selection; when credentials are missing or
4
+ rejected in an interactive terminal, it delegates setup to `databricks auth login` and retries.
5
+ The root group falls back to the saved profile whenever `-p` is omitted. Without a saved profile,
6
+ the Databricks SDK performs its normal default authentication resolution. `logout` removes only
7
+ Mason's saved selection, not the underlying credentials. State lives in a small JSON file under
8
+ `~/.mason` (override the directory with `MASON_CONFIG_HOME`, mainly for tests).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import pathlib
16
+ import subprocess
17
+ import sys
18
+ from typing import Optional
19
+
20
+ import click
21
+ from databricks.sdk.errors import Unauthenticated
22
+
23
+ from databricks_mason import render
24
+ from databricks_mason.client import MasonClient
25
+ from databricks_mason.errors import AgentCliError
26
+
27
+
28
+ def _config_file() -> pathlib.Path:
29
+ base = os.environ.get("MASON_CONFIG_HOME")
30
+ root = pathlib.Path(base) if base else pathlib.Path.home() / ".mason"
31
+ return root / "config.json"
32
+
33
+
34
+ def load_default_profile() -> Optional[str]:
35
+ """The profile saved by `mason login`, or None if the user never logged in."""
36
+ try:
37
+ return json.loads(_config_file().read_text()).get("profile")
38
+ except (OSError, json.JSONDecodeError):
39
+ return None
40
+
41
+
42
+ def _save_default_profile(profile: str) -> None:
43
+ path = _config_file()
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ path.write_text(json.dumps({"profile": profile}, indent=2) + "\n")
46
+
47
+
48
+ def _validate_profile(profile: str) -> tuple[MasonClient, str]:
49
+ client = MasonClient(profile)
50
+ return client, client.current_user
51
+
52
+
53
+ def _is_interactive() -> bool:
54
+ return sys.stdin.isatty()
55
+
56
+
57
+ def _run_databricks_login(profile: str) -> None:
58
+ command = ["databricks", "auth", "login", "--profile", profile]
59
+ try:
60
+ # Keep the child process interactive while preserving stdout for Mason's JSON output.
61
+ result = subprocess.run(command, text=True, check=False, stdout=sys.stderr)
62
+ except FileNotFoundError as exc:
63
+ raise AgentCliError(
64
+ "Could not configure Databricks authentication: the `databricks` CLI was not found.",
65
+ hint=f"Install the Databricks CLI, then retry `mason login --profile {profile}`.",
66
+ ) from exc
67
+ if result.returncode != 0:
68
+ raise AgentCliError(
69
+ f"`databricks auth login --profile {profile}` failed (exit {result.returncode})."
70
+ )
71
+
72
+
73
+ def _authenticate_profile(profile: str) -> tuple[MasonClient, str]:
74
+ try:
75
+ return _validate_profile(profile)
76
+ except (AgentCliError, Unauthenticated) as initial_error:
77
+ if not _is_interactive():
78
+ raise AgentCliError(
79
+ f"Could not validate Databricks profile {profile!r}: {initial_error}",
80
+ hint="Run this command in an interactive terminal so Mason can open "
81
+ "Databricks login, or authenticate first with "
82
+ f"`databricks auth login --profile {profile}`.",
83
+ ) from initial_error
84
+ except Exception as validation_error: # noqa: BLE001 - normalize unexpected API failures
85
+ raise AgentCliError(
86
+ f"Could not validate Databricks profile {profile!r}: {validation_error}"
87
+ ) from validation_error
88
+
89
+ _run_databricks_login(profile)
90
+ try:
91
+ return _validate_profile(profile)
92
+ except Exception as retry_error: # noqa: BLE001 - normalize the post-login failure
93
+ raise AgentCliError(
94
+ f"Databricks login completed, but profile {profile!r} could not be validated: "
95
+ f"{retry_error}"
96
+ ) from retry_error
97
+
98
+
99
+ @click.command()
100
+ @click.option(
101
+ "--profile",
102
+ "-p",
103
+ default=None,
104
+ help="Profile to authenticate with and remember as the default.",
105
+ )
106
+ @click.pass_obj
107
+ def login(obj, profile) -> None:
108
+ """Authenticate a profile and save it as the default, so later commands can omit -p."""
109
+ profile = profile or obj.profile
110
+ if not profile:
111
+ raise AgentCliError(
112
+ "No profile to save.",
113
+ hint="Pass one to remember, e.g. `mason login --profile <profile>`.",
114
+ )
115
+ client, user = _authenticate_profile(profile)
116
+ _save_default_profile(profile)
117
+ if obj.output == "json":
118
+ render.emit_json({"profile": profile, "user": user, "host": client.host})
119
+ return
120
+ render.success(
121
+ f"Logged in as {user}",
122
+ fields={"Profile": profile, "Host": client.host},
123
+ next_steps=[
124
+ ("mason init my-agent", "Scaffold a new agent project"),
125
+ ],
126
+ )
127
+
128
+
129
+ @click.command()
130
+ @click.pass_obj
131
+ def logout(obj) -> None:
132
+ """Forget the saved profile selection without deleting its credentials."""
133
+ path = _config_file()
134
+ existed = path.exists()
135
+ path.unlink(missing_ok=True)
136
+ if obj.output == "json":
137
+ render.emit_json({"logged_out": existed})
138
+ return
139
+ render.success("Logged out" if existed else "No saved login to clear")
@@ -14,16 +14,23 @@ from __future__ import annotations
14
14
  import configparser
15
15
  import os
16
16
  import pathlib
17
+ import time
17
18
  from typing import Any, Optional
18
19
 
19
20
  from databricks.sdk import WorkspaceClient
20
21
 
21
22
  from databricks_mason import models
22
- from databricks_mason.errors import AgentCliError, wrap_api_error
23
+ from databricks_mason.errors import TRANSIENT_ERROR_CODES, AgentCliError, wrap_api_error
23
24
 
24
25
  _BASE = "/api/agents/v1"
25
26
  _MCP_SERVICES_PATH = "/api/2.1/unity-catalog/mcp-services"
26
27
 
28
+ # Transient backend failures (e.g. a CANCELLED RPC) usually clear on a retry, so retry safe
29
+ # requests once before surfacing them. Mutating requests must opt in explicitly: their first
30
+ # attempt may have committed even when its response was lost.
31
+ _MAX_ATTEMPTS = 2
32
+ _RETRY_BASE_DELAY_S = 0.2
33
+
27
34
 
28
35
  def _query(**kwargs: Any) -> dict[str, Any]:
29
36
  """Build a query dict, dropping None and empty values."""
@@ -118,8 +125,8 @@ class MasonClient:
118
125
  except Exception as exc: # noqa: BLE001 - surfaced as a clean CLI error
119
126
  raise AgentCliError(
120
127
  f"Could not initialize Databricks auth: {exc}",
121
- hint="Check your profile (`databricks auth login --profile <name>`) "
122
- "or pass --profile.",
128
+ hint="Select an existing profile with `mason --profile <name> <command>` "
129
+ "or authenticate and save it with `mason login --profile <name>`.",
123
130
  ) from exc
124
131
 
125
132
  @property
@@ -140,12 +147,27 @@ class MasonClient:
140
147
  self._w.workspace.mkdirs(path)
141
148
 
142
149
  def _do(
143
- self, method: str, path: str, *, query: Optional[dict] = None, body: Optional[dict] = None
150
+ self,
151
+ method: str,
152
+ path: str,
153
+ *,
154
+ query: Optional[dict] = None,
155
+ body: Optional[dict] = None,
156
+ safe_to_retry: bool = False,
144
157
  ) -> Any:
145
- try:
146
- return self._w.api_client.do(method, path, query=query, body=body)
147
- except Exception as exc: # noqa: BLE001 - normalized to AgentCliError
148
- raise wrap_api_error(exc) from exc
158
+ retry_allowed = method == "GET" or safe_to_retry
159
+ delay = _RETRY_BASE_DELAY_S
160
+ for attempt in range(1, _MAX_ATTEMPTS + 1):
161
+ try:
162
+ return self._w.api_client.do(method, path, query=query, body=body)
163
+ except Exception as exc: # noqa: BLE001 - normalized to AgentCliError
164
+ retryable = (
165
+ retry_allowed and getattr(exc, "error_code", None) in TRANSIENT_ERROR_CODES
166
+ )
167
+ if not retryable or attempt == _MAX_ATTEMPTS:
168
+ raise wrap_api_error(exc) from exc
169
+ time.sleep(delay)
170
+ delay *= 2
149
171
 
150
172
  # --- Unity Catalog MCP Services -----------------------------------------
151
173
 
@@ -162,10 +184,22 @@ class MasonClient:
162
184
  # --- memory stores -------------------------------------------------------
163
185
 
164
186
  def create_memory_store(
165
- self, display_name: str, description: Optional[str] = None
187
+ self,
188
+ display_name: str,
189
+ description: Optional[str] = None,
190
+ *,
191
+ retry_transient: bool = False,
166
192
  ) -> models.MemoryStore:
167
193
  body = _query(display_name=display_name, description=description)
168
- return _as(models.MemoryStore, self._do("POST", f"{_BASE}/memory-stores", body=body))
194
+ return _as(
195
+ models.MemoryStore,
196
+ self._do(
197
+ "POST",
198
+ f"{_BASE}/memory-stores",
199
+ body=body,
200
+ safe_to_retry=retry_transient,
201
+ ),
202
+ )
169
203
 
170
204
  def get_memory_store(self, name: str) -> models.MemoryStore:
171
205
  return _as(models.MemoryStore, self._do("GET", f"{_BASE}/{memory_store_path(name)}"))
@@ -262,7 +296,12 @@ class MasonClient:
262
296
  body = _query(actor_id=actor_id, query=query, limit=limit)
263
297
  return _as(
264
298
  models.MemorySearchResult,
265
- self._do("POST", f"{_BASE}/{memory_store_path(store)}/entries:search", body=body),
299
+ self._do(
300
+ "POST",
301
+ f"{_BASE}/{memory_store_path(store)}/entries:search",
302
+ body=body,
303
+ safe_to_retry=True,
304
+ ),
266
305
  )
267
306
 
268
307
  def update_memory_entry(
@@ -286,13 +325,22 @@ class MasonClient:
286
325
  # --- session stores ------------------------------------------------------
287
326
 
288
327
  def create_session_store(
289
- self, name: str, description: Optional[str] = None, metadata: Optional[dict] = None
328
+ self,
329
+ name: str,
330
+ description: Optional[str] = None,
331
+ metadata: Optional[dict] = None,
332
+ *,
333
+ retry_transient: bool = False,
290
334
  ) -> models.SessionStore:
291
335
  body = _query(description=description, metadata=metadata)
292
336
  return _as(
293
337
  models.SessionStore,
294
338
  self._do(
295
- "POST", f"{_BASE}/session-stores", query={"session_store_name": name}, body=body
339
+ "POST",
340
+ f"{_BASE}/session-stores",
341
+ query={"session_store_name": name},
342
+ body=body,
343
+ safe_to_retry=retry_transient,
296
344
  ),
297
345
  )
298
346