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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. databricks_agentkit/timefmt.py +85 -0
@@ -0,0 +1,184 @@
1
+ # Managed agent deployment and runtime
2
+
3
+ Agent Bricks CLI takes your agent code from a local project to a hosted endpoint on Databricks. Start with a
4
+ LangGraph or OpenAI Agents template, or bring an existing agent.
5
+
6
+ - **Deployment:** Scaffold a project, run it locally, and deploy it to Databricks Apps. The CLI
7
+ provisions the stores declared in your project, grants the app access, and configures tracing.
8
+ - **Runtime:** `DurableAgentServer` provides synchronous, streaming, and background execution, with persistent
9
+ results and automatic crash recovery on deployment. Request-user authentication is attached only
10
+ to the active first attempt and is never persisted.
11
+
12
+ ## Choose your server
13
+
14
+ **Managed server (`server = "agentbricks"`).** Register your agent with `DurableAgentServer` to use the managed
15
+ invocation API. This existing `server` value identifies the managed server in `agent.toml`. Deployed
16
+ managed servers receive a Lakebase-backed Runtime Store. Register a recovery handler so the runtime
17
+ can restart interrupted app-auth work and mark interrupted request-user work failed.
18
+ `DurableAgentServer` is a FastAPI application: you can add custom endpoints alongside the invocation API.
19
+
20
+ If any configured tool uses `auth = "user"`, trusted Databricks Apps ingress headers supply a
21
+ process-local credential for the first execution attempt. Synchronous, streaming, and background
22
+ requests continue through the normal Runtime queue and Runtime Store. The store contains token-free
23
+ request state, events, and results; it never contains the forwarded credential. A replacement
24
+ attempt after failure recovery stops before agent code runs because the original credential is no
25
+ longer available.
26
+
27
+ **Your own server (`server = "custom"`).** Keep your existing HTTP server, or scaffold a minimal
28
+ FastAPI server with `ab init --server custom`. You own the endpoints, request and response
29
+ formats, and execution behavior. `ab dev` and `ab deploy` still run and deploy the project;
30
+ the managed runtime does not provision a Runtime Store for it.
31
+
32
+ ## From a new agent to a deployed endpoint
33
+
34
+ ```bash
35
+ ab init my-agent --framework langgraph --server agentbricks --profile <profile>
36
+ cd my-agent
37
+ ab dev
38
+ # Stop the local server when ready to deploy.
39
+ ab --profile <profile> deploy my-agent
40
+ ```
41
+
42
+ Use `--framework openai` for OpenAI Agents. Managed-server templates include a chat UI and tests;
43
+ pass `--disable-chat-app` for an API-only project.
44
+
45
+ 1. **Initialize:** `ab init` generates the agent code and runtime adapter separately. It records
46
+ the server choice and default `my-agent-memory` / `my-agent-session` bindings in `agent.toml`.
47
+ 2. **Develop:** Edit your model, prompts, and tools in `agent/`. `ab dev` runs the project locally.
48
+ Synchronous, streaming, and background requests use the same Runtime for both authorization
49
+ policies.
50
+ 3. **Deploy:** `ab deploy` creates or reuses the declared Session and Memory Stores, grants the
51
+ app's service principal access, configures tracing, and deploys the app. For a managed server,
52
+ it also creates or reuses the deployment's Runtime Store.
53
+
54
+ The generated configuration starts with:
55
+
56
+ ```toml
57
+ schema_version = 1
58
+
59
+ [agent]
60
+ framework = "langgraph"
61
+ server = "agentbricks"
62
+
63
+ [memory_store]
64
+ name = "my-agent-memory"
65
+
66
+ [session_store]
67
+ name = "my-agent-session"
68
+
69
+ [tracing]
70
+ experiment_name = "/Shared/agentbricks_traces/my-agent"
71
+ ```
72
+
73
+ Override store names at initialization with `--memory-store` and `--session-store`, or later with
74
+ `ab memory bind <name>` and `ab sessions bind <name>`. Custom-server templates declare these
75
+ stores only when explicitly requested. Tracing is bound by experiment **name** (its presence turns
76
+ tracing on); rebind or clear it with `ab tracing bind --experiment-name <path>` / `ab tracing
77
+ unbind`.
78
+
79
+ Use `ab deployments list`, `get`, `logs`, `start`, `stop`, and `delete` to manage deployed apps.
80
+ See the [CLI documentation](../../../README.md#commands) for command options.
81
+
82
+ ## Connect your agent to the runtime
83
+
84
+ Keep framework code in `agent/` and HTTP setup and event translation in `runtime/`. The generated
85
+ [LangGraph](../../databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py) and
86
+ [OpenAI Agents](../../databricks_agentbricks/templates/agent-openai/runtime/adapter.py) adapters show how to
87
+ connect framework-native agent loops to the managed runtime.
88
+
89
+ - **`@app.invoke`:** Register an async handler that receives the request's `input` and an
90
+ `InvocationContext`. Return a JSON-serializable result.
91
+ - **`await context.emit(event)`:** Publish a JSON event from the handler or adapter. The Runtime
92
+ stores and delivers events through its streaming API.
93
+ - **`@app.recover`:** Register the handler the runtime calls for a replacement attempt after interrupted
94
+ execution. It receives the original input and a recovery context. Restore a framework checkpoint
95
+ from the Session Store, or replay the input if that is safe for your agent. Request-user recovery
96
+ stops with `MCP_USER_AUTH_RECOVERY_UNSUPPORTED` before this handler runs.
97
+
98
+ The client chooses `background` and `stream` on each request; separate agent handlers are not
99
+ needed. Registering a recovery hook enables automatic recovery when the Runtime uses a persistent
100
+ store.
101
+
102
+ For an existing agent, retain its framework code and add the runtime adapter and `DurableAgentServer`
103
+ entrypoint. Set `[agent].server = "agentbricks"` and have `app.yaml` start that entrypoint. Keep the
104
+ existing `agentbricks` value for projects that use the managed server. Changing the configuration field
105
+ alone does not convert a custom HTTP server into `DurableAgentServer`.
106
+
107
+ Newly generated runtime code imports from `databricks_agentkit`. The AgentKit client uses
108
+ `databricks_agentkit.AgentKitClient`.
109
+
110
+ ## Invoke, stream, and reconnect
111
+
112
+ An **invocation** is one managed agent run. The client-supplied UUID `id` acts as an idempotency key
113
+ while its Runtime record is retained. Request-user invocation IDs are internally namespaced by the
114
+ forwarded principal so users cannot collide with each other.
115
+
116
+ - **Synchronous:** Wait for the result in the POST response.
117
+ - **Streaming (`stream: true`):** Receive progress events as Server-Sent Events (SSE).
118
+ - **Background (`background: true`):** Return immediately with `202`, then poll for the result.
119
+ Add `stream: true` to include an events URL in the response.
120
+ - **Reconnect:** Read stored events with `GET .../events?after=<last-event-id>`.
121
+
122
+ These examples use an agent that returns `{"answer":"Hello"}` and emits `delta` events. The input,
123
+ output, and application event payloads are defined by your agent or framework adapter.
124
+
125
+ | API endpoint | Request | Response |
126
+ | --- | --- | --- |
127
+ | `POST /api/invocations` | `{"id":"550e8400-e29b-41d4-a716-446655440000","input":{"messages":[{"role":"user","content":"Hello"}]}}` | `200`<br>`{"id":"550e8400-e29b-41d4-a716-446655440000","status":"completed","output":{"answer":"Hello"}}` |
128
+ | `POST /api/invocations` | `{"id":"550e8400-e29b-41d4-a716-446655440000","input":{"messages":[{"role":"user","content":"Hello"}]},"stream":true}` | `200 text/event-stream`<br>`id: 1`<br>`event: run.started`<br>`data: {"type":"run.started"}`<br><br>`id: 2`<br>`event: delta`<br>`data: {"type":"delta","content":"Hello"}`<br><br>`id: 3`<br>`event: run.completed`<br>`data: {"type":"run.completed"}` |
129
+ | `POST /api/invocations` | `{"id":"550e8400-e29b-41d4-a716-446655440000","input":{"messages":[{"role":"user","content":"Hello"}]},"background":true}` | `202`<br>`{"id":"550e8400-e29b-41d4-a716-446655440000","status":"queued","status_url":"/api/invocations/550e8400-e29b-41d4-a716-446655440000"}` |
130
+ | `POST /api/invocations` | `{"id":"550e8400-e29b-41d4-a716-446655440000","input":{"messages":[{"role":"user","content":"Hello"}]},"background":true,"stream":true}` | `202`<br>`{"id":"550e8400-e29b-41d4-a716-446655440000","status":"queued","status_url":"/api/invocations/550e8400-e29b-41d4-a716-446655440000","events_url":"/api/invocations/550e8400-e29b-41d4-a716-446655440000/events"}` |
131
+ | `GET /api/invocations/550e8400-e29b-41d4-a716-446655440000` | — | `200`<br>`{"id":"550e8400-e29b-41d4-a716-446655440000","status":"completed","output":{"answer":"Hello"}}` |
132
+ | `GET /api/invocations/550e8400-e29b-41d4-a716-446655440000/events?after=1` | — | `200 text/event-stream`<br>`id: 2`<br>`event: delta`<br>`data: {"type":"delta","content":"Hello"}`<br><br>`id: 3`<br>`event: run.completed`<br>`data: {"type":"run.completed"}` |
133
+
134
+ ## Execution state and agent state
135
+
136
+ The state diagram below applies to both authorization policies. Request-user authentication is a
137
+ process-local input to the active first attempt; it is not part of the Runtime Store, Session Store,
138
+ or Memory Store.
139
+
140
+ ```mermaid
141
+ flowchart LR
142
+ CLIENT[Client] <-->|invoke, poll, stream, reconnect| RUNTIME["Managed Runtime / HTTP server"]
143
+ RUNTIME -->|invoke or recovery hook| AGENT["Agent loop"]
144
+ AGENT -->|stream events and result| RUNTIME
145
+ RUNTIME <--> RUNTIME_STORE[("Runtime Store<br/>requests, status, heartbeats,<br/>events, and results")]
146
+ AGENT <--> SESSION_STORE[("Session Store<br/>conversation state and checkpoints")]
147
+ ```
148
+
149
+ - **Runtime Store:** Requests, status, heartbeats, events, and results used by the runtime to manage
150
+ invocations, serve polling and reconnection, and detect interrupted work.
151
+ - **Session Store:** Conversation history and framework checkpoints used by the agent. A recovery
152
+ handler can use these checkpoints to resume the agent loop.
153
+ - **Memory Store:** Long-term facts and preferences reused across conversations.
154
+
155
+ ### Local development
156
+
157
+ `ab dev` supports the same invocation APIs with an **In-process Runtime Store**. Run state,
158
+ events, and results are lost when the serving process exits. Interrupted work is not automatically
159
+ restarted. Session and Memory Store persistence is separate from this local execution state.
160
+
161
+ ### Deployed execution
162
+
163
+ `ab deploy` provisions a dedicated PostgreSQL database for each deployment with `server = "agentbricks"` and
164
+ reuses it on redeployment. Results and events survive worker restarts, and any replica can serve
165
+ polling and stream-reconnection requests. With a recovery handler registered, the runtime detects stale
166
+ heartbeats and starts a replacement attempt on an available worker.
167
+
168
+ App-auth replacement attempts call the registered recovery handler. Request-user replacement
169
+ attempts fail with `MCP_USER_AUTH_RECOVERY_UNSUPPORTED` before agent code runs because the original
170
+ forwarded credential was intentionally not persisted.
171
+
172
+ Recovery is **at-least-once**: an interrupted attempt may already have performed external side
173
+ effects before its replacement starts. Make those operations idempotent. Request deduplication
174
+ does not guarantee that agent code or external side effects execute only once.
175
+
176
+ The managed deployment provisions the Runtime Store's database, schema, and access, so you do not
177
+ create or bind it separately. Session and Memory Stores are independently named resources that
178
+ can be shared intentionally between agents.
179
+
180
+ Changing the server type of an existing deployment is not supported. Scaffold a new project with
181
+ the desired `ab init --server` option and deploy it under a new name. `agent.toml` remains editable,
182
+ but changing its `server` field does not convert application code or clean up deployment resources.
183
+
184
+ For runtime contributors, see the [architecture and code map](ARCHITECTURE.md).
@@ -0,0 +1,69 @@
1
+ """Framework-neutral runtime helpers for an agent deployed on Databricks (via ``databricks-agentbricks``).
2
+
3
+ These have no agent-framework dependency — MLflow tracing setup and workspace-routed SDK client
4
+ construction — so they work regardless of which framework an agent is built with. Framework-specific
5
+ helpers (session-store checkpointer, MCP tools, memory tools) live in the per-framework adapter
6
+ package, e.g. :mod:`databricks_agentkit.langgraph`, which re-exports these for a single import point.
7
+
8
+ ``__all__`` is the supported surface. ``tool_manifest`` and ``session_store_client`` are internal and
9
+ reachable by their submodule paths but not re-exported here.
10
+
11
+ The re-exports below are resolved lazily (PEP 562) so importing a neutral submodule such as
12
+ ``databricks_agentkit.runtime.tool_manifest`` does not pull in the tracing module's ``mlflow`` dependency.
13
+ """
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from databricks_agentkit.runtime.app import AgentApp, DurableAgentServer
19
+ from databricks_agentkit.runtime.auth import (
20
+ AuthError,
21
+ InvocationAuthPolicy,
22
+ RequestAuthContext,
23
+ )
24
+ from databricks_agentkit.runtime.model_services import list_ai_gateway_model_services
25
+ from databricks_agentkit.runtime.tracing import configure_tracing, start_trace
26
+ from databricks_agentkit.runtime.types import InvocationContext
27
+ from databricks_agentkit.runtime.workspace import workspace_client, workspace_headers
28
+
29
+ __all__ = [
30
+ "DurableAgentServer",
31
+ "AgentApp",
32
+ "InvocationContext",
33
+ "AuthError",
34
+ "InvocationAuthPolicy",
35
+ "RequestAuthContext",
36
+ # MLflow tracing — call configure_tracing() once at startup (pass the framework's autolog, or use
37
+ # a framework adapter that binds it). Wrap each invocation in start_trace() so a trace is recorded
38
+ # (framework autolog only nests under an active trace); pass session_id= to group traces by session.
39
+ "configure_tracing",
40
+ "start_trace",
41
+ # Workspace SDK client construction (account-host / run-local routing handled).
42
+ "workspace_client",
43
+ "workspace_headers",
44
+ # Unity Catalog AI Gateway discovery (system.ai chat model services, for the demo UI's picker).
45
+ "list_ai_gateway_model_services",
46
+ ]
47
+
48
+ _MODULE_BY_NAME = {
49
+ "DurableAgentServer": "app",
50
+ "AgentApp": "app",
51
+ "InvocationContext": "types",
52
+ "AuthError": "auth",
53
+ "InvocationAuthPolicy": "auth",
54
+ "RequestAuthContext": "auth",
55
+ "configure_tracing": "tracing",
56
+ "start_trace": "tracing",
57
+ "workspace_client": "workspace",
58
+ "workspace_headers": "workspace",
59
+ "list_ai_gateway_model_services": "model_services",
60
+ }
61
+
62
+
63
+ def __getattr__(name: str) -> object:
64
+ module = _MODULE_BY_NAME.get(name)
65
+ if module is None:
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
67
+ import importlib
68
+
69
+ return getattr(importlib.import_module(f"{__name__}.{module}"), name)
@@ -0,0 +1,316 @@
1
+ """FastAPI adapter for the Agent Bricks agent runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import copy
7
+ import json
8
+ import logging
9
+ from collections.abc import AsyncIterator
10
+ from contextlib import asynccontextmanager
11
+ from uuid import UUID
12
+
13
+ from fastapi import FastAPI, HTTPException, Request, Response
14
+ from fastapi.responses import JSONResponse, StreamingResponse
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+ from pydantic import JsonValue as PydanticJsonValue
17
+
18
+ from databricks_agentkit.runtime.auth import AuthError, InvocationAuthPolicy, RequestAuthContext
19
+ from databricks_agentkit.runtime.runtime import Runtime
20
+ from databricks_agentkit.runtime.store import RuntimeStore
21
+ from databricks_agentkit.runtime.types import (
22
+ Invocation,
23
+ InvocationAttemptContext,
24
+ InvocationConflictError,
25
+ InvocationContext,
26
+ InvocationFailedError,
27
+ InvocationHook,
28
+ InvocationStatus,
29
+ JsonObject,
30
+ JsonValue,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ _ROUTING_COOKIE = "__Host-databricks-app-router"
36
+ _API_ROOT = "/api/invocations"
37
+
38
+
39
+ class _InvocationRequest(BaseModel):
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+ id: UUID
43
+ input: PydanticJsonValue = Field(default_factory=list)
44
+ background: bool = False
45
+ stream: bool = False
46
+
47
+
48
+ class DurableAgentServer(FastAPI):
49
+ """Expose agent handlers through the Agent Bricks invocation HTTP protocol.
50
+
51
+ ``ab dev`` selects a process-local Runtime Store. A deployed Agent Bricks server receives a
52
+ Lakebase-backed Runtime Store, which preserves invocation state and can recover stale work when
53
+ a handler is registered with :meth:`recover`.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ runtime_store: RuntimeStore | None = None,
60
+ auth_policy: InvocationAuthPolicy | None = None,
61
+ ) -> None:
62
+ self.auth_policy = (
63
+ auth_policy
64
+ if auth_policy is not None
65
+ else InvocationAuthPolicy.from_manifest(allow_missing=True)
66
+ )
67
+ self._invoke_hook: InvocationHook | None = None
68
+ self._recovery_hook: InvocationHook | None = None
69
+ self._request_auth: dict[str, RequestAuthContext] = {}
70
+ if runtime_store is None:
71
+ self._runtime = Runtime.from_environment(
72
+ self._execute,
73
+ recovery_enabled=lambda: self._recovery_hook is not None,
74
+ )
75
+ else:
76
+ self._runtime = Runtime.from_store(
77
+ self._execute,
78
+ runtime_store=runtime_store,
79
+ recovery_enabled=lambda: self._recovery_hook is not None,
80
+ )
81
+
82
+ @asynccontextmanager
83
+ async def lifespan(_: FastAPI):
84
+ if self._invoke_hook is None:
85
+ raise RuntimeError("register an invocation handler with @app.invoke")
86
+ if self._runtime.is_durable and self._recovery_hook is None:
87
+ logger.warning(
88
+ "No @app.recover handler is registered; automatic crash recovery is disabled."
89
+ )
90
+ await self._runtime.start()
91
+ try:
92
+ yield
93
+ finally:
94
+ await self._runtime.stop()
95
+ self._close_request_auth()
96
+
97
+ super().__init__(
98
+ title="Databricks Agent Runtime",
99
+ lifespan=lifespan,
100
+ docs_url=None,
101
+ redoc_url=None,
102
+ openapi_url=None,
103
+ )
104
+ self.add_exception_handler(AuthError, self._auth_error)
105
+ self.middleware("http")(self._bind_session)
106
+ self.add_api_route(_API_ROOT, self._invoke_request, methods=["POST"])
107
+ self.add_api_route(f"{_API_ROOT}/{{invocation_id}}", self._get_request, methods=["GET"])
108
+ self.add_api_route(
109
+ f"{_API_ROOT}/{{invocation_id}}/events",
110
+ self._events,
111
+ methods=["GET"],
112
+ )
113
+
114
+ def invoke(self, function: InvocationHook) -> InvocationHook:
115
+ """Register the handler for an invocation's first attempt."""
116
+ if self._invoke_hook is not None:
117
+ raise ValueError("an invocation handler is already registered")
118
+ self._invoke_hook = function
119
+ return function
120
+
121
+ def recover(self, function: InvocationHook) -> InvocationHook:
122
+ """Register the handler used after an interrupted attempt becomes stale."""
123
+ if self._recovery_hook is not None:
124
+ raise ValueError("a recovery handler is already registered")
125
+ self._recovery_hook = function
126
+ return function
127
+
128
+ async def _bind_session(self, request: Request, call_next) -> Response:
129
+ # TODO: Read the standard session header once Databricks Apps supports one. The Apps proxy
130
+ # currently consumes its routing cookie before forwarding deployed requests, so the
131
+ # invocation ID becomes the deterministic session fallback in _invoke_request.
132
+ request.state.session_id = request.cookies.get(_ROUTING_COOKIE)
133
+ return await call_next(request)
134
+
135
+ async def _execute(
136
+ self,
137
+ invocation_request: JsonValue,
138
+ attempt_context: InvocationAttemptContext,
139
+ ) -> JsonValue:
140
+ if not isinstance(invocation_request, dict):
141
+ raise TypeError("invocation request must be an object")
142
+ session_id = invocation_request.get("session_id")
143
+ if not isinstance(session_id, str) or "input" not in invocation_request:
144
+ raise TypeError("invocation request must contain session_id and input")
145
+ invocation_id = attempt_context.invocation_id
146
+ request_auth = None
147
+ if self.auth_policy.requires_user:
148
+ request_auth = self._request_auth.pop(attempt_context.invocation_id, None)
149
+ if attempt_context.is_recovery:
150
+ if request_auth is not None:
151
+ request_auth.close()
152
+ raise AuthError(
153
+ "MCP_USER_AUTH_RECOVERY_UNSUPPORTED",
154
+ "Request-user execution does not survive failure recovery yet",
155
+ 500,
156
+ )
157
+ if request_auth is None:
158
+ raise AuthError(
159
+ "MCP_USER_AUTH_RECOVERY_UNSUPPORTED",
160
+ "Request-user execution does not survive failure recovery yet",
161
+ 500,
162
+ )
163
+ invocation_id = invocation_request.get("invocation_id")
164
+ if not isinstance(invocation_id, str):
165
+ request_auth.close()
166
+ raise TypeError("request-user invocation must contain invocation_id")
167
+
168
+ context = InvocationContext(
169
+ invocation_id=invocation_id,
170
+ session_id=session_id,
171
+ attempt=attempt_context.attempt,
172
+ _attempt_context=attempt_context,
173
+ request_auth=request_auth,
174
+ )
175
+ function = self._recovery_hook if context.is_recovery else self._invoke_hook
176
+ try:
177
+ if function is None:
178
+ handler = "@app.recover" if context.is_recovery else "@app.invoke"
179
+ raise RuntimeError(f"no {handler} handler is registered")
180
+ return await function(copy.deepcopy(invocation_request["input"]), context)
181
+ finally:
182
+ if request_auth is not None:
183
+ request_auth.close()
184
+
185
+ async def _invoke_request(self, request: Request, body: _InvocationRequest) -> Response:
186
+ invocation_id = str(body.id)
187
+ runtime_invocation_id = invocation_id
188
+ request_auth = None
189
+ registered_auth = False
190
+ execution_owns_auth = False
191
+ session_id = request.state.session_id or invocation_id
192
+ if self.auth_policy.requires_user:
193
+ request_auth = RequestAuthContext.from_headers(request.headers)
194
+ runtime_invocation_id = request_auth.namespace("invocation", invocation_id)
195
+ session_id = request_auth.namespace("session", session_id)
196
+ existing_auth = self._request_auth.setdefault(runtime_invocation_id, request_auth)
197
+ registered_auth = existing_auth is request_auth
198
+ if not registered_auth:
199
+ request_auth.close()
200
+ invocation_request: JsonObject = {
201
+ "session_id": session_id,
202
+ "input": copy.deepcopy(body.input),
203
+ }
204
+ if self.auth_policy.requires_user:
205
+ invocation_request["invocation_id"] = invocation_id
206
+ try:
207
+ if body.background:
208
+ state = await self._runtime.submit(runtime_invocation_id, invocation_request)
209
+ execution_owns_auth = registered_auth and state.status == InvocationStatus.QUEUED
210
+ return JSONResponse(
211
+ self._accepted_payload(state, stream=body.stream, invocation_id=invocation_id),
212
+ status_code=202,
213
+ )
214
+ if body.stream:
215
+ state = await self._runtime.submit(runtime_invocation_id, invocation_request)
216
+ execution_owns_auth = registered_auth and state.status == InvocationStatus.QUEUED
217
+ return StreamingResponse(
218
+ self._event_stream(runtime_invocation_id),
219
+ media_type="text/event-stream",
220
+ )
221
+ output = await self._runtime.invoke(runtime_invocation_id, invocation_request)
222
+ return JSONResponse({"id": invocation_id, "status": "completed", "output": output})
223
+ except InvocationConflictError as exc:
224
+ raise HTTPException(409, "id was already used for another request") from exc
225
+ except InvocationFailedError as exc:
226
+ raise HTTPException(500, "agent invocation failed") from exc
227
+ finally:
228
+ if registered_auth and not execution_owns_auth and request_auth is not None:
229
+ self._discard_request_auth(runtime_invocation_id, request_auth)
230
+
231
+ async def _auth_error(self, request: Request, error: Exception) -> JSONResponse:
232
+ assert isinstance(error, AuthError)
233
+ return JSONResponse({"error": error.payload()}, status_code=error.status_code)
234
+
235
+ async def _get_request(self, request: Request, invocation_id: UUID) -> JSONResponse:
236
+ public_invocation_id = str(invocation_id)
237
+ runtime_invocation_id = self._runtime_invocation_id(request, public_invocation_id)
238
+ state = await self._runtime.get_invocation(runtime_invocation_id)
239
+ if state is None:
240
+ raise HTTPException(404, "invocation not found")
241
+ return JSONResponse(self._state_payload(state, invocation_id=public_invocation_id))
242
+
243
+ async def _events(
244
+ self, request: Request, invocation_id: UUID, after: int = 0
245
+ ) -> StreamingResponse:
246
+ public_invocation_id = str(invocation_id)
247
+ runtime_invocation_id = self._runtime_invocation_id(request, public_invocation_id)
248
+ if await self._runtime.get_invocation(runtime_invocation_id) is None:
249
+ raise HTTPException(404, "invocation not found")
250
+ return StreamingResponse(
251
+ self._event_stream(runtime_invocation_id, after),
252
+ media_type="text/event-stream",
253
+ )
254
+
255
+ async def _event_stream(self, invocation_id: str, after: int = 0) -> AsyncIterator[str]:
256
+ cursor = after
257
+ while True:
258
+ for event in await self._runtime.get_events(invocation_id, after_sequence=cursor):
259
+ cursor = event.sequence_number
260
+ event_type = event.event.get("type", "message")
261
+ yield f"id: {cursor}\nevent: {event_type}\ndata: {json.dumps(event.event)}\n\n"
262
+
263
+ state = await self._runtime.get_invocation(invocation_id)
264
+ if state is None or state.is_terminal:
265
+ return
266
+ await asyncio.sleep(self._runtime.poll_seconds)
267
+
268
+ @staticmethod
269
+ def _accepted_payload(
270
+ state: Invocation, *, stream: bool, invocation_id: str | None = None
271
+ ) -> JsonObject:
272
+ invocation_id = invocation_id or state.invocation_id
273
+ payload: JsonObject = {
274
+ "id": invocation_id,
275
+ "status": state.status.value.lower(),
276
+ "status_url": f"{_API_ROOT}/{invocation_id}",
277
+ }
278
+ if stream:
279
+ payload["events_url"] = f"{_API_ROOT}/{invocation_id}/events"
280
+ return payload
281
+
282
+ @staticmethod
283
+ def _state_payload(state: Invocation, *, invocation_id: str | None = None) -> JsonObject:
284
+ payload: JsonObject = {
285
+ "id": invocation_id or state.invocation_id,
286
+ "status": state.status.value.lower(),
287
+ }
288
+ if state.status == InvocationStatus.COMPLETED:
289
+ payload["output"] = copy.deepcopy(state.response)
290
+ elif state.status == InvocationStatus.FAILED:
291
+ payload["error"] = "agent invocation failed"
292
+ return payload
293
+
294
+ def _runtime_invocation_id(self, request: Request, invocation_id: str) -> str:
295
+ if not self.auth_policy.requires_user:
296
+ return invocation_id
297
+ request_auth = RequestAuthContext.from_headers(request.headers)
298
+ try:
299
+ return request_auth.namespace("invocation", invocation_id)
300
+ finally:
301
+ request_auth.close()
302
+
303
+ def _discard_request_auth(self, invocation_id: str, request_auth: RequestAuthContext) -> None:
304
+ if self._request_auth.get(invocation_id) is request_auth:
305
+ self._request_auth.pop(invocation_id)
306
+ request_auth.close()
307
+
308
+ def _close_request_auth(self) -> None:
309
+ request_auths = list(self._request_auth.values())
310
+ self._request_auth.clear()
311
+ for request_auth in request_auths:
312
+ request_auth.close()
313
+
314
+
315
+ # Deprecated compatibility alias. New code should use DurableAgentServer.
316
+ AgentApp = DurableAgentServer