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,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
|