@databricks/appkit 0.68.0 → 0.69.0

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 (44) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/core/agent/skills/types.d.ts +6 -1
  3. package/dist/core/agent/skills/types.d.ts.map +1 -1
  4. package/dist/plugins/agents/active-stream-tracker.js +52 -0
  5. package/dist/plugins/agents/active-stream-tracker.js.map +1 -0
  6. package/dist/plugins/agents/adapter-extensions.js +50 -0
  7. package/dist/plugins/agents/adapter-extensions.js.map +1 -0
  8. package/dist/plugins/agents/agents.d.ts +6 -77
  9. package/dist/plugins/agents/agents.d.ts.map +1 -1
  10. package/dist/plugins/agents/agents.js +57 -431
  11. package/dist/plugins/agents/agents.js.map +1 -1
  12. package/dist/plugins/agents/approval.js +28 -0
  13. package/dist/plugins/agents/approval.js.map +1 -0
  14. package/dist/plugins/agents/builtin-tools.js +39 -0
  15. package/dist/plugins/agents/builtin-tools.js.map +1 -0
  16. package/dist/plugins/agents/prompt.js +27 -0
  17. package/dist/plugins/agents/prompt.js.map +1 -0
  18. package/dist/plugins/agents/registry.js +56 -0
  19. package/dist/plugins/agents/registry.js.map +1 -0
  20. package/dist/plugins/agents/resolve-config.js +40 -0
  21. package/dist/plugins/agents/resolve-config.js.map +1 -0
  22. package/dist/plugins/agents/skill-loader.js +139 -0
  23. package/dist/plugins/agents/skill-loader.js.map +1 -0
  24. package/dist/plugins/agents/tool-dispatch.js +124 -0
  25. package/dist/plugins/agents/tool-dispatch.js.map +1 -0
  26. package/dist/plugins/server/index.js +2 -2
  27. package/dist/plugins/server/index.js.map +1 -1
  28. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  29. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  30. package/dist/plugins/server/static-server.js +3 -3
  31. package/dist/plugins/server/static-server.js.map +1 -1
  32. package/dist/plugins/server/utils.js +3 -3
  33. package/dist/plugins/server/utils.js.map +1 -1
  34. package/dist/plugins/server/vite-dev-server.js +4 -4
  35. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  36. package/dist/shared/src/schemas/manifest.d.ts +33 -33
  37. package/dist/type-generator/database/generate.js +3 -3
  38. package/dist/type-generator/database/generate.js.map +1 -1
  39. package/dist/type-generator/migration.js +2 -2
  40. package/dist/type-generator/migration.js.map +1 -1
  41. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  42. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  43. package/package.json +1 -1
  44. package/sbom.cdx.json +1 -1
@@ -0,0 +1,27 @@
1
+ import { renderSkillCatalog } from "../../core/agent/skills/render.js";
2
+ import "../../core/agent/skills/index.js";
3
+ import { buildBaseSystemPrompt, composeSystemPrompt } from "../../core/agent/system-prompt.js";
4
+
5
+ //#region src/plugins/agents/prompt.ts
6
+ /**
7
+ * Composes an agent's full system prompt: resolves the base prompt (per-agent
8
+ * override, else plugin-level, else the built-in default), combines it with the
9
+ * agent's instructions, then appends the always-on skill catalog.
10
+ */
11
+ function composePromptForAgent(registered, pluginLevel, ctx) {
12
+ const perAgent = registered.baseSystemPrompt;
13
+ const resolved = perAgent !== void 0 ? perAgent : pluginLevel;
14
+ let base = "";
15
+ if (resolved === false) base = "";
16
+ else if (typeof resolved === "string") base = resolved;
17
+ else if (typeof resolved === "function") base = resolved(ctx);
18
+ else base = buildBaseSystemPrompt(ctx);
19
+ const composed = composeSystemPrompt(base, registered.instructions);
20
+ const catalog = registered.skills?.catalog;
21
+ if (!catalog || catalog.length === 0) return composed;
22
+ return `${composed}\n\n${renderSkillCatalog(catalog)}`;
23
+ }
24
+
25
+ //#endregion
26
+ export { composePromptForAgent };
27
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","names":[],"sources":["../../../src/plugins/agents/prompt.ts"],"sourcesContent":["import { renderSkillCatalog } from \"../../core/agent/skills\";\nimport {\n buildBaseSystemPrompt,\n composeSystemPrompt,\n} from \"../../core/agent/system-prompt\";\nimport type {\n BaseSystemPromptOption,\n PromptContext,\n RegisteredAgent,\n} from \"../../core/agent/types\";\n\n/**\n * Composes an agent's full system prompt: resolves the base prompt (per-agent\n * override, else plugin-level, else the built-in default), combines it with the\n * agent's instructions, then appends the always-on skill catalog.\n */\nexport function composePromptForAgent(\n registered: RegisteredAgent,\n pluginLevel: BaseSystemPromptOption | undefined,\n ctx: PromptContext,\n): string {\n const perAgent = registered.baseSystemPrompt;\n const resolved = perAgent !== undefined ? perAgent : pluginLevel;\n\n let base = \"\";\n if (resolved === false) {\n base = \"\";\n } else if (typeof resolved === \"string\") {\n base = resolved;\n } else if (typeof resolved === \"function\") {\n base = resolved(ctx);\n } else {\n base = buildBaseSystemPrompt(ctx);\n }\n\n const composed = composeSystemPrompt(base, registered.instructions);\n\n // Append the always-on skill catalog (name + description only). Done here,\n // after composeSystemPrompt, so it survives a custom/`false` base prompt.\n const catalog = registered.skills?.catalog;\n if (!catalog || catalog.length === 0) return composed;\n return `${composed}\\n\\n${renderSkillCatalog(catalog)}`;\n}\n"],"mappings":";;;;;;;;;;AAgBA,SAAgB,sBACd,YACA,aACA,KACQ;CACR,MAAM,WAAW,WAAW;CAC5B,MAAM,WAAW,aAAa,SAAY,WAAW;CAErD,IAAI,OAAO;AACX,KAAI,aAAa,MACf,QAAO;UACE,OAAO,aAAa,SAC7B,QAAO;UACE,OAAO,aAAa,WAC7B,QAAO,SAAS,IAAI;KAEpB,QAAO,sBAAsB,IAAI;CAGnC,MAAM,WAAW,oBAAoB,MAAM,WAAW,aAAa;CAInE,MAAM,UAAU,WAAW,QAAQ;AACnC,KAAI,CAAC,WAAW,QAAQ,WAAW,EAAG,QAAO;AAC7C,QAAO,GAAG,SAAS,MAAM,mBAAmB,QAAQ"}
@@ -0,0 +1,56 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import { loadCodeAgentsFromDir, resolveCodeAgentsDir } from "../../core/agent/load-code-agents.js";
3
+ import path from "node:path";
4
+ import { existsSync, readdirSync } from "node:fs";
5
+
6
+ //#region src/plugins/agents/registry.ts
7
+ const logger = createLogger("agents");
8
+ /** True when `dir` holds at least one `<id>/agent.ts` folder. */
9
+ function hasCodeAgentSources(dir) {
10
+ try {
11
+ return readdirSync(dir, { withFileTypes: true }).some((e) => {
12
+ if (!e.isDirectory() && !e.isSymbolicLink()) return false;
13
+ try {
14
+ return readdirSync(path.join(dir, e.name)).includes("agent.ts");
15
+ } catch {
16
+ return false;
17
+ }
18
+ });
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Discovers code agents (see {@link resolveCodeAgentsDir} and
25
+ * {@link loadCodeAgentsFromDir}). Warns if sources exist but nothing was
26
+ * discovered — usually the build didn't emit the compiled agents — unless
27
+ * the deprecated `agents({ agents })` map is carrying them instead.
28
+ */
29
+ async function loadCodeAgents(opts) {
30
+ const resolved = resolveCodeAgentsDir({
31
+ cwd: process.cwd(),
32
+ exists: existsSync
33
+ });
34
+ const discovered = await loadCodeAgentsFromDir(resolved.dir, { extensions: resolved.extensions });
35
+ if (Object.keys(discovered).length === 0 && !opts.hasDeprecatedMap && hasCodeAgentSources(opts.agentsDir)) 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.", opts.agentsDir, resolved.dir);
36
+ return discovered;
37
+ }
38
+ /**
39
+ * Resolves the default agent. Precedence: explicit `configDefault` >
40
+ * a code/discovered agent flagged `default: true` (stable id order) >
41
+ * markdown `default: true` > first registered (insertion order).
42
+ */
43
+ function resolveDefaultAgent(agents, merged, fileDefault, configDefault) {
44
+ if (configDefault) {
45
+ if (!agents.has(configDefault)) throw new Error(`defaultAgent '${configDefault}' is not registered. Available: ${Array.from(agents.keys()).join(", ")}`);
46
+ return configDefault;
47
+ }
48
+ const codeDefault = Object.keys(merged).filter((id) => merged[id].src.origin === "code" && merged[id].def.default).sort()[0];
49
+ if (codeDefault) return codeDefault;
50
+ if (fileDefault && agents.has(fileDefault)) return fileDefault;
51
+ return agents.keys().next().value ?? null;
52
+ }
53
+
54
+ //#endregion
55
+ export { loadCodeAgents, resolveDefaultAgent };
56
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","names":[],"sources":["../../../src/plugins/agents/registry.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport {\n loadCodeAgentsFromDir,\n resolveCodeAgentsDir,\n} from \"../../core/agent/load-code-agents\";\nimport type { AgentDefinition, RegisteredAgent } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"agents\");\n\n/**\n * Context flag recorded on the in-memory AgentDefinition to indicate whether\n * it came from markdown (file) or from user code. Drives the asymmetric\n * `autoInheritTools` default.\n */\nexport interface AgentSource {\n origin: \"file\" | \"code\";\n}\n\n/** True when `dir` holds at least one `<id>/agent.ts` folder. */\nfunction hasCodeAgentSources(dir: string): boolean {\n try {\n return readdirSync(dir, { withFileTypes: true }).some((e) => {\n if (!e.isDirectory() && !e.isSymbolicLink()) return false;\n try {\n return readdirSync(path.join(dir, e.name)).includes(\"agent.ts\");\n } catch {\n return false;\n }\n });\n } catch {\n return false;\n }\n}\n\n/**\n * Discovers code agents (see {@link resolveCodeAgentsDir} and\n * {@link loadCodeAgentsFromDir}). Warns if sources exist but nothing was\n * discovered — usually the build didn't emit the compiled agents — unless\n * the deprecated `agents({ agents })` map is carrying them instead.\n */\nexport async function loadCodeAgents(opts: {\n agentsDir: string;\n hasDeprecatedMap: boolean;\n}): Promise<Record<string, AgentDefinition>> {\n const resolved = resolveCodeAgentsDir({\n cwd: process.cwd(),\n exists: existsSync,\n });\n\n const discovered = await loadCodeAgentsFromDir(resolved.dir, {\n extensions: resolved.extensions,\n });\n\n if (\n Object.keys(discovered).length === 0 &&\n !opts.hasDeprecatedMap &&\n hasCodeAgentSources(opts.agentsDir)\n ) {\n logger.warn(\n \"Found code-agent sources in %s but discovered no code agents (scanned %s). \" +\n \"In a production build, ensure `<dir>/*/agent.ts` is included as tsdown entries so the compiled agents are emitted.\",\n opts.agentsDir,\n resolved.dir,\n );\n }\n\n return discovered;\n}\n\n/**\n * Resolves the default agent. Precedence: explicit `configDefault` >\n * a code/discovered agent flagged `default: true` (stable id order) >\n * markdown `default: true` > first registered (insertion order).\n */\nexport function resolveDefaultAgent(\n agents: Map<string, RegisteredAgent>,\n merged: Record<string, { def: AgentDefinition; src: AgentSource }>,\n fileDefault: string | null,\n configDefault: string | undefined,\n): string | null {\n if (configDefault) {\n if (!agents.has(configDefault)) {\n throw new Error(\n `defaultAgent '${configDefault}' is not registered. Available: ${Array.from(agents.keys()).join(\", \")}`,\n );\n }\n return configDefault;\n }\n\n const codeDefault = Object.keys(merged)\n .filter((id) => merged[id].src.origin === \"code\" && merged[id].def.default)\n .sort()[0];\n if (codeDefault) return codeDefault;\n\n if (fileDefault && agents.has(fileDefault)) return fileDefault;\n\n return agents.keys().next().value ?? null;\n}\n"],"mappings":";;;;;;AAUA,MAAM,SAAS,aAAa,SAAS;;AAYrC,SAAS,oBAAoB,KAAsB;AACjD,KAAI;AACF,SAAO,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,CAAC,MAAM,MAAM;AAC3D,OAAI,CAAC,EAAE,aAAa,IAAI,CAAC,EAAE,gBAAgB,CAAE,QAAO;AACpD,OAAI;AACF,WAAO,YAAY,KAAK,KAAK,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,WAAW;WACzD;AACN,WAAO;;IAET;SACI;AACN,SAAO;;;;;;;;;AAUX,eAAsB,eAAe,MAGQ;CAC3C,MAAM,WAAW,qBAAqB;EACpC,KAAK,QAAQ,KAAK;EAClB,QAAQ;EACT,CAAC;CAEF,MAAM,aAAa,MAAM,sBAAsB,SAAS,KAAK,EAC3D,YAAY,SAAS,YACtB,CAAC;AAEF,KACE,OAAO,KAAK,WAAW,CAAC,WAAW,KACnC,CAAC,KAAK,oBACN,oBAAoB,KAAK,UAAU,CAEnC,QAAO,KACL,iMAEA,KAAK,WACL,SAAS,IACV;AAGH,QAAO;;;;;;;AAQT,SAAgB,oBACd,QACA,QACA,aACA,eACe;AACf,KAAI,eAAe;AACjB,MAAI,CAAC,OAAO,IAAI,cAAc,CAC5B,OAAM,IAAI,MACR,iBAAiB,cAAc,kCAAkC,MAAM,KAAK,OAAO,MAAM,CAAC,CAAC,KAAK,KAAK,GACtG;AAEH,SAAO;;CAGT,MAAM,cAAc,OAAO,KAAK,OAAO,CACpC,QAAQ,OAAO,OAAO,IAAI,IAAI,WAAW,UAAU,OAAO,IAAI,IAAI,QAAQ,CAC1E,MAAM,CAAC;AACV,KAAI,YAAa,QAAO;AAExB,KAAI,eAAe,OAAO,IAAI,YAAY,CAAE,QAAO;AAEnD,QAAO,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS"}
@@ -0,0 +1,40 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+
3
+ //#region src/plugins/agents/resolve-config.ts
4
+ const logger = createLogger("agents");
5
+ const APPROVAL_TIMEOUT_FLOOR_MS = 1e3;
6
+ const APPROVAL_TIMEOUT_DEFAULT_MS = 6e4;
7
+ /**
8
+ * Effective approval policy with defaults applied. `timeoutMs` is clamped to a
9
+ * 1s floor so a misconfigured value (`0`, negative, or `NaN`) can't degrade
10
+ * into immediate auto-denial of every mutating tool call.
11
+ *
12
+ * The caller memoises the result, so the floor warning fires at most once per
13
+ * plugin instance rather than on every chat stream.
14
+ */
15
+ function resolveApprovalPolicy(config) {
16
+ const cfg = config.approval ?? {};
17
+ let timeoutMs = cfg.timeoutMs ?? APPROVAL_TIMEOUT_DEFAULT_MS;
18
+ if (!Number.isFinite(timeoutMs) || timeoutMs < APPROVAL_TIMEOUT_FLOOR_MS) {
19
+ 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);
20
+ timeoutMs = APPROVAL_TIMEOUT_DEFAULT_MS;
21
+ }
22
+ return {
23
+ requireForDestructive: cfg.requireForDestructive ?? true,
24
+ timeoutMs
25
+ };
26
+ }
27
+ /** Effective DoS limits with defaults applied. */
28
+ function resolveLimits(config) {
29
+ const cfg = config.limits ?? {};
30
+ return {
31
+ maxConcurrentStreamsPerUser: cfg.maxConcurrentStreamsPerUser ?? 5,
32
+ maxToolCalls: cfg.maxToolCalls ?? 50,
33
+ maxSubAgentDepth: cfg.maxSubAgentDepth ?? 3,
34
+ toolCallTimeoutMs: cfg.toolCallTimeoutMs ?? 3e5
35
+ };
36
+ }
37
+
38
+ //#endregion
39
+ export { resolveApprovalPolicy, resolveLimits };
40
+ //# sourceMappingURL=resolve-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-config.js","names":[],"sources":["../../../src/plugins/agents/resolve-config.ts"],"sourcesContent":["import type { AgentsPluginConfig } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"agents\");\n\nconst APPROVAL_TIMEOUT_FLOOR_MS = 1_000;\nconst APPROVAL_TIMEOUT_DEFAULT_MS = 60_000;\n\n/**\n * Effective approval policy with defaults applied. `timeoutMs` is clamped to a\n * 1s floor so a misconfigured value (`0`, negative, or `NaN`) can't degrade\n * into immediate auto-denial of every mutating tool call.\n *\n * The caller memoises the result, so the floor warning fires at most once per\n * plugin instance rather than on every chat stream.\n */\nexport function resolveApprovalPolicy(config: AgentsPluginConfig): {\n requireForDestructive: boolean;\n timeoutMs: number;\n} {\n const cfg = config.approval ?? {};\n let timeoutMs = cfg.timeoutMs ?? APPROVAL_TIMEOUT_DEFAULT_MS;\n if (!Number.isFinite(timeoutMs) || timeoutMs < APPROVAL_TIMEOUT_FLOOR_MS) {\n logger.warn(\n \"approval.timeoutMs=%s is below the %sms floor; using default %sms instead. Mutating tool calls would otherwise auto-deny before any UI could respond.\",\n cfg.timeoutMs,\n APPROVAL_TIMEOUT_FLOOR_MS,\n APPROVAL_TIMEOUT_DEFAULT_MS,\n );\n timeoutMs = APPROVAL_TIMEOUT_DEFAULT_MS;\n }\n return {\n requireForDestructive: cfg.requireForDestructive ?? true,\n timeoutMs,\n };\n}\n\n/** Effective DoS limits with defaults applied. */\nexport function resolveLimits(config: AgentsPluginConfig): {\n maxConcurrentStreamsPerUser: number;\n maxToolCalls: number;\n maxSubAgentDepth: number;\n toolCallTimeoutMs: number;\n} {\n const cfg = config.limits ?? {};\n return {\n maxConcurrentStreamsPerUser: cfg.maxConcurrentStreamsPerUser ?? 5,\n maxToolCalls: cfg.maxToolCalls ?? 50,\n maxSubAgentDepth: cfg.maxSubAgentDepth ?? 3,\n // 5 minutes is the floor for cold SQL Warehouse / long Genie /\n // long Lakebase calls. The previous PluginContext default of 30s\n // truncated legitimate analytics queries on cold compute.\n toolCallTimeoutMs: cfg.toolCallTimeoutMs ?? 300_000,\n };\n}\n"],"mappings":";;;AAGA,MAAM,SAAS,aAAa,SAAS;AAErC,MAAM,4BAA4B;AAClC,MAAM,8BAA8B;;;;;;;;;AAUpC,SAAgB,sBAAsB,QAGpC;CACA,MAAM,MAAM,OAAO,YAAY,EAAE;CACjC,IAAI,YAAY,IAAI,aAAa;AACjC,KAAI,CAAC,OAAO,SAAS,UAAU,IAAI,YAAY,2BAA2B;AACxE,SAAO,KACL,yJACA,IAAI,WACJ,2BACA,4BACD;AACD,cAAY;;AAEd,QAAO;EACL,uBAAuB,IAAI,yBAAyB;EACpD;EACD;;;AAIH,SAAgB,cAAc,QAK5B;CACA,MAAM,MAAM,OAAO,UAAU,EAAE;AAC/B,QAAO;EACL,6BAA6B,IAAI,+BAA+B;EAChE,cAAc,IAAI,gBAAgB;EAClC,kBAAkB,IAAI,oBAAoB;EAI1C,mBAAmB,IAAI,qBAAqB;EAC7C"}
@@ -0,0 +1,139 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import { FilesConnector } from "../../connectors/files/client.js";
3
+ import "../../connectors/files/index.js";
4
+ import { parseSkill } from "../../core/agent/skills/parse-skill.js";
5
+ import { loadSkillsFromDir } from "../../core/agent/skills/load-skills.js";
6
+ import { readSkillResource } from "../../core/agent/skills/read-resource.js";
7
+ import { renderLoadedSkill } from "../../core/agent/skills/render.js";
8
+ import { resolveSkill, resolveSkillCatalog } from "../../core/agent/skills/resolve-catalog.js";
9
+ import "../../core/agent/skills/index.js";
10
+ import path from "node:path";
11
+
12
+ //#region src/plugins/agents/skill-loader.ts
13
+ const logger = createLogger("agents");
14
+ /** Loads the shared global skill pool from `<agentsDir>/skills/`. */
15
+ async function loadGlobalSkills(agentsDir) {
16
+ if (!agentsDir) return [];
17
+ return loadSkillsFromDir(path.join(agentsDir, "skills"), "bundle-global");
18
+ }
19
+ /** Configured catalog-skills volume path, or null when none is set. */
20
+ function resolveSkillsVolume(config) {
21
+ const configured = config.skillsVolume ?? process.env.DATABRICKS_VOLUME_AGENT_SKILLS;
22
+ return configured && configured.trim() !== "" ? configured.trim() : null;
23
+ }
24
+ /**
25
+ * Discovers catalog skills from the configured UC Volume, read as the
26
+ * service principal at boot (and on `reload()`). Each `<volume>/<name>/`
27
+ * folder with a `SKILL.md` becomes a `source: "volume"` skill. Best-effort:
28
+ * a missing volume, unavailable workspace client, or a malformed individual
29
+ * skill is logged and skipped rather than failing the whole registry build.
30
+ *
31
+ * `getClient` is a thunk so the workspace-client resolution (the single
32
+ * switch point for a future OBO mode) stays with the caller and is resolved
33
+ * lazily inside the try/catch here.
34
+ */
35
+ async function loadVolumeSkills(config, getClient) {
36
+ const volume = resolveSkillsVolume(config);
37
+ if (!volume) return [];
38
+ if ((config.skillCredentialMode ?? "sp") === "obo") logger.warn("skillCredentialMode 'obo' is not wired yet; reading catalog skills as the service principal.");
39
+ let client;
40
+ try {
41
+ client = getClient();
42
+ } catch (err) {
43
+ logger.warn("Skipping catalog skills at '%s': no workspace client available (%s).", volume, err instanceof Error ? err.message : String(err));
44
+ return [];
45
+ }
46
+ const connector = new FilesConnector({ defaultVolume: volume });
47
+ let entries;
48
+ try {
49
+ entries = await connector.list(client, volume);
50
+ } catch (err) {
51
+ logger.warn("Failed to list catalog skills volume '%s': %s", volume, err instanceof Error ? err.message : String(err));
52
+ return [];
53
+ }
54
+ return (await Promise.all(entries.map(async (entry) => {
55
+ if (!entry.is_directory || !entry.name || !entry.path) return null;
56
+ const skillDir = entry.path;
57
+ const skillFile = `${skillDir}/SKILL.md`;
58
+ try {
59
+ const parsed = parseSkill(await connector.read(client, skillFile), skillFile);
60
+ const files = await listVolumeSkillFiles(connector, client, skillDir);
61
+ return {
62
+ name: parsed.name,
63
+ description: parsed.description,
64
+ body: parsed.body,
65
+ source: "volume",
66
+ dir: skillDir,
67
+ files,
68
+ allowedTools: parsed.allowedTools
69
+ };
70
+ } catch (err) {
71
+ logger.warn("Skipping catalog skill '%s': %s", skillDir, err instanceof Error ? err.message : String(err));
72
+ return null;
73
+ }
74
+ }))).filter((s) => s !== null);
75
+ }
76
+ /** Lists a volume skill's resource files (one level, excluding SKILL.md). */
77
+ async function listVolumeSkillFiles(connector, client, skillDir) {
78
+ try {
79
+ return (await connector.list(client, skillDir)).filter((e) => !e.is_directory && e.name && e.name !== "SKILL.md").map((e) => e.name).sort();
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+ /**
85
+ * Resolves the per-agent skill catalog: loads this agent's private skills
86
+ * (`<agentsDir>/<id>/skills/`, file-origin only), then applies visibility
87
+ * (opt-in via `def.skills` or the resolved `autoInherit`) and collision rules
88
+ * against the shared global pool. Returns `undefined` when nothing is visible
89
+ * so the prompt catalog and dispatch can cheaply skip skills.
90
+ */
91
+ async function resolveAgentSkills(opts) {
92
+ const perAgentSkills = opts.isFileOrigin && opts.agentsDir ? await loadSkillsFromDir(path.join(opts.agentsDir, opts.name, "skills"), "bundle-agent") : [];
93
+ const catalog = resolveSkillCatalog({
94
+ agentName: opts.name,
95
+ agentSkillNames: opts.def.skills,
96
+ perAgentSkills,
97
+ globalSkills: opts.globalSkills,
98
+ autoInherit: opts.autoInherit
99
+ });
100
+ return catalog.byAddress.size > 0 ? catalog : void 0;
101
+ }
102
+ /**
103
+ * Executes the built-in `load_skill` / `read_skill_file` tools against the
104
+ * agent's resolved skill catalog. `load_skill` returns a skill's body plus a
105
+ * manifest of bundled files; `read_skill_file` returns the contents of one
106
+ * of those files.
107
+ */
108
+ async function dispatchSkillTool(entry, args, getClient) {
109
+ const obj = typeof args === "object" && args !== null ? args : {};
110
+ const skillName = typeof obj.skill === "string" ? obj.skill.trim() : "";
111
+ if (!skillName) throw new Error(`'${entry.builtin}' requires a 'skill' argument naming the skill to use.`);
112
+ const skill = resolveSkill(entry.catalog, skillName);
113
+ if (entry.builtin === "load_skill") return renderLoadedSkill(skill);
114
+ const filePath = typeof obj.path === "string" ? obj.path.trim() : "";
115
+ if (!filePath) throw new Error("'read_skill_file' requires a 'path' argument.");
116
+ if (!skill.files.includes(filePath)) throw new Error(`Skill '${skill.name}' has no bundled file '${filePath}'. Available: ${skill.files.join(", ") || "<none>"}.`);
117
+ if (skill.source === "volume") return new FilesConnector({ defaultVolume: skill.dir }).read(getClient(), `${skill.dir}/${filePath}`);
118
+ return readSkillResource(skill.dir, filePath);
119
+ }
120
+ /**
121
+ * Renders the prompt addendum for a force-loaded skill (`/skill-name`).
122
+ * Returns null when the agent has no catalog or the name doesn't resolve —
123
+ * an unusable request shouldn't fail the whole turn, so it's logged and the
124
+ * model proceeds with the catalog + `load_skill` still available.
125
+ */
126
+ function renderForcedSkill(registered, name) {
127
+ if (!registered.skills) return null;
128
+ try {
129
+ const skill = resolveSkill(registered.skills, name);
130
+ return `The user explicitly requested the "${skill.name}" skill for this turn. Its instructions:\n\n${renderLoadedSkill(skill)}`;
131
+ } catch (err) {
132
+ logger.warn("Ignoring forced skill '%s': %s", name, err instanceof Error ? err.message : String(err));
133
+ return null;
134
+ }
135
+ }
136
+
137
+ //#endregion
138
+ export { dispatchSkillTool, loadGlobalSkills, loadVolumeSkills, renderForcedSkill, resolveAgentSkills };
139
+ //# sourceMappingURL=skill-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-loader.js","names":[],"sources":["../../../src/plugins/agents/skill-loader.ts"],"sourcesContent":["import path from \"node:path\";\n\nimport { FilesConnector } from \"../../connectors/files\";\nimport {\n loadSkillsFromDir,\n parseSkill,\n readSkillResource,\n renderLoadedSkill,\n type ResolvedSkillCatalog,\n resolveSkill,\n resolveSkillCatalog,\n type SkillDefinition,\n} from \"../../core/agent/skills\";\nimport type {\n AgentDefinition,\n AgentsPluginConfig,\n RegisteredAgent,\n ResolvedToolEntry,\n} from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\n\nconst logger = createLogger(\"agents\");\n\n/** Loads the shared global skill pool from `<agentsDir>/skills/`. */\nexport async function loadGlobalSkills(\n agentsDir: string,\n): Promise<SkillDefinition[]> {\n if (!agentsDir) return [];\n return loadSkillsFromDir(path.join(agentsDir, \"skills\"), \"bundle-global\");\n}\n\n/** Configured catalog-skills volume path, or null when none is set. */\nfunction resolveSkillsVolume(config: AgentsPluginConfig): string | null {\n const configured =\n config.skillsVolume ?? process.env.DATABRICKS_VOLUME_AGENT_SKILLS;\n return configured && configured.trim() !== \"\" ? configured.trim() : null;\n}\n\n/**\n * Discovers catalog skills from the configured UC Volume, read as the\n * service principal at boot (and on `reload()`). Each `<volume>/<name>/`\n * folder with a `SKILL.md` becomes a `source: \"volume\"` skill. Best-effort:\n * a missing volume, unavailable workspace client, or a malformed individual\n * skill is logged and skipped rather than failing the whole registry build.\n *\n * `getClient` is a thunk so the workspace-client resolution (the single\n * switch point for a future OBO mode) stays with the caller and is resolved\n * lazily inside the try/catch here.\n */\nexport async function loadVolumeSkills(\n config: AgentsPluginConfig,\n getClient: () => WorkspaceClient,\n): Promise<SkillDefinition[]> {\n const volume = resolveSkillsVolume(config);\n if (!volume) return [];\n\n if ((config.skillCredentialMode ?? \"sp\") === \"obo\") {\n logger.warn(\n \"skillCredentialMode 'obo' is not wired yet; reading catalog skills as the service principal.\",\n );\n }\n\n let client: WorkspaceClient;\n try {\n client = getClient();\n } catch (err) {\n logger.warn(\n \"Skipping catalog skills at '%s': no workspace client available (%s).\",\n volume,\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n\n const connector = new FilesConnector({ defaultVolume: volume });\n let entries: Awaited<ReturnType<FilesConnector[\"list\"]>>;\n try {\n entries = await connector.list(client, volume);\n } catch (err) {\n logger.warn(\n \"Failed to list catalog skills volume '%s': %s\",\n volume,\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n\n // Read every skill concurrently — each is a network round-trip to the\n // volume, so serial reads would add up. A per-skill failure skips only\n // that skill; Promise.all preserves the (sorted) entry order.\n const loaded = await Promise.all(\n entries.map(async (entry): Promise<SkillDefinition | null> => {\n if (!entry.is_directory || !entry.name || !entry.path) return null;\n const skillDir = entry.path;\n const skillFile = `${skillDir}/SKILL.md`;\n try {\n const raw = await connector.read(client, skillFile);\n const parsed = parseSkill(raw, skillFile);\n const files = await listVolumeSkillFiles(connector, client, skillDir);\n return {\n name: parsed.name,\n description: parsed.description,\n body: parsed.body,\n source: \"volume\",\n dir: skillDir,\n files,\n allowedTools: parsed.allowedTools,\n };\n } catch (err) {\n logger.warn(\n \"Skipping catalog skill '%s': %s\",\n skillDir,\n err instanceof Error ? err.message : String(err),\n );\n return null;\n }\n }),\n );\n return loaded.filter((s): s is SkillDefinition => s !== null);\n}\n\n/** Lists a volume skill's resource files (one level, excluding SKILL.md). */\nasync function listVolumeSkillFiles(\n connector: FilesConnector,\n client: WorkspaceClient,\n skillDir: string,\n): Promise<string[]> {\n try {\n const entries = await connector.list(client, skillDir);\n return entries\n .filter((e) => !e.is_directory && e.name && e.name !== \"SKILL.md\")\n .map((e) => e.name as string)\n .sort();\n } catch {\n return [];\n }\n}\n\n/**\n * Resolves the per-agent skill catalog: loads this agent's private skills\n * (`<agentsDir>/<id>/skills/`, file-origin only), then applies visibility\n * (opt-in via `def.skills` or the resolved `autoInherit`) and collision rules\n * against the shared global pool. Returns `undefined` when nothing is visible\n * so the prompt catalog and dispatch can cheaply skip skills.\n */\nexport async function resolveAgentSkills(opts: {\n name: string;\n def: AgentDefinition;\n agentsDir: string;\n isFileOrigin: boolean;\n autoInherit: boolean;\n globalSkills: SkillDefinition[];\n}): Promise<ResolvedSkillCatalog | undefined> {\n const perAgentSkills =\n opts.isFileOrigin && opts.agentsDir\n ? await loadSkillsFromDir(\n path.join(opts.agentsDir, opts.name, \"skills\"),\n \"bundle-agent\",\n )\n : [];\n\n const catalog = resolveSkillCatalog({\n agentName: opts.name,\n agentSkillNames: opts.def.skills,\n perAgentSkills,\n globalSkills: opts.globalSkills,\n autoInherit: opts.autoInherit,\n });\n\n return catalog.byAddress.size > 0 ? catalog : undefined;\n}\n\n/**\n * Executes the built-in `load_skill` / `read_skill_file` tools against the\n * agent's resolved skill catalog. `load_skill` returns a skill's body plus a\n * manifest of bundled files; `read_skill_file` returns the contents of one\n * of those files.\n */\nexport async function dispatchSkillTool(\n entry: Extract<ResolvedToolEntry, { source: \"skill\" }>,\n args: unknown,\n getClient: () => WorkspaceClient,\n): Promise<string> {\n const obj =\n typeof args === \"object\" && args !== null\n ? (args as Record<string, unknown>)\n : {};\n const skillName = typeof obj.skill === \"string\" ? obj.skill.trim() : \"\";\n if (!skillName) {\n throw new Error(\n `'${entry.builtin}' requires a 'skill' argument naming the skill to use.`,\n );\n }\n\n const skill = resolveSkill(entry.catalog, skillName);\n\n if (entry.builtin === \"load_skill\") {\n return renderLoadedSkill(skill);\n }\n\n // read_skill_file\n const filePath = typeof obj.path === \"string\" ? obj.path.trim() : \"\";\n if (!filePath) {\n throw new Error(\"'read_skill_file' requires a 'path' argument.\");\n }\n if (!skill.files.includes(filePath)) {\n throw new Error(\n `Skill '${skill.name}' has no bundled file '${filePath}'. Available: ${\n skill.files.join(\", \") || \"<none>\"\n }.`,\n );\n }\n\n if (skill.source === \"volume\") {\n const connector = new FilesConnector({ defaultVolume: skill.dir });\n return connector.read(getClient(), `${skill.dir}/${filePath}`);\n }\n return readSkillResource(skill.dir, filePath);\n}\n\n/**\n * Renders the prompt addendum for a force-loaded skill (`/skill-name`).\n * Returns null when the agent has no catalog or the name doesn't resolve —\n * an unusable request shouldn't fail the whole turn, so it's logged and the\n * model proceeds with the catalog + `load_skill` still available.\n */\nexport function renderForcedSkill(\n registered: RegisteredAgent,\n name: string,\n): string | null {\n if (!registered.skills) return null;\n try {\n const skill = resolveSkill(registered.skills, name);\n return `The user explicitly requested the \"${skill.name}\" skill for this turn. Its instructions:\\n\\n${renderLoadedSkill(skill)}`;\n } catch (err) {\n logger.warn(\n \"Ignoring forced skill '%s': %s\",\n name,\n err instanceof Error ? err.message : String(err),\n );\n return null;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import { consumeAdapterStream } from "../../core/agent/consume-adapter-stream.js";
2
+ import { buildAdapterExtensions } from "./adapter-extensions.js";
3
+ import { requiresApproval } from "./approval.js";
4
+ import { traceTool } from "./mlflow.js";
5
+ import { composePromptForAgent } from "./prompt.js";
6
+ import { normalizeToolResult } from "../../core/agent/normalize-result.js";
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ //#region src/plugins/agents/tool-dispatch.ts
10
+ /**
11
+ * Dispatch a single tool call from either the top-level adapter or a
12
+ * sub-agent. Centralising this in one function is what makes the budget
13
+ * counter, approval gate, and abort signal observe sub-agent activity:
14
+ * `runSubAgent` reuses the same `runState` and so increments the same
15
+ * counter and emits approval events through the same channel.
16
+ *
17
+ * `depth` is the current sub-agent recursion depth (0 at the top level).
18
+ * It is forwarded to `runSubAgent` when the dispatched entry is itself a
19
+ * sub-agent, so depth limits remain enforced.
20
+ */
21
+ async function dispatchToolCall(deps, runState, toolIndex, name, args, depth) {
22
+ if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {
23
+ runState.abortController.abort(/* @__PURE__ */ new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`));
24
+ throw new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`);
25
+ }
26
+ runState.toolCallsUsed.count++;
27
+ const entry = toolIndex.get(name);
28
+ if (!entry) throw new Error(`Unknown tool: ${name}`);
29
+ if (runState.approvalPolicy.requireForDestructive && requiresApproval(entry.def.annotations)) {
30
+ const approvalId = randomUUID();
31
+ for (const ev of runState.translator.translate({
32
+ type: "approval_pending",
33
+ approvalId,
34
+ streamId: runState.requestId,
35
+ toolName: name,
36
+ args,
37
+ annotations: entry.def.annotations
38
+ })) runState.outboundEvents.push(ev);
39
+ if (await deps.approvalGate.wait({
40
+ approvalId,
41
+ streamId: runState.requestId,
42
+ userId: runState.userId,
43
+ timeoutMs: runState.approvalPolicy.timeoutMs
44
+ }) === "deny") return `Tool execution denied by user approval gate (tool: ${name}).`;
45
+ }
46
+ return normalizeToolResult(await traceTool(name, args, async () => {
47
+ let result;
48
+ if (entry.source === "toolkit") {
49
+ if (!deps.context) throw new Error("Plugin tool execution requires PluginContext; this should never happen through createApp");
50
+ result = await deps.context.executeTool(runState.req, entry.pluginName, entry.localName, args, runState.signal, runState.limits.toolCallTimeoutMs);
51
+ } else if (entry.source === "function") {
52
+ 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.`);
53
+ result = await entry.functionTool.execute(args);
54
+ } else if (entry.source === "mcp") {
55
+ const mcpClient = deps.getMcpClient();
56
+ if (!mcpClient) throw new Error("MCP client not connected");
57
+ const oboToken = runState.req.headers["x-forwarded-access-token"];
58
+ const mcpAuth = typeof oboToken === "string" ? { Authorization: `Bearer ${oboToken}` } : void 0;
59
+ result = await mcpClient.callTool(entry.mcpToolName, args, mcpAuth);
60
+ } else if (entry.source === "subagent") {
61
+ const childAgent = deps.agents.get(entry.agentName);
62
+ if (!childAgent) throw new Error(`Sub-agent not found: ${entry.agentName}`);
63
+ result = await runSubAgent(deps, runState, childAgent, args, depth + 1);
64
+ } 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.`);
65
+ else if (entry.source === "skill") result = await deps.dispatchSkillTool(entry, args);
66
+ return result;
67
+ }));
68
+ }
69
+ /**
70
+ * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the
71
+ * concatenated text output to hand back to the parent adapter as the tool
72
+ * result.
73
+ *
74
+ * `depth` starts at 1 for a top-level sub-agent invocation and increments on
75
+ * each nested call. Depths exceeding `limits.maxSubAgentDepth` are rejected
76
+ * before any adapter work.
77
+ *
78
+ * Sub-agent tool calls run through `dispatchToolCall` with the same
79
+ * `runState` as the parent — the budget counter and approval gate are
80
+ * therefore enforced for every nested call, not only at the top level.
81
+ */
82
+ async function runSubAgent(deps, runState, child, args, depth) {
83
+ 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.`);
84
+ const input = typeof args === "object" && args !== null && typeof args.input === "string" ? args.input : JSON.stringify(args);
85
+ const childTools = Array.from(child.toolIndex.values()).filter((e) => e.source !== "hosted-supervisor").map((e) => e.def);
86
+ const childExecute = (name, childArgs) => dispatchToolCall(deps, runState, child.toolIndex, name, childArgs, depth);
87
+ const runContext = {
88
+ executeTool: childExecute,
89
+ signal: runState.signal
90
+ };
91
+ const pluginNames = deps.context ? deps.context.getPluginNames().filter((n) => n !== deps.pluginName && n !== "server") : [];
92
+ const messages = [{
93
+ id: "system",
94
+ role: "system",
95
+ content: composePromptForAgent(child, deps.baseSystemPrompt, {
96
+ agentName: child.name,
97
+ pluginNames,
98
+ toolNames: childTools.map((t) => t.name)
99
+ }),
100
+ createdAt: /* @__PURE__ */ new Date()
101
+ }, {
102
+ id: randomUUID(),
103
+ role: "user",
104
+ content: input,
105
+ createdAt: /* @__PURE__ */ new Date()
106
+ }];
107
+ return consumeAdapterStream(child.adapter.run({
108
+ messages,
109
+ tools: childTools,
110
+ threadId: randomUUID(),
111
+ signal: runState.signal,
112
+ extensions: buildAdapterExtensions(child.toolIndex)
113
+ }, runContext), {
114
+ signal: runState.signal,
115
+ onEvent: (event) => {
116
+ if (event.type === "metadata") return;
117
+ for (const translated of runState.translator.translate(event)) runState.outboundEvents.push(translated);
118
+ }
119
+ });
120
+ }
121
+
122
+ //#endregion
123
+ export { dispatchToolCall };
124
+ //# sourceMappingURL=tool-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-dispatch.js","names":[],"sources":["../../../src/plugins/agents/tool-dispatch.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { AgentRunContext, Message, ResponseStreamEvent } from \"shared\";\n\nimport type { AppKitMcpClient } from \"../../connectors/mcp\";\nimport { consumeAdapterStream } from \"../../core/agent/consume-adapter-stream\";\nimport { normalizeToolResult } from \"../../core/agent/normalize-result\";\nimport type {\n BaseSystemPromptOption,\n RegisteredAgent,\n ResolvedToolEntry,\n} from \"../../core/agent/types\";\nimport type { PluginContext } from \"../../core/plugin-context\";\nimport { buildAdapterExtensions } from \"./adapter-extensions\";\nimport { requiresApproval } from \"./approval\";\nimport type { EventChannel } from \"./event-channel\";\nimport type { AgentEventTranslator } from \"./event-translator\";\nimport { traceTool } from \"./mlflow\";\nimport { composePromptForAgent } from \"./prompt\";\nimport type { ToolApprovalGate } from \"./tool-approval-gate\";\n\n/**\n * Per-stream state shared between the top-level `executeTool` and any\n * `runSubAgent` calls below it. Carrying the budget counter, abort signal,\n * approval policy, and event-channel through one object is what lets the\n * sub-agent path enforce the same limits and approval gate as the parent.\n *\n * Without this shared state the sub-agent path silently bypassed both the\n * tool-call budget and the destructive-tool approval gate.\n */\nexport interface RunState {\n req: express.Request;\n userId: string;\n requestId: string;\n abortController: AbortController;\n signal: AbortSignal;\n approvalPolicy: { requireForDestructive: boolean; timeoutMs: number };\n limits: {\n maxConcurrentStreamsPerUser: number;\n maxToolCalls: number;\n maxSubAgentDepth: number;\n toolCallTimeoutMs: number;\n };\n translator: AgentEventTranslator;\n outboundEvents: EventChannel<ResponseStreamEvent>;\n /** Boxed mutable counter shared across parent + all sub-agent dispatches. */\n toolCallsUsed: { count: number };\n}\n\n/**\n * Plugin-instance collaborators the dispatch path needs. Passed as one object\n * so `dispatchToolCall`/`runSubAgent` stay free functions with a bounded\n * interface instead of reaching into the plugin. `getMcpClient` is a thunk so\n * a client connected after this object is built is still seen.\n */\nexport interface ToolDispatchDeps {\n approvalGate: ToolApprovalGate;\n context: PluginContext | undefined;\n getMcpClient: () => AppKitMcpClient | null;\n agents: Map<string, RegisteredAgent>;\n dispatchSkillTool: (\n entry: Extract<ResolvedToolEntry, { source: \"skill\" }>,\n args: unknown,\n ) => Promise<string>;\n pluginName: string;\n baseSystemPrompt: BaseSystemPromptOption | undefined;\n}\n\n/**\n * Dispatch a single tool call from either the top-level adapter or a\n * sub-agent. Centralising this in one function is what makes the budget\n * counter, approval gate, and abort signal observe sub-agent activity:\n * `runSubAgent` reuses the same `runState` and so increments the same\n * counter and emits approval events through the same channel.\n *\n * `depth` is the current sub-agent recursion depth (0 at the top level).\n * It is forwarded to `runSubAgent` when the dispatched entry is itself a\n * sub-agent, so depth limits remain enforced.\n */\nexport async function dispatchToolCall(\n deps: ToolDispatchDeps,\n runState: RunState,\n toolIndex: Map<string, ResolvedToolEntry>,\n name: string,\n args: unknown,\n depth: number,\n): Promise<unknown> {\n if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {\n runState.abortController.abort(\n new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`,\n ),\n );\n throw new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`,\n );\n }\n runState.toolCallsUsed.count++;\n\n const entry = toolIndex.get(name);\n if (!entry) throw new Error(`Unknown tool: ${name}`);\n\n if (\n runState.approvalPolicy.requireForDestructive &&\n requiresApproval(entry.def.annotations)\n ) {\n const approvalId = randomUUID();\n for (const ev of runState.translator.translate({\n type: \"approval_pending\",\n approvalId,\n streamId: runState.requestId,\n toolName: name,\n args,\n annotations: entry.def.annotations,\n })) {\n runState.outboundEvents.push(ev);\n }\n const decision = await deps.approvalGate.wait({\n approvalId,\n streamId: runState.requestId,\n userId: runState.userId,\n timeoutMs: runState.approvalPolicy.timeoutMs,\n });\n if (decision === \"deny\") {\n return `Tool execution denied by user approval gate (tool: ${name}).`;\n }\n }\n\n // Traced from here so the span covers execution only, not the approval\n // wait above (which is human latency).\n const toolResult = await traceTool(name, args, async () => {\n let result: unknown;\n if (entry.source === \"toolkit\") {\n if (!deps.context) {\n throw new Error(\n \"Plugin tool execution requires PluginContext; this should never happen through createApp\",\n );\n }\n result = await deps.context.executeTool(\n runState.req,\n entry.pluginName,\n entry.localName,\n args,\n runState.signal,\n runState.limits.toolCallTimeoutMs,\n );\n } else if (entry.source === \"function\") {\n // Function tools declare their parameters as a JSON-object schema,\n // so adapters always serialize `args` as an object. A non-object\n // value here means the upstream model emitted malformed tool-call\n // JSON; surface a clear error rather than silently passing through\n // a wrong-shape value the tool will then choke on.\n if (typeof args !== \"object\" || args === null || Array.isArray(args)) {\n throw new Error(\n `Function tool '${name}' received non-object arguments (got ${args === null ? \"null\" : Array.isArray(args) ? \"array\" : typeof args}); expected a JSON object.`,\n );\n }\n result = await entry.functionTool.execute(\n args as Record<string, unknown>,\n );\n } else if (entry.source === \"mcp\") {\n const mcpClient = deps.getMcpClient();\n if (!mcpClient) throw new Error(\"MCP client not connected\");\n const oboToken = runState.req.headers[\"x-forwarded-access-token\"];\n const mcpAuth =\n typeof oboToken === \"string\"\n ? { Authorization: `Bearer ${oboToken}` }\n : undefined;\n result = await mcpClient.callTool(entry.mcpToolName, args, mcpAuth);\n } else if (entry.source === \"subagent\") {\n const childAgent = deps.agents.get(entry.agentName);\n if (!childAgent)\n throw new Error(`Sub-agent not found: ${entry.agentName}`);\n result = await runSubAgent(deps, runState, childAgent, args, depth + 1);\n } else if (entry.source === \"hosted-supervisor\") {\n // Defense-in-depth: should never fire. Hosted-supervisor entries are\n // routed via `AgentInput.extensions` and the SA endpoint executes\n // them server-side; their `def` is filtered out of the adapter's\n // `tools` array, so the model never sees a callable schema for them.\n // If we reach here, the agent is paired with a non-SA adapter that\n // somehow surfaced the placeholder def to the model — surface a\n // clear error rather than crash later in `normalizeToolResult`.\n throw new Error(\n `Tool '${name}' is a hosted-supervisor tool and cannot be invoked from the Node process. ` +\n \"It is executed server-side by the Databricks AI Gateway and is only reachable when the agent's model is a Supervisor API adapter.\",\n );\n } else if (entry.source === \"skill\") {\n result = await deps.dispatchSkillTool(entry, args);\n }\n\n return result;\n });\n\n return normalizeToolResult(toolResult);\n}\n\n/**\n * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the\n * concatenated text output to hand back to the parent adapter as the tool\n * result.\n *\n * `depth` starts at 1 for a top-level sub-agent invocation and increments on\n * each nested call. Depths exceeding `limits.maxSubAgentDepth` are rejected\n * before any adapter work.\n *\n * Sub-agent tool calls run through `dispatchToolCall` with the same\n * `runState` as the parent — the budget counter and approval gate are\n * therefore enforced for every nested call, not only at the top level.\n */\nexport async function runSubAgent(\n deps: ToolDispatchDeps,\n runState: RunState,\n child: RegisteredAgent,\n args: unknown,\n depth: number,\n): Promise<string> {\n if (depth > runState.limits.maxSubAgentDepth) {\n throw new Error(\n `Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). ` +\n `Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`,\n );\n }\n\n const input =\n typeof args === \"object\" &&\n args !== null &&\n typeof (args as { input?: unknown }).input === \"string\"\n ? (args as { input: string }).input\n : JSON.stringify(args);\n // Same filter as the top-level path: hosted-supervisor `def` is a\n // placeholder, not a callable function — exclude from the adapter's\n // `tools` array. The specs are routed via `extensions` instead.\n const childTools = Array.from(child.toolIndex.values())\n .filter((e) => e.source !== \"hosted-supervisor\")\n .map((e) => e.def);\n\n const childExecute = (name: string, childArgs: unknown): Promise<unknown> =>\n dispatchToolCall(deps, runState, child.toolIndex, name, childArgs, depth);\n\n const runContext: AgentRunContext = {\n executeTool: childExecute,\n signal: runState.signal,\n };\n\n const pluginNames = deps.context\n ? deps.context\n .getPluginNames()\n .filter((n) => n !== deps.pluginName && n !== \"server\")\n : [];\n const systemPrompt = composePromptForAgent(child, deps.baseSystemPrompt, {\n agentName: child.name,\n pluginNames,\n toolNames: childTools.map((t) => t.name),\n });\n\n const messages: Message[] = [\n {\n id: \"system\",\n role: \"system\",\n content: systemPrompt,\n createdAt: new Date(),\n },\n {\n id: randomUUID(),\n role: \"user\",\n content: input,\n createdAt: new Date(),\n },\n ];\n\n return consumeAdapterStream(\n child.adapter.run(\n {\n messages,\n tools: childTools,\n threadId: randomUUID(),\n signal: runState.signal,\n extensions: buildAdapterExtensions(child.toolIndex),\n },\n runContext,\n ),\n {\n signal: runState.signal,\n // Forward every sub-agent event into the parent's outbound SSE\n // stream so the client sees nested tool_call / tool_result events\n // (UI-action tools like apply_filter / highlight_period rely on\n // this) and the sub-agent's streaming text as it's generated.\n //\n // `metadata` is the one exception: sub-agents have their own\n // threadId, and forwarding it would overwrite the parent's\n // thread state on the client and break multi-turn continuity.\n // Approval-pending events emitted by `dispatchToolCall` already\n // reach `outboundEvents` directly, so they are not routed here.\n onEvent: (event) => {\n if (event.type === \"metadata\") return;\n for (const translated of runState.translator.translate(event)) {\n runState.outboundEvents.push(translated);\n }\n },\n },\n );\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ import { getRoutes, printRoutes } from "./utils.js";
19
19
  import { StaticServer } from "./static-server.js";
20
20
  import { ViteDevServer } from "./vite-dev-server.js";
21
21
  import path from "node:path";
22
- import fs from "node:fs";
22
+ import fsSync from "node:fs";
23
23
  import dotenv from "dotenv";
24
24
  import express from "express";
25
25
  import getPort, { portNumbers } from "get-port";
@@ -246,7 +246,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
246
246
  const cwd = process.cwd();
247
247
  for (const p of staticPaths) {
248
248
  const fullPath = path.resolve(cwd, p);
249
- if (fs.existsSync(path.resolve(fullPath, "index.html"))) {
249
+ if (fsSync.existsSync(path.resolve(fullPath, "index.html"))) {
250
250
  logger.debug("Static files: serving from %s", fullPath);
251
251
  return fullPath;
252
252
  }