@databricks/appkit 0.68.0 → 0.69.1

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 (31) hide show
  1. package/dist/agents/databricks.d.ts.map +1 -1
  2. package/dist/agents/databricks.js +31 -8
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/appkit/package.js +1 -1
  5. package/dist/core/agent/skills/types.d.ts +6 -1
  6. package/dist/core/agent/skills/types.d.ts.map +1 -1
  7. package/dist/plugins/agents/active-stream-tracker.js +52 -0
  8. package/dist/plugins/agents/active-stream-tracker.js.map +1 -0
  9. package/dist/plugins/agents/adapter-extensions.js +50 -0
  10. package/dist/plugins/agents/adapter-extensions.js.map +1 -0
  11. package/dist/plugins/agents/agents.d.ts +6 -77
  12. package/dist/plugins/agents/agents.d.ts.map +1 -1
  13. package/dist/plugins/agents/agents.js +57 -431
  14. package/dist/plugins/agents/agents.js.map +1 -1
  15. package/dist/plugins/agents/approval.js +28 -0
  16. package/dist/plugins/agents/approval.js.map +1 -0
  17. package/dist/plugins/agents/builtin-tools.js +39 -0
  18. package/dist/plugins/agents/builtin-tools.js.map +1 -0
  19. package/dist/plugins/agents/prompt.js +27 -0
  20. package/dist/plugins/agents/prompt.js.map +1 -0
  21. package/dist/plugins/agents/registry.js +56 -0
  22. package/dist/plugins/agents/registry.js.map +1 -0
  23. package/dist/plugins/agents/resolve-config.js +40 -0
  24. package/dist/plugins/agents/resolve-config.js.map +1 -0
  25. package/dist/plugins/agents/skill-loader.js +139 -0
  26. package/dist/plugins/agents/skill-loader.js.map +1 -0
  27. package/dist/plugins/agents/tool-dispatch.js +124 -0
  28. package/dist/plugins/agents/tool-dispatch.js.map +1 -0
  29. package/dist/shared/src/schemas/manifest.d.ts +33 -33
  30. package/package.json +1 -1
  31. package/sbom.cdx.json +1 -1
@@ -6,12 +6,10 @@ import { toPlugin } from "../../plugin/to-plugin.js";
6
6
  import "../../plugin/index.js";
7
7
  import { defineManifest } from "../../registry/manifest-loader.js";
8
8
  import "../../registry/index.js";
9
- import { FilesConnector } from "../../connectors/files/client.js";
10
- import "../../connectors/files/index.js";
11
9
  import { buildMcpHostPolicy } from "../../connectors/mcp/host-policy.js";
12
10
  import { AppKitMcpClient } from "../../connectors/mcp/client.js";
13
11
  import "../../connectors/mcp/index.js";
14
- import { SUPERVISOR_EXTENSION_KEY, isSupervisorTool } from "../../agents/supervisor-api.js";
12
+ import { isSupervisorTool } from "../../agents/supervisor-api.js";
15
13
  import { consumeAdapterStream } from "../../core/agent/consume-adapter-stream.js";
16
14
  import { createPluginsProxy } from "../../core/agent/plugins-map.js";
17
15
  import { resolveToolkitFromProvider } from "../../core/agent/toolkit-resolver.js";
@@ -20,68 +18,43 @@ import { isHostedTool, resolveHostedTools } from "../../core/agent/tools/hosted-
20
18
  import { isToolkitEntry } from "../../core/agent/types.js";
21
19
  import "../../core/agent/tools/index.js";
22
20
  import { loadAgentsFromDir } from "../../core/agent/load-agents.js";
23
- import { CODE_AGENTS_SOURCE_DIR, loadCodeAgentsFromDir, resolveCodeAgentsDir } from "../../core/agent/load-code-agents.js";
24
- import { normalizeToolResult } from "../../core/agent/normalize-result.js";
25
- import { parseSkill } from "../../core/agent/skills/parse-skill.js";
26
- import { loadSkillsFromDir } from "../../core/agent/skills/load-skills.js";
27
- import { readSkillResource } from "../../core/agent/skills/read-resource.js";
28
- import { renderLoadedSkill, renderSkillCatalog } from "../../core/agent/skills/render.js";
29
- import { resolveSkill, resolveSkillCatalog } from "../../core/agent/skills/resolve-catalog.js";
30
- import "../../core/agent/skills/index.js";
31
- import { buildBaseSystemPrompt, composeSystemPrompt } from "../../core/agent/system-prompt.js";
21
+ import { CODE_AGENTS_SOURCE_DIR } from "../../core/agent/load-code-agents.js";
22
+ import { ActiveStreamTracker } from "./active-stream-tracker.js";
23
+ import { buildAdapterExtensions, supervisorToolDescription, warnOnCapabilityMismatch } from "./adapter-extensions.js";
24
+ import { requiresApproval } from "./approval.js";
25
+ import { LOAD_SKILL_TOOL_DEF, READ_SKILL_FILE_TOOL_DEF } from "./builtin-tools.js";
32
26
  import { agentStreamDefaults } from "./defaults.js";
33
27
  import { EventChannel } from "./event-channel.js";
34
28
  import { AgentEventTranslator } from "./event-translator.js";
35
29
  import manifest_default from "./manifest.js";
36
- import { currentTraceId, initAgentTracing, linkTraceToRun, traceAgent, traceTool } from "./mlflow.js";
30
+ import { currentTraceId, initAgentTracing, linkTraceToRun, traceAgent } from "./mlflow.js";
31
+ import { composePromptForAgent } from "./prompt.js";
32
+ import { loadCodeAgents, resolveDefaultAgent } from "./registry.js";
33
+ import { resolveApprovalPolicy, resolveLimits } from "./resolve-config.js";
37
34
  import { approvalRequestSchema, cancelRequestSchema, chatRequestSchema, invocationsRequestSchema } from "./schemas.js";
35
+ import { dispatchSkillTool, loadGlobalSkills, loadVolumeSkills, renderForcedSkill, resolveAgentSkills } from "./skill-loader.js";
38
36
  import { InMemoryThreadStore } from "./thread-store.js";
39
37
  import { ToolApprovalGate } from "./tool-approval-gate.js";
38
+ import { dispatchToolCall } from "./tool-dispatch.js";
40
39
  import { randomUUID } from "node:crypto";
41
40
  import pc from "picocolors";
42
41
  import path from "node:path";
43
- import { existsSync, readdirSync } from "node:fs";
42
+ import { existsSync } from "node:fs";
44
43
 
45
44
  //#region src/plugins/agents/agents.ts
46
45
  const logger = createLogger("agents");
47
46
  /** Deprecated markdown location, read as a fallback with a one-time warning. */
48
47
  const LEGACY_MARKDOWN_DIR = "config/agents";
49
- /**
50
- * Decide whether a tool call must traverse the approval gate. Honours both
51
- * the modern `effect` field (mutating values: write / update / destructive)
52
- * and the legacy `destructive: true` boolean. The contract is documented on
53
- * `ToolAnnotations.effect` in shared/agent.ts.
54
- *
55
- * Without this, a tool authored only with `effect: "destructive"` (the
56
- * preferred API) bypassed the gate entirely.
57
- */
58
- function requiresApproval(annotations) {
59
- if (!annotations) return false;
60
- if (annotations.destructive === true) return true;
61
- switch (annotations.effect) {
62
- case "write":
63
- case "update":
64
- case "destructive": return true;
65
- case "read":
66
- case void 0: return false;
67
- default:
68
- annotations.effect;
69
- return false;
70
- }
71
- }
72
48
  var AgentsPlugin = class extends Plugin {
73
49
  static manifest = defineManifest(manifest_default);
74
50
  static phase = "deferred";
75
51
  agents = /* @__PURE__ */ new Map();
76
52
  defaultAgentName = null;
77
- activeStreams = /* @__PURE__ */ new Map();
78
53
  /**
79
- * Per-user stream count, kept in sync with `activeStreams` so the
80
- * concurrent-stream rate limit check is O(1) instead of O(n) over every
81
- * active stream on every request. Mutated only via {@link trackStream}
82
- * and {@link untrackStream}.
54
+ * Active SSE streams + per-user counts (the O(1) concurrency-limit check).
55
+ * Mutated only via {@link trackStream} / {@link untrackStream}.
83
56
  */
84
- userStreamCounts = /* @__PURE__ */ new Map();
57
+ streams = new ActiveStreamTracker();
85
58
  mcpClient = null;
86
59
  threadStore;
87
60
  approvalGate = new ToolApprovalGate();
@@ -118,59 +91,22 @@ var AgentsPlugin = class extends Plugin {
118
91
  cachedApprovalPolicy = null;
119
92
  get resolvedApprovalPolicy() {
120
93
  if (this.cachedApprovalPolicy) return this.cachedApprovalPolicy;
121
- const cfg = this.config.approval ?? {};
122
- const APPROVAL_TIMEOUT_FLOOR_MS = 1e3;
123
- const APPROVAL_TIMEOUT_DEFAULT_MS = 6e4;
124
- let timeoutMs = cfg.timeoutMs ?? APPROVAL_TIMEOUT_DEFAULT_MS;
125
- if (!Number.isFinite(timeoutMs) || timeoutMs < APPROVAL_TIMEOUT_FLOOR_MS) {
126
- logger.warn("approval.timeoutMs=%s is below the %sms floor; using default %sms instead. Mutating tool calls would otherwise auto-deny before any UI could respond.", cfg.timeoutMs, APPROVAL_TIMEOUT_FLOOR_MS, APPROVAL_TIMEOUT_DEFAULT_MS);
127
- timeoutMs = APPROVAL_TIMEOUT_DEFAULT_MS;
128
- }
129
- this.cachedApprovalPolicy = {
130
- requireForDestructive: cfg.requireForDestructive ?? true,
131
- timeoutMs
132
- };
94
+ this.cachedApprovalPolicy = resolveApprovalPolicy(this.config);
133
95
  return this.cachedApprovalPolicy;
134
96
  }
135
97
  /** Effective DoS limits with defaults applied. */
136
98
  get resolvedLimits() {
137
- const cfg = this.config.limits ?? {};
138
- return {
139
- maxConcurrentStreamsPerUser: cfg.maxConcurrentStreamsPerUser ?? 5,
140
- maxToolCalls: cfg.maxToolCalls ?? 50,
141
- maxSubAgentDepth: cfg.maxSubAgentDepth ?? 3,
142
- toolCallTimeoutMs: cfg.toolCallTimeoutMs ?? 3e5
143
- };
99
+ return resolveLimits(this.config);
144
100
  }
145
101
  /** Count active streams owned by a given user. O(1). */
146
102
  countUserStreams(userId) {
147
- return this.userStreamCounts.get(userId) ?? 0;
103
+ return this.streams.count(userId);
148
104
  }
149
- /**
150
- * Register a stream for `userId` and bump the per-user counter. Paired
151
- * with {@link untrackStream}; the two helpers are the only writers to
152
- * `activeStreams` + `userStreamCounts`, so the counter cannot drift from
153
- * the map.
154
- */
155
105
  trackStream(requestId, userId, controller) {
156
- this.activeStreams.set(requestId, {
157
- controller,
158
- userId
159
- });
160
- this.userStreamCounts.set(userId, (this.userStreamCounts.get(userId) ?? 0) + 1);
106
+ this.streams.track(requestId, userId, controller);
161
107
  }
162
- /**
163
- * Remove a stream from the active map and decrement the per-user
164
- * counter. Idempotent — calling twice for the same `requestId` is a
165
- * no-op (the second call sees no entry and returns early).
166
- */
167
108
  untrackStream(requestId) {
168
- const entry = this.activeStreams.get(requestId);
169
- if (!entry) return;
170
- this.activeStreams.delete(requestId);
171
- const next = (this.userStreamCounts.get(entry.userId) ?? 0) - 1;
172
- if (next <= 0) this.userStreamCounts.delete(entry.userId);
173
- else this.userStreamCounts.set(entry.userId, next);
109
+ this.streams.untrack(requestId);
174
110
  }
175
111
  async setup() {
176
112
  await initAgentTracing();
@@ -258,14 +194,7 @@ var AgentsPlugin = class extends Plugin {
258
194
  * markdown `default: true` > first registered (insertion order).
259
195
  */
260
196
  resolveDefaultAgent(agents, merged, fileDefault) {
261
- if (this.config.defaultAgent) {
262
- if (!agents.has(this.config.defaultAgent)) throw new Error(`defaultAgent '${this.config.defaultAgent}' is not registered. Available: ${Array.from(agents.keys()).join(", ")}`);
263
- return this.config.defaultAgent;
264
- }
265
- const codeDefault = Object.keys(merged).filter((id) => merged[id].src.origin === "code" && merged[id].def.default).sort()[0];
266
- if (codeDefault) return codeDefault;
267
- if (fileDefault && agents.has(fileDefault)) return fileDefault;
268
- return agents.keys().next().value ?? null;
197
+ return resolveDefaultAgent(agents, merged, fileDefault, this.config.defaultAgent);
269
198
  }
270
199
  /**
271
200
  * Emits the one-time deprecation warning for the `agents({ agents })` map.
@@ -288,37 +217,11 @@ var AgentsPlugin = class extends Plugin {
288
217
  resolvedAgentsDir() {
289
218
  return path.resolve(process.cwd(), CODE_AGENTS_SOURCE_DIR);
290
219
  }
291
- /**
292
- * Discovers code agents (see {@link resolveCodeAgentsDir} and
293
- * {@link loadCodeAgentsFromDir}). Warns if sources exist but nothing was
294
- * discovered — usually the build didn't emit the compiled agents — unless
295
- * the deprecated `agents({ agents })` map is carrying them instead.
296
- */
297
- async loadCodeAgents() {
298
- const resolved = resolveCodeAgentsDir({
299
- cwd: process.cwd(),
300
- exists: existsSync
220
+ loadCodeAgents() {
221
+ return loadCodeAgents({
222
+ agentsDir: this.resolvedAgentsDir(),
223
+ hasDeprecatedMap: Object.keys(this.config.agents ?? {}).length > 0
301
224
  });
302
- const discovered = await loadCodeAgentsFromDir(resolved.dir, { extensions: resolved.extensions });
303
- const usingDeprecatedMap = Object.keys(this.config.agents ?? {}).length > 0;
304
- const sourceDir = this.resolvedAgentsDir();
305
- if (Object.keys(discovered).length === 0 && !usingDeprecatedMap && this.hasCodeAgentSources(sourceDir)) logger.warn("Found code-agent sources in %s but discovered no code agents (scanned %s). In a production build, ensure `<dir>/*/agent.ts` is included as tsdown entries so the compiled agents are emitted.", sourceDir, resolved.dir);
306
- return discovered;
307
- }
308
- /** True when `dir` holds at least one `<id>/agent.ts` folder. */
309
- hasCodeAgentSources(dir) {
310
- try {
311
- return readdirSync(dir, { withFileTypes: true }).some((e) => {
312
- if (!e.isDirectory() && !e.isSymbolicLink()) return false;
313
- try {
314
- return readdirSync(path.join(dir, e.name)).includes("agent.ts");
315
- } catch {
316
- return false;
317
- }
318
- });
319
- } catch {
320
- return false;
321
- }
322
225
  }
323
226
  async loadFileDefinitions(codeAgents) {
324
227
  const primaryDir = this.resolvedAgentsDir();
@@ -387,15 +290,8 @@ var AgentsPlugin = class extends Plugin {
387
290
  };
388
291
  }
389
292
  /** Loads the shared global skill pool from `<agentsDir>/skills/`. */
390
- async loadGlobalSkills() {
391
- const dir = this.resolvedAgentsDir();
392
- if (!dir) return [];
393
- return loadSkillsFromDir(path.join(dir, "skills"), "bundle-global");
394
- }
395
- /** Configured catalog-skills volume path, or null when none is set. */
396
- resolveSkillsVolume() {
397
- const configured = this.config.skillsVolume ?? process.env.DATABRICKS_VOLUME_AGENT_SKILLS;
398
- return configured && configured.trim() !== "" ? configured.trim() : null;
293
+ loadGlobalSkills() {
294
+ return loadGlobalSkills(this.resolvedAgentsDir());
399
295
  }
400
296
  /**
401
297
  * Workspace client used to read catalog skills. v1 reads as the app service
@@ -406,82 +302,20 @@ var AgentsPlugin = class extends Plugin {
406
302
  skillWorkspaceClient() {
407
303
  return getWorkspaceClient();
408
304
  }
409
- /**
410
- * Discovers catalog skills from the configured UC Volume, read as the
411
- * service principal at boot (and on `reload()`). Each `<volume>/<name>/`
412
- * folder with a `SKILL.md` becomes a `source: "volume"` skill. Best-effort:
413
- * a missing volume, unavailable workspace client, or a malformed individual
414
- * skill is logged and skipped rather than failing the whole registry build.
415
- */
416
- async loadVolumeSkills() {
417
- const volume = this.resolveSkillsVolume();
418
- if (!volume) return [];
419
- if ((this.config.skillCredentialMode ?? "sp") === "obo") logger.warn("skillCredentialMode 'obo' is not wired yet; reading catalog skills as the service principal.");
420
- let client;
421
- try {
422
- client = this.skillWorkspaceClient();
423
- } catch (err) {
424
- logger.warn("Skipping catalog skills at '%s': no workspace client available (%s).", volume, err instanceof Error ? err.message : String(err));
425
- return [];
426
- }
427
- const connector = new FilesConnector({ defaultVolume: volume });
428
- let entries;
429
- try {
430
- entries = await connector.list(client, volume);
431
- } catch (err) {
432
- logger.warn("Failed to list catalog skills volume '%s': %s", volume, err instanceof Error ? err.message : String(err));
433
- return [];
434
- }
435
- return (await Promise.all(entries.map(async (entry) => {
436
- if (!entry.is_directory || !entry.name || !entry.path) return null;
437
- const skillDir = entry.path;
438
- const skillFile = `${skillDir}/SKILL.md`;
439
- try {
440
- const parsed = parseSkill(await connector.read(client, skillFile), skillFile);
441
- const files = await this.listVolumeSkillFiles(connector, client, skillDir);
442
- return {
443
- name: parsed.name,
444
- description: parsed.description,
445
- body: parsed.body,
446
- source: "volume",
447
- dir: skillDir,
448
- files,
449
- allowedTools: parsed.allowedTools
450
- };
451
- } catch (err) {
452
- logger.warn("Skipping catalog skill '%s': %s", skillDir, err instanceof Error ? err.message : String(err));
453
- return null;
454
- }
455
- }))).filter((s) => s !== null);
456
- }
457
- /** Lists a volume skill's resource files (one level, excluding SKILL.md). */
458
- async listVolumeSkillFiles(connector, client, skillDir) {
459
- try {
460
- return (await connector.list(client, skillDir)).filter((e) => !e.is_directory && e.name && e.name !== "SKILL.md").map((e) => e.name).sort();
461
- } catch {
462
- return [];
463
- }
305
+ /** Discovers catalog skills from the configured UC Volume (best-effort). */
306
+ loadVolumeSkills() {
307
+ return loadVolumeSkills(this.config, () => this.skillWorkspaceClient());
464
308
  }
465
- /**
466
- * Resolves the per-agent skill catalog: loads this agent's private skills
467
- * (`<agentsDir>/<id>/skills/`, file-origin only), then applies visibility
468
- * (opt-in via `def.skills` or `autoInheritSkills`) and collision rules
469
- * against the shared global pool. Returns `undefined` when nothing is
470
- * visible so the prompt catalog and dispatch can cheaply skip skills.
471
- */
472
309
  async resolveAgentSkills(name, def, src) {
473
- const dir = this.resolvedAgentsDir();
474
- const perAgentSkills = src.origin === "file" && dir ? await loadSkillsFromDir(path.join(dir, name, "skills"), "bundle-agent") : [];
475
310
  const inherit = normalizeAutoInherit(this.config.autoInheritSkills);
476
- const autoInherit = src.origin === "file" ? inherit.file : inherit.code;
477
- const catalog = resolveSkillCatalog({
478
- agentName: name,
479
- agentSkillNames: def.skills,
480
- perAgentSkills,
481
- globalSkills: this.globalSkills,
482
- autoInherit
311
+ return resolveAgentSkills({
312
+ name,
313
+ def,
314
+ agentsDir: this.resolvedAgentsDir(),
315
+ isFileOrigin: src.origin === "file",
316
+ autoInherit: src.origin === "file" ? inherit.file : inherit.code,
317
+ globalSkills: this.globalSkills
483
318
  });
484
- return catalog.byAddress.size > 0 ? catalog : void 0;
485
319
  }
486
320
  async resolveAdapter(def, name) {
487
321
  const source = def.model ?? this.config.defaultModel;
@@ -930,7 +764,8 @@ var AgentsPlugin = class extends Plugin {
930
764
  outboundEvents,
931
765
  toolCallsUsed: { count: 0 }
932
766
  };
933
- const executeTool = (name, args) => this.dispatchToolCall(runState, registered.toolIndex, name, args, 0);
767
+ const deps = this.toolDispatchDeps();
768
+ const executeTool = (name, args) => dispatchToolCall(deps, runState, registered.toolIndex, name, args, 0);
934
769
  const driver = (async () => {
935
770
  try {
936
771
  for (const evt of translator.translate({
@@ -1071,7 +906,8 @@ var AgentsPlugin = class extends Plugin {
1071
906
  outboundEvents: new EventChannel(),
1072
907
  toolCallsUsed: { count: 0 }
1073
908
  };
1074
- const executeTool = (name, args) => this.dispatchToolCall(runState, registered.toolIndex, name, args, 0);
909
+ const deps = this.toolDispatchDeps();
910
+ const executeTool = (name, args) => dispatchToolCall(deps, runState, registered.toolIndex, name, args, 0);
1075
911
  let fullContent = "";
1076
912
  try {
1077
913
  await traceAgent(registered.name ?? "agent", { messages: thread.messages.map((m) => ({
@@ -1152,82 +988,8 @@ var AgentsPlugin = class extends Plugin {
1152
988
  output: [message]
1153
989
  });
1154
990
  }
1155
- /**
1156
- * Dispatch a single tool call from either the top-level adapter or a
1157
- * sub-agent. Centralising this in one method is what makes the budget
1158
- * counter, approval gate, and abort signal observe sub-agent activity:
1159
- * `runSubAgent` reuses the same `runState` and so increments the same
1160
- * counter and emits approval events through the same channel.
1161
- *
1162
- * `depth` is the current sub-agent recursion depth (0 at the top level).
1163
- * It is forwarded to `runSubAgent` when the dispatched entry is itself a
1164
- * sub-agent, so depth limits remain enforced.
1165
- */
1166
- async dispatchToolCall(runState, toolIndex, name, args, depth) {
1167
- if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {
1168
- runState.abortController.abort(/* @__PURE__ */ new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`));
1169
- throw new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`);
1170
- }
1171
- runState.toolCallsUsed.count++;
1172
- const entry = toolIndex.get(name);
1173
- if (!entry) throw new Error(`Unknown tool: ${name}`);
1174
- if (runState.approvalPolicy.requireForDestructive && requiresApproval(entry.def.annotations)) {
1175
- const approvalId = randomUUID();
1176
- for (const ev of runState.translator.translate({
1177
- type: "approval_pending",
1178
- approvalId,
1179
- streamId: runState.requestId,
1180
- toolName: name,
1181
- args,
1182
- annotations: entry.def.annotations
1183
- })) runState.outboundEvents.push(ev);
1184
- if (await this.approvalGate.wait({
1185
- approvalId,
1186
- streamId: runState.requestId,
1187
- userId: runState.userId,
1188
- timeoutMs: runState.approvalPolicy.timeoutMs
1189
- }) === "deny") return `Tool execution denied by user approval gate (tool: ${name}).`;
1190
- }
1191
- return normalizeToolResult(await traceTool(name, args, async () => {
1192
- let result;
1193
- if (entry.source === "toolkit") {
1194
- if (!this.context) throw new Error("Plugin tool execution requires PluginContext; this should never happen through createApp");
1195
- result = await this.context.executeTool(runState.req, entry.pluginName, entry.localName, args, runState.signal, runState.limits.toolCallTimeoutMs);
1196
- } else if (entry.source === "function") {
1197
- if (typeof args !== "object" || args === null || Array.isArray(args)) throw new Error(`Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`);
1198
- result = await entry.functionTool.execute(args);
1199
- } else if (entry.source === "mcp") {
1200
- if (!this.mcpClient) throw new Error("MCP client not connected");
1201
- const oboToken = runState.req.headers["x-forwarded-access-token"];
1202
- const mcpAuth = typeof oboToken === "string" ? { Authorization: `Bearer ${oboToken}` } : void 0;
1203
- result = await this.mcpClient.callTool(entry.mcpToolName, args, mcpAuth);
1204
- } else if (entry.source === "subagent") {
1205
- const childAgent = this.agents.get(entry.agentName);
1206
- if (!childAgent) throw new Error(`Sub-agent not found: ${entry.agentName}`);
1207
- result = await this.runSubAgent(runState, childAgent, args, depth + 1);
1208
- } else if (entry.source === "hosted-supervisor") throw new Error(`Tool '${name}' is a hosted-supervisor tool and cannot be invoked from the Node process. It is executed server-side by the Databricks AI Gateway and is only reachable when the agent's model is a Supervisor API adapter.`);
1209
- else if (entry.source === "skill") result = await this.dispatchSkillTool(entry, args);
1210
- return result;
1211
- }));
1212
- }
1213
- /**
1214
- * Executes the built-in `load_skill` / `read_skill_file` tools against the
1215
- * agent's resolved skill catalog. `load_skill` returns a skill's body plus a
1216
- * manifest of bundled files; `read_skill_file` returns the contents of one
1217
- * of those files (bundle skills only in v1 — catalog-volume resource reads
1218
- * arrive with the volume source).
1219
- */
1220
- async dispatchSkillTool(entry, args) {
1221
- const obj = typeof args === "object" && args !== null ? args : {};
1222
- const skillName = typeof obj.skill === "string" ? obj.skill.trim() : "";
1223
- if (!skillName) throw new Error(`'${entry.builtin}' requires a 'skill' argument naming the skill to use.`);
1224
- const skill = resolveSkill(entry.catalog, skillName);
1225
- if (entry.builtin === "load_skill") return renderLoadedSkill(skill);
1226
- const filePath = typeof obj.path === "string" ? obj.path.trim() : "";
1227
- if (!filePath) throw new Error("'read_skill_file' requires a 'path' argument.");
1228
- if (!skill.files.includes(filePath)) throw new Error(`Skill '${skill.name}' has no bundled file '${filePath}'. Available: ${skill.files.join(", ") || "<none>"}.`);
1229
- if (skill.source === "volume") return new FilesConnector({ defaultVolume: skill.dir }).read(this.skillWorkspaceClient(), `${skill.dir}/${filePath}`);
1230
- return readSkillResource(skill.dir, filePath);
991
+ dispatchSkillTool(entry, args) {
992
+ return dispatchSkillTool(entry, args, () => this.skillWorkspaceClient());
1231
993
  }
1232
994
  /**
1233
995
  * Renders the prompt addendum for a force-loaded skill (`/skill-name`).
@@ -1236,67 +998,19 @@ var AgentsPlugin = class extends Plugin {
1236
998
  * model proceeds with the catalog + `load_skill` still available.
1237
999
  */
1238
1000
  renderForcedSkill(registered, name) {
1239
- if (!registered.skills) return null;
1240
- try {
1241
- const skill = resolveSkill(registered.skills, name);
1242
- return `The user explicitly requested the "${skill.name}" skill for this turn. Its instructions:\n\n${renderLoadedSkill(skill)}`;
1243
- } catch (err) {
1244
- logger.warn("Ignoring forced skill '%s': %s", name, err instanceof Error ? err.message : String(err));
1245
- return null;
1246
- }
1001
+ return renderForcedSkill(registered, name);
1247
1002
  }
1248
- /**
1249
- * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the
1250
- * concatenated text output to hand back to the parent adapter as the tool
1251
- * result.
1252
- *
1253
- * `depth` starts at 1 for a top-level sub-agent invocation (i.e. the
1254
- * outer `_streamAgent` calls `runSubAgent(..., 1)`) and increments on
1255
- * each nested `runSubAgent` call. Depths exceeding
1256
- * `limits.maxSubAgentDepth` are rejected before any adapter work.
1257
- *
1258
- * Sub-agent tool calls run through `dispatchToolCall` with the same
1259
- * `runState` as the parent — the budget counter and approval gate are
1260
- * therefore enforced for every nested call, not only at the top level.
1261
- */
1262
- async runSubAgent(runState, child, args, depth) {
1263
- if (depth > runState.limits.maxSubAgentDepth) throw new Error(`Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`);
1264
- const input = typeof args === "object" && args !== null && typeof args.input === "string" ? args.input : JSON.stringify(args);
1265
- const childTools = Array.from(child.toolIndex.values()).filter((e) => e.source !== "hosted-supervisor").map((e) => e.def);
1266
- const childExecute = (name, childArgs) => this.dispatchToolCall(runState, child.toolIndex, name, childArgs, depth);
1267
- const runContext = {
1268
- executeTool: childExecute,
1269
- signal: runState.signal
1003
+ /** Collaborators the tool-dispatch path needs, bound to this instance. */
1004
+ toolDispatchDeps() {
1005
+ return {
1006
+ approvalGate: this.approvalGate,
1007
+ context: this.context,
1008
+ getMcpClient: () => this.mcpClient,
1009
+ agents: this.agents,
1010
+ dispatchSkillTool: (entry, args) => this.dispatchSkillTool(entry, args),
1011
+ pluginName: this.name,
1012
+ baseSystemPrompt: this.config.baseSystemPrompt
1270
1013
  };
1271
- const pluginNames = this.context ? this.context.getPluginNames().filter((n) => n !== this.name && n !== "server") : [];
1272
- const messages = [{
1273
- id: "system",
1274
- role: "system",
1275
- content: composePromptForAgent(child, this.config.baseSystemPrompt, {
1276
- agentName: child.name,
1277
- pluginNames,
1278
- toolNames: childTools.map((t) => t.name)
1279
- }),
1280
- createdAt: /* @__PURE__ */ new Date()
1281
- }, {
1282
- id: randomUUID(),
1283
- role: "user",
1284
- content: input,
1285
- createdAt: /* @__PURE__ */ new Date()
1286
- }];
1287
- return consumeAdapterStream(child.adapter.run({
1288
- messages,
1289
- tools: childTools,
1290
- threadId: randomUUID(),
1291
- signal: runState.signal,
1292
- extensions: buildAdapterExtensions(child.toolIndex)
1293
- }, runContext), {
1294
- signal: runState.signal,
1295
- onEvent: (event) => {
1296
- if (event.type === "metadata") return;
1297
- for (const translated of runState.translator.translate(event)) runState.outboundEvents.push(translated);
1298
- }
1299
- });
1300
1014
  }
1301
1015
  async _handleCancel(req, res) {
1302
1016
  const parsed = cancelRequestSchema.safeParse(req.body);
@@ -1308,7 +1022,7 @@ var AgentsPlugin = class extends Plugin {
1308
1022
  return;
1309
1023
  }
1310
1024
  const { streamId } = parsed.data;
1311
- const entry = this.activeStreams.get(streamId);
1025
+ const entry = this.streams.get(streamId);
1312
1026
  if (!entry) {
1313
1027
  res.json({ cancelled: true });
1314
1028
  return;
@@ -1333,7 +1047,7 @@ var AgentsPlugin = class extends Plugin {
1333
1047
  return;
1334
1048
  }
1335
1049
  const { streamId, approvalId, decision } = parsed.data;
1336
- const streamEntry = this.activeStreams.get(streamId);
1050
+ const streamEntry = this.streams.get(streamId);
1337
1051
  if (!streamEntry) {
1338
1052
  res.status(404).json({ error: "Stream not found or already completed" });
1339
1053
  return;
@@ -1436,94 +1150,6 @@ function normalizeAutoInherit(value) {
1436
1150
  code: value.code ?? false
1437
1151
  };
1438
1152
  }
1439
- /** Built-in tool the model calls to load a skill's full instructions on demand. */
1440
- const LOAD_SKILL_TOOL_DEF = {
1441
- name: "load_skill",
1442
- description: "Load the full instructions for one of the available skills by name. Call this before acting on a task that matches a skill's description. Returns the skill's instructions plus a list of any bundled files you can read with read_skill_file.",
1443
- parameters: {
1444
- type: "object",
1445
- properties: { skill: {
1446
- type: "string",
1447
- description: "The exact skill name to load, as shown in the available-skills list."
1448
- } },
1449
- required: ["skill"]
1450
- },
1451
- annotations: { effect: "read" }
1452
- };
1453
- /** Built-in tool for reading a resource file that a loaded skill references. */
1454
- const READ_SKILL_FILE_TOOL_DEF = {
1455
- name: "read_skill_file",
1456
- description: "Read a bundled resource file that a loaded skill references (e.g. a reference doc). Only files listed by load_skill for that skill are readable.",
1457
- parameters: {
1458
- type: "object",
1459
- properties: {
1460
- skill: {
1461
- type: "string",
1462
- description: "The skill that owns the file."
1463
- },
1464
- path: {
1465
- type: "string",
1466
- description: "Relative path of the file within the skill, as listed by load_skill."
1467
- }
1468
- },
1469
- required: ["skill", "path"]
1470
- },
1471
- annotations: { effect: "read" }
1472
- };
1473
- function composePromptForAgent(registered, pluginLevel, ctx) {
1474
- const perAgent = registered.baseSystemPrompt;
1475
- const resolved = perAgent !== void 0 ? perAgent : pluginLevel;
1476
- let base = "";
1477
- if (resolved === false) base = "";
1478
- else if (typeof resolved === "string") base = resolved;
1479
- else if (typeof resolved === "function") base = resolved(ctx);
1480
- else base = buildBaseSystemPrompt(ctx);
1481
- const composed = composeSystemPrompt(base, registered.instructions);
1482
- const catalog = registered.skills?.catalog;
1483
- if (!catalog || catalog.length === 0) return composed;
1484
- return `${composed}\n\n${renderSkillCatalog(catalog)}`;
1485
- }
1486
- /**
1487
- * Pulls the LLM-readable description off any {@link SupervisorTool} kind.
1488
- * Used to populate the synthetic placeholder `def.description` on
1489
- * hosted-supervisor tool-index entries.
1490
- */
1491
- function supervisorToolDescription(spec) {
1492
- switch (spec.type) {
1493
- case "genie_space": return spec.genie_space.description;
1494
- case "uc_function": return spec.uc_function.description;
1495
- case "knowledge_assistant": return spec.knowledge_assistant.description;
1496
- case "app": return spec.app.description;
1497
- case "uc_connection": return spec.uc_connection.description;
1498
- }
1499
- }
1500
- /**
1501
- * Builds the `AgentInput.extensions` payload from a tool index, aggregating
1502
- * the hosted-supervisor specs under {@link SUPERVISOR_EXTENSION_KEY}. Returns
1503
- * `undefined` when there are no adapter-side hosted tools so the field stays
1504
- * absent on the wire — adapters that don't read extensions never see it.
1505
- */
1506
- function buildAdapterExtensions(toolIndex) {
1507
- const supervisorSpecs = [];
1508
- for (const entry of toolIndex.values()) if (entry.source === "hosted-supervisor") supervisorSpecs.push(entry.spec);
1509
- if (supervisorSpecs.length === 0) return void 0;
1510
- return { [SUPERVISOR_EXTENSION_KEY]: { hostedTools: supervisorSpecs } };
1511
- }
1512
- /**
1513
- * Compares the adapter's declared capabilities against the tool index and
1514
- * logs a warning when the agent's tool declarations would be silently
1515
- * dropped at runtime. Warn-not-throw: misconfiguration is loud enough to
1516
- * notice without taking the whole app down.
1517
- */
1518
- function warnOnCapabilityMismatch(agentName, adapter, toolIndex) {
1519
- const accepted = new Set(adapter.acceptsExtensions ?? []);
1520
- const hostedSupervisorKeys = [];
1521
- const inputToolKeys = [];
1522
- for (const [key, entry] of toolIndex) if (entry.source === "hosted-supervisor") hostedSupervisorKeys.push(key);
1523
- else inputToolKeys.push(key);
1524
- if (hostedSupervisorKeys.length > 0 && !accepted.has(SUPERVISOR_EXTENSION_KEY)) logger.warn(`Agent '${agentName}' declares hosted-supervisor tools (${hostedSupervisorKeys.join(", ")}) but its model adapter does not accept the 'databricks.supervisor' extension. These tools will not reach the model. Pair them with \`DatabricksAdapter.fromSupervisorApi(...)\`, or remove them.`);
1525
- if (adapter.consumesInputTools === false && inputToolKeys.length > 0) logger.warn(`Agent '${agentName}' declares function tools / sub-agents / MCP tools (${inputToolKeys.join(", ")}) but its model adapter does not consume input.tools (Supervisor API owns its own tool loop). These tools will not be exposed to the model. See docs/plugins/agents.md.`);
1526
- }
1527
1153
  /**
1528
1154
  * Plugin factory for the agents plugin. Discovers agents from
1529
1155
  * `server/agents/<id>/agent.{ts,md}` by default (markdown still in