@dbx-tools/appkit-mastra 0.9.61 → 0.9.65
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 +21 -0
- package/index.ts +5 -2
- package/lib/index.d.ts +5 -2
- package/lib/index.js +4 -2
- package/lib/src/agents.js +26 -3
- package/lib/src/config.d.ts +19 -0
- package/lib/src/config.js +9 -1
- package/lib/src/mlflow.js +5 -2
- package/lib/src/skill-cache.d.ts +70 -0
- package/lib/src/skill-cache.js +194 -0
- package/lib/src/workspaces.d.ts +10 -0
- package/lib/src/workspaces.js +84 -24
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -14
- package/src/agents.ts +29 -2
- package/src/config.ts +30 -0
- package/src/mlflow.ts +4 -1
- package/src/skill-cache.ts +312 -0
- package/src/workspaces.ts +107 -22
package/src/mlflow.ts
CHANGED
|
@@ -51,7 +51,10 @@ let loadedUcPrefix: { prefix: string | undefined } | undefined;
|
|
|
51
51
|
|
|
52
52
|
/** Strip the `trace:/` URI wrapper and trailing slashes from a UC prefix. */
|
|
53
53
|
function normalizeUcPrefix(raw: string | undefined): string | undefined {
|
|
54
|
-
const value = raw
|
|
54
|
+
const value = raw
|
|
55
|
+
?.trim()
|
|
56
|
+
.replace(/^trace:\//, "")
|
|
57
|
+
.replace(/\/+$/, "");
|
|
55
58
|
return value || undefined;
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit-backed caching for Databricks workspace skill mounts.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { CacheManager } from "@databricks/appkit";
|
|
9
|
+
import { hash, object } from "@dbx-tools/shared-core";
|
|
10
|
+
import type {
|
|
11
|
+
CopyOptions,
|
|
12
|
+
FileContent,
|
|
13
|
+
FileEntry,
|
|
14
|
+
FileStat,
|
|
15
|
+
FileSystem,
|
|
16
|
+
ListOptions,
|
|
17
|
+
MakeDirectoryOptions,
|
|
18
|
+
ReadFileOptions,
|
|
19
|
+
RemoveOptions,
|
|
20
|
+
WriteFileOptions,
|
|
21
|
+
} from "@dbx-tools/shared-fs";
|
|
22
|
+
|
|
23
|
+
import { filesystems, type MastraFileSystemAdapter } from "./filesystems.ts";
|
|
24
|
+
|
|
25
|
+
const CACHE_NAMESPACE = "mastra:workspace-skills";
|
|
26
|
+
const DEFAULT_MAX_SCOPED_MOUNTS = 256;
|
|
27
|
+
|
|
28
|
+
interface CachedFileStat extends Omit<FileStat, "accessedAt" | "createdAt" | "modifiedAt"> {
|
|
29
|
+
accessedAt?: number;
|
|
30
|
+
createdAt?: number;
|
|
31
|
+
modifiedAt?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ScopedMountEntry {
|
|
35
|
+
adapter: MastraFileSystemAdapter;
|
|
36
|
+
cache: AppKitCachedFileSystem;
|
|
37
|
+
host: string;
|
|
38
|
+
root: string;
|
|
39
|
+
userKey: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const scopedMounts = new Map<string, ScopedMountEntry>();
|
|
43
|
+
|
|
44
|
+
/** Default remote skill metadata and content cache lifetime. */
|
|
45
|
+
export const DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
/** Options for one AppKit-cached workspace skill filesystem. */
|
|
48
|
+
export interface AppKitCachedFileSystemOptions {
|
|
49
|
+
host: string;
|
|
50
|
+
source: FileSystem;
|
|
51
|
+
ttlMs?: number;
|
|
52
|
+
userKey: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stable Mastra mount plus the identity key used for source reuse. */
|
|
56
|
+
export interface CachedWorkspaceSkillMount {
|
|
57
|
+
cacheKey: string;
|
|
58
|
+
filesystem: MastraFileSystemAdapter;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Cache read-only filesystem operations through AppKit while delegating
|
|
63
|
+
* mutations to the current request's source.
|
|
64
|
+
*/
|
|
65
|
+
export class AppKitCachedFileSystem implements FileSystem<"appkit-cache"> {
|
|
66
|
+
readonly backend = "appkit-cache" as const;
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly readOnly: boolean;
|
|
69
|
+
readonly root: string;
|
|
70
|
+
|
|
71
|
+
private source: FileSystem;
|
|
72
|
+
private readonly host: string;
|
|
73
|
+
private readonly ttlSec: number;
|
|
74
|
+
private readonly userKey: string;
|
|
75
|
+
|
|
76
|
+
constructor(options: AppKitCachedFileSystemOptions) {
|
|
77
|
+
this.host = options.host;
|
|
78
|
+
this.source = options.source;
|
|
79
|
+
this.root = options.source.root;
|
|
80
|
+
this.readOnly = options.source.readOnly;
|
|
81
|
+
this.ttlSec = cacheTtlSec(options.ttlMs);
|
|
82
|
+
this.userKey = options.userKey;
|
|
83
|
+
this.id = `appkit-skill-cache-${hash.fnvHash(this.host, this.userKey, this.root)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Rebind cache misses and writes to the current request's OBO filesystem. */
|
|
87
|
+
bind(source: FileSystem): void {
|
|
88
|
+
if (source.root !== this.root || source.readOnly !== this.readOnly) {
|
|
89
|
+
throw new Error("Cannot rebind a workspace skill cache to a different filesystem scope");
|
|
90
|
+
}
|
|
91
|
+
this.source = source;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async init(): Promise<void> {
|
|
95
|
+
await this.source.init();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Keep the shared scope alive when one request releases its workspace view. */
|
|
99
|
+
close(): Promise<void> {
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resolvePath(inputPath: string): string {
|
|
104
|
+
return this.source.resolvePath(inputPath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async readFile(inputPath: string): Promise<Uint8Array>;
|
|
108
|
+
async readFile(
|
|
109
|
+
inputPath: string,
|
|
110
|
+
options: ReadFileOptions & { encoding: string },
|
|
111
|
+
): Promise<string>;
|
|
112
|
+
async readFile(inputPath: string, options?: ReadFileOptions): Promise<string | Uint8Array> {
|
|
113
|
+
const value = await this.cached("readFile", inputPath, undefined, async (source) => ({
|
|
114
|
+
base64: Buffer.from(await source.readFile(inputPath)).toString("base64"),
|
|
115
|
+
}));
|
|
116
|
+
const bytes = Buffer.from(value.base64, "base64");
|
|
117
|
+
return options?.encoding ? bytes.toString(options.encoding as BufferEncoding) : bytes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async writeFile(
|
|
121
|
+
inputPath: string,
|
|
122
|
+
content: FileContent,
|
|
123
|
+
options?: WriteFileOptions,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
await this.mutate((source) => source.writeFile(inputPath, content, options));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async appendFile(inputPath: string, content: FileContent): Promise<void> {
|
|
129
|
+
await this.mutate((source) => source.appendFile(inputPath, content));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async deleteFile(inputPath: string, options?: RemoveOptions): Promise<void> {
|
|
133
|
+
await this.mutate((source) => source.deleteFile(inputPath, options));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async copyFile(
|
|
137
|
+
sourcePath: string,
|
|
138
|
+
destinationPath: string,
|
|
139
|
+
options?: CopyOptions,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
await this.mutate((source) => source.copyFile(sourcePath, destinationPath, options));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async moveFile(
|
|
145
|
+
sourcePath: string,
|
|
146
|
+
destinationPath: string,
|
|
147
|
+
options?: CopyOptions,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
await this.mutate((source) => source.moveFile(sourcePath, destinationPath, options));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async mkdir(inputPath: string, options?: MakeDirectoryOptions): Promise<void> {
|
|
153
|
+
await this.mutate((source) => source.mkdir(inputPath, options));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async rmdir(inputPath: string, options?: RemoveOptions): Promise<void> {
|
|
157
|
+
await this.mutate((source) => source.rmdir(inputPath, options));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async readdir(inputPath: string, options?: ListOptions): Promise<FileEntry[]> {
|
|
161
|
+
return this.cached("readdir", inputPath, options, (source) =>
|
|
162
|
+
source.readdir(inputPath, options),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async exists(inputPath: string): Promise<boolean> {
|
|
167
|
+
return this.cached("exists", inputPath, undefined, (source) => source.exists(inputPath));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async stat(inputPath: string): Promise<FileStat> {
|
|
171
|
+
const value = await this.cached("stat", inputPath, undefined, async (source) =>
|
|
172
|
+
serializeStat(await source.stat(inputPath)),
|
|
173
|
+
);
|
|
174
|
+
return deserializeStat(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Rotate this scope's cache generation so the next read reloads it. */
|
|
178
|
+
async invalidate(): Promise<void> {
|
|
179
|
+
const cache = await CacheManager.getInstance();
|
|
180
|
+
await cache.set(this.generationKey(cache), randomUUID(), {
|
|
181
|
+
ttl: this.ttlSec * 2,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async cached<T>(
|
|
186
|
+
operation: string,
|
|
187
|
+
inputPath: string,
|
|
188
|
+
options: unknown,
|
|
189
|
+
load: (source: FileSystem) => Promise<T>,
|
|
190
|
+
): Promise<T> {
|
|
191
|
+
const source = this.source;
|
|
192
|
+
const cache = await CacheManager.getInstance();
|
|
193
|
+
const generation = await this.generation(cache);
|
|
194
|
+
return cache.getOrExecute(
|
|
195
|
+
[
|
|
196
|
+
CACHE_NAMESPACE,
|
|
197
|
+
this.host,
|
|
198
|
+
this.root,
|
|
199
|
+
generation,
|
|
200
|
+
operation,
|
|
201
|
+
source.resolvePath(inputPath),
|
|
202
|
+
object.toStableKey(options ?? {}),
|
|
203
|
+
],
|
|
204
|
+
() => load(source),
|
|
205
|
+
this.userKey,
|
|
206
|
+
{ ttl: this.ttlSec },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async generation(cache: CacheManager): Promise<string> {
|
|
211
|
+
return cache.getOrExecute(
|
|
212
|
+
[CACHE_NAMESPACE, "generation", this.host, this.root],
|
|
213
|
+
async () => randomUUID(),
|
|
214
|
+
this.userKey,
|
|
215
|
+
{ ttl: this.ttlSec * 2 },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private generationKey(cache: CacheManager): string {
|
|
220
|
+
return cache.generateKey([CACHE_NAMESPACE, "generation", this.host, this.root], this.userKey);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async mutate(run: (source: FileSystem) => Promise<void>): Promise<void> {
|
|
224
|
+
const source = this.source;
|
|
225
|
+
await run(source);
|
|
226
|
+
await this.invalidate();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Return one stable Mastra mount per resolved user, host, and source root. */
|
|
231
|
+
export function cachedWorkspaceSkillMount(
|
|
232
|
+
options: AppKitCachedFileSystemOptions,
|
|
233
|
+
): CachedWorkspaceSkillMount {
|
|
234
|
+
const cacheKey = object.toStableKey({
|
|
235
|
+
host: options.host,
|
|
236
|
+
readOnly: options.source.readOnly,
|
|
237
|
+
root: options.source.root,
|
|
238
|
+
userKey: options.userKey,
|
|
239
|
+
});
|
|
240
|
+
const existing = scopedMounts.get(cacheKey);
|
|
241
|
+
if (existing) {
|
|
242
|
+
existing.cache.bind(options.source);
|
|
243
|
+
scopedMounts.delete(cacheKey);
|
|
244
|
+
scopedMounts.set(cacheKey, existing);
|
|
245
|
+
return { cacheKey, filesystem: existing.adapter };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cache = new AppKitCachedFileSystem(options);
|
|
249
|
+
const entry: ScopedMountEntry = {
|
|
250
|
+
adapter: filesystems(cache, {
|
|
251
|
+
id: cache.id,
|
|
252
|
+
readOnly: options.source.readOnly,
|
|
253
|
+
}),
|
|
254
|
+
cache,
|
|
255
|
+
host: options.host,
|
|
256
|
+
root: options.source.root,
|
|
257
|
+
userKey: options.userKey,
|
|
258
|
+
};
|
|
259
|
+
scopedMounts.set(cacheKey, entry);
|
|
260
|
+
trimScopedMounts();
|
|
261
|
+
return { cacheKey, filesystem: entry.adapter };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Invalidate one user's cached workspace skill scope. */
|
|
265
|
+
export async function clearWorkspaceSkillCache(options: {
|
|
266
|
+
host: string;
|
|
267
|
+
root?: string;
|
|
268
|
+
userKey: string;
|
|
269
|
+
}): Promise<void> {
|
|
270
|
+
const matches = [...scopedMounts.entries()].filter(
|
|
271
|
+
([, entry]) =>
|
|
272
|
+
entry.host === options.host &&
|
|
273
|
+
entry.userKey === options.userKey &&
|
|
274
|
+
(options.root === undefined || entry.root === options.root),
|
|
275
|
+
);
|
|
276
|
+
await Promise.all(matches.map(([, entry]) => entry.cache.invalidate()));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function cacheTtlSec(ttlMs = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS): number {
|
|
280
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
281
|
+
throw new Error("Workspace skill cache ttlMs must be a positive finite number");
|
|
282
|
+
}
|
|
283
|
+
return Math.max(1, Math.ceil(ttlMs / 1000));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function serializeStat(value: FileStat): CachedFileStat {
|
|
287
|
+
const { accessedAt, createdAt, modifiedAt, ...stat } = value;
|
|
288
|
+
return {
|
|
289
|
+
...stat,
|
|
290
|
+
...(accessedAt ? { accessedAt: accessedAt.getTime() } : {}),
|
|
291
|
+
...(createdAt ? { createdAt: createdAt.getTime() } : {}),
|
|
292
|
+
...(modifiedAt ? { modifiedAt: modifiedAt.getTime() } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function deserializeStat(value: CachedFileStat): FileStat {
|
|
297
|
+
const { accessedAt, createdAt, modifiedAt, ...stat } = value;
|
|
298
|
+
return {
|
|
299
|
+
...stat,
|
|
300
|
+
...(accessedAt !== undefined ? { accessedAt: new Date(accessedAt) } : {}),
|
|
301
|
+
...(createdAt !== undefined ? { createdAt: new Date(createdAt) } : {}),
|
|
302
|
+
...(modifiedAt !== undefined ? { modifiedAt: new Date(modifiedAt) } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function trimScopedMounts(): void {
|
|
307
|
+
while (scopedMounts.size > DEFAULT_MAX_SCOPED_MOUNTS) {
|
|
308
|
+
const oldest = scopedMounts.keys().next().value;
|
|
309
|
+
if (oldest === undefined) return;
|
|
310
|
+
scopedMounts.delete(oldest);
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/workspaces.ts
CHANGED
|
@@ -34,10 +34,17 @@ import {
|
|
|
34
34
|
type WorkspaceSandboxResolver,
|
|
35
35
|
} from "@mastra/core/workspace";
|
|
36
36
|
|
|
37
|
-
import {
|
|
38
|
-
|
|
37
|
+
import {
|
|
38
|
+
MASTRA_SCOPES_KEY,
|
|
39
|
+
MASTRA_USER_EMAIL_KEY,
|
|
40
|
+
MASTRA_USER_KEY,
|
|
41
|
+
resolveUserKey,
|
|
42
|
+
type User,
|
|
43
|
+
} from "./config.ts";
|
|
44
|
+
import { scratchFilesystem } from "./filesystems.ts";
|
|
39
45
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
40
46
|
import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
|
|
47
|
+
import { cachedWorkspaceSkillMount, DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS } from "./skill-cache.ts";
|
|
41
48
|
import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
|
|
42
49
|
|
|
43
50
|
/* ------------------------------ constants ------------------------------ */
|
|
@@ -46,6 +53,8 @@ import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-p
|
|
|
46
53
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
47
54
|
|
|
48
55
|
const logger = log.logger("mastra/workspaces");
|
|
56
|
+
const MAX_SCOPED_WORKSPACE_FILESYSTEMS = 16;
|
|
57
|
+
const scopedWorkspaceFilesystems = new Map<string, WorkspaceFilesystem>();
|
|
49
58
|
|
|
50
59
|
/* -------------------------------- types -------------------------------- */
|
|
51
60
|
|
|
@@ -93,6 +102,11 @@ export interface WorkspaceMountContribution {
|
|
|
93
102
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
94
103
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
95
104
|
skillPaths?: string[];
|
|
105
|
+
/**
|
|
106
|
+
* Stable identity for this contribution. When every non-empty contribution
|
|
107
|
+
* supplies one, Mastra reuses the resolved source across requests.
|
|
108
|
+
*/
|
|
109
|
+
cacheKey?: string;
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
/** Contributes filesystem mounts (and optional skill paths) for one request. */
|
|
@@ -139,6 +153,11 @@ export interface CreateWorkspaceOptions {
|
|
|
139
153
|
skills?: SkillsResolver;
|
|
140
154
|
/** Forwarded to Mastra when skill discovery is enabled. */
|
|
141
155
|
checkSkillFileMtime?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* AppKit cache TTL for Databricks skill metadata and content.
|
|
158
|
+
* Defaults to five minutes.
|
|
159
|
+
*/
|
|
160
|
+
workspaceSkillRefreshTtlMs?: number;
|
|
142
161
|
/** Enable BM25 keyword search over indexed workspace content. */
|
|
143
162
|
bm25?: boolean;
|
|
144
163
|
/**
|
|
@@ -220,7 +239,11 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
220
239
|
const { id, name } = resolveWorkspaceIdentity(options);
|
|
221
240
|
const skillFolders = resolveSkillFolders(options);
|
|
222
241
|
const folderNames = Object.keys(skillFolders);
|
|
223
|
-
const resolvers = buildMountResolvers(
|
|
242
|
+
const resolvers = buildMountResolvers(
|
|
243
|
+
skillFolders,
|
|
244
|
+
options.mounts,
|
|
245
|
+
options.workspaceSkillRefreshTtlMs,
|
|
246
|
+
);
|
|
224
247
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
225
248
|
const skills =
|
|
226
249
|
options.skills ??
|
|
@@ -238,6 +261,8 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
238
261
|
customMountResolvers: options.mounts?.length ?? 0,
|
|
239
262
|
customSkillsResolver: Boolean(options.skills),
|
|
240
263
|
checkSkillFileMtime,
|
|
264
|
+
workspaceSkillRefreshTtlMs:
|
|
265
|
+
options.workspaceSkillRefreshTtlMs ?? DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
241
266
|
bm25,
|
|
242
267
|
extraSkillPaths: extraSkillPaths.length,
|
|
243
268
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
@@ -372,9 +397,11 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
372
397
|
async function resolveSkillFolderMounts(
|
|
373
398
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
374
399
|
context: WorkspaceMountContext,
|
|
400
|
+
workspaceSkillRefreshTtlMs: number,
|
|
375
401
|
): Promise<WorkspaceMountContribution> {
|
|
376
402
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
377
403
|
const skillPaths: string[] = [];
|
|
404
|
+
const cacheKeys: string[] = [];
|
|
378
405
|
const requestContext = context.requestContext;
|
|
379
406
|
|
|
380
407
|
if (!requestContext || !shouldMountSkillFolders(requestContext)) {
|
|
@@ -388,12 +415,20 @@ async function resolveSkillFolderMounts(
|
|
|
388
415
|
|
|
389
416
|
const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
|
|
390
417
|
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
418
|
+
const userKey = user ? resolveUserKey(requestContext) : undefined;
|
|
419
|
+
const host = client ? (await client.config.getHost()).toString() : undefined;
|
|
391
420
|
|
|
392
421
|
for (const [name, folder] of Object.entries(skillFolders)) {
|
|
393
|
-
const
|
|
394
|
-
|
|
422
|
+
const resolved = await resolveSkillFolderFilesystem(name, folder, context, {
|
|
423
|
+
client,
|
|
424
|
+
host,
|
|
425
|
+
userKey,
|
|
426
|
+
workspaceSkillRefreshTtlMs,
|
|
427
|
+
});
|
|
428
|
+
if (!resolved) continue;
|
|
395
429
|
const mount = folder.mount ?? `/${name}`;
|
|
396
|
-
mounts[mount] = filesystem;
|
|
430
|
+
mounts[mount] = resolved.filesystem;
|
|
431
|
+
if (resolved.cacheKey) cacheKeys.push(resolved.cacheKey);
|
|
397
432
|
if (folder.readable !== false) skillPaths.push(mount);
|
|
398
433
|
}
|
|
399
434
|
|
|
@@ -402,7 +437,13 @@ async function resolveSkillFolderMounts(
|
|
|
402
437
|
skillPaths,
|
|
403
438
|
});
|
|
404
439
|
|
|
405
|
-
return {
|
|
440
|
+
return {
|
|
441
|
+
mounts,
|
|
442
|
+
skillPaths,
|
|
443
|
+
...(cacheKeys.length === Object.keys(mounts).length && cacheKeys.length > 0
|
|
444
|
+
? { cacheKey: object.toStableKey(cacheKeys.sort()) }
|
|
445
|
+
: {}),
|
|
446
|
+
};
|
|
406
447
|
}
|
|
407
448
|
|
|
408
449
|
/**
|
|
@@ -413,13 +454,19 @@ async function resolveSkillFolderFilesystem(
|
|
|
413
454
|
name: string,
|
|
414
455
|
folder: SkillFolderOptions,
|
|
415
456
|
context: WorkspaceMountContext,
|
|
416
|
-
|
|
417
|
-
|
|
457
|
+
cache: {
|
|
458
|
+
client: WorkspaceClient | undefined;
|
|
459
|
+
host: string | undefined;
|
|
460
|
+
userKey: string | undefined;
|
|
461
|
+
workspaceSkillRefreshTtlMs: number;
|
|
462
|
+
},
|
|
463
|
+
): Promise<{ cacheKey?: string; filesystem: WorkspaceFilesystem } | undefined> {
|
|
418
464
|
if (folder.filesystem !== undefined) {
|
|
419
|
-
|
|
465
|
+
const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
|
|
466
|
+
return filesystem ? { filesystem } : undefined;
|
|
420
467
|
}
|
|
421
468
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
422
|
-
if (folder.path === undefined || !client) {
|
|
469
|
+
if (folder.path === undefined || !cache.client || !cache.host || !cache.userKey) {
|
|
423
470
|
logger.debug("skill-folder:skipped", {
|
|
424
471
|
name,
|
|
425
472
|
reason: folder.path === undefined ? "no-location" : "missing-obo-client",
|
|
@@ -428,7 +475,11 @@ async function resolveSkillFolderFilesystem(
|
|
|
428
475
|
}
|
|
429
476
|
const root = stringUtils.trimToNull(await resolveSkillFolderValue(folder.path, context));
|
|
430
477
|
if (!root) return undefined;
|
|
431
|
-
return databricksFilesystem(client, root, folder.writable !== true
|
|
478
|
+
return databricksFilesystem(cache.client, root, folder.writable !== true, {
|
|
479
|
+
host: cache.host,
|
|
480
|
+
ttlMs: cache.workspaceSkillRefreshTtlMs,
|
|
481
|
+
userKey: cache.userKey,
|
|
482
|
+
});
|
|
432
483
|
}
|
|
433
484
|
|
|
434
485
|
/** Read a {@link SkillFolderValue}, calling it when it is a per-request resolver. */
|
|
@@ -464,11 +515,14 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
464
515
|
function buildMountResolvers(
|
|
465
516
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
466
517
|
mounts: WorkspaceMountResolver[] | undefined,
|
|
518
|
+
workspaceSkillRefreshTtlMs = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
467
519
|
): WorkspaceMountResolver[] {
|
|
468
520
|
const resolvers: WorkspaceMountResolver[] = [];
|
|
469
521
|
const folderCount = Object.keys(skillFolders).length;
|
|
470
522
|
if (folderCount > 0) {
|
|
471
|
-
resolvers.push((context) =>
|
|
523
|
+
resolvers.push((context) =>
|
|
524
|
+
resolveSkillFolderMounts(skillFolders, context, workspaceSkillRefreshTtlMs),
|
|
525
|
+
);
|
|
472
526
|
}
|
|
473
527
|
if (mounts?.length) {
|
|
474
528
|
resolvers.push(...mounts);
|
|
@@ -506,18 +560,23 @@ async function databricksFilesystem(
|
|
|
506
560
|
client: WorkspaceClient,
|
|
507
561
|
root: string,
|
|
508
562
|
readOnly: boolean = true,
|
|
509
|
-
|
|
563
|
+
cache: { host: string; ttlMs: number; userKey: string },
|
|
564
|
+
): Promise<{ cacheKey: string; filesystem: WorkspaceFilesystem } | undefined> {
|
|
510
565
|
const fs = new DatabricksFileSystem({
|
|
511
566
|
client: workspaceClient.toLegacyWorkspaceClient(client),
|
|
512
567
|
root,
|
|
513
568
|
readOnly,
|
|
514
569
|
createRoot: !readOnly,
|
|
515
570
|
});
|
|
571
|
+
const cached = cachedWorkspaceSkillMount({
|
|
572
|
+
host: cache.host,
|
|
573
|
+
source: fs,
|
|
574
|
+
ttlMs: cache.ttlMs,
|
|
575
|
+
userKey: cache.userKey,
|
|
576
|
+
});
|
|
516
577
|
try {
|
|
517
|
-
await
|
|
518
|
-
if (await
|
|
519
|
-
return filesystems(fs, { readOnly });
|
|
520
|
-
}
|
|
578
|
+
await cached.filesystem.init();
|
|
579
|
+
if (await cached.filesystem.exists(".")) return cached;
|
|
521
580
|
} catch (err) {
|
|
522
581
|
logger.debug("databricks-mount:scratch-fallback", {
|
|
523
582
|
root,
|
|
@@ -525,7 +584,7 @@ async function databricksFilesystem(
|
|
|
525
584
|
error: errorUtils.errorMessage(err),
|
|
526
585
|
});
|
|
527
586
|
}
|
|
528
|
-
return
|
|
587
|
+
return undefined;
|
|
529
588
|
}
|
|
530
589
|
|
|
531
590
|
/**
|
|
@@ -538,6 +597,8 @@ async function resolveWorkspaceContribution(
|
|
|
538
597
|
): Promise<WorkspaceMountContribution> {
|
|
539
598
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
540
599
|
const skillPaths: string[] = [];
|
|
600
|
+
const cacheKeys: string[] = [];
|
|
601
|
+
let cacheable = true;
|
|
541
602
|
|
|
542
603
|
for (const [index, resolver] of resolvers.entries()) {
|
|
543
604
|
const contribution = await resolver(context);
|
|
@@ -547,6 +608,10 @@ async function resolveWorkspaceContribution(
|
|
|
547
608
|
skillPaths: contribution.skillPaths ?? [],
|
|
548
609
|
});
|
|
549
610
|
Object.assign(mounts, contribution.mounts);
|
|
611
|
+
if (Object.keys(contribution.mounts).length > 0) {
|
|
612
|
+
if (contribution.cacheKey) cacheKeys.push(contribution.cacheKey);
|
|
613
|
+
else cacheable = false;
|
|
614
|
+
}
|
|
550
615
|
if (contribution.skillPaths?.length) {
|
|
551
616
|
skillPaths.push(...contribution.skillPaths);
|
|
552
617
|
}
|
|
@@ -557,7 +622,13 @@ async function resolveWorkspaceContribution(
|
|
|
557
622
|
skillPaths,
|
|
558
623
|
});
|
|
559
624
|
|
|
560
|
-
return {
|
|
625
|
+
return {
|
|
626
|
+
mounts,
|
|
627
|
+
skillPaths,
|
|
628
|
+
...(cacheable && cacheKeys.length > 0
|
|
629
|
+
? { cacheKey: object.toStableKey(cacheKeys.sort()) }
|
|
630
|
+
: {}),
|
|
631
|
+
};
|
|
561
632
|
}
|
|
562
633
|
|
|
563
634
|
/**
|
|
@@ -570,7 +641,7 @@ async function resolveWorkspaceFilesystem(
|
|
|
570
641
|
resolvers: WorkspaceMountResolver[],
|
|
571
642
|
context: WorkspaceMountContext,
|
|
572
643
|
): Promise<WorkspaceFilesystem> {
|
|
573
|
-
const { mounts } = await resolveWorkspaceContribution(resolvers, context);
|
|
644
|
+
const { cacheKey, mounts } = await resolveWorkspaceContribution(resolvers, context);
|
|
574
645
|
const mountKeys = Object.keys(mounts);
|
|
575
646
|
if (mountKeys.length === 0) {
|
|
576
647
|
logger.debug("filesystem:scratch", {
|
|
@@ -579,7 +650,21 @@ async function resolveWorkspaceFilesystem(
|
|
|
579
650
|
return scratchFilesystem();
|
|
580
651
|
}
|
|
581
652
|
logger.debug("filesystem:composite", { mountKeys });
|
|
582
|
-
return new CompositeFilesystem({ mounts });
|
|
653
|
+
if (!cacheKey) return new CompositeFilesystem({ mounts });
|
|
654
|
+
const existing = scopedWorkspaceFilesystems.get(cacheKey);
|
|
655
|
+
if (existing) {
|
|
656
|
+
scopedWorkspaceFilesystems.delete(cacheKey);
|
|
657
|
+
scopedWorkspaceFilesystems.set(cacheKey, existing);
|
|
658
|
+
return existing;
|
|
659
|
+
}
|
|
660
|
+
const filesystem = new CompositeFilesystem({ mounts });
|
|
661
|
+
scopedWorkspaceFilesystems.set(cacheKey, filesystem);
|
|
662
|
+
while (scopedWorkspaceFilesystems.size > MAX_SCOPED_WORKSPACE_FILESYSTEMS) {
|
|
663
|
+
const oldest = scopedWorkspaceFilesystems.keys().next().value;
|
|
664
|
+
if (oldest === undefined) break;
|
|
665
|
+
scopedWorkspaceFilesystems.delete(oldest);
|
|
666
|
+
}
|
|
667
|
+
return filesystem;
|
|
583
668
|
}
|
|
584
669
|
|
|
585
670
|
/**
|