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.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Transient authentication for request-bound, user-authorized tool calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, NoReturn, SupportsIndex
|
|
11
|
+
|
|
12
|
+
from databricks_agentkit.runtime.store import RUNTIME_STORE_LOCAL_ENV
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from databricks.sdk import WorkspaceClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthError(RuntimeError):
|
|
19
|
+
"""A credential-free error suitable for returning to the caller."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, code: str, message: str, status_code: int = 401, integration_id: str | None = None
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.code = code
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.integration_id = integration_id
|
|
28
|
+
|
|
29
|
+
def payload(self) -> dict[str, str]:
|
|
30
|
+
result = {"code": self.code, "message": str(self)}
|
|
31
|
+
if self.integration_id is not None:
|
|
32
|
+
result["integration_id"] = self.integration_id
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class InvocationAuthPolicy:
|
|
38
|
+
"""Whether an invocation needs credentials scoped to its active execution attempt."""
|
|
39
|
+
|
|
40
|
+
user_tools: tuple[str, ...] = ()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def requires_user(self) -> bool:
|
|
44
|
+
return bool(self.user_tools)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_manifest(cls, *, allow_missing: bool = False) -> InvocationAuthPolicy:
|
|
48
|
+
"""Infer request-user tools from ``agent.toml`` in the active project."""
|
|
49
|
+
from databricks_agentkit.runtime.tool_manifest import load_tools, project_root, tomllib
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
root = project_root()
|
|
53
|
+
except RuntimeError:
|
|
54
|
+
if allow_missing and not os.getenv("AGENTBRICKS_PROJECT_ROOT"):
|
|
55
|
+
return cls()
|
|
56
|
+
raise
|
|
57
|
+
with (root / "agent.toml").open("rb") as source:
|
|
58
|
+
document = tomllib.load(source)
|
|
59
|
+
tools = load_tools(expected_framework=document.get("agent", {}).get("framework", ""))
|
|
60
|
+
return cls(tuple(tool.id for tool in tools if tool.auth == "user"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RequestAuthContext:
|
|
64
|
+
"""Private request credentials, never part of a persisted invocation payload.
|
|
65
|
+
|
|
66
|
+
Deployed Apps must only be reachable through the trusted Apps ingress, which supplies the
|
|
67
|
+
forwarded identity headers. Local development deliberately ignores those headers.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = (
|
|
71
|
+
"_token",
|
|
72
|
+
"_principal",
|
|
73
|
+
"_host",
|
|
74
|
+
"_app",
|
|
75
|
+
"_workspace",
|
|
76
|
+
"_local",
|
|
77
|
+
"_clients",
|
|
78
|
+
"_closed",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __init__(self, *, token: str | None, principal: str, local: bool) -> None:
|
|
82
|
+
self._token = token
|
|
83
|
+
self._principal = principal
|
|
84
|
+
self._host = os.getenv("DATABRICKS_HOST", "").rstrip("/")
|
|
85
|
+
self._app = os.getenv("DATABRICKS_APP_NAME", "local")
|
|
86
|
+
self._workspace = os.getenv("DATABRICKS_WORKSPACE_ID", "")
|
|
87
|
+
self._local = local
|
|
88
|
+
self._clients: dict[str, WorkspaceClient] = {}
|
|
89
|
+
self._closed = False
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_headers(cls, headers: Mapping[str, str]) -> RequestAuthContext:
|
|
93
|
+
local_runtime = os.getenv(RUNTIME_STORE_LOCAL_ENV, "").lower() == "true"
|
|
94
|
+
if local_runtime or not os.getenv("DATABRICKS_APP_NAME"):
|
|
95
|
+
return cls(token=None, principal="local-developer", local=True)
|
|
96
|
+
normalized = {name.lower(): value for name, value in headers.items()}
|
|
97
|
+
token = normalized.get("x-forwarded-access-token", "").strip()
|
|
98
|
+
principal = normalized.get("x-forwarded-user", "").strip()
|
|
99
|
+
if not token:
|
|
100
|
+
raise AuthError("MCP_USER_AUTH_REQUIRED", "Missing request-user access token")
|
|
101
|
+
if not principal:
|
|
102
|
+
raise AuthError("MCP_USER_IDENTITY_MISSING", "Missing trusted request-user identity")
|
|
103
|
+
return cls(token=token, principal=principal, local=False)
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
return f"RequestAuthContext(closed={self._closed})"
|
|
107
|
+
|
|
108
|
+
def __reduce_ex__(self, protocol: SupportsIndex, /) -> NoReturn:
|
|
109
|
+
raise TypeError("RequestAuthContext cannot be serialized")
|
|
110
|
+
|
|
111
|
+
def namespace(self, kind: str, value: str) -> str:
|
|
112
|
+
material = json.dumps(
|
|
113
|
+
[self._host, self._workspace, self._app, self._principal, kind, value],
|
|
114
|
+
separators=(",", ":"),
|
|
115
|
+
)
|
|
116
|
+
return hashlib.sha256(material.encode()).hexdigest()
|
|
117
|
+
|
|
118
|
+
def client_for(self, mode: str) -> WorkspaceClient:
|
|
119
|
+
if self._closed:
|
|
120
|
+
raise AuthError(
|
|
121
|
+
"MCP_USER_AUTH_EXPIRED", "Request-user authentication is no longer active"
|
|
122
|
+
)
|
|
123
|
+
if mode not in ("user", "app"):
|
|
124
|
+
raise ValueError("auth must be 'user' or 'app'")
|
|
125
|
+
if mode not in self._clients:
|
|
126
|
+
from databricks_agentkit.runtime.workspace import workspace_client, workspace_headers
|
|
127
|
+
|
|
128
|
+
if mode == "app" or self._local:
|
|
129
|
+
self._clients[mode] = workspace_client()
|
|
130
|
+
else:
|
|
131
|
+
from databricks.sdk import WorkspaceClient
|
|
132
|
+
|
|
133
|
+
if not self._host:
|
|
134
|
+
raise AuthError(
|
|
135
|
+
"MCP_USER_AUTH_CONFIGURATION", "Workspace host is not configured", 500
|
|
136
|
+
)
|
|
137
|
+
self._clients[mode] = WorkspaceClient(
|
|
138
|
+
host=self._host,
|
|
139
|
+
token=self._token,
|
|
140
|
+
auth_type="pat",
|
|
141
|
+
custom_headers=workspace_headers(),
|
|
142
|
+
)
|
|
143
|
+
return self._clients[mode]
|
|
144
|
+
|
|
145
|
+
def close(self) -> None:
|
|
146
|
+
"""Drop credentials and prohibit further client resolution."""
|
|
147
|
+
self._closed = True
|
|
148
|
+
self._token = None
|
|
149
|
+
self._clients.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Durability helpers for the canonical AgentKit runtime."""
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Durable scheduling and execution of fenced invocation attempts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
from databricks_agentkit.runtime.durability.heartbeat import Heartbeat
|
|
10
|
+
from databricks_agentkit.runtime.durability.recovery import RecoveryScheduler
|
|
11
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
12
|
+
from databricks_agentkit.runtime.execution import AttemptExecution, InvocationExecutor
|
|
13
|
+
from databricks_agentkit.runtime.types import Invocation, InvocationStatus
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DurableInvocationExecutor(InvocationExecutor):
|
|
19
|
+
"""Claim durable attempts and wrap shared execution with heartbeats."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
execution: AttemptExecution,
|
|
24
|
+
*,
|
|
25
|
+
runtime_store: DurableRuntimeStore,
|
|
26
|
+
recovery_enabled: Callable[[], bool],
|
|
27
|
+
heartbeat_seconds: float,
|
|
28
|
+
stale_seconds: float,
|
|
29
|
+
scan_seconds: float,
|
|
30
|
+
) -> None:
|
|
31
|
+
if stale_seconds <= heartbeat_seconds:
|
|
32
|
+
raise ValueError("stale_seconds must be greater than heartbeat_seconds")
|
|
33
|
+
self._execution = execution
|
|
34
|
+
self._runtime_store = runtime_store
|
|
35
|
+
self._recovery_enabled = recovery_enabled
|
|
36
|
+
self._heartbeat_seconds = heartbeat_seconds
|
|
37
|
+
self._stale_seconds = stale_seconds
|
|
38
|
+
self._scan_seconds = scan_seconds
|
|
39
|
+
self._tasks: dict[str, asyncio.Task[None]] = {}
|
|
40
|
+
self._queued_scanner: asyncio.Task[None] | None = None
|
|
41
|
+
self._recovery_scheduler: RecoveryScheduler | None = None
|
|
42
|
+
|
|
43
|
+
async def start(self) -> None:
|
|
44
|
+
self._queued_scanner = asyncio.create_task(
|
|
45
|
+
self._scan_queued_loop(),
|
|
46
|
+
name="databricks-durable-runtime-queued-scanner",
|
|
47
|
+
)
|
|
48
|
+
self._recovery_scheduler = RecoveryScheduler(
|
|
49
|
+
scheduler=self,
|
|
50
|
+
runtime_store=self._runtime_store,
|
|
51
|
+
stale_seconds=self._stale_seconds,
|
|
52
|
+
scan_seconds=self._scan_seconds,
|
|
53
|
+
)
|
|
54
|
+
self._recovery_scheduler.start(recover=self._recovery_enabled())
|
|
55
|
+
|
|
56
|
+
async def stop(self) -> None:
|
|
57
|
+
if self._queued_scanner is not None:
|
|
58
|
+
self._queued_scanner.cancel()
|
|
59
|
+
if self._recovery_scheduler is not None:
|
|
60
|
+
await self._recovery_scheduler.stop()
|
|
61
|
+
self._recovery_scheduler = None
|
|
62
|
+
await asyncio.gather(
|
|
63
|
+
*([self._queued_scanner] if self._queued_scanner is not None else []),
|
|
64
|
+
return_exceptions=True,
|
|
65
|
+
)
|
|
66
|
+
self._queued_scanner = None
|
|
67
|
+
tasks = list(self._tasks.values())
|
|
68
|
+
for task in tasks:
|
|
69
|
+
task.cancel()
|
|
70
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
71
|
+
self._tasks.clear()
|
|
72
|
+
|
|
73
|
+
def ensure_scheduled(self, state: Invocation) -> None:
|
|
74
|
+
"""Schedule a newly queued invocation on this worker."""
|
|
75
|
+
if state.status != InvocationStatus.QUEUED:
|
|
76
|
+
return
|
|
77
|
+
self._schedule(state.invocation_id, recovery=False)
|
|
78
|
+
|
|
79
|
+
def ensure_recovery_scheduled(self, invocation_id: str) -> None:
|
|
80
|
+
"""Schedule stale active work discovered by the recovery scanner."""
|
|
81
|
+
self._schedule(invocation_id, recovery=True)
|
|
82
|
+
|
|
83
|
+
async def _scan_queued_loop(self) -> None:
|
|
84
|
+
"""Continuously schedule persisted first attempts, including after process restart."""
|
|
85
|
+
while True:
|
|
86
|
+
try:
|
|
87
|
+
for invocation_id in await self._runtime_store.queued_invocation_ids():
|
|
88
|
+
self._schedule(invocation_id, recovery=False)
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
raise
|
|
91
|
+
except Exception:
|
|
92
|
+
logger.exception("Databricks durable runtime queued invocation scan failed")
|
|
93
|
+
await asyncio.sleep(self._scan_seconds)
|
|
94
|
+
|
|
95
|
+
def _schedule(self, invocation_id: str, *, recovery: bool) -> None:
|
|
96
|
+
current = self._tasks.get(invocation_id)
|
|
97
|
+
if current is not None and not current.done():
|
|
98
|
+
return
|
|
99
|
+
task = asyncio.create_task(
|
|
100
|
+
self._run(invocation_id, recovery=recovery),
|
|
101
|
+
name=f"durable-invocation-{invocation_id}",
|
|
102
|
+
)
|
|
103
|
+
self._tasks[invocation_id] = task
|
|
104
|
+
task.add_done_callback(lambda completed: self._discard_task(invocation_id, completed))
|
|
105
|
+
|
|
106
|
+
async def _run(self, invocation_id: str, *, recovery: bool) -> None:
|
|
107
|
+
try:
|
|
108
|
+
if recovery:
|
|
109
|
+
claimed = await self._runtime_store.claim_recoverable(
|
|
110
|
+
invocation_id,
|
|
111
|
+
self._stale_seconds,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
claimed = await self._runtime_store.claim(invocation_id)
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.exception("Failed to claim durable invocation: %s", invocation_id)
|
|
117
|
+
return
|
|
118
|
+
if claimed is None:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
async with Heartbeat(
|
|
122
|
+
runtime_store=self._runtime_store,
|
|
123
|
+
invocation_id=invocation_id,
|
|
124
|
+
attempt=claimed.attempt,
|
|
125
|
+
heartbeat_seconds=self._heartbeat_seconds,
|
|
126
|
+
):
|
|
127
|
+
await self._execution.run(claimed)
|
|
128
|
+
|
|
129
|
+
def _discard_task(self, invocation_id: str, completed: asyncio.Task[None]) -> None:
|
|
130
|
+
if self._tasks.get(invocation_id) is completed:
|
|
131
|
+
self._tasks.pop(invocation_id, None)
|
|
132
|
+
if not completed.cancelled():
|
|
133
|
+
completed.exception()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Heartbeat lifecycle for a durable invocation attempt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Heartbeat:
|
|
14
|
+
"""Refresh attempt ownership until execution finishes or ownership changes."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
runtime_store: DurableRuntimeStore,
|
|
20
|
+
invocation_id: str,
|
|
21
|
+
attempt: int,
|
|
22
|
+
heartbeat_seconds: float,
|
|
23
|
+
) -> None:
|
|
24
|
+
if heartbeat_seconds <= 0:
|
|
25
|
+
raise ValueError("heartbeat_seconds must be positive")
|
|
26
|
+
self._runtime_store = runtime_store
|
|
27
|
+
self._invocation_id = invocation_id
|
|
28
|
+
self._attempt = attempt
|
|
29
|
+
self._heartbeat_seconds = heartbeat_seconds
|
|
30
|
+
self._task: asyncio.Task[None] | None = None
|
|
31
|
+
|
|
32
|
+
async def __aenter__(self) -> Heartbeat:
|
|
33
|
+
self._task = asyncio.create_task(
|
|
34
|
+
self._run(),
|
|
35
|
+
name=f"durable-heartbeat-{self._invocation_id}-{self._attempt}",
|
|
36
|
+
)
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
async def __aexit__(self, exc_type, exc, traceback) -> None:
|
|
40
|
+
if self._task is None:
|
|
41
|
+
return
|
|
42
|
+
self._task.cancel()
|
|
43
|
+
await asyncio.gather(self._task, return_exceptions=True)
|
|
44
|
+
self._task = None
|
|
45
|
+
|
|
46
|
+
async def _run(self) -> None:
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
owns_attempt = await self._runtime_store.heartbeat(
|
|
50
|
+
self._invocation_id,
|
|
51
|
+
self._attempt,
|
|
52
|
+
)
|
|
53
|
+
except Exception:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"Durable heartbeat failed: %s attempt=%d",
|
|
56
|
+
self._invocation_id,
|
|
57
|
+
self._attempt,
|
|
58
|
+
exc_info=True,
|
|
59
|
+
)
|
|
60
|
+
await asyncio.sleep(self._heartbeat_seconds)
|
|
61
|
+
continue
|
|
62
|
+
if not owns_attempt:
|
|
63
|
+
return
|
|
64
|
+
await asyncio.sleep(self._heartbeat_seconds)
|