@dbx-tools/appkit-mastra 0.9.60 → 0.9.64

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/src/workspaces.ts CHANGED
@@ -34,10 +34,17 @@ import {
34
34
  type WorkspaceSandboxResolver,
35
35
  } from "@mastra/core/workspace";
36
36
 
37
- import { MASTRA_SCOPES_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, type User } from "./config.ts";
38
- import { scratchFilesystem, filesystems } from "./filesystems.ts";
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(skillFolders, options.mounts);
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 filesystem = await resolveSkillFolderFilesystem(name, folder, context, client);
394
- if (!filesystem) continue;
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 { mounts, skillPaths };
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
- client: WorkspaceClient | undefined,
417
- ): Promise<WorkspaceFilesystem | undefined> {
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
- return resolveSkillFolderValue(folder.filesystem, context);
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) => resolveSkillFolderMounts(skillFolders, 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
- ): Promise<WorkspaceFilesystem> {
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 fs.init();
518
- if (await fs.exists(".")) {
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 scratchFilesystem();
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 { mounts, skillPaths };
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
  /**