diff --git a/integrations/agentbricks/cli.md b/integrations/agentbricks/cli.md index be71ea21e..c9fc0a035 100644 --- a/integrations/agentbricks/cli.md +++ b/integrations/agentbricks/cli.md @@ -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-`. 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 ` and +`agentbricks deployments logs ` 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. diff --git a/integrations/agentbricks/src/databricks_agentbricks/clients/apps_client.py b/integrations/agentbricks/src/databricks_agentbricks/clients/apps_client.py index b39afb10c..3ed59be67 100644 --- a/integrations/agentbricks/src/databricks_agentbricks/clients/apps_client.py +++ b/integrations/agentbricks/src/databricks_agentbricks/clients/apps_client.py @@ -14,6 +14,7 @@ import json import os import pathlib +import shlex import subprocess import time from collections.abc import Collection, Sequence @@ -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, diff --git a/integrations/agentbricks/tests/unit_tests/apps_client_test.py b/integrations/agentbricks/tests/unit_tests/apps_client_test.py index 22927edfc..c051a57ce 100644 --- a/integrations/agentbricks/tests/unit_tests/apps_client_test.py +++ b/integrations/agentbricks/tests/unit_tests/apps_client_test.py @@ -2,6 +2,7 @@ from __future__ import annotations +import shlex import types import pytest @@ -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'."}, + ) + ]