@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.
- package/README.md +53 -42
- package/index.ts +8 -3
- package/lib/index.d.ts +8 -3
- package/lib/index.js +6 -3
- package/lib/src/agents.js +38 -5
- package/lib/src/config.d.ts +7 -7
- package/lib/src/config.js +4 -4
- package/lib/src/genie.js +9 -15
- package/lib/src/mlflow.js +5 -5
- package/lib/src/monty-native.d.ts +40 -0
- package/lib/src/monty-native.js +100 -0
- package/lib/src/monty-sandbox.js +7 -3
- package/lib/src/remote-skills.js +26 -2
- package/lib/src/sandbox.js +2 -1
- package/lib/src/skill-cache.d.ts +80 -56
- package/lib/src/skill-cache.js +215 -158
- package/lib/src/skill-search.d.ts +30 -0
- package/lib/src/skill-search.js +171 -0
- package/lib/src/workspaces.d.ts +10 -5
- package/lib/src/workspaces.js +132 -117
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -15
- package/src/agents.ts +39 -6
- package/src/config.ts +10 -10
- package/src/genie.ts +8 -13
- package/src/mlflow.ts +4 -4
- package/src/monty-native.ts +149 -0
- package/src/monty-sandbox.ts +7 -2
- package/src/remote-skills.ts +32 -1
- package/src/sandbox.ts +1 -0
- package/src/skill-cache.ts +278 -243
- package/src/skill-search.ts +203 -0
- package/src/workspaces.ts +177 -139
package/src/workspaces.ts
CHANGED
|
@@ -24,12 +24,12 @@ import type { WorkspaceClient } from "@databricks/appkit";
|
|
|
24
24
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
25
25
|
import { LocalFileSystem } from "@dbx-tools/fs";
|
|
26
26
|
import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
|
|
27
|
-
import {
|
|
27
|
+
import type { FileSystem } from "@dbx-tools/shared-fs";
|
|
28
|
+
import { LRUCache } from "lru-cache";
|
|
28
29
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
29
30
|
import {
|
|
30
31
|
CompositeFilesystem,
|
|
31
32
|
Workspace,
|
|
32
|
-
type SkillsContext,
|
|
33
33
|
type SkillsResolver,
|
|
34
34
|
type WorkspaceFilesystem,
|
|
35
35
|
type WorkspaceSandbox,
|
|
@@ -46,7 +46,12 @@ import {
|
|
|
46
46
|
import { filesystems, scratchFilesystem } from "./filesystems.ts";
|
|
47
47
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
48
48
|
import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
51
|
+
workspaceSkillCatalogue,
|
|
52
|
+
type SkillCatalogueResolver,
|
|
53
|
+
type SkillCatalogueSource,
|
|
54
|
+
} from "./skill-cache.ts";
|
|
50
55
|
import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
|
|
51
56
|
|
|
52
57
|
/* ------------------------------ constants ------------------------------ */
|
|
@@ -56,7 +61,10 @@ const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] a
|
|
|
56
61
|
|
|
57
62
|
const logger = log.logger("mastra/workspaces");
|
|
58
63
|
const MAX_SCOPED_WORKSPACE_FILESYSTEMS = 16;
|
|
59
|
-
const scopedWorkspaceFilesystems = new
|
|
64
|
+
const scopedWorkspaceFilesystems = new LRUCache<string, WorkspaceFilesystem>({
|
|
65
|
+
max: MAX_SCOPED_WORKSPACE_FILESYSTEMS,
|
|
66
|
+
});
|
|
67
|
+
const workspaceCatalogueResolvers = new WeakMap<Workspace, SkillCatalogueResolver>();
|
|
60
68
|
|
|
61
69
|
/* -------------------------------- types -------------------------------- */
|
|
62
70
|
|
|
@@ -102,6 +110,8 @@ export interface SkillFolderOptions {
|
|
|
102
110
|
/** Mount map plus optional Mastra skill scan roots for one resolver. */
|
|
103
111
|
export interface WorkspaceMountContribution {
|
|
104
112
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
113
|
+
/** Ordered source roots included in catalogue discovery. */
|
|
114
|
+
skillSources?: SkillCatalogueSource[];
|
|
105
115
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
106
116
|
skillPaths?: string[];
|
|
107
117
|
/**
|
|
@@ -156,16 +166,16 @@ export interface CreateWorkspaceOptions {
|
|
|
156
166
|
/** Forwarded to Mastra when skill discovery is enabled. */
|
|
157
167
|
checkSkillFileMtime?: boolean;
|
|
158
168
|
/**
|
|
159
|
-
*
|
|
160
|
-
* Defaults to five minutes.
|
|
169
|
+
* Refresh TTL for the complete parsed skill catalogue. Auxiliary reads are
|
|
170
|
+
* always fresh. Defaults to five minutes.
|
|
161
171
|
*/
|
|
162
172
|
workspaceSkillRefreshTtlMs?: number;
|
|
163
173
|
/** Enable BM25 keyword search over indexed workspace content. */
|
|
164
174
|
bm25?: boolean;
|
|
165
175
|
/**
|
|
166
|
-
* Command sandbox. Defaults to
|
|
167
|
-
*
|
|
168
|
-
*
|
|
176
|
+
* Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
|
|
177
|
+
* object for Databricks Sandbox, `false` to disable command execution, or an
|
|
178
|
+
* explicit Mastra sandbox/provider resolver.
|
|
169
179
|
*/
|
|
170
180
|
sandbox?: WorkspaceSandboxSelection;
|
|
171
181
|
/**
|
|
@@ -245,12 +255,10 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
245
255
|
const resolvers = buildMountResolvers(
|
|
246
256
|
skillFolders,
|
|
247
257
|
[...(options.mounts ?? []), ...localSkillMountResolvers(extraSkillPaths)],
|
|
248
|
-
options.workspaceSkillRefreshTtlMs,
|
|
249
258
|
);
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const checkSkillFileMtime = options.checkSkillFileMtime ?? folderNames.length > 0;
|
|
259
|
+
const resolveContribution = contributionResolver(resolvers);
|
|
260
|
+
const skills = options.skills;
|
|
261
|
+
const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
|
|
254
262
|
const bm25 = options.bm25 !== false;
|
|
255
263
|
const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
|
|
256
264
|
logger.debug("workspace:create", {
|
|
@@ -268,10 +276,10 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
268
276
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
269
277
|
});
|
|
270
278
|
|
|
271
|
-
|
|
279
|
+
const workspace = new Workspace({
|
|
272
280
|
id,
|
|
273
281
|
name,
|
|
274
|
-
filesystem: (context) => resolveWorkspaceFilesystem(
|
|
282
|
+
filesystem: (context) => resolveWorkspaceFilesystem(resolveContribution, context),
|
|
275
283
|
...(skills
|
|
276
284
|
? {
|
|
277
285
|
skills,
|
|
@@ -286,6 +294,20 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
286
294
|
: {}),
|
|
287
295
|
bm25,
|
|
288
296
|
});
|
|
297
|
+
if (!options.skills && resolvers.length > 0) {
|
|
298
|
+
workspaceCatalogueResolvers.set(
|
|
299
|
+
workspace,
|
|
300
|
+
buildSkillCatalogueResolver(resolveContribution, options.workspaceSkillRefreshTtlMs),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return workspace;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Return the dbx-tools catalogue resolver attached to one workspace. */
|
|
307
|
+
export function workspaceSkillCatalogueResolver(
|
|
308
|
+
workspace: Workspace | undefined,
|
|
309
|
+
): SkillCatalogueResolver | undefined {
|
|
310
|
+
return workspace ? workspaceCatalogueResolvers.get(workspace) : undefined;
|
|
289
311
|
}
|
|
290
312
|
|
|
291
313
|
/**
|
|
@@ -316,7 +338,7 @@ function resolveWorkspaceSandbox(
|
|
|
316
338
|
workspaceId: string,
|
|
317
339
|
workspaceName: string,
|
|
318
340
|
): WorkspaceSandbox | WorkspaceSandboxResolver | undefined {
|
|
319
|
-
const configured = selection ?? "
|
|
341
|
+
const configured = selection ?? "monty";
|
|
320
342
|
if (configured === false) return undefined;
|
|
321
343
|
if (configured === "monty") return new MontySandbox();
|
|
322
344
|
if (typeof configured === "function" || isSandbox(configured)) return configured;
|
|
@@ -370,8 +392,8 @@ function isSandbox(value: unknown): value is WorkspaceSandbox {
|
|
|
370
392
|
}
|
|
371
393
|
|
|
372
394
|
function sandboxName(selection: WorkspaceSandboxSelection | undefined): string {
|
|
373
|
-
if (selection === undefined || selection === "
|
|
374
|
-
if (selection === "
|
|
395
|
+
if (selection === undefined || selection === "monty") return "monty";
|
|
396
|
+
if (selection === "databricks") return "databricks";
|
|
375
397
|
if (selection === false) return "disabled";
|
|
376
398
|
if (typeof selection === "function") return "resolver";
|
|
377
399
|
return isSandbox(selection) ? selection.provider : (selection.provider ?? "databricks");
|
|
@@ -397,9 +419,9 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
397
419
|
async function resolveSkillFolderMounts(
|
|
398
420
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
399
421
|
context: WorkspaceMountContext,
|
|
400
|
-
workspaceSkillRefreshTtlMs: number,
|
|
401
422
|
): Promise<WorkspaceMountContribution> {
|
|
402
423
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
424
|
+
const skillSources: SkillCatalogueSource[] = [];
|
|
403
425
|
const skillPaths: string[] = [];
|
|
404
426
|
const cacheKeys: string[] = [];
|
|
405
427
|
const requestContext = context.requestContext;
|
|
@@ -423,13 +445,15 @@ async function resolveSkillFolderMounts(
|
|
|
423
445
|
client,
|
|
424
446
|
host,
|
|
425
447
|
userKey,
|
|
426
|
-
workspaceSkillRefreshTtlMs,
|
|
427
448
|
});
|
|
428
449
|
if (!resolved) continue;
|
|
429
450
|
const mount = folder.mount ?? `/${name}`;
|
|
430
451
|
mounts[mount] = resolved.filesystem;
|
|
431
452
|
if (resolved.cacheKey) cacheKeys.push(resolved.cacheKey);
|
|
432
|
-
if (folder.readable !== false)
|
|
453
|
+
if (folder.readable !== false) {
|
|
454
|
+
skillPaths.push(mount);
|
|
455
|
+
skillSources.push({ ...resolved.skillSource, id: object.toStableKey({ mount, source: resolved.skillSource.id }) });
|
|
456
|
+
}
|
|
433
457
|
}
|
|
434
458
|
|
|
435
459
|
logger.debug("skill-folders:mounted", {
|
|
@@ -439,6 +463,7 @@ async function resolveSkillFolderMounts(
|
|
|
439
463
|
|
|
440
464
|
return {
|
|
441
465
|
mounts,
|
|
466
|
+
skillSources,
|
|
442
467
|
skillPaths,
|
|
443
468
|
...(cacheKeys.length === Object.keys(mounts).length && cacheKeys.length > 0
|
|
444
469
|
? { cacheKey: object.toStableKey(cacheKeys.sort()) }
|
|
@@ -458,12 +483,30 @@ async function resolveSkillFolderFilesystem(
|
|
|
458
483
|
client: WorkspaceClient | undefined;
|
|
459
484
|
host: string | undefined;
|
|
460
485
|
userKey: string | undefined;
|
|
461
|
-
workspaceSkillRefreshTtlMs: number;
|
|
462
486
|
},
|
|
463
|
-
): Promise<
|
|
487
|
+
): Promise<
|
|
488
|
+
| {
|
|
489
|
+
cacheKey?: string;
|
|
490
|
+
filesystem: WorkspaceFilesystem;
|
|
491
|
+
skillSource: SkillCatalogueSource;
|
|
492
|
+
}
|
|
493
|
+
| undefined
|
|
494
|
+
> {
|
|
464
495
|
if (folder.filesystem !== undefined) {
|
|
465
496
|
const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
|
|
466
|
-
return filesystem
|
|
497
|
+
return filesystem
|
|
498
|
+
? {
|
|
499
|
+
filesystem,
|
|
500
|
+
skillSource: mastraSkillSource(
|
|
501
|
+
object.toStableKey({
|
|
502
|
+
id: filesystem.id,
|
|
503
|
+
name,
|
|
504
|
+
provider: filesystem.provider,
|
|
505
|
+
}),
|
|
506
|
+
filesystem,
|
|
507
|
+
),
|
|
508
|
+
}
|
|
509
|
+
: undefined;
|
|
467
510
|
}
|
|
468
511
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
469
512
|
if (folder.path === undefined || !cache.client || !cache.host || !cache.userKey) {
|
|
@@ -477,7 +520,6 @@ async function resolveSkillFolderFilesystem(
|
|
|
477
520
|
if (!root) return undefined;
|
|
478
521
|
return databricksFilesystem(cache.client, root, folder.writable !== true, {
|
|
479
522
|
host: cache.host,
|
|
480
|
-
ttlMs: cache.workspaceSkillRefreshTtlMs,
|
|
481
523
|
userKey: cache.userKey,
|
|
482
524
|
});
|
|
483
525
|
}
|
|
@@ -515,14 +557,11 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
515
557
|
function buildMountResolvers(
|
|
516
558
|
skillFolders: Record<string, SkillFolderOptions>,
|
|
517
559
|
mounts: WorkspaceMountResolver[] | undefined,
|
|
518
|
-
workspaceSkillRefreshTtlMs = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
|
|
519
560
|
): WorkspaceMountResolver[] {
|
|
520
561
|
const resolvers: WorkspaceMountResolver[] = [];
|
|
521
562
|
const folderCount = Object.keys(skillFolders).length;
|
|
522
563
|
if (folderCount > 0) {
|
|
523
|
-
resolvers.push((context) =>
|
|
524
|
-
resolveSkillFolderMounts(skillFolders, context, workspaceSkillRefreshTtlMs),
|
|
525
|
-
);
|
|
564
|
+
resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context));
|
|
526
565
|
}
|
|
527
566
|
if (mounts?.length) {
|
|
528
567
|
resolvers.push(...mounts);
|
|
@@ -539,21 +578,59 @@ function buildMountResolvers(
|
|
|
539
578
|
function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResolver[] {
|
|
540
579
|
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root, index) => {
|
|
541
580
|
const mount = `/remote-skills/${index}`;
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
{ readOnly: true },
|
|
549
|
-
);
|
|
581
|
+
const source = new LocalFileSystem({
|
|
582
|
+
root,
|
|
583
|
+
readOnly: true,
|
|
584
|
+
createRoot: false,
|
|
585
|
+
});
|
|
586
|
+
const filesystem = filesystems(source, { readOnly: true });
|
|
550
587
|
return () => ({
|
|
551
588
|
mounts: { [mount]: filesystem },
|
|
589
|
+
skillSources: [sharedSkillSource(object.toStableKey({ mount, root }), source)],
|
|
552
590
|
skillPaths: [mount],
|
|
591
|
+
cacheKey: object.toStableKey({ mount, root }),
|
|
553
592
|
});
|
|
554
593
|
});
|
|
555
594
|
}
|
|
556
595
|
|
|
596
|
+
/** Adapt a portable dbx-tools filesystem to catalogue discovery callbacks. */
|
|
597
|
+
function sharedSkillSource(id: string, filesystem: FileSystem): SkillCatalogueSource {
|
|
598
|
+
return {
|
|
599
|
+
id,
|
|
600
|
+
async list(path) {
|
|
601
|
+
await filesystem.init();
|
|
602
|
+
return (await filesystem.readdir(path)).flatMap((entry) =>
|
|
603
|
+
entry.type === "file" || entry.type === "directory"
|
|
604
|
+
? [
|
|
605
|
+
{
|
|
606
|
+
name: entry.name,
|
|
607
|
+
type: entry.type,
|
|
608
|
+
...(entry.size !== undefined ? { size: entry.size } : {}),
|
|
609
|
+
...(entry.metadata ? { metadata: entry.metadata } : {}),
|
|
610
|
+
},
|
|
611
|
+
]
|
|
612
|
+
: [],
|
|
613
|
+
);
|
|
614
|
+
},
|
|
615
|
+
async read(path) {
|
|
616
|
+
await filesystem.init();
|
|
617
|
+
return filesystem.readFile(path, { encoding: "utf8" });
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Adapt an arbitrary Mastra filesystem when no portable source is available. */
|
|
623
|
+
function mastraSkillSource(id: string, filesystem: WorkspaceFilesystem): SkillCatalogueSource {
|
|
624
|
+
return {
|
|
625
|
+
id,
|
|
626
|
+
list: (path) => filesystem.readdir(path),
|
|
627
|
+
async read(path) {
|
|
628
|
+
const value = await filesystem.readFile(path, { encoding: "utf8" });
|
|
629
|
+
return typeof value === "string" ? value : value.toString("utf8");
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
557
634
|
/**
|
|
558
635
|
* Gate skill-folder mounts on the request's token.
|
|
559
636
|
*
|
|
@@ -579,23 +656,37 @@ async function databricksFilesystem(
|
|
|
579
656
|
client: WorkspaceClient,
|
|
580
657
|
root: string,
|
|
581
658
|
readOnly: boolean = true,
|
|
582
|
-
cache: { host: string;
|
|
583
|
-
): Promise<
|
|
659
|
+
cache: { host: string; userKey: string },
|
|
660
|
+
): Promise<
|
|
661
|
+
{
|
|
662
|
+
cacheKey: string;
|
|
663
|
+
filesystem: WorkspaceFilesystem;
|
|
664
|
+
skillSource: SkillCatalogueSource;
|
|
665
|
+
}
|
|
666
|
+
| undefined
|
|
667
|
+
> {
|
|
584
668
|
const fs = new DatabricksFileSystem({
|
|
585
669
|
client: workspaceClient.toLegacyWorkspaceClient(client),
|
|
586
670
|
root,
|
|
587
671
|
readOnly,
|
|
588
672
|
createRoot: !readOnly,
|
|
589
673
|
});
|
|
590
|
-
const
|
|
674
|
+
const cacheKey = object.toStableKey({
|
|
591
675
|
host: cache.host,
|
|
592
|
-
|
|
593
|
-
|
|
676
|
+
readOnly,
|
|
677
|
+
root,
|
|
594
678
|
userKey: cache.userKey,
|
|
595
679
|
});
|
|
680
|
+
const filesystem = filesystems(fs, { readOnly });
|
|
596
681
|
try {
|
|
597
|
-
await
|
|
598
|
-
if (await
|
|
682
|
+
await filesystem.init();
|
|
683
|
+
if (await filesystem.exists(".")) {
|
|
684
|
+
return {
|
|
685
|
+
cacheKey,
|
|
686
|
+
filesystem,
|
|
687
|
+
skillSource: sharedSkillSource(cacheKey, fs),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
599
690
|
} catch (err) {
|
|
600
691
|
logger.debug("databricks-mount:scratch-fallback", {
|
|
601
692
|
root,
|
|
@@ -615,6 +706,7 @@ async function resolveWorkspaceContribution(
|
|
|
615
706
|
context: WorkspaceMountContext,
|
|
616
707
|
): Promise<WorkspaceMountContribution> {
|
|
617
708
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
709
|
+
const skillSources: SkillCatalogueSource[] = [];
|
|
618
710
|
const skillPaths: string[] = [];
|
|
619
711
|
const cacheKeys: string[] = [];
|
|
620
712
|
let cacheable = true;
|
|
@@ -634,6 +726,9 @@ async function resolveWorkspaceContribution(
|
|
|
634
726
|
if (contribution.skillPaths?.length) {
|
|
635
727
|
skillPaths.push(...contribution.skillPaths);
|
|
636
728
|
}
|
|
729
|
+
if (contribution.skillSources?.length) {
|
|
730
|
+
skillSources.push(...contribution.skillSources);
|
|
731
|
+
}
|
|
637
732
|
}
|
|
638
733
|
|
|
639
734
|
logger.debug("mounts:merged", {
|
|
@@ -643,9 +738,10 @@ async function resolveWorkspaceContribution(
|
|
|
643
738
|
|
|
644
739
|
return {
|
|
645
740
|
mounts,
|
|
741
|
+
skillSources,
|
|
646
742
|
skillPaths,
|
|
647
743
|
...(cacheable && cacheKeys.length > 0
|
|
648
|
-
? { cacheKey: object.toStableKey(
|
|
744
|
+
? { cacheKey: object.toStableKey({ mounts: Object.keys(mounts), sources: cacheKeys }) }
|
|
649
745
|
: {}),
|
|
650
746
|
};
|
|
651
747
|
}
|
|
@@ -657,10 +753,10 @@ async function resolveWorkspaceContribution(
|
|
|
657
753
|
* fresh {@link scratchFilesystem} so Mastra always has a writable local root.
|
|
658
754
|
*/
|
|
659
755
|
async function resolveWorkspaceFilesystem(
|
|
660
|
-
|
|
756
|
+
resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
|
|
661
757
|
context: WorkspaceMountContext,
|
|
662
758
|
): Promise<WorkspaceFilesystem> {
|
|
663
|
-
const { cacheKey, mounts } = await
|
|
759
|
+
const { cacheKey, mounts } = await resolveContribution(context);
|
|
664
760
|
const mountKeys = Object.keys(mounts);
|
|
665
761
|
if (mountKeys.length === 0) {
|
|
666
762
|
logger.debug("filesystem:scratch", {
|
|
@@ -671,104 +767,46 @@ async function resolveWorkspaceFilesystem(
|
|
|
671
767
|
logger.debug("filesystem:composite", { mountKeys });
|
|
672
768
|
if (!cacheKey) return new CompositeFilesystem({ mounts });
|
|
673
769
|
const existing = scopedWorkspaceFilesystems.get(cacheKey);
|
|
674
|
-
if (existing)
|
|
675
|
-
scopedWorkspaceFilesystems.delete(cacheKey);
|
|
676
|
-
scopedWorkspaceFilesystems.set(cacheKey, existing);
|
|
677
|
-
return existing;
|
|
678
|
-
}
|
|
770
|
+
if (existing) return existing;
|
|
679
771
|
const filesystem = new CompositeFilesystem({ mounts });
|
|
680
772
|
scopedWorkspaceFilesystems.set(cacheKey, filesystem);
|
|
681
|
-
while (scopedWorkspaceFilesystems.size > MAX_SCOPED_WORKSPACE_FILESYSTEMS) {
|
|
682
|
-
const oldest = scopedWorkspaceFilesystems.keys().next().value;
|
|
683
|
-
if (oldest === undefined) break;
|
|
684
|
-
scopedWorkspaceFilesystems.delete(oldest);
|
|
685
|
-
}
|
|
686
773
|
return filesystem;
|
|
687
774
|
}
|
|
688
775
|
|
|
689
|
-
/**
|
|
690
|
-
|
|
691
|
-
* every mount resolver on each request.
|
|
692
|
-
*/
|
|
693
|
-
function buildWorkspaceSkillsResolver(
|
|
776
|
+
/** Memoize one complete mount contribution for each request context. */
|
|
777
|
+
function contributionResolver(
|
|
694
778
|
resolvers: WorkspaceMountResolver[],
|
|
695
|
-
):
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
});
|
|
703
|
-
return skillPaths;
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/** Expand mounted roots and keep the first concrete skill for each directory name. */
|
|
708
|
-
async function uniqueSkillPaths(contribution: WorkspaceMountContribution): Promise<string[]> {
|
|
709
|
-
const selected = new Map<string, string>();
|
|
710
|
-
const unresolved: string[] = [];
|
|
711
|
-
for (const root of contribution.skillPaths ?? []) {
|
|
712
|
-
const mount = mountedFilesystem(root, contribution.mounts);
|
|
713
|
-
if (!mount) {
|
|
714
|
-
unresolved.push(root);
|
|
715
|
-
continue;
|
|
779
|
+
): (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution> {
|
|
780
|
+
const scoped = new WeakMap<RequestContext, Promise<WorkspaceMountContribution>>();
|
|
781
|
+
let unscoped: Promise<WorkspaceMountContribution> | undefined;
|
|
782
|
+
return (context) => {
|
|
783
|
+
if (!context.requestContext) {
|
|
784
|
+
unscoped ??= resolveWorkspaceContribution(resolvers, context);
|
|
785
|
+
return unscoped;
|
|
716
786
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
error: errorUtils.errorMessage(error),
|
|
724
|
-
});
|
|
725
|
-
unresolved.push(root);
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
if (candidates.length === 0) {
|
|
729
|
-
unresolved.push(root);
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
for (const candidate of candidates) {
|
|
733
|
-
const name = posixPath.basename(candidate);
|
|
734
|
-
const existing = selected.get(name);
|
|
735
|
-
if (existing) {
|
|
736
|
-
logger.debug("skills:duplicate-skipped", { name, path: candidate, selected: existing });
|
|
737
|
-
} else {
|
|
738
|
-
selected.set(name, candidate);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return [...selected.values(), ...unresolved];
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/** Find the most-specific mounted filesystem containing one skill path. */
|
|
746
|
-
function mountedFilesystem(
|
|
747
|
-
skillPath: string,
|
|
748
|
-
mounts: Readonly<Record<string, WorkspaceFilesystem>>,
|
|
749
|
-
): { filesystem: WorkspaceFilesystem; path: string } | undefined {
|
|
750
|
-
return Object.entries(mounts)
|
|
751
|
-
.filter(([mount]) => skillPath === mount || skillPath.startsWith(`${mount}/`))
|
|
752
|
-
.sort(([left], [right]) => right.length - left.length)
|
|
753
|
-
.map(([path, filesystem]) => ({ filesystem, path }))[0];
|
|
787
|
+
const existing = scoped.get(context.requestContext);
|
|
788
|
+
if (existing) return existing;
|
|
789
|
+
const pending = resolveWorkspaceContribution(resolvers, context);
|
|
790
|
+
scoped.set(context.requestContext, pending);
|
|
791
|
+
return pending;
|
|
792
|
+
};
|
|
754
793
|
}
|
|
755
794
|
|
|
756
|
-
/**
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
return candidates;
|
|
795
|
+
/** Resolve one semantic catalogue owner from a request's ordered skill roots. */
|
|
796
|
+
function buildSkillCatalogueResolver(
|
|
797
|
+
resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
|
|
798
|
+
ttlMs: number | undefined,
|
|
799
|
+
): SkillCatalogueResolver {
|
|
800
|
+
return async ({ requestContext }) => {
|
|
801
|
+
const contribution = await resolveContribution({ requestContext });
|
|
802
|
+
const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
|
|
803
|
+
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
804
|
+
const host = client ? (await client.config.getHost()).toString() : "local";
|
|
805
|
+
return workspaceSkillCatalogue({
|
|
806
|
+
host,
|
|
807
|
+
sources: contribution.skillSources ?? [],
|
|
808
|
+
ttlMs,
|
|
809
|
+
userKey: user?.id ?? "service",
|
|
810
|
+
});
|
|
811
|
+
};
|
|
774
812
|
}
|