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,93 @@
|
|
|
1
|
+
"""OpenAI Agents SDK adapter for running an agent on Databricks (installed via ``databricks-agentbricks[openai]``).
|
|
2
|
+
|
|
3
|
+
Composable pieces you drop into an existing OpenAI Agents SDK agent — a session store, MCP servers
|
|
4
|
+
declared in ``agent.toml``, long-term memory tools, and MLflow tracing. Each maps onto a slot the
|
|
5
|
+
Agents SDK already has, so migrating an existing agent is a graft, not a rewrite::
|
|
6
|
+
|
|
7
|
+
from databricks_agentkit.openai import (
|
|
8
|
+
session_store,
|
|
9
|
+
mcp_servers,
|
|
10
|
+
memory_tools,
|
|
11
|
+
configure_tracing,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
configure_tracing()
|
|
15
|
+
agent = Agent(
|
|
16
|
+
name="Agent",
|
|
17
|
+
model="databricks-gpt-5-2",
|
|
18
|
+
tools=[*your_tools, *memory_tools()],
|
|
19
|
+
mcp_servers=await mcp_servers(), # your agent.toml servers + any you pass
|
|
20
|
+
)
|
|
21
|
+
result = await Runner.run(agent, messages, session=session_store(session_id))
|
|
22
|
+
|
|
23
|
+
These need the agent stack (openai-agents, databricks-openai, mlflow), so they sit behind the
|
|
24
|
+
``[openai]`` extra to keep a plain ``databricks-agentbricks`` installation independent of agent frameworks.
|
|
25
|
+
|
|
26
|
+
``__all__`` is the curated surface. Other entry points (``DatabricksSessionStore``) are reachable by
|
|
27
|
+
their submodule paths but not re-exported here.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from databricks_agentkit.openai.genie import genie_tools
|
|
34
|
+
from databricks_agentkit.openai.mcp import mcp_servers
|
|
35
|
+
from databricks_agentkit.openai.memory import memory_tools
|
|
36
|
+
from databricks_agentkit.openai.sessions import session_store
|
|
37
|
+
from databricks_agentkit.runtime import (
|
|
38
|
+
start_trace,
|
|
39
|
+
workspace_client,
|
|
40
|
+
workspace_headers,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def configure_tracing() -> None:
|
|
45
|
+
"""Enable MLflow tracing with OpenAI autologging. Call once at startup.
|
|
46
|
+
|
|
47
|
+
Safe to call unconditionally — tracing turns on only when the MLflow destination and experiment
|
|
48
|
+
are configured in the environment (see :func:`databricks_agentkit.runtime.configure_tracing`).
|
|
49
|
+
"""
|
|
50
|
+
import mlflow
|
|
51
|
+
|
|
52
|
+
from databricks_agentkit.runtime import configure_tracing as _configure_tracing
|
|
53
|
+
|
|
54
|
+
_configure_tracing(autolog=mlflow.openai.autolog)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"genie_tools",
|
|
59
|
+
# MCP servers from agent.toml (plus any you pass) — hand them to Agent(mcp_servers=...).
|
|
60
|
+
"mcp_servers",
|
|
61
|
+
# Long-term memory tools (opt-in via AGENT_MEMORY_STORE) — add to your tool list.
|
|
62
|
+
"memory_tools",
|
|
63
|
+
# Session persistence — pass session_store(session_id) to Runner.run(session=...).
|
|
64
|
+
"session_store",
|
|
65
|
+
# MLflow tracing (OpenAI autolog bound in) — call configure_tracing() once at startup.
|
|
66
|
+
"configure_tracing",
|
|
67
|
+
# Wrap each invocation in start_trace() so a trace is recorded (pass session_id= to tag it).
|
|
68
|
+
"start_trace",
|
|
69
|
+
# Workspace SDK client construction.
|
|
70
|
+
"workspace_client",
|
|
71
|
+
"workspace_headers",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Re-exports resolved lazily (PEP 562) so importing one submodule (e.g. ``.mcp``) does not eagerly
|
|
75
|
+
# pull in the others' dependencies. ``configure_tracing`` is defined above (binds OpenAI autolog).
|
|
76
|
+
_MODULE_BY_NAME = {
|
|
77
|
+
"genie_tools": "databricks_agentkit.openai.genie",
|
|
78
|
+
"mcp_servers": "databricks_agentkit.openai.mcp",
|
|
79
|
+
"memory_tools": "databricks_agentkit.openai.memory",
|
|
80
|
+
"session_store": "databricks_agentkit.openai.sessions",
|
|
81
|
+
"start_trace": "databricks_agentkit.runtime",
|
|
82
|
+
"workspace_client": "databricks_agentkit.runtime",
|
|
83
|
+
"workspace_headers": "databricks_agentkit.runtime",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def __getattr__(name: str) -> object:
|
|
88
|
+
module = _MODULE_BY_NAME.get(name)
|
|
89
|
+
if module is None:
|
|
90
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
91
|
+
import importlib
|
|
92
|
+
|
|
93
|
+
return getattr(importlib.import_module(module), name)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Native Genie Agent tools for OpenAI Agents, configured through ``agent.toml``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from agents import FunctionTool, function_tool
|
|
10
|
+
|
|
11
|
+
from databricks_agentkit.runtime.auth import AuthError
|
|
12
|
+
from databricks_agentkit.runtime.genie import GenieAgent
|
|
13
|
+
from databricks_agentkit.runtime.tool_manifest import load_tools
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from databricks.sdk import WorkspaceClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def genie_tools(
|
|
20
|
+
*, workspace_client_for: Callable[[str], WorkspaceClient] | None = None
|
|
21
|
+
) -> list[FunctionTool]:
|
|
22
|
+
"""Build ask, poll and query-result tools for each configured Genie Agent.
|
|
23
|
+
|
|
24
|
+
Space IDs are bound from the manifest, not exposed as model arguments. Construction performs
|
|
25
|
+
no authentication or network requests; an agent with no Genie Agent bindings gets no tools.
|
|
26
|
+
"""
|
|
27
|
+
tools = []
|
|
28
|
+
for record in load_tools(expected_framework="openai"):
|
|
29
|
+
if record.kind != "genie_agent":
|
|
30
|
+
continue
|
|
31
|
+
if (
|
|
32
|
+
record.auth == "user"
|
|
33
|
+
and workspace_client_for is None
|
|
34
|
+
and os.getenv("DATABRICKS_APP_NAME")
|
|
35
|
+
):
|
|
36
|
+
raise AuthError(
|
|
37
|
+
"MCP_USER_AUTHORIZATION_MISSING",
|
|
38
|
+
"This deployed Genie integration requires request-user authorization.",
|
|
39
|
+
integration_id=record.id,
|
|
40
|
+
)
|
|
41
|
+
agent = GenieAgent(
|
|
42
|
+
record.space_id or "",
|
|
43
|
+
auth=record.auth or "app",
|
|
44
|
+
workspace_client_for=workspace_client_for,
|
|
45
|
+
)
|
|
46
|
+
tools.extend(
|
|
47
|
+
function_tool(method, name_override=f"{record.id}_{method.__name__}")
|
|
48
|
+
for method in (agent.ask, agent.poll, agent.query_result)
|
|
49
|
+
)
|
|
50
|
+
return tools
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Build MCP servers for the agent from the ones declared in ``agent.toml`` (plus any the agent adds).
|
|
2
|
+
|
|
3
|
+
``mcp_servers()`` is the entry point: it reads the MCP servers declared in ``agent.toml``
|
|
4
|
+
(sandbox/mcp + uc_function) and returns Agents SDK ``McpServer`` objects, with sandbox downscoping
|
|
5
|
+
applied. Hand them to ``Agent(mcp_servers=...)``; the Agents SDK connects and lists their tools
|
|
6
|
+
lazily inside ``Runner.run``. An agent with its own hand-built servers passes them as
|
|
7
|
+
``extra_servers``; leaving ``agent.toml`` empty simply yields no declared servers. Typical use::
|
|
8
|
+
|
|
9
|
+
servers = await mcp_servers() # just the agent.toml servers
|
|
10
|
+
servers = await mcp_servers(build_mcp_servers()) # agent.toml servers + the agent's own
|
|
11
|
+
|
|
12
|
+
Unlike a fetch-once tool list, these are connection objects: open them for the life of the request
|
|
13
|
+
(e.g. via ``AsyncExitStack``), because the SDK lists each server's tools only when the run needs them.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from databricks_openai.agents import McpServer
|
|
21
|
+
|
|
22
|
+
from databricks_agentkit.runtime import mcp_auth
|
|
23
|
+
from databricks_agentkit.runtime.auth import AuthError
|
|
24
|
+
from databricks_agentkit.runtime.tool_manifest import (
|
|
25
|
+
ToolRecord,
|
|
26
|
+
downscope_wire,
|
|
27
|
+
load_tools,
|
|
28
|
+
)
|
|
29
|
+
from databricks_agentkit.runtime.workspace import workspace_client, workspace_headers
|
|
30
|
+
|
|
31
|
+
_FRAMEWORK = "openai"
|
|
32
|
+
_auth_error = mcp_auth.mcp_auth_error
|
|
33
|
+
_tool_error = mcp_auth.mcp_tool_error
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ConfiguredMcpServer(McpServer):
|
|
37
|
+
"""Keep request-user failures out of SDK best-effort discovery and tool results."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *args: Any, request_user: bool, **kwargs: Any) -> None:
|
|
40
|
+
self._agentbricks_request_user = request_user
|
|
41
|
+
if request_user:
|
|
42
|
+
kwargs["failure_error_function"] = self._raise_tool_error
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
def _raise_tool_error(self, context: Any, error: Exception) -> str:
|
|
46
|
+
raise _auth_error(error, self.name) or AuthError(
|
|
47
|
+
"MCP_TOOL_FAILED", "The configured MCP tool failed.", 502, self.name
|
|
48
|
+
) from None
|
|
49
|
+
|
|
50
|
+
async def connect(self):
|
|
51
|
+
if not self._agentbricks_request_user:
|
|
52
|
+
return await super().connect()
|
|
53
|
+
try:
|
|
54
|
+
return await super().connect()
|
|
55
|
+
except Exception as error:
|
|
56
|
+
raise _auth_error(error, self.name) or AuthError(
|
|
57
|
+
"MCP_TOOL_FAILED",
|
|
58
|
+
"Could not connect to the configured MCP service.",
|
|
59
|
+
502,
|
|
60
|
+
self.name,
|
|
61
|
+
) from None
|
|
62
|
+
|
|
63
|
+
async def list_tools(self, *args, **kwargs):
|
|
64
|
+
if not self._agentbricks_request_user:
|
|
65
|
+
return await super().list_tools(*args, **kwargs)
|
|
66
|
+
try:
|
|
67
|
+
return await super().list_tools(*args, **kwargs)
|
|
68
|
+
except Exception as error:
|
|
69
|
+
raise _auth_error(error, self.name) or AuthError(
|
|
70
|
+
"MCP_TOOL_FAILED", "Could not discover configured MCP tools.", 502, self.name
|
|
71
|
+
) from None
|
|
72
|
+
|
|
73
|
+
async def call_tool(self, tool_name, arguments, **kwargs):
|
|
74
|
+
if not self._agentbricks_request_user:
|
|
75
|
+
return await super().call_tool(tool_name, arguments, **kwargs)
|
|
76
|
+
try:
|
|
77
|
+
call = getattr(McpServer.call_tool, "__wrapped__", McpServer.call_tool)
|
|
78
|
+
result = await call(self, tool_name, arguments, **kwargs)
|
|
79
|
+
except Exception as error:
|
|
80
|
+
raise _auth_error(error, self.name) or AuthError(
|
|
81
|
+
"MCP_TOOL_FAILED", "The configured MCP tool failed.", 502, self.name
|
|
82
|
+
) from None
|
|
83
|
+
if getattr(result, "isError", False) and (error := _tool_error(result, self.name)):
|
|
84
|
+
raise error
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _DownscopedMcpServer(_ConfiguredMcpServer):
|
|
89
|
+
"""An ``McpServer`` that injects a sandbox downscope into every ``call_tool``.
|
|
90
|
+
|
|
91
|
+
The Databricks sandbox MCP applies the downscope from the call's ``_meta``; the Agents SDK does
|
|
92
|
+
not surface a per-call hook, so bind the manifest's downscope to the server and add it on each
|
|
93
|
+
invocation. Only sandbox bindings need this — plain MCP / UC-function servers use the base class.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, *args: Any, downscope: dict[str, Any], **kwargs: Any) -> None:
|
|
97
|
+
super().__init__(*args, **kwargs)
|
|
98
|
+
self._downscope = downscope
|
|
99
|
+
|
|
100
|
+
async def call_tool(self, tool_name, arguments, **kwargs):
|
|
101
|
+
meta = {**(kwargs.pop("meta", None) or {}), "downscope": self._downscope}
|
|
102
|
+
return await super().call_tool(tool_name, arguments, meta=meta, **kwargs)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _server_from_tool(
|
|
106
|
+
tool: ToolRecord,
|
|
107
|
+
*,
|
|
108
|
+
workspace_client_for: mcp_auth.WorkspaceClientResolver | None = None,
|
|
109
|
+
) -> McpServer | None:
|
|
110
|
+
if tool.kind not in {"sandbox", "mcp", "uc_function", "genie_one"}:
|
|
111
|
+
return None
|
|
112
|
+
mode = tool.auth or "app"
|
|
113
|
+
client = mcp_auth.resolve_mcp_workspace_client(
|
|
114
|
+
mode, tool.id, workspace_client_for, workspace_client
|
|
115
|
+
)
|
|
116
|
+
host = client.config.host.rstrip("/")
|
|
117
|
+
if tool.kind in {"sandbox", "mcp", "genie_one"}:
|
|
118
|
+
url = (
|
|
119
|
+
f"{host}/api/2.0/mcp/genie"
|
|
120
|
+
if tool.kind == "genie_one"
|
|
121
|
+
else f"{host}/ai-gateway/mcp-services/{tool.service}"
|
|
122
|
+
)
|
|
123
|
+
if tool.kind == "sandbox":
|
|
124
|
+
return _DownscopedMcpServer(
|
|
125
|
+
url=url,
|
|
126
|
+
name=tool.id,
|
|
127
|
+
workspace_client=client,
|
|
128
|
+
timeout=120.0,
|
|
129
|
+
downscope=downscope_wire(tool),
|
|
130
|
+
request_user=mode == "user",
|
|
131
|
+
)
|
|
132
|
+
if tool.kind == "genie_one":
|
|
133
|
+
return _ConfiguredMcpServer(
|
|
134
|
+
name=tool.id,
|
|
135
|
+
workspace_client=client,
|
|
136
|
+
timeout=120.0,
|
|
137
|
+
params={"url": url, "headers": workspace_headers()},
|
|
138
|
+
request_user=mode == "user",
|
|
139
|
+
)
|
|
140
|
+
return _ConfiguredMcpServer(
|
|
141
|
+
url=url,
|
|
142
|
+
name=tool.id,
|
|
143
|
+
workspace_client=client,
|
|
144
|
+
timeout=120.0,
|
|
145
|
+
request_user=mode == "user",
|
|
146
|
+
)
|
|
147
|
+
if tool.kind == "uc_function":
|
|
148
|
+
catalog, schema, function_name = (tool.function or "").split(".")
|
|
149
|
+
return _ConfiguredMcpServer.from_uc_function(
|
|
150
|
+
catalog=catalog,
|
|
151
|
+
schema=schema,
|
|
152
|
+
function_name=function_name,
|
|
153
|
+
name=tool.id,
|
|
154
|
+
workspace_client=client,
|
|
155
|
+
timeout=120.0,
|
|
156
|
+
request_user=mode == "user",
|
|
157
|
+
)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _declared_servers(
|
|
162
|
+
*,
|
|
163
|
+
workspace_client_for: mcp_auth.WorkspaceClientResolver | None = None,
|
|
164
|
+
) -> list[McpServer]:
|
|
165
|
+
"""The MCP servers declared in the agent's ``agent.toml`` (may be empty)."""
|
|
166
|
+
tools = load_tools(expected_framework=_FRAMEWORK)
|
|
167
|
+
return [
|
|
168
|
+
server
|
|
169
|
+
for tool in tools
|
|
170
|
+
if (server := _server_from_tool(tool, workspace_client_for=workspace_client_for))
|
|
171
|
+
is not None
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def mcp_servers(
|
|
176
|
+
extra_servers: list[McpServer] | None = None,
|
|
177
|
+
*,
|
|
178
|
+
workspace_client_for: mcp_auth.WorkspaceClientResolver | None = None,
|
|
179
|
+
) -> list[McpServer]:
|
|
180
|
+
"""Build the agent's configured servers with request identity and sandbox downscoping.
|
|
181
|
+
|
|
182
|
+
Includes the MCP servers declared in ``agent.toml``; pass ``extra_servers`` to add servers the
|
|
183
|
+
agent builds itself. Request-user failures propagate. App and extra servers retain the SDK's
|
|
184
|
+
best-effort behavior; the request resolver is never forwarded to extra servers.
|
|
185
|
+
"""
|
|
186
|
+
return [
|
|
187
|
+
*_declared_servers(workspace_client_for=workspace_client_for),
|
|
188
|
+
*(extra_servers or []),
|
|
189
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Long-term memory tools — opt-in, gated on ``AGENT_MEMORY_STORE``.
|
|
2
|
+
|
|
3
|
+
Unlike the session store (short-term transcript for one conversation), long-term memory is exposed
|
|
4
|
+
to the model as two tools — ``remember`` and ``recall`` — over the Databricks managed memory store's
|
|
5
|
+
``2.0/agents`` entries API. Facts persist across conversations.
|
|
6
|
+
|
|
7
|
+
``memory_tools(actor)`` returns the tools when ``AGENT_MEMORY_STORE`` is set, else an empty list, so
|
|
8
|
+
the model never sees them when memory is unconfigured. The agent composes them into its tool list.
|
|
9
|
+
This is a stand-in for a future ``databricks-openai`` memory helper — when the SDK provides one, swap
|
|
10
|
+
the import in ``agent.py`` and delete this file.
|
|
11
|
+
|
|
12
|
+
Memory entries are per-actor. ``actor`` is the identity whose memory these tools read and write — the
|
|
13
|
+
caller (the agent server) supplies it, typically the signed-in user, so each user gets their own
|
|
14
|
+
long-term memory. It is **closed over**, not a tool argument: the model only ever sees ``remember``'s
|
|
15
|
+
and ``recall``'s own parameters and cannot set or spoof the actor. Pass a fixed value for one shared
|
|
16
|
+
memory (e.g. a team knowledge base).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from agents import FunctionTool, function_tool
|
|
20
|
+
|
|
21
|
+
from databricks_agentkit.runtime.workspace import workspace_client
|
|
22
|
+
|
|
23
|
+
_AGENTS_API = "/api/2.0/agents"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _api():
|
|
27
|
+
# Build the client lazily (needs workspace auth) so importing this module stays cheap.
|
|
28
|
+
return workspace_client().api_client
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def memory_tools(actor: str, store: str | None = None) -> list[FunctionTool]:
|
|
32
|
+
"""The long-term-memory tools for ``actor`` when a memory store is configured, else none.
|
|
33
|
+
|
|
34
|
+
The store resolves ``store`` arg → ``AGENT_MEMORY_STORE`` env → the ``[memory_store]`` binding in
|
|
35
|
+
agent.toml (`ab memory bind`) → none (no tools). ``actor`` partitions the store; it is captured
|
|
36
|
+
in the tools' closures (not exposed to the model). Call this per request with the identity whose
|
|
37
|
+
memory to use (e.g. the signed-in user).
|
|
38
|
+
"""
|
|
39
|
+
from databricks_agentkit.runtime.tool_manifest import resolve_memory_store
|
|
40
|
+
|
|
41
|
+
store = resolve_memory_store(store)
|
|
42
|
+
if not store:
|
|
43
|
+
return []
|
|
44
|
+
store_path = f"{_AGENTS_API}/memory-stores/{store}"
|
|
45
|
+
|
|
46
|
+
@function_tool
|
|
47
|
+
def remember(fact: str, topic: str) -> str:
|
|
48
|
+
"""Persist a durable fact about the user in long-term memory."""
|
|
49
|
+
_api().do(
|
|
50
|
+
"POST",
|
|
51
|
+
f"{store_path}/entries",
|
|
52
|
+
body={"actor_id": actor, "path": f"/{topic}/{fact[:8]}.md", "content": fact},
|
|
53
|
+
)
|
|
54
|
+
return "stored"
|
|
55
|
+
|
|
56
|
+
@function_tool
|
|
57
|
+
def recall(query: str) -> str:
|
|
58
|
+
"""Search the user's long-term memory for facts relevant to the query."""
|
|
59
|
+
data = _api().do(
|
|
60
|
+
"POST",
|
|
61
|
+
f"{store_path}/entries:search",
|
|
62
|
+
body={"actor_id": actor, "query": query, "limit": 5},
|
|
63
|
+
)
|
|
64
|
+
entries = data.get("managed_memory_entries") or []
|
|
65
|
+
return "\n".join(f"- {e.get('content')}" for e in entries) or "No relevant memories."
|
|
66
|
+
|
|
67
|
+
return [remember, recall]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Conversation session store for the agent.
|
|
2
|
+
|
|
3
|
+
The OpenAI Agents SDK persists conversation state through a **Session** — an object with
|
|
4
|
+
``get_items``/``add_items``/``pop_item``/``clear_session`` over a list of Responses input items.
|
|
5
|
+
``Runner.run(..., session=session_store(session_id))`` reads prior turns from it and appends the new
|
|
6
|
+
ones. ``session_store(session_id)`` returns the Session for a given conversation.
|
|
7
|
+
|
|
8
|
+
Default (no config): an in-process ``SQLiteSession`` backed by ``:memory:``, cached per session id —
|
|
9
|
+
multi-turn history is preserved within a single running process, no database. It does NOT survive
|
|
10
|
+
restarts or span replicas.
|
|
11
|
+
|
|
12
|
+
Durable (``AGENT_SESSION_STORE`` set): a ``DatabricksSessionStore``. Instead of a database the app
|
|
13
|
+
connects to directly, it stores each Responses item as one **session item** through the managed
|
|
14
|
+
Session Store REST API (over RPCs only — no Lakebase/Postgres connection), so the transcript is
|
|
15
|
+
durable across restarts and replicas. Setting the env var is the only change; the agent code is
|
|
16
|
+
identical.
|
|
17
|
+
|
|
18
|
+
Note — unlike the LangGraph checkpointer, a Session persists only the conversation transcript, not
|
|
19
|
+
paused human-in-the-loop run state. Durable HITL would need the paused ``RunState`` stashed
|
|
20
|
+
separately; ``agent.py`` keeps pending runs in-process only (see ``PendingRuns``).
|
|
21
|
+
|
|
22
|
+
The durable store uses a vendored REST client (``session_store_client.py``) so the template needs no
|
|
23
|
+
unpublished dependency; swap it for the published package when it lands.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
from agents import SQLiteSession, TResponseInputItem
|
|
31
|
+
from agents.memory import SessionABC
|
|
32
|
+
|
|
33
|
+
from databricks_agentkit.runtime.session_store_client import Session as _StoreSession
|
|
34
|
+
from databricks_agentkit.runtime.session_store_client import SessionStoreClient
|
|
35
|
+
|
|
36
|
+
# Fetch items oldest-first so the transcript replays in write order.
|
|
37
|
+
_ORDER_BY = "create_time asc"
|
|
38
|
+
|
|
39
|
+
# One in-process Session per session id, built lazily and shared — that's what makes multi-turn work
|
|
40
|
+
# in-process (the durable store needs no cache; each call resolves the same REST-backed session).
|
|
41
|
+
_local_sessions: dict[str, SQLiteSession] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def session_store(
|
|
45
|
+
session_id: str, actor: str | None = None, store: str | None = None
|
|
46
|
+
) -> SessionABC:
|
|
47
|
+
"""The Session the agent persists conversation state to for ``session_id``.
|
|
48
|
+
|
|
49
|
+
In-memory ``SQLiteSession`` by default; a durable ``DatabricksSessionStore`` when a managed store
|
|
50
|
+
is configured. The store name resolves ``store`` arg → ``AGENT_SESSION_STORE`` env → the
|
|
51
|
+
``[session_store]`` binding in agent.toml (`ab sessions bind`) → none. ``actor`` partitions the
|
|
52
|
+
durable store — the caller supplies it (typically the signed-in user), so each user's transcripts
|
|
53
|
+
stay separate; it defaults to ``session_id`` and is ignored by the in-memory store.
|
|
54
|
+
"""
|
|
55
|
+
from databricks_agentkit.runtime.tool_manifest import resolve_session_store
|
|
56
|
+
|
|
57
|
+
store = resolve_session_store(store)
|
|
58
|
+
if store:
|
|
59
|
+
return DatabricksSessionStore(session_id, store, actor_id=actor or session_id)
|
|
60
|
+
cached = _local_sessions.get(session_id)
|
|
61
|
+
if cached is None:
|
|
62
|
+
cached = SQLiteSession(session_id) # ":memory:" — process-local, non-durable
|
|
63
|
+
_local_sessions[session_id] = cached
|
|
64
|
+
return cached
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DatabricksSessionStore(SessionABC):
|
|
68
|
+
"""An Agents SDK ``Session`` over the Databricks Session Store REST API.
|
|
69
|
+
|
|
70
|
+
Each Responses input item is stored as one Session Store item whose ``data`` is the item dict
|
|
71
|
+
(opaque JSON to the store). ``get_items`` replays them in write order; ``pop_item`` and
|
|
72
|
+
``clear_session`` are supported for the SDK's retry rollback path. The session is created on first
|
|
73
|
+
use (get-or-create), keyed by the conversation's ``session_id`` under the configured actor.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
session_id: str,
|
|
79
|
+
session_store_name: str,
|
|
80
|
+
*,
|
|
81
|
+
actor_id: str,
|
|
82
|
+
client: Optional[SessionStoreClient] = None,
|
|
83
|
+
workspace_client: Optional[Any] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
if not session_store_name:
|
|
86
|
+
raise ValueError("session_store_name is required")
|
|
87
|
+
self.session_id = session_id
|
|
88
|
+
self._actor_id = actor_id
|
|
89
|
+
self._client = (client or SessionStoreClient(workspace_client)).set_session_store(
|
|
90
|
+
session_store_name
|
|
91
|
+
)
|
|
92
|
+
self._session: _StoreSession | None = None
|
|
93
|
+
|
|
94
|
+
async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
|
|
95
|
+
items = await _run_sync(self._read_items)
|
|
96
|
+
return items[-limit:] if limit is not None else items
|
|
97
|
+
|
|
98
|
+
async def add_items(self, items: list[TResponseInputItem]) -> None:
|
|
99
|
+
if not items:
|
|
100
|
+
return
|
|
101
|
+
await _run_sync(lambda: self._client.append_items(self._resolve(), items=list(items)))
|
|
102
|
+
|
|
103
|
+
async def pop_item(self) -> TResponseInputItem | None:
|
|
104
|
+
# The REST store has no pop; re-list, clear, and re-append all but the last. Only used on the
|
|
105
|
+
# SDK's best-effort retry rollback, so the extra round-trip is acceptable and rare.
|
|
106
|
+
def _pop() -> TResponseInputItem | None:
|
|
107
|
+
items = self._read_items()
|
|
108
|
+
if not items:
|
|
109
|
+
return None
|
|
110
|
+
session = self._resolve()
|
|
111
|
+
self._client.clear_items(session)
|
|
112
|
+
if items[:-1]:
|
|
113
|
+
self._client.append_items(session, items=items[:-1])
|
|
114
|
+
return items[-1]
|
|
115
|
+
|
|
116
|
+
return await _run_sync(_pop)
|
|
117
|
+
|
|
118
|
+
async def clear_session(self) -> None:
|
|
119
|
+
await _run_sync(lambda: self._client.clear_items(self._resolve()))
|
|
120
|
+
|
|
121
|
+
# ----- internals ----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _resolve(self) -> _StoreSession:
|
|
124
|
+
if self._session is not None:
|
|
125
|
+
return self._session
|
|
126
|
+
try:
|
|
127
|
+
self._session = self._client.get_session(session_id=self.session_id)
|
|
128
|
+
except tuple(_not_found_errors()) as _: # type: ignore[misc]
|
|
129
|
+
self._session = self._client.create_session(
|
|
130
|
+
actor_id=self._actor_id,
|
|
131
|
+
session_id=self.session_id,
|
|
132
|
+
metadata={"client": "agentbricks-openai-agent"},
|
|
133
|
+
)
|
|
134
|
+
return self._session
|
|
135
|
+
|
|
136
|
+
def _read_items(self) -> list[TResponseInputItem]:
|
|
137
|
+
return [item.data for item in self._client.list_items(self._resolve(), order_by=_ORDER_BY)]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _not_found_errors() -> tuple[type, ...]:
|
|
141
|
+
"""Exception types that mean 'session does not exist yet'."""
|
|
142
|
+
try:
|
|
143
|
+
from databricks.sdk.errors import NotFound
|
|
144
|
+
|
|
145
|
+
return (NotFound,)
|
|
146
|
+
except ImportError: # pragma: no cover - SDK always present in practice
|
|
147
|
+
return (_SessionNotFound,)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _SessionNotFound(Exception):
|
|
151
|
+
"""Fallback 'not found' used only when databricks.sdk is unavailable."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _run_sync(fn):
|
|
155
|
+
import asyncio
|
|
156
|
+
|
|
157
|
+
return await asyncio.get_running_loop().run_in_executor(None, fn)
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Managed runtime internals
|
|
2
|
+
|
|
3
|
+
The managed runtime owns the HTTP lifecycle for one agent invocation: accepting an idempotent request,
|
|
4
|
+
scheduling work, storing status and events, returning results, and replaying events to a reconnecting
|
|
5
|
+
client. The public HTTP resource is an **invocation**; its UUID is also the idempotency key.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Client
|
|
9
|
+
│ POST /api/invocations, GET status/events
|
|
10
|
+
▼
|
|
11
|
+
DurableAgentServer
|
|
12
|
+
HTTP adapter and @app.invoke / @app.recover hooks
|
|
13
|
+
▼
|
|
14
|
+
Runtime
|
|
15
|
+
submit, wait/poll, and event replay
|
|
16
|
+
├── RuntimeStore
|
|
17
|
+
│ accept/get/claim/complete/fail/events
|
|
18
|
+
│ ├── InMemoryRuntimeStore
|
|
19
|
+
│ └── LakebaseDurableRuntimeStore
|
|
20
|
+
└── InvocationExecutor
|
|
21
|
+
├── LocalInvocationExecutor
|
|
22
|
+
└── DurableInvocationExecutor
|
|
23
|
+
├── Heartbeat
|
|
24
|
+
└── RecoveryScheduler
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Lifecycle
|
|
28
|
+
|
|
29
|
+
`DurableAgentServer` uses `Runtime.from_environment(...)` for managed processes and
|
|
30
|
+
`Runtime.from_store(...)` when a caller supplies a store explicitly. The selected Runtime Store
|
|
31
|
+
determines the execution mode:
|
|
32
|
+
|
|
33
|
+
- `InMemoryRuntimeStore` uses `Runtime.local()` and `LocalInvocationExecutor`. It supports the same
|
|
34
|
+
foreground, background, polling, streaming, and replay APIs, but state ends with the process.
|
|
35
|
+
- `LakebaseDurableRuntimeStore` implements `DurableRuntimeStore`, so `Runtime.durable()` uses
|
|
36
|
+
`DurableInvocationExecutor`. It heartbeats the active attempt and can schedule recovery after a
|
|
37
|
+
worker stops heartbeating.
|
|
38
|
+
|
|
39
|
+
`RuntimeStore` deliberately has no heartbeat operation or heartbeat state. Those lease details are
|
|
40
|
+
owned by `DurableRuntimeStore` and its Lakebase implementation.
|
|
41
|
+
|
|
42
|
+
`RuntimeStore.accept(invocation_id, request)` defines idempotency. The same ID and request returns
|
|
43
|
+
the existing invocation; the same ID and different request raises `InvocationConflictError`.
|
|
44
|
+
|
|
45
|
+
The durable executor always scans for persisted `QUEUED` invocations, including after process
|
|
46
|
+
restart, and starts their first attempt through `@app.invoke`. Reads also schedule queued work as a
|
|
47
|
+
safety net when a request moves between replicas. `@app.recover` is optional: registering it enables
|
|
48
|
+
a separate scanner to replace stale `ACTIVE` attempts. Recovery is at least once, so agent side
|
|
49
|
+
effects must be idempotent.
|
|
50
|
+
|
|
51
|
+
## Code map
|
|
52
|
+
|
|
53
|
+
| Path | Responsibility |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `app.py` | FastAPI adapter and agent-hook registration |
|
|
56
|
+
| `runtime.py` | Runtime factories plus submit, polling, and replay facade |
|
|
57
|
+
| `store.py` | Shared Runtime Store contract and in-memory implementation |
|
|
58
|
+
| `execution.py` | Common attempt execution and process-local scheduler |
|
|
59
|
+
| `types.py` | Invocation records, contexts, errors, and JSON types |
|
|
60
|
+
| `durability/store.py` | Durable Store extension for leases and recovery claims |
|
|
61
|
+
| `durability/execution.py` | Durable scheduling and heartbeat-wrapped attempts |
|
|
62
|
+
| `durability/heartbeat.py` | Lease refresh lifecycle |
|
|
63
|
+
| `durability/recovery.py` | Stale-attempt scanning and recovery scheduling |
|
|
64
|
+
| `durability/lakebase_runtime_store.py` | Lakebase Runtime Store implementation |
|