@dbx-tools/appkit-mastra 0.9.93 → 0.9.95
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 +85 -40
- package/index.ts +4 -10
- package/lib/index.d.ts +4 -10
- package/lib/index.js +4 -8
- package/lib/src/agents.js +11 -23
- package/lib/src/chat.js +20 -1
- package/lib/src/config.d.ts +9 -11
- package/lib/src/config.js +7 -7
- package/lib/src/filesystems.d.ts +20 -2
- package/lib/src/filesystems.js +29 -2
- package/lib/src/plugin.js +3 -2
- package/lib/src/remote-skills.d.ts +3 -4
- package/lib/src/remote-skills.js +5 -14
- package/lib/src/server.d.ts +7 -0
- package/lib/src/server.js +33 -2
- package/lib/src/skill-paths.d.ts +8 -4
- package/lib/src/skill-paths.js +12 -6
- package/lib/src/workspaces.d.ts +39 -33
- package/lib/src/workspaces.js +208 -168
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -30
- package/src/agents.ts +11 -24
- package/src/chat.ts +24 -0
- package/src/config.ts +15 -17
- package/src/filesystems.ts +43 -0
- package/src/plugin.ts +2 -0
- package/src/remote-skills.ts +5 -15
- package/src/server.ts +33 -1
- package/src/skill-paths.ts +13 -5
- package/src/workspaces.ts +285 -210
- package/lib/src/skill-cache.d.ts +0 -94
- package/lib/src/skill-cache.js +0 -251
- package/lib/src/skill-search.d.ts +0 -30
- package/lib/src/skill-search.js +0 -171
- package/src/skill-cache.ts +0 -347
- package/src/skill-search.ts +0 -203
package/src/skill-cache.ts
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User-scoped workspace skill catalogues cached as one semantic record.
|
|
3
|
-
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { CacheManager } from "@databricks/appkit";
|
|
8
|
-
import { errorUtils, log, object } from "@dbx-tools/shared-core";
|
|
9
|
-
import { posixPath } from "@dbx-tools/shared-fs";
|
|
10
|
-
import type { RequestContext } from "@mastra/core/request-context";
|
|
11
|
-
import { LRUCache } from "lru-cache";
|
|
12
|
-
import { parse as parseYaml } from "yaml";
|
|
13
|
-
import { z } from "zod";
|
|
14
|
-
|
|
15
|
-
const CACHE_NAMESPACE = "mastra:workspace-skill-catalogue";
|
|
16
|
-
const CATALOGUE_VERSION = 1;
|
|
17
|
-
const MAX_SCOPED_CATALOGUES = 256;
|
|
18
|
-
const logger = log.logger("mastra/skill-cache");
|
|
19
|
-
|
|
20
|
-
const SkillCatalogueEntrySchema = z
|
|
21
|
-
.object({
|
|
22
|
-
name: z.string().describe("Unique skill name from SKILL.md frontmatter."),
|
|
23
|
-
description: z.string().describe("Skill description from SKILL.md frontmatter."),
|
|
24
|
-
instructions: z.string().describe("Markdown instructions below SKILL.md frontmatter."),
|
|
25
|
-
sourceId: z.string().describe("Identity of the mounted source that owns this skill."),
|
|
26
|
-
sourcePath: z.string().describe("Path to SKILL.md relative to its source root."),
|
|
27
|
-
signature: z
|
|
28
|
-
.string()
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("Stable metadata signature used to retain unchanged instructions."),
|
|
31
|
-
})
|
|
32
|
-
.describe("One parsed skill in a materialized workspace catalogue.");
|
|
33
|
-
|
|
34
|
-
const SkillCatalogueSchema = z
|
|
35
|
-
.object({
|
|
36
|
-
version: z.literal(CATALOGUE_VERSION).describe("Persisted catalogue schema version."),
|
|
37
|
-
generatedAt: z.string().datetime().describe("Time the catalogue was last refreshed."),
|
|
38
|
-
roots: z.array(z.string()).describe("Ordered source identities included in the catalogue."),
|
|
39
|
-
skills: z
|
|
40
|
-
.array(SkillCatalogueEntrySchema)
|
|
41
|
-
.describe("Parsed skills after ordered duplicate-name precedence is applied."),
|
|
42
|
-
})
|
|
43
|
-
.describe("One user-scoped materialized workspace skill catalogue.");
|
|
44
|
-
|
|
45
|
-
/** Parsed skill retained in a materialized workspace catalogue. */
|
|
46
|
-
export type SkillCatalogueEntry = z.infer<typeof SkillCatalogueEntrySchema>;
|
|
47
|
-
|
|
48
|
-
/** Versioned persisted skill catalogue. */
|
|
49
|
-
export type SkillCatalogue = z.infer<typeof SkillCatalogueSchema>;
|
|
50
|
-
|
|
51
|
-
/** One ordered filesystem root contributing skills to a catalogue. */
|
|
52
|
-
export interface SkillCatalogueSource {
|
|
53
|
-
/** Stable source identity including its mount precedence. */
|
|
54
|
-
id: string;
|
|
55
|
-
/** List one source-relative directory with provider metadata when available. */
|
|
56
|
-
list(path: string): Promise<SkillCatalogueFileEntry[]>;
|
|
57
|
-
/** Read one source-relative file fresh from its owning filesystem. */
|
|
58
|
-
read(path: string): Promise<string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Minimal directory metadata used to compare one catalogue refresh. */
|
|
62
|
-
export interface SkillCatalogueFileEntry {
|
|
63
|
-
metadata?: Readonly<Record<string, unknown>>;
|
|
64
|
-
name: string;
|
|
65
|
-
size?: number;
|
|
66
|
-
type: "directory" | "file";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Options for one user-scoped workspace skill catalogue. */
|
|
70
|
-
export interface WorkspaceSkillCatalogueOptions {
|
|
71
|
-
host: string;
|
|
72
|
-
sources: readonly SkillCatalogueSource[];
|
|
73
|
-
ttlMs?: number;
|
|
74
|
-
userKey: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Request state used to resolve one user-scoped catalogue owner. */
|
|
78
|
-
export interface SkillCatalogueResolveContext {
|
|
79
|
-
abortSignal?: AbortSignal;
|
|
80
|
-
requestContext?: RequestContext;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Resolve one request's complete skill catalogue. */
|
|
84
|
-
export type SkillCatalogueResolver = (
|
|
85
|
-
context: SkillCatalogueResolveContext,
|
|
86
|
-
) => Promise<WorkspaceSkillCatalogue>;
|
|
87
|
-
|
|
88
|
-
interface ParsedSkill {
|
|
89
|
-
description: string;
|
|
90
|
-
instructions: string;
|
|
91
|
-
name: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const scopedCatalogues = new LRUCache<string, WorkspaceSkillCatalogue>({
|
|
95
|
-
max: MAX_SCOPED_CATALOGUES,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
/** Default lifetime for one complete workspace skill catalogue. */
|
|
99
|
-
export const DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
100
|
-
|
|
101
|
-
/** Materialized catalogue with one AppKit cache record and fresh auxiliary reads. */
|
|
102
|
-
export class WorkspaceSkillCatalogue {
|
|
103
|
-
readonly cacheKey: string;
|
|
104
|
-
readonly host: string;
|
|
105
|
-
readonly userKey: string;
|
|
106
|
-
|
|
107
|
-
private lastValid?: SkillCatalogue;
|
|
108
|
-
private readonly ttlMs: number;
|
|
109
|
-
private sources: readonly SkillCatalogueSource[];
|
|
110
|
-
|
|
111
|
-
constructor(private readonly options: WorkspaceSkillCatalogueOptions) {
|
|
112
|
-
this.host = options.host;
|
|
113
|
-
this.userKey = options.userKey;
|
|
114
|
-
this.sources = options.sources;
|
|
115
|
-
this.ttlMs = positiveTtl(options.ttlMs);
|
|
116
|
-
this.cacheKey = object.toStableKey({
|
|
117
|
-
host: options.host,
|
|
118
|
-
roots: options.sources.map(({ id }) => id),
|
|
119
|
-
userKey: options.userKey,
|
|
120
|
-
version: CATALOGUE_VERSION,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Rebind refreshes and lazy reads to the current request's filesystem clients. */
|
|
125
|
-
bind(sources: readonly SkillCatalogueSource[]): void {
|
|
126
|
-
const identities = sources.map(({ id }) => id);
|
|
127
|
-
if (object.toStableKey(identities) !== object.toStableKey(this.sources.map(({ id }) => id))) {
|
|
128
|
-
throw new Error("Cannot rebind a skill catalogue to a different ordered root set");
|
|
129
|
-
}
|
|
130
|
-
this.sources = sources;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Return the cached catalogue, refreshing all roots once on cache miss. */
|
|
134
|
-
async get(signal?: AbortSignal): Promise<SkillCatalogue> {
|
|
135
|
-
const cache = CacheManager.getInstanceSync();
|
|
136
|
-
try {
|
|
137
|
-
const catalogue = SkillCatalogueSchema.parse(
|
|
138
|
-
await cache.getOrExecute(
|
|
139
|
-
[CACHE_NAMESPACE, CATALOGUE_VERSION, this.options.host, this.rootKey()],
|
|
140
|
-
() => this.refresh(),
|
|
141
|
-
this.options.userKey,
|
|
142
|
-
{
|
|
143
|
-
ttl: Math.max(1, Math.ceil(this.ttlMs / 1000)),
|
|
144
|
-
callerSignal: signal,
|
|
145
|
-
},
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
this.lastValid = catalogue;
|
|
149
|
-
return catalogue;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
if (!this.lastValid) throw error;
|
|
152
|
-
logger.warn("skill catalogue refresh failed; retaining last valid catalogue", {
|
|
153
|
-
error: errorUtils.errorMessage(error),
|
|
154
|
-
roots: this.sources.map(({ id }) => id),
|
|
155
|
-
});
|
|
156
|
-
return this.lastValid;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Expire this complete catalogue without touching unrelated cache entries. */
|
|
161
|
-
async invalidate(): Promise<void> {
|
|
162
|
-
const cache = CacheManager.getInstanceSync();
|
|
163
|
-
await cache.delete(this.persistentKey(cache));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Read one selected skill's auxiliary file directly from its current source. */
|
|
167
|
-
async readSkillFile(
|
|
168
|
-
skillName: string,
|
|
169
|
-
inputPath: string,
|
|
170
|
-
signal?: AbortSignal,
|
|
171
|
-
): Promise<string> {
|
|
172
|
-
signal?.throwIfAborted();
|
|
173
|
-
const catalogue = await this.get(signal);
|
|
174
|
-
const skill = catalogue.skills.find(({ name }) => name === skillName);
|
|
175
|
-
if (!skill) throw new Error(`Unknown skill: ${skillName}`);
|
|
176
|
-
const normalized = posixPath.normalize(inputPath);
|
|
177
|
-
if (
|
|
178
|
-
!normalized.ok ||
|
|
179
|
-
normalized.path === "/" ||
|
|
180
|
-
inputPath.split("/").some((segment) => segment === "..")
|
|
181
|
-
) {
|
|
182
|
-
throw new Error(`Invalid skill file path: ${inputPath}`);
|
|
183
|
-
}
|
|
184
|
-
const source = this.sources.find(({ id }) => id === skill.sourceId);
|
|
185
|
-
if (!source) throw new Error(`Skill source is no longer mounted: ${skill.sourceId}`);
|
|
186
|
-
const parent = skill.sourcePath.split("/").slice(0, -1).join("/");
|
|
187
|
-
const path = [parent, normalized.path.replace(/^\/+/, "")].filter(Boolean).join("/");
|
|
188
|
-
return source.read(path);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private async refresh(): Promise<SkillCatalogue> {
|
|
192
|
-
const previous = new Map(
|
|
193
|
-
this.lastValid?.skills.map((skill) => [`${skill.sourceId}\0${skill.sourcePath}`, skill]) ?? [],
|
|
194
|
-
);
|
|
195
|
-
const selected = new Map<string, SkillCatalogueEntry>();
|
|
196
|
-
for (const source of this.sources) {
|
|
197
|
-
for (const candidate of await discoverSkillFiles(source)) {
|
|
198
|
-
const prior = previous.get(`${source.id}\0${candidate.path}`);
|
|
199
|
-
const signature = fileSignature(candidate.entry);
|
|
200
|
-
let parsed: ParsedSkill | undefined;
|
|
201
|
-
if (signature && prior?.signature === signature) {
|
|
202
|
-
parsed = prior;
|
|
203
|
-
} else {
|
|
204
|
-
parsed = parseSkill(
|
|
205
|
-
await source.read(candidate.path),
|
|
206
|
-
candidate.path,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (!parsed || selected.has(parsed.name)) continue;
|
|
210
|
-
selected.set(parsed.name, {
|
|
211
|
-
...parsed,
|
|
212
|
-
sourceId: source.id,
|
|
213
|
-
sourcePath: candidate.path,
|
|
214
|
-
...(signature ? { signature } : {}),
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return SkillCatalogueSchema.parse({
|
|
219
|
-
version: CATALOGUE_VERSION,
|
|
220
|
-
generatedAt: new Date().toISOString(),
|
|
221
|
-
roots: this.sources.map(({ id }) => id),
|
|
222
|
-
skills: [...selected.values()],
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private rootKey(): string {
|
|
227
|
-
return object.toStableKey(this.sources.map(({ id }) => id));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private persistentKey(cache: CacheManager): string {
|
|
231
|
-
return cache.generateKey(
|
|
232
|
-
[CACHE_NAMESPACE, CATALOGUE_VERSION, this.options.host, this.rootKey()],
|
|
233
|
-
this.options.userKey,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Return one stable catalogue owner for a user, host, and ordered root set. */
|
|
239
|
-
export function workspaceSkillCatalogue(
|
|
240
|
-
options: WorkspaceSkillCatalogueOptions,
|
|
241
|
-
): WorkspaceSkillCatalogue {
|
|
242
|
-
const key = object.toStableKey({
|
|
243
|
-
host: options.host,
|
|
244
|
-
roots: options.sources.map(({ id }) => id),
|
|
245
|
-
userKey: options.userKey,
|
|
246
|
-
});
|
|
247
|
-
const existing = scopedCatalogues.get(key);
|
|
248
|
-
if (existing) {
|
|
249
|
-
existing.bind(options.sources);
|
|
250
|
-
return existing;
|
|
251
|
-
}
|
|
252
|
-
const catalogue = new WorkspaceSkillCatalogue(options);
|
|
253
|
-
scopedCatalogues.set(key, catalogue);
|
|
254
|
-
return catalogue;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Invalidate matching complete catalogues and retain their stable owners. */
|
|
258
|
-
export async function clearWorkspaceSkillCache(options: {
|
|
259
|
-
host: string;
|
|
260
|
-
userKey?: string;
|
|
261
|
-
}): Promise<void> {
|
|
262
|
-
const matches = [...scopedCatalogues.values()].filter(
|
|
263
|
-
(catalogue) =>
|
|
264
|
-
catalogue.host === options.host &&
|
|
265
|
-
(options.userKey === undefined || catalogue.userKey === options.userKey),
|
|
266
|
-
);
|
|
267
|
-
await Promise.all(matches.map((catalogue) => catalogue.invalidate()));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function discoverSkillFiles(
|
|
271
|
-
source: SkillCatalogueSource,
|
|
272
|
-
): Promise<Array<{ entry: SkillCatalogueFileEntry; path: string }>> {
|
|
273
|
-
let rootEntries: SkillCatalogueFileEntry[];
|
|
274
|
-
try {
|
|
275
|
-
rootEntries = await source.list(".");
|
|
276
|
-
} catch (error) {
|
|
277
|
-
if (errorUtils.errorContext(error).notAccessible) return [];
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
const candidates: Array<{ entry: SkillCatalogueFileEntry; path: string }> = [];
|
|
281
|
-
const rootSkill = rootEntries.find(
|
|
282
|
-
({ name, type }) => name.toLowerCase() === "skill.md" && type === "file",
|
|
283
|
-
);
|
|
284
|
-
if (rootSkill) candidates.push({ entry: rootSkill, path: rootSkill.name });
|
|
285
|
-
await Promise.all(
|
|
286
|
-
rootEntries
|
|
287
|
-
.filter(({ type }) => type === "directory")
|
|
288
|
-
.map(async (directory) => {
|
|
289
|
-
const entries = await source.list(directory.name);
|
|
290
|
-
const skill = entries.find(
|
|
291
|
-
({ name, type }) => name.toLowerCase() === "skill.md" && type === "file",
|
|
292
|
-
);
|
|
293
|
-
if (skill) candidates.push({ entry: skill, path: posixPath.join(directory.name, skill.name) });
|
|
294
|
-
}),
|
|
295
|
-
);
|
|
296
|
-
return candidates.sort((left, right) => left.path.localeCompare(right.path));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function parseSkill(source: string, path: string): ParsedSkill | undefined {
|
|
300
|
-
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(source);
|
|
301
|
-
if (!match) {
|
|
302
|
-
logger.warn("skill skipped (missing YAML frontmatter)", { path });
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
const metadata = parseYaml(match[1]!) as unknown;
|
|
306
|
-
if (!object.isRecord(metadata)) {
|
|
307
|
-
logger.warn("skill skipped (invalid YAML frontmatter)", { path });
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
const parsed = z
|
|
311
|
-
.object({
|
|
312
|
-
name: z.string().trim().min(1),
|
|
313
|
-
description: z.string().trim().min(1),
|
|
314
|
-
})
|
|
315
|
-
.safeParse(metadata);
|
|
316
|
-
if (!parsed.success) {
|
|
317
|
-
logger.warn("skill skipped (invalid metadata)", {
|
|
318
|
-
path,
|
|
319
|
-
error: parsed.error.message,
|
|
320
|
-
});
|
|
321
|
-
return undefined;
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
name: parsed.data.name,
|
|
325
|
-
description: parsed.data.description,
|
|
326
|
-
instructions: match[2]!.trim(),
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function fileSignature(entry: SkillCatalogueFileEntry): string | undefined {
|
|
331
|
-
const metadata = object.isRecord(entry.metadata) ? entry.metadata : {};
|
|
332
|
-
const values = {
|
|
333
|
-
modifiedAt: metadata.modifiedAt,
|
|
334
|
-
objectId: metadata.objectId,
|
|
335
|
-
size: entry.size,
|
|
336
|
-
};
|
|
337
|
-
return Object.values(values).some((value) => value !== undefined)
|
|
338
|
-
? object.toStableKey(values)
|
|
339
|
-
: undefined;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function positiveTtl(value = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS): number {
|
|
343
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
344
|
-
throw new RangeError("Workspace skill catalogue ttlMs must be a positive finite number");
|
|
345
|
-
}
|
|
346
|
-
return value;
|
|
347
|
-
}
|
package/src/skill-search.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* On-demand skill discovery backed by a materialized workspace catalogue.
|
|
3
|
-
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { errorUtils } from "@dbx-tools/shared-core";
|
|
8
|
-
import type { ProcessInputStepArgs, Processor } from "@mastra/core/processors";
|
|
9
|
-
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
|
|
10
|
-
import { createTool } from "@mastra/core/tools";
|
|
11
|
-
import Fuse from "fuse.js";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import type { SkillCatalogueResolver } from "./skill-cache.ts";
|
|
14
|
-
|
|
15
|
-
const DEFAULT_STATE_TTL_MS = 60 * 60 * 1000;
|
|
16
|
-
|
|
17
|
-
interface ThreadState {
|
|
18
|
-
lastAccessed: number;
|
|
19
|
-
skills: Map<string, string>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Configuration for {@link CatalogueSkillSearchProcessor}. */
|
|
23
|
-
export interface CatalogueSkillSearchProcessorOptions {
|
|
24
|
-
minScore?: number;
|
|
25
|
-
resolve: SkillCatalogueResolver;
|
|
26
|
-
topK?: number;
|
|
27
|
-
ttlMs?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Mastra-compatible search, load, and lazy-read tools over one cached catalogue. */
|
|
31
|
-
export class CatalogueSkillSearchProcessor implements Processor<"skill-catalogue-search"> {
|
|
32
|
-
readonly id = "skill-catalogue-search";
|
|
33
|
-
readonly name = "Skill Catalogue Search";
|
|
34
|
-
readonly providesSkillDiscovery = "on-demand" as const;
|
|
35
|
-
|
|
36
|
-
private readonly minScore: number;
|
|
37
|
-
private readonly resolve: SkillCatalogueResolver;
|
|
38
|
-
private readonly states = new Map<string, ThreadState>();
|
|
39
|
-
private readonly topK: number;
|
|
40
|
-
private readonly ttlMs: number;
|
|
41
|
-
|
|
42
|
-
constructor(options: CatalogueSkillSearchProcessorOptions) {
|
|
43
|
-
this.resolve = options.resolve;
|
|
44
|
-
this.topK = options.topK ?? 5;
|
|
45
|
-
this.minScore = options.minScore ?? 0.1;
|
|
46
|
-
this.ttlMs = options.ttlMs ?? DEFAULT_STATE_TTL_MS;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async processInputStep(args: ProcessInputStepArgs): Promise<{ tools: Record<string, unknown> }> {
|
|
50
|
-
const owner = await this.resolve(args);
|
|
51
|
-
const catalogue = await owner.get(args.abortSignal);
|
|
52
|
-
const state = this.threadState(args);
|
|
53
|
-
args.messageList.addSystem(
|
|
54
|
-
"To discover available skills, call search_skills with a keyword query. " +
|
|
55
|
-
"To load a skill's instructions, call load_skill with the exact skill name. " +
|
|
56
|
-
"Use skill_read only after loading a skill when its instructions require a reference, script, or asset.",
|
|
57
|
-
);
|
|
58
|
-
for (const [skillName, instructions] of state.skills) {
|
|
59
|
-
args.messageList.addSystem(`[Skill: ${skillName}]\n\n${instructions}`);
|
|
60
|
-
}
|
|
61
|
-
const search = new Fuse(catalogue.skills, {
|
|
62
|
-
includeScore: true,
|
|
63
|
-
ignoreLocation: true,
|
|
64
|
-
threshold: 1,
|
|
65
|
-
keys: [
|
|
66
|
-
{ name: "name", weight: 0.5 },
|
|
67
|
-
{ name: "description", weight: 0.35 },
|
|
68
|
-
{ name: "instructions", weight: 0.15 },
|
|
69
|
-
],
|
|
70
|
-
});
|
|
71
|
-
return {
|
|
72
|
-
tools: {
|
|
73
|
-
...(args.tools ?? {}),
|
|
74
|
-
search_skills: createTool({
|
|
75
|
-
id: "search_skills",
|
|
76
|
-
description:
|
|
77
|
-
"Search for available skills by keyword. Load a useful result with load_skill.",
|
|
78
|
-
inputSchema: z.object({
|
|
79
|
-
query: z.string().trim().min(1, "Query is required").describe("Search keywords."),
|
|
80
|
-
}),
|
|
81
|
-
outputSchema: z.object({
|
|
82
|
-
results: z.array(
|
|
83
|
-
z.object({
|
|
84
|
-
name: z.string(),
|
|
85
|
-
description: z.string(),
|
|
86
|
-
score: z.number(),
|
|
87
|
-
}),
|
|
88
|
-
),
|
|
89
|
-
message: z.string(),
|
|
90
|
-
}),
|
|
91
|
-
execute: async ({ query }) => {
|
|
92
|
-
const results = search
|
|
93
|
-
.search(query)
|
|
94
|
-
.map((result) => ({
|
|
95
|
-
name: result.item.name,
|
|
96
|
-
description:
|
|
97
|
-
result.item.description.length > 150
|
|
98
|
-
? `${result.item.description.slice(0, 147)}...`
|
|
99
|
-
: result.item.description,
|
|
100
|
-
score: Math.round((1 - (result.score ?? 1)) * 100) / 100,
|
|
101
|
-
}))
|
|
102
|
-
.filter(({ score }) => score >= this.minScore)
|
|
103
|
-
.slice(0, this.topK);
|
|
104
|
-
return {
|
|
105
|
-
results,
|
|
106
|
-
message:
|
|
107
|
-
results.length === 0
|
|
108
|
-
? `No skills found matching "${query}". Try different keywords.`
|
|
109
|
-
: `Found ${results.length} skill(s). Use load_skill with the exact skill name to load its instructions.`,
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
}),
|
|
113
|
-
load_skill: createTool({
|
|
114
|
-
id: "load_skill",
|
|
115
|
-
description: "Load a skill's full instructions into the conversation.",
|
|
116
|
-
inputSchema: z.object({
|
|
117
|
-
skillName: z.string().describe("Exact skill name returned by search_skills."),
|
|
118
|
-
}),
|
|
119
|
-
outputSchema: z.object({
|
|
120
|
-
success: z.boolean(),
|
|
121
|
-
message: z.string(),
|
|
122
|
-
skillName: z.string().optional(),
|
|
123
|
-
}),
|
|
124
|
-
execute: async ({ skillName }) => {
|
|
125
|
-
if (state.skills.has(skillName)) {
|
|
126
|
-
return {
|
|
127
|
-
success: true,
|
|
128
|
-
message: `Skill "${skillName}" is already loaded.`,
|
|
129
|
-
skillName,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const skill = catalogue.skills.find(({ name }) => name === skillName);
|
|
133
|
-
if (!skill) {
|
|
134
|
-
return {
|
|
135
|
-
success: false,
|
|
136
|
-
message: `Skill "${skillName}" not found. Use search_skills to find available skills.`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
state.skills.set(skillName, skill.instructions);
|
|
140
|
-
return {
|
|
141
|
-
success: true,
|
|
142
|
-
message: `Skill "${skillName}" loaded. Its instructions are now available as context.`,
|
|
143
|
-
skillName,
|
|
144
|
-
};
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
|
-
skill_read: createTool({
|
|
148
|
-
id: "skill_read",
|
|
149
|
-
description:
|
|
150
|
-
"Read a reference, script, template, or asset from a loaded skill. The path is relative to the skill root.",
|
|
151
|
-
inputSchema: z.object({
|
|
152
|
-
skillName: z.string().describe("Exact loaded skill name."),
|
|
153
|
-
path: z.string().describe("File path relative to the skill root."),
|
|
154
|
-
startLine: z.number().int().positive().optional(),
|
|
155
|
-
endLine: z.number().int().positive().optional(),
|
|
156
|
-
}),
|
|
157
|
-
execute: async ({ skillName, path, startLine, endLine }) => {
|
|
158
|
-
if (!state.skills.has(skillName)) {
|
|
159
|
-
return `Skill "${skillName}" is not loaded. Call load_skill first.`;
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const source = await owner.readSkillFile(skillName, path, args.abortSignal);
|
|
163
|
-
return lineRange(source, path, startLine, endLine);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return `Could not read "${path}" from skill "${skillName}": ${errorUtils.errorMessage(error)}`;
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
}),
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private threadState(args: ProcessInputStepArgs): ThreadState {
|
|
174
|
-
const now = Date.now();
|
|
175
|
-
for (const [key, state] of this.states) {
|
|
176
|
-
if (now - state.lastAccessed > this.ttlMs) this.states.delete(key);
|
|
177
|
-
}
|
|
178
|
-
const key = args.requestContext?.get(MASTRA_THREAD_ID_KEY) ?? "default";
|
|
179
|
-
const current = this.states.get(String(key)) ?? {
|
|
180
|
-
lastAccessed: now,
|
|
181
|
-
skills: new Map<string, string>(),
|
|
182
|
-
};
|
|
183
|
-
current.lastAccessed = now;
|
|
184
|
-
this.states.set(String(key), current);
|
|
185
|
-
return current;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function lineRange(
|
|
190
|
-
source: string,
|
|
191
|
-
path: string,
|
|
192
|
-
startLine: number | undefined,
|
|
193
|
-
endLine: number | undefined,
|
|
194
|
-
): string {
|
|
195
|
-
if (startLine === undefined && endLine === undefined) return source;
|
|
196
|
-
const lines = source.split(/\r?\n/);
|
|
197
|
-
const start = Math.max(1, startLine ?? 1);
|
|
198
|
-
const end = Math.min(lines.length, endLine ?? lines.length);
|
|
199
|
-
if (start > end || start > lines.length) {
|
|
200
|
-
return `File "${path}" has ${lines.length} lines; requested range ${start}-${end} is empty.`;
|
|
201
|
-
}
|
|
202
|
-
return `${path} (lines ${start}-${end} of ${lines.length})\n${lines.slice(start - 1, end).join("\n")}`;
|
|
203
|
-
}
|