Skip to content

Commit abe6d76

Browse files
committed
refactor(appkit): restructure supervisor-api adapter per PR #345 review
Address structural feedback from Mario Cadenas' review of PR #345 (sections 1-8). Stacked on top of the §9 defensive fixes commit. API changes (BETA surface only): - Add `DatabricksAdapter.fromSupervisorApi` static factory for discoverability alongside `.fromChatCompletions`. - Shrink `SupervisorApiAdapterOptions` to `{ model, workspaceClient? }`; tools no longer live on the adapter. - Hosted tools (`supervisorTools.*`) now return tagged `HostedSupervisorTool` records and accept named options instead of positional args. - Declare hosted tools on the agent's `tools` map (same place as function tools / sub-agents); the agents plugin and `runAgent` route them to the adapter via the new `AgentInput.extensions[SUPERVISOR_EXTENSION_KEY]`. - Add capability-negotiation fields to `AgentAdapter`: `acceptsExtensions?` + `consumesInputTools?`. The agents plugin and `runAgent` warn at registration when adapter capabilities don't match declared tools. Internals: - Extend `ResolvedToolEntry` / `StandaloneEntry` with a `hosted-supervisor` branch; `classifyTool` matches it before MCP hosted-tool rejection so standalone `runAgent` supports supervisor tools. - Defense-in-depth: both indexers throw if a `hosted-supervisor` entry is ever dispatched as a callable function. - `DatabricksAdapter.fromSupervisorApi` uses a dynamic import to avoid load-time cycles. Docs: - Rewrite supervisor-API section in docs/plugins/agents.md for the new shape. - Add cross-adapter sub-agent composition note (one-directional: chat parents can call supervisor children, not vice-versa, until SA's function-call events are routed back through `context.executeTool`). Playground: - Update dev-playground supervisor agent to the new shape (`DatabricksAdapter.fromSupervisorApi(...)` + tools on `createAgent`). Tests: - Rewrite supervisor-api.test.ts factory + adapter tests for the new shape. - Add `isSupervisorTool` and `DatabricksAdapter.fromSupervisorApi` tests. - New regression tests in `run-agent.test.ts` covering the hosted-supervisor extension-routing path and both capability-mismatch warnings. - New agents-plugin tests covering the same warning paths and the new `hosted-supervisor` tool-index branch. Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
1 parent 6514dc0 commit abe6d76

29 files changed

Lines changed: 1971 additions & 165 deletions

apps/dev-playground/server/index.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import {
1515
agents,
1616
createAgent,
17-
fromSupervisorApi,
17+
DatabricksAdapter,
1818
tool,
1919
} from "@databricks/appkit/beta";
2020
import { WorkspaceClient } from "@databricks/sdk-experimental";
@@ -73,10 +73,12 @@ const helper = createAgent({
7373
},
7474
});
7575

76-
// Supervisor API demo agent. Tools are configured on the adapter (the SA
77-
// endpoint executes them server-side), not on the createAgent definition.
78-
// Uncomment a `supervisorTools.*` entry (and import 'supervisorTools' from
79-
// '@databricks/appkit/beta') to give the model real powers.
76+
// Supervisor API demo agent. The Databricks AI Gateway executes hosted
77+
// tools server-side; declare them via `createAgent({ tools })` like any
78+
// other agent tool — the agents plugin classifies the tagged record and
79+
// routes it to the adapter via AgentInput.extensions. Import
80+
// `supervisorTools` from '@databricks/appkit/beta' and uncomment an
81+
// entry below to give the model real powers.
8082
//
8183
// `createAgent({ model })` accepts an adapter promise, so the factory's
8284
// host/credential resolution is awaited lazily on first dispatch (via
@@ -85,18 +87,18 @@ const helper = createAgent({
8587
const supervisor = createAgent({
8688
instructions:
8789
"You are an assistant powered by the Databricks Supervisor API.",
88-
model: fromSupervisorApi({
90+
model: DatabricksAdapter.fromSupervisorApi({
8991
model: "databricks-claude-sonnet-4-5",
90-
tools: [
91-
// supervisorTools.genieSpace(
92-
// "01ABCDEF12345678",
93-
// "NYC taxi trip records and zones",
94-
// ),
95-
// supervisorTools.ucFunction(
96-
// "main.default.add",
97-
// "Adds two integers and returns the sum.",
98-
// ),
99-
],
92+
}),
93+
tools: () => ({
94+
// nyc: supervisorTools.genieSpace({
95+
// id: "01ABCDEF12345678",
96+
// description: "NYC taxi trip records and zones",
97+
// }),
98+
// add: supervisorTools.ucFunction({
99+
// name: "main.default.add",
100+
// description: "Adds two integers and returns the sum.",
101+
// }),
100102
}),
101103
});
102104

docs/docs/api/appkit/Class.DatabricksAdapter.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,40 @@ serving surface — no bespoke `fetch()` + `authenticate()` plumbing.
150150
#### Returns
151151

152152
`Promise`\<`DatabricksAdapter`\>
153+
154+
***
155+
156+
### fromSupervisorApi()
157+
158+
```ts
159+
static fromSupervisorApi(options: SupervisorApiAdapterOptions): Promise<SupervisorApiAdapter>;
160+
```
161+
162+
Discoverability shim for the Supervisor API adapter. Returns a
163+
import("./supervisor-api").SupervisorApiAdapter, NOT a
164+
DatabricksAdapter — the two are separate classes (different
165+
wire formats, different lifecycle). Surfaced here so application
166+
developers see a single `DatabricksAdapter.from*` autocomplete root.
167+
168+
Dynamic-imports `./supervisor-api` to avoid forming a load-time cycle:
169+
both files share `connectors/serving/client.ts`.
170+
171+
#### Parameters
172+
173+
| Parameter | Type |
174+
| ------ | ------ |
175+
| `options` | [`SupervisorApiAdapterOptions`](Interface.SupervisorApiAdapterOptions.md) |
176+
177+
#### Returns
178+
179+
`Promise`\<[`SupervisorApiAdapter`](Class.SupervisorApiAdapter.md)\>
180+
181+
#### Example
182+
183+
```ts
184+
import { DatabricksAdapter } from "@databricks/appkit/beta";
185+
186+
const model = await DatabricksAdapter.fromSupervisorApi({
187+
model: "databricks-claude-sonnet-4-5",
188+
});
189+
```
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Class: SupervisorApiAdapter
2+
3+
Adapter that calls the Databricks AI Gateway Responses API
4+
(`/ai-gateway/mlflow/v1/responses`).
5+
6+
Streams SSE events in the OpenAI Responses API wire format and maps them
7+
to the AppKit `AgentEvent` protocol. Tool execution is handled
8+
server-side, so the adapter ignores the agents-plugin tool index.
9+
10+
Authentication is handled via the Databricks SDK credential chain — the
11+
same mechanism used by `DatabricksAdapter.fromModelServing`. The transport
12+
is injected via [SupervisorApiAdapterCtorOptions.streamBody](Interface.SupervisorApiAdapterCtorOptions.md#streambody); the
13+
[fromSupervisorApi](Function.fromSupervisorApi.md) factory wires it through the SDK's
14+
`apiClient.request({ raw: true })`.
15+
16+
Set `DEBUG=appkit:agents:supervisor-api` to log the outbound request
17+
shape (model, instructions length, input shape, tool count) and to be
18+
notified when the recovery path engages (no incremental deltas, text
19+
pulled from `response.completed.output[]`). The no-delta warning includes
20+
a per-turn event-type histogram and the SA-reported status/error/
21+
incomplete_details, so it's already actionable without DEBUG.
22+
23+
Tools are not configured on the adapter. Declare them via
24+
`createAgent({ tools: () => ({ key: supervisorTools.genieSpace({...}) }) })`
25+
(or markdown frontmatter referencing an ambient `supervisorTools.*` entry);
26+
the agents plugin / standalone `runAgent` aggregates hosted-supervisor
27+
entries and routes them to the adapter via
28+
`AgentInput.extensions[SUPERVISOR_EXTENSION_KEY]`. Advanced callers
29+
invoking `adapter.run(...)` directly populate that key themselves.
30+
31+
## Example
32+
33+
```ts
34+
import { createApp, createAgent } from "@databricks/appkit";
35+
import {
36+
agents,
37+
DatabricksAdapter,
38+
supervisorTools,
39+
} from "@databricks/appkit/beta";
40+
41+
await createApp({
42+
plugins: [
43+
agents({
44+
agents: {
45+
assistant: createAgent({
46+
instructions: "You are a helpful assistant.",
47+
model: DatabricksAdapter.fromSupervisorApi({
48+
model: "databricks-claude-sonnet-4",
49+
}),
50+
tools: () => ({
51+
nyc: supervisorTools.genieSpace({
52+
id: "01ABCDEF12345678",
53+
description: "NYC taxi trip records and zones",
54+
}),
55+
}),
56+
}),
57+
},
58+
}),
59+
],
60+
});
61+
```
62+
63+
## Implements
64+
65+
- [`AgentAdapter`](Interface.AgentAdapter.md)
66+
67+
## Constructors
68+
69+
### Constructor
70+
71+
```ts
72+
new SupervisorApiAdapter(options: SupervisorApiAdapterCtorOptions): SupervisorApiAdapter;
73+
```
74+
75+
#### Parameters
76+
77+
| Parameter | Type |
78+
| ------ | ------ |
79+
| `options` | [`SupervisorApiAdapterCtorOptions`](Interface.SupervisorApiAdapterCtorOptions.md) |
80+
81+
#### Returns
82+
83+
`SupervisorApiAdapter`
84+
85+
## Properties
86+
87+
### acceptsExtensions
88+
89+
```ts
90+
readonly acceptsExtensions: readonly ["databricks.supervisor"];
91+
```
92+
93+
Capability negotiation: the adapter reads its hosted-tool payload
94+
from [AgentInput.extensions](Interface.AgentInput.md#extensions) under [SUPERVISOR\_EXTENSION\_KEY](Variable.SUPERVISOR_EXTENSION_KEY.md).
95+
The agents plugin uses this list to warn at registration when the tool
96+
index produces extensions the adapter wouldn't consume.
97+
98+
#### Implementation of
99+
100+
[`AgentAdapter`](Interface.AgentAdapter.md).[`acceptsExtensions`](Interface.AgentAdapter.md#acceptsextensions)
101+
102+
***
103+
104+
### consumesInputTools
105+
106+
```ts
107+
readonly consumesInputTools: false = false;
108+
```
109+
110+
Capability negotiation: the adapter does not consume `input.tools`.
111+
Tool execution is owned by the Databricks AI Gateway server-side, so
112+
any function tools or local sub-agents declared on this agent would
113+
be silently dropped — the agents plugin warns at registration when
114+
that combination is detected.
115+
116+
#### Implementation of
117+
118+
[`AgentAdapter`](Interface.AgentAdapter.md).[`consumesInputTools`](Interface.AgentAdapter.md#consumesinputtools)
119+
120+
## Methods
121+
122+
### run()
123+
124+
```ts
125+
run(input: AgentInput, context: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown>;
126+
```
127+
128+
#### Parameters
129+
130+
| Parameter | Type |
131+
| ------ | ------ |
132+
| `input` | [`AgentInput`](Interface.AgentInput.md) |
133+
| `context` | [`AgentRunContext`](Interface.AgentRunContext.md) |
134+
135+
#### Returns
136+
137+
`AsyncGenerator`\<[`AgentEvent`](TypeAlias.AgentEvent.md), `void`, `unknown`\>
138+
139+
#### Implementation of
140+
141+
[`AgentAdapter`](Interface.AgentAdapter.md).[`run`](Interface.AgentAdapter.md#run)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Function: fromSupervisorApi()
2+
3+
```ts
4+
function fromSupervisorApi(options: SupervisorApiAdapterOptions): Promise<SupervisorApiAdapter>;
5+
```
6+
7+
Creates an [AgentAdapter](Interface.AgentAdapter.md) backed by the Databricks AI Gateway
8+
Responses API (`/ai-gateway/mlflow/v1/responses`).
9+
10+
Uses the SDK's default credential chain for auth (reads DATABRICKS_HOST,
11+
DATABRICKS_TOKEN, OAuth config, etc.). Tools are declared on the agent
12+
(via `createAgent({ tools })`), not on this factory.
13+
14+
Application code should prefer the
15+
[DatabricksAdapter.fromSupervisorApi](Class.DatabricksAdapter.md#fromsupervisorapi) static — it delegates here
16+
and keeps a single `DatabricksAdapter.from*` autocomplete root for all
17+
Databricks-backed adapters. This free function is the implementation
18+
behind the static and remains exported for callers that want to import
19+
it directly without pulling in [DatabricksAdapter](Class.DatabricksAdapter.md).
20+
21+
## Parameters
22+
23+
| Parameter | Type |
24+
| ------ | ------ |
25+
| `options` | [`SupervisorApiAdapterOptions`](Interface.SupervisorApiAdapterOptions.md) |
26+
27+
## Returns
28+
29+
`Promise`\<[`SupervisorApiAdapter`](Class.SupervisorApiAdapter.md)\>
30+
31+
## Example
32+
33+
```ts
34+
import { createApp, createAgent } from "@databricks/appkit";
35+
import {
36+
agents,
37+
DatabricksAdapter,
38+
supervisorTools,
39+
} from "@databricks/appkit/beta";
40+
41+
await createApp({
42+
plugins: [
43+
agents({
44+
agents: {
45+
assistant: createAgent({
46+
instructions: "You are a helpful assistant.",
47+
model: DatabricksAdapter.fromSupervisorApi({
48+
model: "databricks-claude-sonnet-4",
49+
}),
50+
tools: () => ({
51+
nyc: supervisorTools.genieSpace({
52+
id: "01ABCDEF12345678",
53+
description: "NYC taxi trip records and zones",
54+
}),
55+
}),
56+
}),
57+
},
58+
}),
59+
],
60+
});
61+
```
62+
63+
## Remarks
64+
65+
⚠ When passing your own `workspaceClient`, see the warning on
66+
[SupervisorApiAdapterOptions.workspaceClient](Interface.SupervisorApiAdapterOptions.md#workspaceclient) — the client is
67+
captured once and reused, so per-request OBO clients would leak
68+
identity across requests.
69+
70+
## See
71+
72+
[DatabricksAdapter.fromSupervisorApi](Class.DatabricksAdapter.md#fromsupervisorapi) — the recommended
73+
application-facing entry point.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Function: isSupervisorTool()
2+
3+
```ts
4+
function isSupervisorTool(value: unknown): value is HostedSupervisorTool;
5+
```
6+
7+
Type guard for [HostedSupervisorTool](Interface.HostedSupervisorTool.md). Used by the agents plugin
8+
(`buildToolIndex`) and standalone `runAgent` (`classifyTool`) to route
9+
supervisor-hosted tools to the extensions payload rather than the
10+
adapter's `tools` array.
11+
12+
## Parameters
13+
14+
| Parameter | Type |
15+
| ------ | ------ |
16+
| `value` | `unknown` |
17+
18+
## Returns
19+
20+
`value is HostedSupervisorTool`

docs/docs/api/appkit/Interface.AgentAdapter.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Interface: AgentAdapter
22

3+
## Properties
4+
5+
### acceptsExtensions?
6+
7+
```ts
8+
readonly optional acceptsExtensions: readonly string[];
9+
```
10+
11+
Extension keys this adapter consumes from [AgentInput.extensions](Interface.AgentInput.md#extensions).
12+
The agents plugin (and standalone `runAgent`) warns at registration
13+
if the tool index produces extensions whose keys aren't listed here.
14+
15+
Adapters that don't read extensions can omit this field.
16+
17+
***
18+
19+
### consumesInputTools?
20+
21+
```ts
22+
readonly optional consumesInputTools: boolean;
23+
```
24+
25+
Whether the adapter consumes tools from `input.tools`. Defaults to
26+
true. Adapters whose tool execution happens elsewhere (e.g. the
27+
Supervisor API, where SA owns the tool loop server-side) declare
28+
false; the agents plugin warns at registration if the agent declares
29+
function tools or local sub-agents alongside such an adapter, since
30+
those tools would never reach the model.
31+
332
## Methods
433

534
### run()

docs/docs/api/appkit/Interface.AgentInput.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
## Properties
44

5+
### extensions?
6+
7+
```ts
8+
optional extensions: Readonly<Record<string, unknown>>;
9+
```
10+
11+
Adapter-specific opaque payloads, keyed by adapter namespace. The
12+
shared contract intentionally does not enumerate keys — see each
13+
adapter's docs for which keys it reads and the shape of each value.
14+
15+
The agents plugin and standalone `runAgent` populate this from the
16+
agent's tool index when entries declare an adapter-side spec (e.g.
17+
Supervisor API hosted tools). Adapters that don't read extensions
18+
should leave it untouched.
19+
20+
***
21+
522
### messages
623

724
```ts

0 commit comments

Comments
 (0)