Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions integrations/agentbricks/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,11 @@ Rolls the agent out to Databricks Apps and prints the URL where you (or anyone y

NAME is recorded in agent.toml on the first deploy, so a later `agentbricks deploy` from the project directory can omit it (passing NAME again updates the recorded name). Deployed apps are named `agent-bricks-<name>`. A project with a recorded base name reuses that app when NAME is omitted; you can also pass the full app name to update it. Use the full app name with the `agentbricks deployments` commands.

If the Apps rollout fails, the error includes `agentbricks deployments get <name>` and
`agentbricks deployments logs <name>` commands using the selected profile. Inspect the status and
build/startup logs, address the reported failure, then rerun the deploy command from the same project
directory.

Any memory/session store declared in agent.toml (for example, by `agentbricks memory/sessions bind`) is created if it doesn't exist yet; agent.toml itself is never modified for stores.

Scaling to multiple instances (--instances) uses best-effort sticky routing, so a browser session automatically stays on one instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import json
import os
import pathlib
import shlex
import subprocess
import time
from collections.abc import Collection, Sequence
Expand Down Expand Up @@ -353,11 +354,28 @@ def sync_source(self, name: str, source_dir, ws_path: str) -> None:

def deploy(self, name: str, ws_path: str) -> None:
"""Roll out the uploaded source as the app's active deployment."""
self._run(
["apps", "deploy", name, "--source-code-path", ws_path],
self._profile,
action=f"Could not deploy '{name}'.",
)
try:
self._run(
["apps", "deploy", name, "--source-code-path", ws_path],
self._profile,
action=f"Could not deploy '{name}'.",
)
except AgentCliError as exc:
# The blocking Apps command streams its diagnostic, but may only suggest `/logz`.
# Give users commands they can run in the same workspace without guessing the cause
# of a build, startup, permission, or service failure (or blindly retrying it).
command = ["agentbricks"]
if self._profile:
command += ["--profile", self._profile]
status = shlex.join([*command, "deployments", "get", name])
logs = shlex.join([*command, "deployments", "logs", name])
guidance = (
f"Inspect `{status}` for deployment status and `{logs}` for build/startup logs "
"before retrying. After addressing the reported failure, rerun your deploy "
"command from the same project directory."
)
hint = f"{exc.hint}\n{guidance}" if exc.hint else guidance
raise AgentCliError(exc.message, error_code=exc.error_code, hint=hint) from exc

def run_local(
self,
Expand Down
67 changes: 67 additions & 0 deletions integrations/agentbricks/tests/unit_tests/apps_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import shlex
import types

import pytest
Expand Down Expand Up @@ -127,3 +128,69 @@ def test_wait_for_running_times_out(monkeypatch):
monkeypatch.setattr(apps_client_mod.time, "sleep", lambda s: None) # don't actually wait
with pytest.raises(AgentCliError):
client.wait_for_active("app", timeout_s=0)


@pytest.mark.parametrize("profile", [None, "DEFAULT", "profile with spaces"])
def test_deploy_failure_suggests_status_and_logs_with_the_selected_profile(monkeypatch, profile):
# Exercise the real runner's uncaptured failure path: Apps emits its own diagnostic to
# the terminal, then Agent Bricks must explain how to investigate it in this workspace.
from databricks_agentbricks.clients.databricks_cli import _databricks

calls = []

def fail_deploy(cmd, **kwargs):
calls.append((cmd, kwargs))
return types.SimpleNamespace(returncode=1, stdout=None, stderr=None)

monkeypatch.setattr(apps_client_mod.subprocess, "run", fail_deploy)
with pytest.raises(AgentCliError, match="Could not deploy 'agent-bricks-demo'") as raised:
AppsClient(profile, runner=_databricks).deploy("agent-bricks-demo", "/Workspace/source")

hint = raised.value.hint or ""
prefix = ["agentbricks"] + (["--profile", profile] if profile else [])
assert shlex.join([*prefix, "deployments", "get", "agent-bricks-demo"]) in hint
assert shlex.join([*prefix, "deployments", "logs", "agent-bricks-demo"]) in hint
assert "before retrying" in hint
assert "same project" in hint
# Preserve the blocking, streaming invocation; do not retry or run extra control-plane calls.
assert len(calls) == 1
assert calls[0][0][1:6] == [
"apps",
"deploy",
"agent-bricks-demo",
"--source-code-path",
"/Workspace/source",
]
assert calls[0][1]["capture_output"] is False


def test_deploy_failure_preserves_original_message_code_and_detail():
original = AgentCliError(
"The deployment request was rejected.",
error_code="PERMISSION_DENIED",
hint="Backend detail.",
)

def fail_deploy(*args, **kwargs):
raise original

with pytest.raises(AgentCliError) as raised:
AppsClient("DEFAULT", runner=fail_deploy).deploy("agent-bricks-demo", "/Workspace/source")

assert raised.value.message == original.message
assert raised.value.error_code == original.error_code
assert (raised.value.hint or "").startswith("Backend detail.")
assert "deployments logs agent-bricks-demo" in (raised.value.hint or "")
assert raised.value.__cause__ is original


def test_successful_deploy_preserves_the_streaming_invocation():
calls = []
AppsClient("DEFAULT", runner=_runner(calls)).deploy("agent-bricks-demo", "/Workspace/source")
assert calls == [
(
["apps", "deploy", "agent-bricks-demo", "--source-code-path", "/Workspace/source"],
"DEFAULT",
{"action": "Could not deploy 'agent-bricks-demo'."},
)
]
Loading