Skip to content

Commit 4e6da4d

Browse files
feat(appkit): supervisor API adapter for agents plugin (#400)
* feat(appkit): supervisor api adapter * fix(appkit): address PR #345 review findings (section 9) Apply Mario's defensive/correctness fixes from the supervisor API adapter review without touching the public API shape (sections 1-8 will land in a stacked branch). Highlights: High - Route the three SSE error-leak sites in supervisor-api.ts (streamBody catch, mapEvent "error", output_item.done with id="error") through a single emitError helper that returns a stable client-facing code (`Supervisor API error (transport|upstream_failed|upstream_tool| upstream_unknown)`) and logs the verbose detail server-side only. Addresses CWE-209 verbatim-upstream-error-text leak. Medium - Gate the terminal {status:"complete"} emission on lastCompleted.status / .error / .incomplete_details so a `response.completed` with a nested failed status no longer silently succeeds; surface as upstream_failed instead. Regression tests added. - Skip the terminal error in the streamBody catch when signal.aborted — consumer-initiated aborts now end with a clean stop, not a contradictory terminal error event. Regression test added. - Tighten the output_item.done error match: require item.type === "error" (or pair the reserved id="error" with a non-message type) so a stray assistant message with id="error" is not mis-classified. - Add maxLineChars / maxBufferChars caps to readSseEvents with 1 MiB / 8 MiB defaults; throw on overflow. Addresses CWE-770. Tests added. - Docs: add a CWE-1427 callout warning that hosted-tool `description` is a prompt-injection sink — do not derive it from untrusted input. - Redact the no-delta warning log: summariseErrorPayload extracts a short `type: message` line; full payload only via DEBUG. Addresses CWE-532. - Gate the buffer-level CRLF normalize in sse-reader on `\r` presence to skip the regex on LF-only steady state. Low - mapEvent("error") fallback no longer wraps "Unknown error" with literal JSON quotes (uses string branch). - Drop the misleading "we await the factory at module init" comment in dev-playground; the code never awaits. - Fix @example imports in supervisor-api.ts JSDoc to use @databricks/appkit/beta (the actual public re-export). - Replace trimStart() with single-U+0020 strip in sse-reader per the SSE spec; remove the now-dead per-line `\r$` strip after the buffer-level CRLF normalise. - Flag streamPath as @internal in connectors/serving/client.ts noting the CWE-918 SSRF risk if it ever leaks to user-controlled input. - Add JSDoc warning to workspaceClient on SupervisorApiAdapterOptions: passing a per-request OBO client would leak identity across requests (CWE-664). Signed-off-by: Hubert Zub <hubert.zub@databricks.com> * fix(appkit): address pkosiec feedback Signed-off-by: Hubert Zub <hubert.zub@databricks.com> * 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> * fix(appkit): pkosiec's feedback Signed-off-by: Hubert Zub <hubert.zub@databricks.com> * fix(appkit): address supervisor adapter feedback 2026-07-28: first round Signed-off-by: Hubert Zub <hubert.zub@databricks.com> * fix(appkit): add agent selector to dev-playground Signed-off-by: Hubert Zub <hubert.zub@databricks.com> * Apply suggestions from code review Co-authored-by: Pawel Kosiec <pawel.kosiec@gmail.com> --------- Signed-off-by: Hubert Zub <hubert.zub@databricks.com> Co-authored-by: Pawel Kosiec <pawel.kosiec@gmail.com>
1 parent af65ac3 commit 4e6da4d

35 files changed

Lines changed: 4257 additions & 58 deletions

apps/dev-playground/.env.dist

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ OTEL_RESOURCE_ATTRIBUTES='service.sample_attribute=dev'
88
OTEL_SERVICE_NAME='dev-playground'
99
DATABRICKS_VOLUME_PLAYGROUND=
1010
DATABRICKS_VOLUME_OTHER=
11-
DATABRICKS_GENIE_SPACE_ID=
11+
DATABRICKS_GENIE_SPACE_ID='01f0b972bf391ccd8b60840332bb2e5b' # appkit NYC Taxi space
12+
DATABRICKS_UC_FUNCTION_NAME='system.ai.python_exec'
1213
DATABRICKS_JOB_ID=
1314
DATABRICKS_SERVING_ENDPOINT_NAME=
1415
LAKEBASE_ENDPOINT='' # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} — use the `name` field from the output

apps/dev-playground/client/src/routes/agent.route.tsx

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import { getPluginClientConfig } from "@databricks/appkit-ui/js";
2-
import { Button } from "@databricks/appkit-ui/react";
2+
import {
3+
Button,
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@databricks/appkit-ui/react";
310
import { createFileRoute } from "@tanstack/react-router";
411
import { useCallback, useEffect, useRef, useState } from "react";
512

613
export const Route = createFileRoute("/agent")({
714
component: AgentRoute,
815
});
916

17+
const AGENT_OPTIONS = [
18+
{ value: "helper", label: "Helper — general assistant" },
19+
{ value: "sql_analyst", label: "SQL Analyst — NYC taxi queries" },
20+
{ value: "supervisor", label: "Supervisor — Databricks-hosted tools" },
21+
] as const;
22+
1023
interface SSEEvent {
1124
type: string;
1225
delta?: string;
@@ -142,6 +155,7 @@ function AgentRoute() {
142155
const [input, setInput] = useState("");
143156
const [isLoading, setIsLoading] = useState(false);
144157
const [threadId, setThreadId] = useState<string | null>(null);
158+
const [agent, setAgent] = useState<string>(AGENT_OPTIONS[0].value);
145159
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>(
146160
[],
147161
);
@@ -211,6 +225,7 @@ function AgentRoute() {
211225
headers: { "Content-Type": "application/json" },
212226
body: JSON.stringify({
213227
message: userMessage,
228+
agent,
214229
...(threadId && { threadId }),
215230
}),
216231
});
@@ -310,7 +325,7 @@ function AgentRoute() {
310325
} finally {
311326
setIsLoading(false);
312327
}
313-
}, [input, isLoading, threadId, clearSuggestion]);
328+
}, [input, isLoading, threadId, agent, clearSuggestion]);
314329

315330
const handleInputChange = (value: string) => {
316331
setInput(value);
@@ -328,23 +343,42 @@ function AgentRoute() {
328343
return (
329344
<div className="min-h-screen bg-background">
330345
<div className="max-w-7xl mx-auto px-6 py-12">
331-
<div className="mb-8 flex items-end justify-between">
332-
<div>
333-
<h1 className="text-3xl font-bold mb-2">Agent Chat</h1>
334-
<p className="text-base text-muted-foreground">
335-
AI agent with auto-discovered tools from all AppKit plugins.
336-
{threadId && (
337-
<span className="ml-2 text-xs font-mono opacity-60">
338-
Thread: {threadId.slice(0, 8)}...
339-
</span>
340-
)}
341-
</p>
346+
<div className="flex items-center gap-6 justify-between">
347+
<div className="mb-8 flex-1 flex items-end justify-between">
348+
<div>
349+
<h1 className="text-3xl font-bold mb-2">Agent Chat</h1>
350+
<p className="text-base text-muted-foreground">
351+
AI agent with auto-discovered tools from all AppKit plugins.
352+
{threadId && (
353+
<span className="ml-2 text-xs font-mono opacity-60">
354+
Thread: {threadId.slice(0, 8)}...
355+
</span>
356+
)}
357+
</p>
358+
</div>
359+
{hasAutocomplete && (
360+
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
361+
Autocomplete enabled
362+
</span>
363+
)}
364+
</div>
365+
<div className="w-80 flex items-center gap-3">
366+
<div className="flex flex-1 flex-col gap-1">
367+
<span className="text-xs font-medium">Agent</span>
368+
<Select value={agent} onValueChange={setAgent}>
369+
<SelectTrigger className="w-80 h-8">
370+
<SelectValue />
371+
</SelectTrigger>
372+
<SelectContent>
373+
{AGENT_OPTIONS.map((option) => (
374+
<SelectItem key={option.value} value={option.value}>
375+
{option.label}
376+
</SelectItem>
377+
))}
378+
</SelectContent>
379+
</Select>
380+
</div>
342381
</div>
343-
{hasAutocomplete && (
344-
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
345-
Autocomplete enabled
346-
</span>
347-
)}
348382
</div>
349383

350384
<div className="flex gap-6 h-[700px]">

apps/dev-playground/server/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
serving,
1212
WRITE_ACTIONS,
1313
} from "@databricks/appkit";
14-
import { agents, createAgent, tool } from "@databricks/appkit/beta";
14+
import {
15+
agents,
16+
createAgent,
17+
DatabricksAdapter,
18+
supervisorTools,
19+
tool,
20+
} from "@databricks/appkit/beta";
1521
import { WorkspaceClient } from "@databricks/sdk-experimental";
1622
import { z } from "zod";
1723
import { lakebaseExamples } from "./lakebase-examples-plugin";
@@ -68,6 +74,35 @@ const helper = createAgent({
6874
},
6975
});
7076

77+
// Supervisor API demo agent. The Databricks AI Gateway executes hosted
78+
// tools server-side; declare them via `createAgent({ tools })` like any
79+
// other agent tool — the agents plugin classifies the tagged record and
80+
// routes it to the adapter via AgentInput.extensions. Import
81+
// `supervisorTools` from '@databricks/appkit/beta' and uncomment an
82+
// entry below to give the model real powers.
83+
//
84+
// `createAgent({ model })` accepts an adapter promise, so the factory's
85+
// host/credential resolution is awaited lazily on first dispatch (via
86+
// `resolveAdapter` in the agents plugin). A misconfigured workspace will
87+
// surface at first chat request, not at module init.
88+
const supervisor = createAgent({
89+
instructions:
90+
"You are an assistant powered by the Databricks Supervisor API.",
91+
model: DatabricksAdapter.fromSupervisorApi({
92+
model: "databricks-claude-sonnet-4-5",
93+
}),
94+
tools: () => ({
95+
nyc: supervisorTools.genieSpace({
96+
id: process.env.DATABRICKS_GENIE_SPACE_ID ?? "",
97+
description: "NYC taxi trip records and zones",
98+
}),
99+
add: supervisorTools.ucFunction({
100+
name: process.env.DATABRICKS_UC_FUNCTION_NAME ?? "",
101+
description: "Adds two integers and returns the sum.",
102+
}),
103+
}),
104+
});
105+
71106
/*
72107
* Smart-Dashboard agents.
73108
*
@@ -385,7 +420,7 @@ createApp({
385420
}),
386421
serving(),
387422
agents({
388-
agents: { helper, sql_analyst, dashboard_pilot },
423+
agents: { helper, sql_analyst, dashboard_pilot, supervisor },
389424
// `query` (markdown dispatcher) + `sql_analyst` + `dashboard_pilot`
390425
// wire the /smart-dashboard route. `insights` and `anomaly` are
391426
// ephemeral markdown agents auto-fired by the route's AgentSidebar.

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

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/api/appkit/Class.SupervisorApiAdapter.md

Lines changed: 141 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)