@dbx-tools/appkit-mastra 0.9.74 → 0.9.93

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.
@@ -1 +1 @@
1
- {"root":["../src/agents.ts","../src/chart.ts","../src/chat.ts","../src/config.ts","../src/defaults.ts","../src/evaluation.ts","../src/filesystems.ts","../src/genie.ts","../src/identity.ts","../src/mcp.ts","../src/memory.ts","../src/mlflow.ts","../src/model.ts","../src/monty-sandbox.ts","../src/observability.ts","../src/openapi-tool.ts","../src/plugin.ts","../src/processors.ts","../src/remote-skills.ts","../src/rest.ts","../src/sandbox.ts","../src/server.ts","../src/serving-sanitize.ts","../src/serving.ts","../src/skill-cache.ts","../src/skill-paths.ts","../src/statement.ts","../src/storage-schema.ts","../src/style.ts","../src/summarize.ts","../src/telemetry.ts","../src/trace-attributes.ts","../src/validation.ts","../src/workspaces.ts","../src/writer.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/agents.ts","../src/chart.ts","../src/chat.ts","../src/config.ts","../src/defaults.ts","../src/evaluation.ts","../src/filesystems.ts","../src/genie.ts","../src/identity.ts","../src/mcp.ts","../src/memory.ts","../src/mlflow.ts","../src/model.ts","../src/monty-sandbox.ts","../src/observability.ts","../src/openapi-tool.ts","../src/plugin.ts","../src/processors.ts","../src/remote-skills.ts","../src/rest.ts","../src/sandbox.ts","../src/server.ts","../src/serving-sanitize.ts","../src/serving.ts","../src/skill-cache.ts","../src/skill-paths.ts","../src/skill-search.ts","../src/statement.ts","../src/storage-schema.ts","../src/style.ts","../src/summarize.ts","../src/telemetry.ts","../src/trace-attributes.ts","../src/validation.ts","../src/workspaces.ts","../src/writer.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -34,19 +34,19 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@databricks/appkit": "0.81.0",
37
- "@dbx-tools/appkit": "^0.9.74",
38
- "@dbx-tools/core": "^0.9.74",
39
- "@dbx-tools/databricks": "^0.9.74",
40
- "@dbx-tools/fs": "^0.9.74",
41
- "@dbx-tools/genie": "^0.9.74",
42
- "@dbx-tools/model": "^0.9.74",
43
- "@dbx-tools/path": "^0.9.74",
44
- "@dbx-tools/postgres": "^0.9.74",
45
- "@dbx-tools/shared-core": "^0.9.74",
46
- "@dbx-tools/shared-fs": "^0.9.74",
47
- "@dbx-tools/shared-genie": "^0.9.74",
48
- "@dbx-tools/shared-mastra": "^0.9.74",
49
- "@dbx-tools/shared-model": "^0.9.74",
37
+ "@dbx-tools/appkit": "^0.9.93",
38
+ "@dbx-tools/core": "^0.9.93",
39
+ "@dbx-tools/databricks": "^0.9.93",
40
+ "@dbx-tools/fs": "^0.9.93",
41
+ "@dbx-tools/genie": "^0.9.93",
42
+ "@dbx-tools/model": "^0.9.93",
43
+ "@dbx-tools/path": "^0.9.93",
44
+ "@dbx-tools/postgres": "^0.9.93",
45
+ "@dbx-tools/shared-core": "^0.9.93",
46
+ "@dbx-tools/shared-fs": "^0.9.93",
47
+ "@dbx-tools/shared-genie": "^0.9.93",
48
+ "@dbx-tools/shared-mastra": "^0.9.93",
49
+ "@dbx-tools/shared-model": "^0.9.93",
50
50
  "@mastra/ai-sdk": "1.10.5",
51
51
  "@mastra/core": "1.71.0",
52
52
  "@mastra/express": "1.5.15",
@@ -61,9 +61,12 @@
61
61
  "@opentelemetry/core": "2.11.0",
62
62
  "@opentelemetry/sdk-trace-base": "2.8.0",
63
63
  "@opentelemetry/sdk-trace-node": "2.8.0",
64
- "@pydantic/monty": "0.0.23",
64
+ "@pydantic/monty": "1.0.0",
65
65
  "express": "^5.1.0",
66
+ "fuse.js": "^7.4.2",
67
+ "lru-cache": "11.5.3",
66
68
  "pg": "^8.22.0",
69
+ "yaml": "^2.9.0",
67
70
  "zod": "4.3.6"
68
71
  },
69
72
  "main": "./lib/index.js",
@@ -181,6 +184,10 @@
181
184
  "types": "./lib/src/skill-paths.d.ts",
182
185
  "default": "./lib/src/skill-paths.js"
183
186
  },
187
+ "./skill-search": {
188
+ "types": "./lib/src/skill-search.d.ts",
189
+ "default": "./lib/src/skill-search.js"
190
+ },
184
191
  "./statement": {
185
192
  "types": "./lib/src/statement.d.ts",
186
193
  "default": "./lib/src/statement.js"
@@ -220,7 +227,7 @@
220
227
  "./package.json": "./package.json"
221
228
  }
222
229
  },
223
- "version": "0.9.74",
230
+ "version": "0.9.93",
224
231
  "types": "./lib/index.d.ts",
225
232
  "dbxToolsConfig": {
226
233
  "tags": [
@@ -337,6 +344,10 @@
337
344
  "types": "./lib/src/skill-paths.d.ts",
338
345
  "default": "./lib/src/skill-paths.js"
339
346
  },
347
+ "./skill-search": {
348
+ "types": "./lib/src/skill-search.d.ts",
349
+ "default": "./lib/src/skill-search.js"
350
+ },
340
351
  "./statement": {
341
352
  "types": "./lib/src/statement.d.ts",
342
353
  "default": "./lib/src/statement.js"
package/src/agents.ts CHANGED
@@ -25,7 +25,7 @@ import type {
25
25
  } from "@databricks/appkit/beta";
26
26
  import { pluginRegistry, toolkitEntries } from "@dbx-tools/appkit";
27
27
  import type { AgentToolExecutionContext } from "@dbx-tools/appkit/tool-provider";
28
- import { log, object, stringUtils } from "@dbx-tools/shared-core";
28
+ import { errorUtils, log, object, stringUtils } from "@dbx-tools/shared-core";
29
29
  import { TOOL_PROGRESS_PART_TYPE, ToolProgressEventSchema } from "@dbx-tools/shared-mastra/wire";
30
30
  import type {
31
31
  AgentConfig,
@@ -35,6 +35,7 @@ import type {
35
35
  } from "@mastra/core/agent";
36
36
  import { Agent } from "@mastra/core/agent";
37
37
  import { SkillSearchProcessor } from "@mastra/core/processors";
38
+ import type { OutputProcessor } from "@mastra/core/processors";
38
39
  import { MASTRA_RESOURCE_ID_KEY } from "@mastra/core/request-context";
39
40
  import type { Tool } from "@mastra/core/tools";
40
41
  import { createTool } from "@mastra/core/tools";
@@ -48,9 +49,10 @@ import type { MemoryBuilder } from "./memory.ts";
48
49
  import { buildModel, RESPONSES_PROVIDER_OPTIONS } from "./model.ts";
49
50
  import { stripStaleChartsProcessor } from "./processors.ts";
50
51
  import { MASTRA_RESOLVED_MODEL_KEY } from "./serving.ts";
52
+ import { CatalogueSkillSearchProcessor } from "./skill-search.ts";
51
53
  import { TYPOGRAPHY_RULE } from "./style.ts";
52
54
  import { buildSummarizeTool } from "./summarize.ts";
53
- import { createWorkspace } from "./workspaces.ts";
55
+ import { createWorkspace, workspaceSkillCatalogueResolver } from "./workspaces.ts";
54
56
 
55
57
  /**
56
58
  * Tool record accepted by every Mastra `Agent.tools` field and by the
@@ -583,8 +585,7 @@ export async function buildAgents(opts: {
583
585
  ...(config.workspaceSkillRefreshTtlMs !== undefined
584
586
  ? { workspaceSkillRefreshTtlMs: config.workspaceSkillRefreshTtlMs }
585
587
  : {}),
586
- sandbox:
587
- config.sandbox === undefined || config.sandbox === true ? "databricks" : config.sandbox,
588
+ sandbox: config.sandbox === true ? "databricks" : config.sandbox,
588
589
  });
589
590
  markDefaultWorkspace(workspace);
590
591
  }
@@ -613,6 +614,7 @@ export async function buildAgents(opts: {
613
614
  ...(memory ? { memory } : {}),
614
615
  ...(workspace ? { workspace } : {}),
615
616
  inputProcessors: [...inputProcessors, ...workspaceSkillInputProcessors(workspace, config)],
617
+ outputProcessors: [toolErrorLoggingProcessor(log)],
616
618
  });
617
619
  // Surface the effective default model per agent so operators can
618
620
  // see at a glance which endpoint each agent points at without
@@ -644,13 +646,44 @@ export async function buildAgents(opts: {
644
646
  return { agents, defaultAgentId, defaultModels, ambientTools };
645
647
  }
646
648
 
649
+ /** Log failed tool calls without changing the chunks returned to the client. */
650
+ function toolErrorLoggingProcessor(logger: log.Logger): OutputProcessor {
651
+ return {
652
+ id: "tool-error-logger",
653
+ async processOutputStream({ part }) {
654
+ if (part.type === "tool-error") {
655
+ const error = errorUtils.toError(part.payload.error);
656
+ logger.error("tool execution failed", {
657
+ toolName: part.payload.toolName,
658
+ toolCallId: part.payload.toolCallId,
659
+ error: error.message,
660
+ stack: error.stack,
661
+ });
662
+ }
663
+ return part;
664
+ },
665
+ };
666
+ }
667
+
647
668
  function workspaceSkillInputProcessors(
648
669
  workspace: Workspace | undefined,
649
670
  config: MastraPluginConfig,
650
- ): SkillSearchProcessor[] {
651
- if (!workspace?.skills || config.workspaceSkillSearch === false) return [];
671
+ ): Array<SkillSearchProcessor | CatalogueSkillSearchProcessor> {
672
+ if (!workspace || config.workspaceSkillSearch === false) return [];
652
673
  const options =
653
674
  typeof config.workspaceSkillSearch === "object" ? config.workspaceSkillSearch : undefined;
675
+ const catalogue = workspaceSkillCatalogueResolver(workspace);
676
+ if (catalogue) {
677
+ return [
678
+ new CatalogueSkillSearchProcessor({
679
+ resolve: catalogue,
680
+ topK: options?.topK ?? 5,
681
+ minScore: options?.minScore ?? 0.1,
682
+ ...(options?.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
683
+ }),
684
+ ];
685
+ }
686
+ if (!workspace.skills) return [];
654
687
  return [
655
688
  new SkillSearchProcessor({
656
689
  workspace,
package/src/config.ts CHANGED
@@ -211,20 +211,20 @@ export interface MastraPluginConfig extends BasePluginConfig {
211
211
  */
212
212
  memory?: boolean | MastraMemoryConfig;
213
213
  /**
214
- * Sandbox for auto-created agent workspaces. Defaults to Databricks Sandbox.
215
- * `true` selects Databricks with defaults, an object configures its lifecycle,
216
- * and `false` disables workspace command execution. An agent with an explicit
214
+ * Sandbox for auto-created agent workspaces. Defaults to Monty. `true`,
215
+ * `"databricks"`, or an object selects/configures Databricks Sandbox, and
216
+ * `false` disables workspace command execution. An agent with an explicit
217
217
  * custom `workspace` keeps that workspace and its sandbox.
218
218
  */
219
219
  sandbox?: boolean | "databricks" | "monty" | DatabricksWorkspaceSandboxOptions;
220
220
  /**
221
- * AppKit cache TTL for user-scoped Databricks workspace skill files.
222
- * Defaults to five minutes.
221
+ * Refresh TTL for the complete user-scoped workspace skill catalogue.
222
+ * Defaults to five minutes; auxiliary files remain uncached.
223
223
  */
224
224
  workspaceSkillRefreshTtlMs?: number;
225
225
  /**
226
- * Use Mastra's on-demand skill search instead of injecting the full skill
227
- * catalogue. Defaults to true. Pass an object to tune search.
226
+ * Use dbx-tools catalogue-backed on-demand skill search. Defaults to true.
227
+ * Pass an object to tune search.
228
228
  */
229
229
  workspaceSkillSearch?: boolean | MastraSkillSearchConfig;
230
230
  /**
@@ -665,17 +665,17 @@ export const MASTRA_CONFIG_SCHEMA: ConfigSchema = {
665
665
  sandbox: {
666
666
  type: ["boolean", "string", "object"],
667
667
  description:
668
- 'Command sandbox for auto-created agent workspaces. Defaults to Databricks Sandbox with Monty fallback; false disables command execution, "monty" selects Monty directly, and an object configures Databricks lifecycle/fallback settings.',
668
+ 'Command sandbox for auto-created agent workspaces. Defaults to Monty; false disables command execution, and true, "databricks", or an object selects/configures Databricks Sandbox with optional fallback.',
669
669
  },
670
670
  workspaceSkillRefreshTtlMs: {
671
671
  type: "number",
672
672
  description:
673
- "AppKit cache TTL in milliseconds for user-scoped Databricks workspace skill files. Defaults to 5 minutes.",
673
+ "Refresh TTL in milliseconds for the complete user-scoped workspace skill catalogue. Defaults to 5 minutes; auxiliary file reads stay fresh.",
674
674
  },
675
675
  workspaceSkillSearch: {
676
676
  type: ["boolean", "object"],
677
677
  description:
678
- "Use Mastra on-demand skill search instead of injecting every skill description. Defaults to true.",
678
+ "Use dbx-tools catalogue-backed on-demand skill search with lazy auxiliary reads. Defaults to true.",
679
679
  },
680
680
  defaultAgent: {
681
681
  type: "string",
package/src/genie.ts CHANGED
@@ -52,6 +52,7 @@ import type { StartedEvent } from "@dbx-tools/shared-mastra";
52
52
  import type { RequestContext } from "@mastra/core/request-context";
53
53
  import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
54
54
  import { createTool, type ToolExecutionContext } from "@mastra/core/tools";
55
+ import { LRUCache } from "lru-cache";
55
56
  import { z } from "zod";
56
57
 
57
58
  import type { MastraTools } from "./agents.ts";
@@ -1089,26 +1090,20 @@ const SUGGESTION_LIMIT = 6;
1089
1090
  */
1090
1091
  const SUGGESTION_CACHE_TTL_MS = 10 * 60_000;
1091
1092
 
1092
- /** Space-id -> cached sample questions with an absolute expiry. */
1093
- const _suggestionCache = new Map<string, { questions: string[]; expires: number }>();
1093
+ /** Bounded process-local cache of parsed sample questions by Genie space id. */
1094
+ const _suggestionCache = new LRUCache<string, string[]>({
1095
+ max: 128,
1096
+ ttl: SUGGESTION_CACHE_TTL_MS,
1097
+ });
1094
1098
 
1095
1099
  /** Read a space's cached questions, or `undefined` on miss / expiry. */
1096
1100
  function readSuggestionCache(spaceId: string): string[] | undefined {
1097
- const entry = _suggestionCache.get(spaceId);
1098
- if (!entry) return undefined;
1099
- if (entry.expires <= Date.now()) {
1100
- _suggestionCache.delete(spaceId);
1101
- return undefined;
1102
- }
1103
- return entry.questions;
1101
+ return _suggestionCache.get(spaceId);
1104
1102
  }
1105
1103
 
1106
1104
  /** Cache a space's parsed questions for {@link SUGGESTION_CACHE_TTL_MS}. */
1107
1105
  function writeSuggestionCache(spaceId: string, questions: string[]): void {
1108
- _suggestionCache.set(spaceId, {
1109
- questions,
1110
- expires: Date.now() + SUGGESTION_CACHE_TTL_MS,
1111
- });
1106
+ _suggestionCache.set(spaceId, questions);
1112
1107
  }
1113
1108
 
1114
1109
  /**
package/src/mlflow.ts CHANGED
@@ -30,8 +30,8 @@ import { appkit } from "@dbx-tools/appkit";
30
30
  import { asyncUtils, errorUtils, log, object } from "@dbx-tools/shared-core";
31
31
  import { feedback } from "@dbx-tools/shared-mastra";
32
32
  import { TraceLocationType, TraceMetadataKey } from "@mlflow/core";
33
- import { TraceInfo } from "@mlflow/core/dist/core/entities/trace_info";
34
- import { TraceState } from "@mlflow/core/dist/core/entities/trace_state";
33
+ import { TraceInfo } from "@mlflow/core/dist/core/entities/trace_info.js";
34
+ import { TraceState } from "@mlflow/core/dist/core/entities/trace_state.js";
35
35
  import type { Context } from "@opentelemetry/api";
36
36
  import type { ReadableSpan, Span as SdkSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
37
37
  import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
@@ -455,7 +455,7 @@ async function detectedDirectMlflowTraceLocation(
455
455
  const client = new mlflow.MlflowClient({ trackingUri, authProvider });
456
456
  const experiment = await client.getExperiment(experimentId);
457
457
  if (!experiment) return undefined;
458
- const { ucLocationFromExperimentTags } = await import("@mlflow/core/dist/core/destination");
458
+ const { ucLocationFromExperimentTags } = await import("@mlflow/core/dist/core/destination.js");
459
459
  const location = ucLocationFromExperimentTags(experiment.tags);
460
460
  if (!location?.tablePrefix) return undefined;
461
461
  return {
@@ -505,7 +505,7 @@ export async function initializeDirectMlflowTracing(): Promise<boolean> {
505
505
  }
506
506
  const [{ DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor }, sdk] =
507
507
  await Promise.all([
508
- import("@mlflow/core/dist/exporters/uc_table"),
508
+ import("@mlflow/core/dist/exporters/uc_table.js"),
509
509
  import("@opentelemetry/sdk-trace-node"),
510
510
  ]);
511
511
  const authProvider = mlflow.createAuthProvider({
@@ -1,33 +1,70 @@
1
1
  /**
2
2
  * Pydantic Monty fallback for Mastra command execution.
3
3
  *
4
- * Uses the Node subprocess-worker build of Monty. It executes Python source
4
+ * Prefers Monty's native Node subprocess workers and falls back to its WASM
5
+ * workers when the native package is unavailable. It executes Python source
5
6
  * with no host filesystem, network, environment, shell, or third-party package
6
- * access unless a future caller explicitly adds those capabilities.
7
+ * access.
7
8
  *
8
9
  * @module
9
10
  */
10
11
 
11
- import { errorUtils, functionUtils } from "@dbx-tools/shared-core";
12
+ import { configUtils } from "@dbx-tools/core";
13
+ import { errorUtils, functionUtils, log } from "@dbx-tools/shared-core";
12
14
  import type {
13
15
  CommandResult,
14
16
  ExecuteCommandOptions,
15
17
  SandboxInfo,
16
18
  WorkspaceSandbox,
17
19
  } from "@mastra/core/workspace";
18
- import type { CheckoutOptions, Monty as MontyPool, MontySession } from "@pydantic/monty/node";
20
+ import type { CheckoutOptions, Monty, MontyCrashedError, MontySession } from "@pydantic/monty";
19
21
 
20
22
  const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
21
23
  const DEFAULT_MAX_MEMORY_BYTES = 10_000_000;
24
+ const NATIVE_MONTY_TARGET = {
25
+ name: "native Monty",
26
+ load: () => import("@pydantic/monty/node"),
27
+ } as const;
28
+ const WASM_MONTY_TARGET = {
29
+ name: "WASM Monty",
30
+ load: () => import("@pydantic/monty/wasm"),
31
+ } as const;
32
+ const logger = log.logger("mastra/monty");
33
+
34
+ type MontyPool = Pick<Monty, "checkout">;
35
+
36
+ interface MontyContext {
37
+ crashedError: typeof MontyCrashedError;
38
+ pool: MontyPool;
39
+ }
22
40
 
23
- const montyModule = functionUtils.memoize(() => import("@pydantic/monty/node"));
24
- const montyPool = functionUtils.memoize(async (): Promise<MontyPool> => {
25
- const { Monty } = await montyModule();
26
- return Monty.create({
27
- minProcesses: 1,
28
- maxProcesses: 4,
29
- requestTimeout: 35,
30
- });
41
+ const montyContext = functionUtils.memoize(async (): Promise<MontyContext> => {
42
+ const errors: Error[] = [];
43
+ const targets = configUtils.boolean(undefined, "MONTY_FORCE_WASM")
44
+ ? [WASM_MONTY_TARGET]
45
+ : [NATIVE_MONTY_TARGET, WASM_MONTY_TARGET];
46
+ for (const target of targets) {
47
+ try {
48
+ const module = await target.load();
49
+ logger.debug(`${target.name} module available`);
50
+ const pool = await module.Monty.create({
51
+ minProcesses: 1,
52
+ maxProcesses: 4,
53
+ requestTimeout: 35,
54
+ });
55
+ logger.debug(`${target.name} pool created`);
56
+ return {
57
+ crashedError: module.MontyCrashedError,
58
+ pool,
59
+ };
60
+ } catch (error) {
61
+ errors.push(errorUtils.toError(error));
62
+ logger.debug(`${target.name} unavailable`, {
63
+ error: errorUtils.errorMessage(error),
64
+ });
65
+ }
66
+ }
67
+ throw new AggregateError(errors, "Failed to create Monty pool");
31
68
  });
32
69
 
33
70
  /** Resource controls for the Monty Python fallback. */
@@ -44,7 +81,7 @@ export interface MontySandboxOptions {
44
81
  typeCheck?: boolean;
45
82
  }
46
83
 
47
- /** Python-only, deny-by-default sandbox backed by Monty's Node worker pool. */
84
+ /** Python-only, deny-by-default sandbox backed by Monty's selected worker pool. */
48
85
  export class MontySandbox implements WorkspaceSandbox {
49
86
  readonly id: string;
50
87
  readonly name: string;
@@ -72,7 +109,7 @@ export class MontySandbox implements WorkspaceSandbox {
72
109
  this.status = "starting";
73
110
  this.error = undefined;
74
111
  try {
75
- await montyPool();
112
+ await montyContext();
76
113
  this.status = "running";
77
114
  } catch (caught) {
78
115
  this.status = "error";
@@ -135,12 +172,12 @@ export class MontySandbox implements WorkspaceSandbox {
135
172
 
136
173
  await this.start();
137
174
  const timeout = options.timeout ?? this.commandTimeoutMs;
138
- const pool = await montyPool();
175
+ const pool = (await montyContext()).pool;
139
176
  const session = await checkoutSession(
140
177
  pool,
141
178
  {
142
179
  limits: {
143
- maxDurationSecs: Math.max(0.001, timeout / 1_000),
180
+ maxFeedDurationSecs: Math.max(0.001, timeout / 1_000),
144
181
  maxMemory: this.maxMemoryBytes,
145
182
  },
146
183
  typeCheck: this.typeCheck,
@@ -183,8 +220,8 @@ export class MontySandbox implements WorkspaceSandbox {
183
220
  }
184
221
  const message = errorUtils.errorMessage(caught);
185
222
  output.emit("stderr", output.stderr.value ? `\n${message}` : message);
186
- const { MontyCrashedError } = await montyModule();
187
- const timedOut = caught instanceof MontyCrashedError && caught.timedOut;
223
+ const { crashedError } = await montyContext();
224
+ const timedOut = caught instanceof crashedError && caught.timedOut;
188
225
  return {
189
226
  command,
190
227
  args,
@@ -404,3 +441,10 @@ function failedResult(
404
441
  executionTimeMs: performance.now() - started,
405
442
  };
406
443
  }
444
+
445
+ if (import.meta.main) {
446
+ const sandbox = new MontySandbox();
447
+ await sandbox.start();
448
+ console.log(await sandbox.executeCommand("print('Hello, world!')"));
449
+ await sandbox.stop();
450
+ }
@@ -74,6 +74,7 @@ import type { OneOrMany } from "@dbx-tools/shared-core";
74
74
  import type { FileSystem } from "@dbx-tools/shared-fs";
75
75
 
76
76
  import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
77
+ import { clearWorkspaceSkillCache } from "./skill-cache.ts";
77
78
 
78
79
  const logger = log.logger("mastra/remote-skills");
79
80
 
@@ -441,6 +442,7 @@ export async function provisionRemoteSkills(
441
442
  const localSkillPaths: string[] = [];
442
443
  const skillNames: string[] = [];
443
444
  let staging: LocalFileSystem | undefined;
445
+ let workspaceUpdated = false;
444
446
 
445
447
  try {
446
448
  const sources = Array.isArray(options.sources) ? options.sources : [options.sources];
@@ -452,7 +454,8 @@ export async function provisionRemoteSkills(
452
454
  // the source's own stable local dir.
453
455
  const key = cacheKey(sourceOptions);
454
456
  const cacheFS = destination ?? localSkillsFS(key);
455
- const cached = (await readMetadata(cacheFS))?.sources[key];
457
+ const metadata = await readMetadata(cacheFS);
458
+ const cached = metadata?.sources[key];
456
459
  if (cached && isFresh(cached, resolveRefreshTtl(sourceOptions, options))) {
457
460
  skillNames.push(...cached.skills);
458
461
  if (!destination) localSkillPaths.push(cacheFS.root);
@@ -483,10 +486,12 @@ export async function provisionRemoteSkills(
483
486
  ...(policy ? { policy } : {}),
484
487
  };
485
488
  if (destination && databricksBasePath) {
489
+ await removeDeletedSkills(destination, key, metadata, record.skills);
486
490
  await copySkillDirs(destination, staged);
487
491
  // Only after the copy lands: a metadata entry written first would
488
492
  // mark a failed provision as fresh and suppress the retry for a day.
489
493
  await writeMetadata(destination, key, record);
494
+ workspaceUpdated = true;
490
495
  } else {
491
496
  localSkillPaths.push(await persistLocally(key, staged, record));
492
497
  }
@@ -515,9 +520,35 @@ export async function provisionRemoteSkills(
515
520
  }
516
521
  }
517
522
 
523
+ if (workspaceUpdated && client?.config?.getHost) {
524
+ await clearWorkspaceSkillCache({
525
+ host: (await client.config.getHost()).toString(),
526
+ });
527
+ }
528
+
518
529
  return { localSkillPaths, databricksBasePath, skillNames };
519
530
  }
520
531
 
532
+ /** Remove directories dropped by one source unless another source still owns them. */
533
+ async function removeDeletedSkills(
534
+ destination: FileSystem,
535
+ key: string,
536
+ metadata: RemoteSkillsMetadata | undefined,
537
+ nextSkills: readonly string[],
538
+ ): Promise<void> {
539
+ const previous = metadata?.sources[key]?.skills ?? [];
540
+ const retained = new Set(nextSkills);
541
+ for (const [sourceKey, entry] of Object.entries(metadata?.sources ?? {})) {
542
+ if (sourceKey === key) continue;
543
+ for (const skill of entry.skills) retained.add(skill);
544
+ }
545
+ await Promise.all(
546
+ previous
547
+ .filter((skill) => !retained.has(skill))
548
+ .map((skill) => destination.rmdir(skill, { force: true, recursive: true })),
549
+ );
550
+ }
551
+
521
552
  /** Probe file {@link openWritableWorkspace} writes and removes. */
522
553
  const WRITE_PROBE_FILE = ".dbx-tools-write-probe";
523
554