@dbx-tools/appkit-mastra 0.9.73 → 0.9.79

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-native.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.73",
38
- "@dbx-tools/core": "^0.9.73",
39
- "@dbx-tools/databricks": "^0.9.73",
40
- "@dbx-tools/fs": "^0.9.73",
41
- "@dbx-tools/genie": "^0.9.73",
42
- "@dbx-tools/model": "^0.9.73",
43
- "@dbx-tools/path": "^0.9.73",
44
- "@dbx-tools/postgres": "^0.9.73",
45
- "@dbx-tools/shared-core": "^0.9.73",
46
- "@dbx-tools/shared-fs": "^0.9.73",
47
- "@dbx-tools/shared-genie": "^0.9.73",
48
- "@dbx-tools/shared-mastra": "^0.9.73",
49
- "@dbx-tools/shared-model": "^0.9.73",
37
+ "@dbx-tools/appkit": "^0.9.79",
38
+ "@dbx-tools/core": "^0.9.79",
39
+ "@dbx-tools/databricks": "^0.9.79",
40
+ "@dbx-tools/fs": "^0.9.79",
41
+ "@dbx-tools/genie": "^0.9.79",
42
+ "@dbx-tools/model": "^0.9.79",
43
+ "@dbx-tools/path": "^0.9.79",
44
+ "@dbx-tools/postgres": "^0.9.79",
45
+ "@dbx-tools/shared-core": "^0.9.79",
46
+ "@dbx-tools/shared-fs": "^0.9.79",
47
+ "@dbx-tools/shared-genie": "^0.9.79",
48
+ "@dbx-tools/shared-mastra": "^0.9.79",
49
+ "@dbx-tools/shared-model": "^0.9.79",
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",
@@ -129,6 +132,10 @@
129
132
  "types": "./lib/src/model.d.ts",
130
133
  "default": "./lib/src/model.js"
131
134
  },
135
+ "./monty-native": {
136
+ "types": "./lib/src/monty-native.d.ts",
137
+ "default": "./lib/src/monty-native.js"
138
+ },
132
139
  "./monty-sandbox": {
133
140
  "types": "./lib/src/monty-sandbox.d.ts",
134
141
  "default": "./lib/src/monty-sandbox.js"
@@ -181,6 +188,10 @@
181
188
  "types": "./lib/src/skill-paths.d.ts",
182
189
  "default": "./lib/src/skill-paths.js"
183
190
  },
191
+ "./skill-search": {
192
+ "types": "./lib/src/skill-search.d.ts",
193
+ "default": "./lib/src/skill-search.js"
194
+ },
184
195
  "./statement": {
185
196
  "types": "./lib/src/statement.d.ts",
186
197
  "default": "./lib/src/statement.js"
@@ -220,7 +231,7 @@
220
231
  "./package.json": "./package.json"
221
232
  }
222
233
  },
223
- "version": "0.9.73",
234
+ "version": "0.9.79",
224
235
  "types": "./lib/index.d.ts",
225
236
  "dbxToolsConfig": {
226
237
  "tags": [
@@ -285,6 +296,10 @@
285
296
  "types": "./lib/src/model.d.ts",
286
297
  "default": "./lib/src/model.js"
287
298
  },
299
+ "./monty-native": {
300
+ "types": "./lib/src/monty-native.d.ts",
301
+ "default": "./lib/src/monty-native.js"
302
+ },
288
303
  "./monty-sandbox": {
289
304
  "types": "./lib/src/monty-sandbox.d.ts",
290
305
  "default": "./lib/src/monty-sandbox.js"
@@ -337,6 +352,10 @@
337
352
  "types": "./lib/src/skill-paths.d.ts",
338
353
  "default": "./lib/src/skill-paths.js"
339
354
  },
355
+ "./skill-search": {
356
+ "types": "./lib/src/skill-search.d.ts",
357
+ "default": "./lib/src/skill-search.js"
358
+ },
340
359
  "./statement": {
341
360
  "types": "./lib/src/statement.d.ts",
342
361
  "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({
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Ensures Monty's platform native package is present before the Node addon loads.
3
+ *
4
+ * `@pydantic/monty` ships `@pydantic/monty-<platform>` as optionalDependencies.
5
+ * npm can skip those (https://github.com/npm/cli/issues/4828), which surfaces as
6
+ * "Cannot find native binding". This module installs only the current platform
7
+ * package, at the already-installed `@pydantic/monty` version, without rewriting
8
+ * package.json.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { createRequire } from "node:module";
14
+ import { COMMAND_NOT_FOUND_EXIT_CODE, exec } from "@dbx-tools/core";
15
+ import { errorUtils, log } from "@dbx-tools/shared-core";
16
+
17
+ const logger = log.logger("mastra/monty-native");
18
+ const requireFromHere = createRequire(import.meta.url);
19
+
20
+ /** Result of spawning the package installer. */
21
+ export interface MontyNativeInstallResult {
22
+ exitCode: number | null;
23
+ stdout: string;
24
+ stderr: string;
25
+ }
26
+
27
+ /** Installer used by {@link ensureMontyNativeBinding}. */
28
+ export type MontyNativeInstaller = (
29
+ command: string,
30
+ args: readonly string[],
31
+ options: { cwd: string },
32
+ ) => Promise<MontyNativeInstallResult>;
33
+
34
+ /** Overrides for {@link ensureMontyNativeBinding} (tests inject resolve/install). */
35
+ export interface EnsureMontyNativeBindingOptions {
36
+ /** Working directory for the in-place install. Defaults to `process.cwd()`. */
37
+ cwd?: string;
38
+ /** Module resolver; defaults to `createRequire(import.meta.url).resolve`. */
39
+ resolve?: (specifier: string) => string;
40
+ /** Package installer; defaults to npm, then bun when npm is missing. */
41
+ install?: MontyNativeInstaller;
42
+ }
43
+
44
+ /**
45
+ * npm package that ships Monty's `.node` addon and `monty` binary for this
46
+ * process. Matches `@pydantic/monty`'s `platformTriple()` / optionalDependencies.
47
+ */
48
+ export function montyNativeBindingPackage(
49
+ platform: NodeJS.Platform = process.platform,
50
+ arch: string = process.arch,
51
+ ): string | undefined {
52
+ if (platform === "darwin" && (arch === "x64" || arch === "arm64")) {
53
+ return `@pydantic/monty-darwin-${arch}`;
54
+ }
55
+ if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
56
+ return `@pydantic/monty-linux-${arch}-gnu`;
57
+ }
58
+ if (platform === "win32" && arch === "x64") {
59
+ return "@pydantic/monty-win32-x64-msvc";
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Resolve the current platform's Monty native package, installing it in place
66
+ * when npm omitted the optional dependency.
67
+ */
68
+ export async function ensureMontyNativeBinding(
69
+ options: EnsureMontyNativeBindingOptions = {},
70
+ ): Promise<void> {
71
+ const pkg = montyNativeBindingPackage();
72
+ if (!pkg) return;
73
+
74
+ const resolve = options.resolve ?? ((specifier: string) => requireFromHere.resolve(specifier));
75
+ if (isResolvable(pkg, resolve)) return;
76
+
77
+ const version = montyPackageVersion(resolve);
78
+ const spec = `${pkg}@${version}`;
79
+ const cwd = options.cwd ?? process.cwd();
80
+ logger.warn("native-binding:missing", { package: spec, cwd });
81
+
82
+ const install = options.install ?? defaultInstaller;
83
+ const output = await installNativePackage(spec, cwd, install);
84
+ if (!isResolvable(pkg, resolve)) {
85
+ throw new Error(
86
+ `Monty native binding ${spec} is still missing after ${output.command}. ${output.detail}`,
87
+ );
88
+ }
89
+ logger.info("native-binding:installed", { package: spec, command: output.command });
90
+ }
91
+
92
+ function isResolvable(specifier: string, resolve: (specifier: string) => string): boolean {
93
+ try {
94
+ resolve(`${specifier}/package.json`);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function montyPackageVersion(resolve: (specifier: string) => string): string {
102
+ const pkg = requireFromHere(resolve("@pydantic/monty/package.json")) as { version?: string };
103
+ const version = pkg.version?.trim();
104
+ if (!version) throw new Error("Installed @pydantic/monty has no version");
105
+ return version;
106
+ }
107
+
108
+ async function installNativePackage(
109
+ spec: string,
110
+ cwd: string,
111
+ install: MontyNativeInstaller,
112
+ ): Promise<{ command: string; detail: string }> {
113
+ const npm = await install("npm", ["install", spec, "--no-save", "--no-package-lock"], { cwd });
114
+ if (npm.exitCode === 0) return { command: `npm install ${spec}`, detail: npm.stdout };
115
+ if (npm.exitCode !== COMMAND_NOT_FOUND_EXIT_CODE) {
116
+ throw new Error(
117
+ `Failed to install ${spec} with npm (exit ${npm.exitCode}): ${installDetail(npm)}`,
118
+ );
119
+ }
120
+
121
+ const bun = await install("bun", ["add", spec, "--no-save"], { cwd });
122
+ if (bun.exitCode === 0) return { command: `bun add ${spec}`, detail: bun.stdout };
123
+ throw new Error(
124
+ `Failed to install ${spec}: npm was not found, bun add exited ${bun.exitCode}: ${installDetail(bun)}`,
125
+ );
126
+ }
127
+
128
+ async function defaultInstaller(
129
+ command: string,
130
+ args: readonly string[],
131
+ options: { cwd: string },
132
+ ): Promise<MontyNativeInstallResult> {
133
+ const result = await exec.spawn(command, [...args], {
134
+ cwd: options.cwd,
135
+ stdout: "capture",
136
+ stderr: "capture",
137
+ stdin: "ignore",
138
+ check: false,
139
+ });
140
+ return {
141
+ exitCode: result.exitCode,
142
+ stdout: result.stdout,
143
+ stderr: result.stderr,
144
+ };
145
+ }
146
+
147
+ function installDetail(result: MontyNativeInstallResult): string {
148
+ return errorUtils.errorMessage(result.stderr || result.stdout || `exit ${result.exitCode}`);
149
+ }
@@ -17,10 +17,15 @@ import type {
17
17
  } from "@mastra/core/workspace";
18
18
  import type { CheckoutOptions, Monty as MontyPool, MontySession } from "@pydantic/monty/node";
19
19
 
20
+ import { ensureMontyNativeBinding } from "./monty-native.ts";
21
+
20
22
  const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
21
23
  const DEFAULT_MAX_MEMORY_BYTES = 10_000_000;
22
24
 
23
- const montyModule = functionUtils.memoize(() => import("@pydantic/monty/node"));
25
+ const montyModule = functionUtils.memoize(async () => {
26
+ await ensureMontyNativeBinding();
27
+ return import("@pydantic/monty/node");
28
+ });
24
29
  const montyPool = functionUtils.memoize(async (): Promise<MontyPool> => {
25
30
  const { Monty } = await montyModule();
26
31
  return Monty.create({
@@ -140,7 +145,7 @@ export class MontySandbox implements WorkspaceSandbox {
140
145
  pool,
141
146
  {
142
147
  limits: {
143
- maxDurationSecs: Math.max(0.001, timeout / 1_000),
148
+ maxFeedDurationSecs: Math.max(0.001, timeout / 1_000),
144
149
  maxMemory: this.maxMemoryBytes,
145
150
  },
146
151
  typeCheck: this.typeCheck,
@@ -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
 
package/src/sandbox.ts CHANGED
@@ -546,6 +546,7 @@ function isDatabricksSandboxUnavailable(caught: unknown): boolean {
546
546
  const context = errorUtils.errorContext(caught);
547
547
  return (
548
548
  context.statusCode === 404 ||
549
+ context.hasMessage("requested", "api", "not", "available") ||
549
550
  context.hasMessage("feature", "disabled") ||
550
551
  context.hasMessage("sandbox", "not", "enabled") ||
551
552
  context.hasMessage("preview", "not", "enabled") ||