@dbx-tools/appkit-mastra 0.9.118 → 0.9.120
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.
- package/README.md +122 -108
- package/index.ts +3 -6
- package/lib/index.d.ts +3 -6
- package/lib/index.js +3 -5
- package/lib/src/agents.js +4 -5
- package/lib/src/chart.js +4 -4
- package/lib/src/chat.js +2 -2
- package/lib/src/config.d.ts +24 -19
- package/lib/src/config.js +6 -6
- package/lib/src/defaults.js +4 -4
- package/lib/src/genie.d.ts +3 -1
- package/lib/src/genie.js +18 -14
- package/lib/src/memory.d.ts +2 -22
- package/lib/src/memory.js +2 -32
- package/lib/src/mlflow.d.ts +3 -10
- package/lib/src/mlflow.js +6 -6
- package/lib/src/monty-sandbox.js +2 -2
- package/lib/src/plugin.d.ts +2 -19
- package/lib/src/plugin.js +14 -72
- package/lib/src/remote-skills.d.ts +32 -28
- package/lib/src/remote-skills.js +113 -33
- package/lib/src/server.js +2 -2
- package/lib/src/workspaces.d.ts +4 -7
- package/lib/src/workspaces.js +59 -56
- package/lib/src/writer.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -24
- package/src/agents.ts +3 -4
- package/src/chart.ts +3 -3
- package/src/chat.ts +1 -1
- package/src/config.ts +33 -24
- package/src/defaults.ts +3 -3
- package/src/genie.ts +18 -15
- package/src/memory.ts +2 -32
- package/src/mlflow.ts +7 -15
- package/src/monty-sandbox.ts +1 -1
- package/src/plugin.ts +15 -70
- package/src/remote-skills.ts +168 -42
- package/src/server.ts +1 -1
- package/src/workspaces.ts +62 -78
- package/src/writer.ts +1 -1
- package/lib/src/sandbox.d.ts +0 -107
- package/lib/src/sandbox.js +0 -461
- package/src/sandbox.ts +0 -556
package/src/plugin.ts
CHANGED
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
type PluginManifest,
|
|
57
57
|
type ResourceRequirement,
|
|
58
58
|
} from "@databricks/appkit";
|
|
59
|
-
import { pluginRegistry } from "@dbx-tools/appkit";
|
|
59
|
+
import { lakebaseResolver, pluginRegistry } from "@dbx-tools/appkit";
|
|
60
60
|
import { modelCatalog } from "@dbx-tools/model";
|
|
61
61
|
import { asyncUtils, errorUtils, log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
62
62
|
import {
|
|
@@ -98,7 +98,7 @@ import {
|
|
|
98
98
|
type MastraIdentityMode,
|
|
99
99
|
} from "./identity.ts";
|
|
100
100
|
import { buildMcpServer, type ResolvedMcp } from "./mcp.ts";
|
|
101
|
-
import { createMemoryBuilder,
|
|
101
|
+
import { createMemoryBuilder, needsLakebase } from "./memory.ts";
|
|
102
102
|
import {
|
|
103
103
|
flushDirectMlflowTracing,
|
|
104
104
|
logFeedback,
|
|
@@ -124,13 +124,6 @@ import { invalidFields } from "./validation.ts";
|
|
|
124
124
|
const GENIE_MANIFEST = pluginRegistry.data(genie).plugin.manifest;
|
|
125
125
|
const LAKEBASE_MANIFEST = pluginRegistry.data(lakebase).plugin.manifest;
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
* Budget for draining the memory service-principal pool on shutdown. Well
|
|
129
|
-
* inside AppKit's 15s graceful-shutdown window, so a stuck connection can
|
|
130
|
-
* never be what holds the process open.
|
|
131
|
-
*/
|
|
132
|
-
const POOL_DRAIN_TIMEOUT_MS = 5_000;
|
|
133
|
-
|
|
134
127
|
/** Ceiling on the `?timeoutMs=` long-poll budget a client may request. */
|
|
135
128
|
const MAX_EMBED_POLL_TIMEOUT_MS = 5 * 60_000;
|
|
136
129
|
|
|
@@ -243,15 +236,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
243
236
|
* boot rather than the first request, and so every request agrees.
|
|
244
237
|
*/
|
|
245
238
|
private identityMode: MastraIdentityMode = "user";
|
|
246
|
-
/**
|
|
247
|
-
* Dedicated service-principal Lakebase pool backing Mastra memory /
|
|
248
|
-
* storage. Built once in {@link buildAgentAndServer} (outside any
|
|
249
|
-
* `asUser` scope, so it never inherits a request's OBO identity) and
|
|
250
|
-
* drained in {@link shutdown}. `null` until setup runs or when
|
|
251
|
-
* Lakebase isn't needed.
|
|
252
|
-
*/
|
|
253
|
-
private servicePrincipalPool: Pool | null = null;
|
|
254
|
-
|
|
255
239
|
override async setup(): Promise<void> {
|
|
256
240
|
// Resolve the identity mode up front so an invalid `genieIdentity` fails the
|
|
257
241
|
// app boot with a clear message rather than 500-ing the first chat request.
|
|
@@ -298,44 +282,19 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
298
282
|
if (this.config.memory === undefined) this.config.memory = true;
|
|
299
283
|
}
|
|
300
284
|
|
|
301
|
-
/**
|
|
302
|
-
* Drain the memory service-principal pool. Idempotent: the handle is
|
|
303
|
-
* cleared before the drain starts, so a second call is a no-op and a
|
|
304
|
-
* later `setup()` rebuilds the pool. Bounded by
|
|
305
|
-
* {@link POOL_DRAIN_TIMEOUT_MS} to stay well inside the 15s graceful
|
|
306
|
-
* shutdown budget.
|
|
307
|
-
*/
|
|
285
|
+
/** Flush pending direct MLflow traces during graceful shutdown. */
|
|
308
286
|
async shutdown(): Promise<void> {
|
|
309
287
|
try {
|
|
310
288
|
await flushDirectMlflowTracing();
|
|
311
289
|
} catch (err) {
|
|
312
290
|
this.logger.warn("error flushing direct MLflow traces", {
|
|
313
|
-
error: errorUtils.
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
const pool = this.servicePrincipalPool;
|
|
317
|
-
if (!pool) return;
|
|
318
|
-
this.servicePrincipalPool = null;
|
|
319
|
-
this.logger.info("closing memory SP pool");
|
|
320
|
-
// The budget timer is cancelled on the way out so a drain that finished
|
|
321
|
-
// early cannot hold the event loop open for the rest of the window.
|
|
322
|
-
const budget = new AbortController();
|
|
323
|
-
try {
|
|
324
|
-
await Promise.race([pool.end(), asyncUtils.sleep(POOL_DRAIN_TIMEOUT_MS, budget.signal)]);
|
|
325
|
-
} catch (err) {
|
|
326
|
-
this.logger.error("error closing memory SP pool", {
|
|
327
|
-
error: errorUtils.errorMessage(err),
|
|
291
|
+
error: errorUtils.toError(err),
|
|
328
292
|
});
|
|
329
|
-
} finally {
|
|
330
|
-
budget.abort();
|
|
331
293
|
}
|
|
332
294
|
}
|
|
333
295
|
|
|
334
296
|
/**
|
|
335
|
-
* Abort in-flight work
|
|
336
|
-
* synchronously and never awaits {@link shutdown}, so the pool drain is
|
|
337
|
-
* started here too; it cannot be awaited from a `void` hook, and
|
|
338
|
-
* {@link shutdown} is idempotent so the duplicate call is free.
|
|
297
|
+
* Abort in-flight work and start the asynchronous trace flush.
|
|
339
298
|
*/
|
|
340
299
|
override abortActiveOperations(): void {
|
|
341
300
|
super.abortActiveOperations();
|
|
@@ -719,7 +678,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
719
678
|
// the upstream status. Log and degrade.
|
|
720
679
|
this.logger.warn("suggestions:error", {
|
|
721
680
|
status: result.status,
|
|
722
|
-
error: result.message,
|
|
681
|
+
error: errorUtils.toError(result.message),
|
|
723
682
|
});
|
|
724
683
|
res.json({ questions: [] });
|
|
725
684
|
return;
|
|
@@ -765,7 +724,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
765
724
|
if (!parsed.success) {
|
|
766
725
|
// A Zod issue string quotes the received body back, so only the
|
|
767
726
|
// offending field names travel; the full issue list is logged.
|
|
768
|
-
this.logger.warn("feedback:invalid", { error: parsed.error
|
|
727
|
+
this.logger.warn("feedback:invalid", { error: parsed.error });
|
|
769
728
|
res.status(400).json({
|
|
770
729
|
ok: false,
|
|
771
730
|
error: INVALID_FEEDBACK_MESSAGE,
|
|
@@ -856,7 +815,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
856
815
|
): void {
|
|
857
816
|
this.logger.warn(`${operation}:error`, {
|
|
858
817
|
status: result.status,
|
|
859
|
-
error: result.message,
|
|
818
|
+
error: errorUtils.toError(result.message),
|
|
860
819
|
});
|
|
861
820
|
if (res.headersSent) return;
|
|
862
821
|
res.status(result.status).json({ error: clientMessage });
|
|
@@ -1074,32 +1033,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1074
1033
|
if (result.ok) return result.data;
|
|
1075
1034
|
this.logger.warn("listModels:error", {
|
|
1076
1035
|
status: result.status,
|
|
1077
|
-
error: result.message,
|
|
1036
|
+
error: errorUtils.toError(result.message),
|
|
1078
1037
|
});
|
|
1079
1038
|
throw new ExecutionError(MODEL_CATALOGUE_FAILED_MESSAGE);
|
|
1080
1039
|
}
|
|
1081
1040
|
|
|
1082
1041
|
private async buildAgentAndServer(): Promise<void> {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
// `getPgConfig()` is read here, outside any `asUser` scope, so it
|
|
1088
|
-
// returns the SP connection target + token refresh plus any
|
|
1089
|
-
// `lakebase({ pool })` overrides; `require` turns a missing
|
|
1090
|
-
// sibling into a clear wiring error. The builder caches the shared
|
|
1091
|
-
// `PgVector` singleton so registering N agents stays cheap. See
|
|
1092
|
-
// `./memory.js`.
|
|
1093
|
-
if (needsLakebase(this.config)) {
|
|
1094
|
-
const spPgConfig = pluginRegistry
|
|
1095
|
-
.require(this.context, lakebase, this.config)
|
|
1096
|
-
.exports()
|
|
1097
|
-
.getPgConfig();
|
|
1098
|
-
this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
|
|
1099
|
-
}
|
|
1100
|
-
const memoryBuilder = this.servicePrincipalPool
|
|
1101
|
-
? createMemoryBuilder(this.config, this.servicePrincipalPool)
|
|
1042
|
+
const memoryPool = needsLakebase(this.config)
|
|
1043
|
+
? (lakebaseResolver.captureServicePrincipalPool(
|
|
1044
|
+
pluginRegistry.require(this.context, lakebase, this.config).exports().pool,
|
|
1045
|
+
) as Pool)
|
|
1102
1046
|
: undefined;
|
|
1047
|
+
const memoryBuilder = memoryPool ? createMemoryBuilder(this.config, memoryPool) : undefined;
|
|
1103
1048
|
|
|
1104
1049
|
// Build every agent declared in `config.agents` (or the built-in
|
|
1105
1050
|
// fallback when none are declared). Each agent's `model` resolves
|
|
@@ -1126,7 +1071,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1126
1071
|
context: this.context,
|
|
1127
1072
|
memoryBuilder,
|
|
1128
1073
|
log: this.logger,
|
|
1129
|
-
extraSkillMounts: provisioned.localSkillMounts,
|
|
1074
|
+
extraSkillMounts: [...(this.config.localSkillMounts ?? []), ...provisioned.localSkillMounts],
|
|
1130
1075
|
});
|
|
1131
1076
|
|
|
1132
1077
|
// `mastra.server.apiRoutes` is only honored by Mastra's standalone
|
package/src/remote-skills.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* {@link AITOOLS_SOURCE} constant, a GitHub `owner/repo`, any git / GitLab URL,
|
|
6
6
|
* or a direct download URL - and optional per-source policy.
|
|
7
7
|
* {@link provisionRemoteSkills} materializes every source into a local
|
|
8
|
-
* `SKILL.md` tree at app boot and returns read-only
|
|
8
|
+
* `SKILL.md` tree at app boot and returns read-only filesystem backings plus
|
|
9
9
|
* their stable virtual workspace mounts.
|
|
10
10
|
*
|
|
11
11
|
* Resolution per source, in order:
|
|
@@ -26,25 +26,16 @@
|
|
|
26
26
|
* source (or the top-level call) sets `failOnError: false`, in which case it is
|
|
27
27
|
* logged and skipped so one bad source never takes the app down.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* record travels WITH the tree rather than in process memory, so the reuse
|
|
35
|
-
* survives a restart - which is the only way it helps a Databricks App at all.
|
|
36
|
-
* Pass `refreshTtlMs: 0` to download on every boot.
|
|
29
|
+
* AI Tools uses the shared `@dbx-tools/cache` project cache with a one-day TTL,
|
|
30
|
+
* then hydrates one process-memory filesystem mounted at
|
|
31
|
+
* {@link AITOOLS_SKILLS_MOUNT} for every default workspace. Other sources keep
|
|
32
|
+
* a {@link METADATA_FILE} beside their provisioned tree and default to a
|
|
33
|
+
* seven-day refresh window. Pass `refreshTtlMs: 0` to download on every boot.
|
|
37
34
|
*
|
|
38
|
-
*
|
|
39
|
-
* (`/Workspace/.assistant/skills`)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* instead. When no Databricks client is resolvable at startup - or the resolved
|
|
43
|
-
* identity cannot WRITE the destination, which is the normal case for a
|
|
44
|
-
* Databricks App service principal against the admin-owned shared tree (see
|
|
45
|
-
* {@link openWritableWorkspace}) - the tree is written under
|
|
46
|
-
* {@link localFS.tmpFS} and exposed through a stable read-only virtual mount
|
|
47
|
-
* for the current process. Storage location degrades; the skills themselves do not.
|
|
35
|
+
* For non-AI-Tools sources, the default destination is the Databricks workspace
|
|
36
|
+
* Assistant skills tree (`/Workspace/.assistant/skills`). Pass `userEmail` or
|
|
37
|
+
* `databricksBasePath` to select another destination. When it is unavailable,
|
|
38
|
+
* the tree is exposed through a stable read-only local mount instead.
|
|
48
39
|
*
|
|
49
40
|
* @module
|
|
50
41
|
*/
|
|
@@ -55,6 +46,7 @@ import { dirname, join, posix } from "node:path";
|
|
|
55
46
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
56
47
|
import { appkit } from "@dbx-tools/appkit";
|
|
57
48
|
import type { WorkspaceClientLike } from "@dbx-tools/appkit";
|
|
49
|
+
import { flushCache, getOrSet, projectCache } from "@dbx-tools/cache/cache";
|
|
58
50
|
import { exec } from "@dbx-tools/core";
|
|
59
51
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
60
52
|
import { localFS, type LocalFileSystem } from "@dbx-tools/fs";
|
|
@@ -70,7 +62,7 @@ import {
|
|
|
70
62
|
stringUtils,
|
|
71
63
|
} from "@dbx-tools/shared-core";
|
|
72
64
|
import type { OneOrMany } from "@dbx-tools/shared-core";
|
|
73
|
-
import type
|
|
65
|
+
import { MemoryFileSystem, type FileSystem } from "@dbx-tools/shared-fs";
|
|
74
66
|
|
|
75
67
|
import { ORGANIZATION_SKILLS_PATH, personalSkillsPath } from "./skill-paths.ts";
|
|
76
68
|
|
|
@@ -116,9 +108,17 @@ const METADATA_FILE = ".metadata.json";
|
|
|
116
108
|
/** Shape marker for {@link METADATA_FILE}; an unknown version is ignored, not read. */
|
|
117
109
|
const METADATA_VERSION = 1;
|
|
118
110
|
|
|
119
|
-
/** How long a provisioned source is reused before being downloaded again. */
|
|
111
|
+
/** How long a general provisioned source is reused before being downloaded again. */
|
|
120
112
|
const DEFAULT_REFRESH_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
121
113
|
|
|
114
|
+
/** How long the shared Databricks AI Tools snapshot is reused. */
|
|
115
|
+
export const AITOOLS_REFRESH_TTL_MS = 24 * 60 * 60 * 1000;
|
|
116
|
+
|
|
117
|
+
const aiToolsCache = projectCache("mastra-aitools", {
|
|
118
|
+
ttl: AITOOLS_REFRESH_TTL_MS,
|
|
119
|
+
maxEntries: 32,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
122
|
/* ------------------------------ AI Tools ------------------------------ */
|
|
123
123
|
|
|
124
124
|
/**
|
|
@@ -203,14 +203,25 @@ export type RemoteSkillsOption =
|
|
|
203
203
|
/** A source entry with its `source` flattened to a plain string. */
|
|
204
204
|
type NormalizedSource = Omit<RemoteSkillSourceOptions, "source"> & { source: string };
|
|
205
205
|
|
|
206
|
-
/** Read-only local
|
|
207
|
-
export interface
|
|
206
|
+
/** Read-only local directory exposed at a stable virtual workspace path. */
|
|
207
|
+
export interface LocalSkillDirectoryMount {
|
|
208
208
|
/** Physical local directory containing the provisioned `SKILL.md` tree. */
|
|
209
209
|
root: string;
|
|
210
210
|
/** Virtual path agents see inside the composed Mastra workspace. */
|
|
211
211
|
mountPath: string;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/** Read-only process-local filesystem exposed at a stable virtual workspace path. */
|
|
215
|
+
export interface MemorySkillMount {
|
|
216
|
+
/** Process-shared filesystem containing the provisioned `SKILL.md` tree. */
|
|
217
|
+
filesystem: FileSystem;
|
|
218
|
+
/** Virtual path agents see inside the composed Mastra workspace. */
|
|
219
|
+
mountPath: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Local skill backing accepted by every auto-created workspace. */
|
|
223
|
+
export type LocalSkillMount = LocalSkillDirectoryMount | MemorySkillMount;
|
|
224
|
+
|
|
214
225
|
/** Top-level remote-skills provisioning options. */
|
|
215
226
|
export interface ProvisionRemoteSkillsOptions {
|
|
216
227
|
/** Sources to materialize at startup. */
|
|
@@ -233,8 +244,8 @@ export interface ProvisionRemoteSkillsOptions {
|
|
|
233
244
|
maxDownloadBytes?: number;
|
|
234
245
|
/**
|
|
235
246
|
* How long a provisioned tree is reused before its source is downloaded
|
|
236
|
-
* again, in milliseconds.
|
|
237
|
-
* boot. A per-source `refreshTtlMs` wins
|
|
247
|
+
* again, in milliseconds. AI Tools defaults to one day and other sources to
|
|
248
|
+
* seven days; `0` re-downloads on every boot. A per-source `refreshTtlMs` wins.
|
|
238
249
|
*/
|
|
239
250
|
refreshTtlMs?: number;
|
|
240
251
|
}
|
|
@@ -242,9 +253,10 @@ export interface ProvisionRemoteSkillsOptions {
|
|
|
242
253
|
/** What {@link provisionRemoteSkills} resolved. */
|
|
243
254
|
export interface ProvisionedRemoteSkills {
|
|
244
255
|
/**
|
|
245
|
-
* Extra read-only
|
|
246
|
-
*
|
|
247
|
-
*
|
|
256
|
+
* Extra read-only local or process-memory skill mounts to hand Mastra. AI
|
|
257
|
+
* Tools always uses process memory; other sources use this fallback when
|
|
258
|
+
* they could not be written to Databricks. Physical paths remain hidden
|
|
259
|
+
* behind stable virtual workspace paths.
|
|
248
260
|
*/
|
|
249
261
|
localSkillMounts: LocalSkillMount[];
|
|
250
262
|
/** Absolute Databricks destination each source was written to, if any. */
|
|
@@ -257,8 +269,8 @@ export interface ProvisionedRemoteSkills {
|
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
/**
|
|
260
|
-
*
|
|
261
|
-
* {@link METADATA_FILE}.
|
|
272
|
+
* Metadata for one resolved source snapshot. General remote sources store it
|
|
273
|
+
* in {@link METADATA_FILE}; AI Tools stores it with the cache-package value.
|
|
262
274
|
*/
|
|
263
275
|
export interface RemoteSkillCacheEntry {
|
|
264
276
|
/** The source string this entry was downloaded from, for readability. */
|
|
@@ -281,6 +293,20 @@ export interface RemoteSkillsMetadata {
|
|
|
281
293
|
sources: Record<string, RemoteSkillCacheEntry>;
|
|
282
294
|
}
|
|
283
295
|
|
|
296
|
+
interface CachedSkillFile {
|
|
297
|
+
path: string;
|
|
298
|
+
content: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface CachedAiToolsTree {
|
|
302
|
+
files: CachedSkillFile[];
|
|
303
|
+
record: RemoteSkillCacheEntry;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface ResolvedAiToolsTree extends CachedAiToolsTree {
|
|
307
|
+
cached: boolean;
|
|
308
|
+
}
|
|
309
|
+
|
|
284
310
|
/* ------------------------------- helpers ------------------------------- */
|
|
285
311
|
|
|
286
312
|
/** Normalize the `remoteSkills` option into a flat options bag. */
|
|
@@ -381,12 +407,16 @@ function remoteSourceIdentifier(source: string): string {
|
|
|
381
407
|
return `${url.hostname}-${path}`;
|
|
382
408
|
}
|
|
383
409
|
|
|
384
|
-
/** The effective reuse window: per-source,
|
|
410
|
+
/** The effective reuse window: per-source, top-level, then the source default. */
|
|
385
411
|
function resolveRefreshTtl(
|
|
386
412
|
sourceOptions: NormalizedSource,
|
|
387
413
|
options: ProvisionRemoteSkillsOptions,
|
|
388
414
|
): number {
|
|
389
|
-
return
|
|
415
|
+
return (
|
|
416
|
+
sourceOptions.refreshTtlMs ??
|
|
417
|
+
options.refreshTtlMs ??
|
|
418
|
+
(isAiToolsSource(sourceOptions.source) ? AITOOLS_REFRESH_TTL_MS : DEFAULT_REFRESH_TTL_MS)
|
|
419
|
+
);
|
|
390
420
|
}
|
|
391
421
|
|
|
392
422
|
/**
|
|
@@ -406,7 +436,7 @@ async function readMetadata(fs: FileSystem): Promise<RemoteSkillsMetadata | unde
|
|
|
406
436
|
if (!object.isRecord(parsed) || parsed.version !== METADATA_VERSION) return undefined;
|
|
407
437
|
return object.isRecord(parsed.sources) ? parsed : undefined;
|
|
408
438
|
} catch (err) {
|
|
409
|
-
logger.debug("cache:unreadable", { error: errorUtils.
|
|
439
|
+
logger.debug("cache:unreadable", { error: errorUtils.toError(err) });
|
|
410
440
|
return undefined;
|
|
411
441
|
}
|
|
412
442
|
}
|
|
@@ -465,12 +495,17 @@ export async function provisionRemoteSkills(
|
|
|
465
495
|
if (!options) return empty;
|
|
466
496
|
|
|
467
497
|
const failDefault = options.failOnError !== false;
|
|
498
|
+
const sources = (Array.isArray(options.sources) ? options.sources : [options.sources]).map(
|
|
499
|
+
toSourceOptions,
|
|
500
|
+
);
|
|
468
501
|
const client = options.client ?? appkit.tryGetExecutionContext()?.client;
|
|
469
502
|
// Probed rather than assumed: an identity that cannot write the workspace
|
|
470
503
|
// tree falls back to the local one instead of failing startup. The probe also
|
|
471
504
|
// creates the root, which the first cache READ needs - `copySkillDirs` would
|
|
472
505
|
// otherwise be the first thing to create it, which is too late.
|
|
473
|
-
const requestedBasePath =
|
|
506
|
+
const requestedBasePath = sources.some((source) => !isAiToolsSource(source.source))
|
|
507
|
+
? resolveDatabricksBasePath(options, client)
|
|
508
|
+
: undefined;
|
|
474
509
|
const destination = requestedBasePath
|
|
475
510
|
? await openWritableWorkspace(requestedBasePath, client as WorkspaceClient)
|
|
476
511
|
: undefined;
|
|
@@ -481,15 +516,30 @@ export async function provisionRemoteSkills(
|
|
|
481
516
|
let staging: LocalFileSystem | undefined;
|
|
482
517
|
|
|
483
518
|
try {
|
|
484
|
-
const
|
|
485
|
-
for (const entry of sources) {
|
|
486
|
-
const sourceOptions = toSourceOptions(entry);
|
|
519
|
+
for (const sourceOptions of sources) {
|
|
487
520
|
const failOnError = sourceOptions.failOnError ?? failDefault;
|
|
488
521
|
try {
|
|
489
522
|
// Where this source's bookkeeping lives: the shared Databricks tree, or
|
|
490
523
|
// the source's own stable local dir.
|
|
491
524
|
const key = cacheKey(sourceOptions);
|
|
492
525
|
const mountPath = localSkillMountPath(sourceOptions, key);
|
|
526
|
+
if (isAiToolsSource(sourceOptions.source)) {
|
|
527
|
+
staging ??= await initializedScratch("mastra-remote-skills");
|
|
528
|
+
const tree = await cachedAiToolsTree(sourceOptions, staging.root, options, key);
|
|
529
|
+
localSkillMounts.push({
|
|
530
|
+
filesystem: await memorySkillTree(tree.files, `aitools-${key}`),
|
|
531
|
+
mountPath,
|
|
532
|
+
});
|
|
533
|
+
skillNames.push(...tree.record.skills);
|
|
534
|
+
logger.info("remote skill ready", {
|
|
535
|
+
source: sourceOptions.source,
|
|
536
|
+
destination: mountPath,
|
|
537
|
+
downloadedAt: tree.record.downloadedAt,
|
|
538
|
+
skills: tree.record.skills,
|
|
539
|
+
cached: tree.cached,
|
|
540
|
+
});
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
493
543
|
const cacheFS = destination ?? localSkillsFS(key);
|
|
494
544
|
const metadata = await readMetadata(cacheFS);
|
|
495
545
|
const cached = metadata?.sources[key];
|
|
@@ -549,7 +599,7 @@ export async function provisionRemoteSkills(
|
|
|
549
599
|
}
|
|
550
600
|
logger.warn("source:skipped", {
|
|
551
601
|
source: sourceOptions.source,
|
|
552
|
-
error: errorUtils.
|
|
602
|
+
error: errorUtils.toError(err),
|
|
553
603
|
});
|
|
554
604
|
}
|
|
555
605
|
}
|
|
@@ -563,17 +613,90 @@ export async function provisionRemoteSkills(
|
|
|
563
613
|
return { localSkillMounts, databricksBasePath, skillNames };
|
|
564
614
|
}
|
|
565
615
|
|
|
616
|
+
async function cachedAiToolsTree(
|
|
617
|
+
sourceOptions: NormalizedSource,
|
|
618
|
+
stagingRoot: string,
|
|
619
|
+
options: ProvisionRemoteSkillsOptions,
|
|
620
|
+
key: string,
|
|
621
|
+
): Promise<ResolvedAiToolsTree> {
|
|
622
|
+
const ttl = resolveRefreshTtl(sourceOptions, options);
|
|
623
|
+
let loaded = false;
|
|
624
|
+
const load = async (): Promise<CachedAiToolsTree> => {
|
|
625
|
+
loaded = true;
|
|
626
|
+
const stagedDir = await stageAiTools(
|
|
627
|
+
sourceOptions,
|
|
628
|
+
join(stagingRoot, `aitools-${hash.id()}`),
|
|
629
|
+
options,
|
|
630
|
+
);
|
|
631
|
+
const staged = await collectSkillDirs(stagedDir);
|
|
632
|
+
if (staged.length === 0) {
|
|
633
|
+
throw new Error(`no SKILL.md found for source "${sourceOptions.source}"`);
|
|
634
|
+
}
|
|
635
|
+
const policy = cachePolicy(sourceOptions);
|
|
636
|
+
return {
|
|
637
|
+
files: await serializeSkillDirs(staged),
|
|
638
|
+
record: {
|
|
639
|
+
source: sourceOptions.source,
|
|
640
|
+
downloadedAt: new Date().toISOString(),
|
|
641
|
+
skills: staged.map((dir) => dir.name),
|
|
642
|
+
...(policy ? { policy } : {}),
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
};
|
|
646
|
+
const tree =
|
|
647
|
+
ttl <= 0
|
|
648
|
+
? await load()
|
|
649
|
+
: await getOrSet(aiToolsCache, key, load, {
|
|
650
|
+
ttl,
|
|
651
|
+
throwErrors: true,
|
|
652
|
+
});
|
|
653
|
+
if (!tree) throw new Error(`AI Tools cache returned no tree for ${key}`);
|
|
654
|
+
await flushCache(aiToolsCache);
|
|
655
|
+
return { ...tree, cached: !loaded };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function serializeSkillDirs(dirs: readonly StagedSkillDir[]): Promise<CachedSkillFile[]> {
|
|
659
|
+
const files: CachedSkillFile[] = [];
|
|
660
|
+
for (const dir of dirs) {
|
|
661
|
+
for await (const relative of find.findFiles("**/*", {
|
|
662
|
+
cwd: dir.absolutePath,
|
|
663
|
+
nodir: true,
|
|
664
|
+
})) {
|
|
665
|
+
files.push({
|
|
666
|
+
path: posix.join(dir.name, relative.split(/[\\/]/).join("/")),
|
|
667
|
+
content: (await readFile(join(dir.absolutePath, relative))).toString("base64"),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return files;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function memorySkillTree(
|
|
675
|
+
files: readonly CachedSkillFile[],
|
|
676
|
+
id: string,
|
|
677
|
+
): Promise<MemoryFileSystem> {
|
|
678
|
+
const filesystem = new MemoryFileSystem({ id, root: "/" });
|
|
679
|
+
await filesystem.init();
|
|
680
|
+
for (const file of files) {
|
|
681
|
+
await filesystem.writeFile(file.path, Buffer.from(file.content, "base64"), {
|
|
682
|
+
overwrite: true,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return filesystem;
|
|
686
|
+
}
|
|
687
|
+
|
|
566
688
|
/** Fail before workspace composition when two sources claim one virtual mount. */
|
|
567
689
|
function assertUniqueLocalSkillMounts(mounts: readonly LocalSkillMount[]): void {
|
|
568
690
|
const rootsByMount = new Map<string, string>();
|
|
569
691
|
for (const mount of mounts) {
|
|
692
|
+
const identity = "root" in mount ? mount.root : mount.filesystem.id;
|
|
570
693
|
const previousRoot = rootsByMount.get(mount.mountPath);
|
|
571
|
-
if (previousRoot && previousRoot !==
|
|
694
|
+
if (previousRoot && previousRoot !== identity) {
|
|
572
695
|
throw new Error(
|
|
573
696
|
`multiple remote skill sources resolve to virtual mount "${mount.mountPath}"; give each source a unique name`,
|
|
574
697
|
);
|
|
575
698
|
}
|
|
576
|
-
rootsByMount.set(mount.mountPath,
|
|
699
|
+
rootsByMount.set(mount.mountPath, identity);
|
|
577
700
|
}
|
|
578
701
|
}
|
|
579
702
|
|
|
@@ -632,7 +755,7 @@ async function openWritableWorkspace(
|
|
|
632
755
|
} catch (err) {
|
|
633
756
|
logger.warn("destination:unwritable", {
|
|
634
757
|
destination: basePath,
|
|
635
|
-
error: errorUtils.
|
|
758
|
+
error: errorUtils.toError(err),
|
|
636
759
|
});
|
|
637
760
|
return undefined;
|
|
638
761
|
}
|
|
@@ -876,7 +999,10 @@ async function collectSkillDirs(root: string): Promise<StagedSkillDir[]> {
|
|
|
876
999
|
async function copySkillDirs(destination: FileSystem, dirs: StagedSkillDir[]): Promise<void> {
|
|
877
1000
|
await destination.init();
|
|
878
1001
|
for (const dir of dirs) {
|
|
879
|
-
for (const relative of find.findFiles("**/*", {
|
|
1002
|
+
for await (const relative of find.findFiles("**/*", {
|
|
1003
|
+
cwd: dir.absolutePath,
|
|
1004
|
+
nodir: true,
|
|
1005
|
+
})) {
|
|
880
1006
|
const buffer = await readFile(join(dir.absolutePath, relative));
|
|
881
1007
|
const skillPath = posix.join(dir.name, relative.split(/[\\/]/).join("/"));
|
|
882
1008
|
await destination.writeFile(skillPath, buffer, { overwrite: true });
|
package/src/server.ts
CHANGED
|
@@ -484,7 +484,7 @@ export function attachStaleResumeRecovery(app: express.Express): void {
|
|
|
484
484
|
}
|
|
485
485
|
logger.warn("ignored stale mastra resume", {
|
|
486
486
|
path: req.path,
|
|
487
|
-
error: errorUtils.
|
|
487
|
+
error: errorUtils.toError(error),
|
|
488
488
|
});
|
|
489
489
|
if (res.headersSent) {
|
|
490
490
|
res.end();
|