@databricks/appkit 0.66.0 → 0.67.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 (55) hide show
  1. package/NOTICE.md +3 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/core/agent/frontmatter.js +23 -0
  4. package/dist/core/agent/frontmatter.js.map +1 -0
  5. package/dist/core/agent/load-agents.d.ts.map +1 -1
  6. package/dist/core/agent/load-agents.js +36 -6
  7. package/dist/core/agent/load-agents.js.map +1 -1
  8. package/dist/core/agent/skills/index.js +7 -0
  9. package/dist/core/agent/skills/load-skills.js +78 -0
  10. package/dist/core/agent/skills/load-skills.js.map +1 -0
  11. package/dist/core/agent/skills/parse-skill.js +69 -0
  12. package/dist/core/agent/skills/parse-skill.js.map +1 -0
  13. package/dist/core/agent/skills/read-resource.js +31 -0
  14. package/dist/core/agent/skills/read-resource.js.map +1 -0
  15. package/dist/core/agent/skills/render.js +33 -0
  16. package/dist/core/agent/skills/render.js.map +1 -0
  17. package/dist/core/agent/skills/resolve-catalog.js +78 -0
  18. package/dist/core/agent/skills/resolve-catalog.js.map +1 -0
  19. package/dist/core/agent/skills/types.d.ts +50 -0
  20. package/dist/core/agent/skills/types.d.ts.map +1 -0
  21. package/dist/core/agent/types.d.ts +47 -0
  22. package/dist/core/agent/types.d.ts.map +1 -1
  23. package/dist/core/agent/types.js.map +1 -1
  24. package/dist/plugins/agents/agents.d.ts +50 -0
  25. package/dist/plugins/agents/agents.d.ts.map +1 -1
  26. package/dist/plugins/agents/agents.js +225 -13
  27. package/dist/plugins/agents/agents.js.map +1 -1
  28. package/dist/plugins/agents/manifest.js +40 -21
  29. package/dist/plugins/agents/schemas.js +2 -1
  30. package/dist/plugins/agents/schemas.js.map +1 -1
  31. package/dist/plugins/lakebase/manifest.js +1 -0
  32. package/dist/plugins/server/index.js +2 -2
  33. package/dist/plugins/server/index.js.map +1 -1
  34. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  35. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  36. package/dist/plugins/server/static-server.js +3 -3
  37. package/dist/plugins/server/static-server.js.map +1 -1
  38. package/dist/plugins/server/utils.js +3 -3
  39. package/dist/plugins/server/utils.js.map +1 -1
  40. package/dist/plugins/server/vite-dev-server.js +4 -4
  41. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  42. package/dist/shared/src/schemas/manifest.d.ts +6 -6
  43. package/dist/type-generator/database/generate.js +3 -3
  44. package/dist/type-generator/database/generate.js.map +1 -1
  45. package/dist/type-generator/migration.js +2 -2
  46. package/dist/type-generator/migration.js.map +1 -1
  47. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  48. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  49. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  50. package/docs/api/appkit/Interface.AgentsPluginConfig.md +35 -0
  51. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  52. package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +46 -0
  53. package/docs/plugins/agents.md +68 -1
  54. package/package.json +1 -1
  55. package/sbom.cdx.json +1 -1
@@ -1,9 +1,13 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
+ import { getWorkspaceClient } from "../../context/execution-context.js";
3
+ import "../../context/index.js";
2
4
  import { Plugin } from "../../plugin/plugin.js";
3
5
  import { toPlugin } from "../../plugin/to-plugin.js";
4
6
  import "../../plugin/index.js";
5
7
  import { defineManifest } from "../../registry/manifest-loader.js";
6
8
  import "../../registry/index.js";
9
+ import { FilesConnector } from "../../connectors/files/client.js";
10
+ import "../../connectors/files/index.js";
7
11
  import { buildMcpHostPolicy } from "../../connectors/mcp/host-policy.js";
8
12
  import { AppKitMcpClient } from "../../connectors/mcp/client.js";
9
13
  import "../../connectors/mcp/index.js";
@@ -18,6 +22,12 @@ import "../../core/agent/tools/index.js";
18
22
  import { loadAgentsFromDir } from "../../core/agent/load-agents.js";
19
23
  import { CODE_AGENTS_SOURCE_DIR, loadCodeAgentsFromDir, resolveCodeAgentsDir } from "../../core/agent/load-code-agents.js";
20
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";
21
31
  import { buildBaseSystemPrompt, composeSystemPrompt } from "../../core/agent/system-prompt.js";
22
32
  import { agentStreamDefaults } from "./defaults.js";
23
33
  import { EventChannel } from "./event-channel.js";
@@ -99,6 +109,12 @@ var AgentsPlugin = class extends Plugin {
99
109
  * negative, or `NaN`) can't degrade into immediate auto-denial of every
100
110
  * mutating tool call.
101
111
  */
112
+ /**
113
+ * Shared global skill pool (bundle `skills/` + catalog volume), loaded once
114
+ * per registry build. Read by {@link buildRegisteredAgent} to resolve each
115
+ * agent's visible catalog and by the live `register` path.
116
+ */
117
+ globalSkills = [];
102
118
  cachedApprovalPolicy = null;
103
119
  get resolvedApprovalPolicy() {
104
120
  if (this.cachedApprovalPolicy) return this.cachedApprovalPolicy;
@@ -183,6 +199,8 @@ var AgentsPlugin = class extends Plugin {
183
199
  async buildAgentRegistry() {
184
200
  const discovered = await this.loadCodeAgents();
185
201
  const deprecatedMapRaw = this.config.agents ?? {};
202
+ const [bundleSkills, volumeSkills] = await Promise.all([this.loadGlobalSkills(), this.loadVolumeSkills()]);
203
+ this.globalSkills = [...bundleSkills, ...volumeSkills];
186
204
  if (Object.keys(deprecatedMapRaw).length > 0) this.warnAgentsMapDeprecated();
187
205
  const deprecatedMap = {};
188
206
  for (const [id, def] of Object.entries(deprecatedMapRaw)) {
@@ -352,7 +370,8 @@ var AgentsPlugin = class extends Plugin {
352
370
  }
353
371
  async buildRegisteredAgent(name, def, src) {
354
372
  const adapter = await this.resolveAdapter(def, name);
355
- const toolIndex = await this.buildToolIndex(name, def, src);
373
+ const skills = await this.resolveAgentSkills(name, def, src);
374
+ const toolIndex = await this.buildToolIndex(name, def, src, skills);
356
375
  warnOnCapabilityMismatch(name, adapter, toolIndex);
357
376
  return {
358
377
  name,
@@ -363,9 +382,107 @@ var AgentsPlugin = class extends Plugin {
363
382
  maxSteps: def.maxSteps,
364
383
  maxTokens: def.maxTokens,
365
384
  generationParams: def.generationParams,
366
- ephemeral: def.ephemeral
385
+ ephemeral: def.ephemeral,
386
+ skills
367
387
  };
368
388
  }
389
+ /** 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;
399
+ }
400
+ /**
401
+ * Workspace client used to read catalog skills. v1 reads as the app service
402
+ * principal; `getWorkspaceClient()` resolves to SP outside a user scope
403
+ * (boot and skill-tool dispatch are both unscoped). This is the single
404
+ * switch point for a future OBO mode.
405
+ */
406
+ skillWorkspaceClient() {
407
+ return getWorkspaceClient();
408
+ }
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
+ }
464
+ }
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
+ 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
+ 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
483
+ });
484
+ return catalog.byAddress.size > 0 ? catalog : void 0;
485
+ }
369
486
  async resolveAdapter(def, name) {
370
487
  const source = def.model ?? this.config.defaultModel;
371
488
  const adapterOptions = {};
@@ -391,7 +508,7 @@ var AgentsPlugin = class extends Plugin {
391
508
  * hosted tools via MCP client. Applies `autoInheritTools` defaults when the
392
509
  * definition has no declared tools/agents.
393
510
  */
394
- async buildToolIndex(agentName, def, src) {
511
+ async buildToolIndex(agentName, def, src, skills) {
395
512
  const index = /* @__PURE__ */ new Map();
396
513
  const hasDeclaredTools = def.tools !== void 0;
397
514
  const toolsRecord = this.resolveDefTools(agentName, def);
@@ -464,6 +581,20 @@ var AgentsPlugin = class extends Plugin {
464
581
  throw new Error(`Agent '${agentName}' tool '${key}' has an unrecognized shape`);
465
582
  }
466
583
  if (hostedToCollect.length > 0) await this.connectHostedTools(hostedToCollect, index);
584
+ if (skills && skills.byAddress.size > 0) {
585
+ index.set("load_skill", {
586
+ source: "skill",
587
+ builtin: "load_skill",
588
+ catalog: skills,
589
+ def: LOAD_SKILL_TOOL_DEF
590
+ });
591
+ index.set("read_skill_file", {
592
+ source: "skill",
593
+ builtin: "read_skill_file",
594
+ catalog: skills,
595
+ def: READ_SKILL_FILE_TOOL_DEF
596
+ });
597
+ }
467
598
  return index;
468
599
  }
469
600
  /**
@@ -644,9 +775,12 @@ var AgentsPlugin = class extends Plugin {
644
775
  });
645
776
  }
646
777
  clientConfig() {
778
+ const skills = {};
779
+ for (const [name, agent] of this.agents) if (agent.skills) skills[name] = agent.skills.catalog;
647
780
  return {
648
781
  agents: Array.from(this.agents.keys()),
649
- defaultAgent: this.defaultAgentName
782
+ defaultAgent: this.defaultAgentName,
783
+ skills
650
784
  };
651
785
  }
652
786
  async _handleChat(req, res) {
@@ -658,7 +792,7 @@ var AgentsPlugin = class extends Plugin {
658
792
  });
659
793
  return;
660
794
  }
661
- const { message, threadId, agent: agentName, mlflowRunId } = parsed.data;
795
+ const { message, threadId, agent: agentName, mlflowRunId, skill } = parsed.data;
662
796
  const registered = this.resolveAgent(agentName);
663
797
  if (!registered) {
664
798
  res.status(400).json({ error: agentName ? `Agent "${agentName}" not found` : "No agent registered" });
@@ -691,7 +825,7 @@ var AgentsPlugin = class extends Plugin {
691
825
  res.status(500).json({ error: "Thread operation failed" });
692
826
  return;
693
827
  }
694
- return this._streamAgent(req, res, registered, thread, userId, mlflowRunId);
828
+ return this._streamAgent(req, res, registered, thread, userId, mlflowRunId, skill);
695
829
  }
696
830
  /**
697
831
  * Returns the names of tools in `registered.toolIndex` whose annotations
@@ -774,7 +908,7 @@ var AgentsPlugin = class extends Plugin {
774
908
  }
775
909
  return this._runAgentNonStreaming(req, res, registered, thread, userId, mlflowRunId);
776
910
  }
777
- async _streamAgent(req, res, registered, thread, userId, mlflowRunId) {
911
+ async _streamAgent(req, res, registered, thread, userId, mlflowRunId, forcedSkill) {
778
912
  const abortController = new AbortController();
779
913
  const signal = abortController.signal;
780
914
  const requestId = randomUUID();
@@ -809,14 +943,19 @@ var AgentsPlugin = class extends Plugin {
809
943
  })) }, async (span) => {
810
944
  if (mlflowRunId) linkTraceToRun(mlflowRunId);
811
945
  const pluginNames = this.context ? this.context.getPluginNames().filter((n) => n !== this.name && n !== "server") : [];
946
+ let fullPrompt = composePromptForAgent(registered, this.config.baseSystemPrompt, {
947
+ agentName: registered.name,
948
+ pluginNames,
949
+ toolNames: tools.map((t) => t.name)
950
+ });
951
+ if (forcedSkill) {
952
+ const addendum = this.renderForcedSkill(registered, forcedSkill);
953
+ if (addendum) fullPrompt = `${fullPrompt}\n\n${addendum}`;
954
+ }
812
955
  const messagesWithSystem = [{
813
956
  id: "system",
814
957
  role: "system",
815
- content: composePromptForAgent(registered, this.config.baseSystemPrompt, {
816
- agentName: registered.name,
817
- pluginNames,
818
- toolNames: tools.map((t) => t.name)
819
- }),
958
+ content: fullPrompt,
820
959
  createdAt: /* @__PURE__ */ new Date()
821
960
  }, ...thread.messages];
822
961
  const fullContent = await consumeAdapterStream(registered.adapter.run({
@@ -1067,10 +1206,46 @@ var AgentsPlugin = class extends Plugin {
1067
1206
  if (!childAgent) throw new Error(`Sub-agent not found: ${entry.agentName}`);
1068
1207
  result = await this.runSubAgent(runState, childAgent, args, depth + 1);
1069
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);
1070
1210
  return result;
1071
1211
  }));
1072
1212
  }
1073
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);
1231
+ }
1232
+ /**
1233
+ * Renders the prompt addendum for a force-loaded skill (`/skill-name`).
1234
+ * Returns null when the agent has no catalog or the name doesn't resolve —
1235
+ * an unusable request shouldn't fail the whole turn, so it's logged and the
1236
+ * model proceeds with the catalog + `load_skill` still available.
1237
+ */
1238
+ 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
+ }
1247
+ }
1248
+ /**
1074
1249
  * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the
1075
1250
  * concatenated text output to hand back to the parent adapter as the tool
1076
1251
  * result.
@@ -1261,6 +1436,40 @@ function normalizeAutoInherit(value) {
1261
1436
  code: value.code ?? false
1262
1437
  };
1263
1438
  }
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
+ };
1264
1473
  function composePromptForAgent(registered, pluginLevel, ctx) {
1265
1474
  const perAgent = registered.baseSystemPrompt;
1266
1475
  const resolved = perAgent !== void 0 ? perAgent : pluginLevel;
@@ -1269,7 +1478,10 @@ function composePromptForAgent(registered, pluginLevel, ctx) {
1269
1478
  else if (typeof resolved === "string") base = resolved;
1270
1479
  else if (typeof resolved === "function") base = resolved(ctx);
1271
1480
  else base = buildBaseSystemPrompt(ctx);
1272
- return composeSystemPrompt(base, registered.instructions);
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)}`;
1273
1485
  }
1274
1486
  /**
1275
1487
  * Pulls the LLM-readable description off any {@link SupervisorTool} kind.