@juspay/neurolink 9.85.1 → 9.86.0
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/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +370 -362
- package/dist/context/contextCompactor.js +16 -2
- package/dist/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/core/conversationMemoryManager.js +13 -2
- package/dist/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/context/contextCompactor.js +16 -2
- package/dist/lib/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/lib/core/conversationMemoryManager.js +13 -2
- package/dist/lib/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/neurolink.d.ts +31 -6
- package/dist/lib/neurolink.js +163 -33
- package/dist/lib/skills/skillMatcher.d.ts +33 -4
- package/dist/lib/skills/skillMatcher.js +81 -6
- package/dist/lib/skills/skillSessionTracker.d.ts +52 -0
- package/dist/lib/skills/skillSessionTracker.js +150 -0
- package/dist/lib/skills/skillStoreRedis.d.ts +8 -0
- package/dist/lib/skills/skillStoreRedis.js +18 -2
- package/dist/lib/skills/skillStoreS3.d.ts +17 -2
- package/dist/lib/skills/skillStoreS3.js +78 -6
- package/dist/lib/skills/skillStores.d.ts +16 -2
- package/dist/lib/skills/skillStores.js +94 -5
- package/dist/lib/skills/skillTools.d.ts +26 -10
- package/dist/lib/skills/skillTools.js +190 -79
- package/dist/lib/skills/skillsManager.d.ts +25 -1
- package/dist/lib/skills/skillsManager.js +46 -4
- package/dist/lib/types/config.d.ts +5 -5
- package/dist/lib/types/conversation.d.ts +27 -0
- package/dist/lib/types/skills.d.ts +145 -14
- package/dist/lib/types/skills.js +3 -3
- package/dist/lib/utils/conversationMemory.d.ts +1 -1
- package/dist/lib/utils/conversationMemory.js +15 -2
- package/dist/neurolink.d.ts +31 -6
- package/dist/neurolink.js +163 -33
- package/dist/skills/skillMatcher.d.ts +33 -4
- package/dist/skills/skillMatcher.js +81 -6
- package/dist/skills/skillSessionTracker.d.ts +52 -0
- package/dist/skills/skillSessionTracker.js +149 -0
- package/dist/skills/skillStoreRedis.d.ts +8 -0
- package/dist/skills/skillStoreRedis.js +18 -2
- package/dist/skills/skillStoreS3.d.ts +17 -2
- package/dist/skills/skillStoreS3.js +78 -6
- package/dist/skills/skillStores.d.ts +16 -2
- package/dist/skills/skillStores.js +94 -5
- package/dist/skills/skillTools.d.ts +26 -10
- package/dist/skills/skillTools.js +190 -79
- package/dist/skills/skillsManager.d.ts +25 -1
- package/dist/skills/skillsManager.js +46 -4
- package/dist/types/config.d.ts +5 -5
- package/dist/types/conversation.d.ts +27 -0
- package/dist/types/skills.d.ts +145 -14
- package/dist/types/skills.js +3 -3
- package/dist/utils/conversationMemory.d.ts +1 -1
- package/dist/utils/conversationMemory.js +15 -2
- package/package.json +1 -1
|
@@ -40,9 +40,23 @@ function isValidSkill(candidate) {
|
|
|
40
40
|
/** In-process store backed by a Map. */
|
|
41
41
|
export class InMemorySkillStore {
|
|
42
42
|
skills = new Map();
|
|
43
|
-
|
|
43
|
+
/** skill id → relative path → content. */
|
|
44
|
+
resources = new Map();
|
|
45
|
+
constructor(seed, resources) {
|
|
46
|
+
for (const [skillId, files] of Object.entries(resources ?? {})) {
|
|
47
|
+
this.resources.set(skillId, new Map(Object.entries(files)));
|
|
48
|
+
}
|
|
44
49
|
for (const skill of seed ?? []) {
|
|
45
|
-
this.
|
|
50
|
+
const files = this.resources.get(skill.id);
|
|
51
|
+
const withRefs = files && !skill.resources
|
|
52
|
+
? {
|
|
53
|
+
...skill,
|
|
54
|
+
resources: Array.from(files.keys())
|
|
55
|
+
.sort()
|
|
56
|
+
.map((p) => ({ path: p })),
|
|
57
|
+
}
|
|
58
|
+
: skill;
|
|
59
|
+
this.skills.set(skill.id, normalizeSkill(withRefs));
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
async get(id) {
|
|
@@ -57,6 +71,9 @@ export class InMemorySkillStore {
|
|
|
57
71
|
async index() {
|
|
58
72
|
return Array.from(this.skills.values()).map(toSkillIndexItem);
|
|
59
73
|
}
|
|
74
|
+
async getResource(id, resourcePath) {
|
|
75
|
+
return this.resources.get(id)?.get(resourcePath) ?? null;
|
|
76
|
+
}
|
|
60
77
|
}
|
|
61
78
|
/**
|
|
62
79
|
* Parse a markdown document with optional YAML frontmatter into a skill.
|
|
@@ -112,17 +129,56 @@ function parseSkillMarkdown(raw, fallbackId) {
|
|
|
112
129
|
});
|
|
113
130
|
return candidate;
|
|
114
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* List every file bundled beside a skill's SKILL.md as a resource ref,
|
|
134
|
+
* relative paths, sorted for deterministic listings. Recurses into
|
|
135
|
+
* subdirectories (references/, assets/, …).
|
|
136
|
+
*/
|
|
137
|
+
function listSkillResourceFiles(skillDir) {
|
|
138
|
+
const refs = [];
|
|
139
|
+
const walk = (current, relative) => {
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const relPath = relative ? `${relative}/${entry.name}` : entry.name;
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
walk(path.join(current, entry.name), relPath);
|
|
151
|
+
}
|
|
152
|
+
else if (relPath !== "SKILL.md") {
|
|
153
|
+
try {
|
|
154
|
+
refs.push({
|
|
155
|
+
path: relPath,
|
|
156
|
+
size: fs.statSync(path.join(current, entry.name)).size,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
refs.push({ path: relPath });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
walk(skillDir, "");
|
|
166
|
+
return refs.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
167
|
+
}
|
|
115
168
|
/**
|
|
116
169
|
* Directory-backed store. Read layouts:
|
|
117
170
|
* - `<dir>/<id>.json` — JSON SkillDefinition (mutable)
|
|
118
171
|
* - `<dir>/<name>.md` — frontmatter markdown (read-only source)
|
|
119
|
-
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source)
|
|
172
|
+
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source);
|
|
173
|
+
* sibling files become on-demand resources (read_skill_resource)
|
|
120
174
|
* Mutations always write `<id>.json`; a JSON file shadows a markdown skill
|
|
121
175
|
* with the same id, so updating a markdown-sourced skill "copies up" to JSON.
|
|
122
176
|
*/
|
|
123
177
|
export class FileSystemSkillStore {
|
|
124
178
|
baseDir;
|
|
125
179
|
cache = null;
|
|
180
|
+
/** skill id → directory, for skills loaded from the SKILL.md layout. */
|
|
181
|
+
skillDirs = new Map();
|
|
126
182
|
constructor(baseDir) {
|
|
127
183
|
this.baseDir = baseDir;
|
|
128
184
|
}
|
|
@@ -133,6 +189,36 @@ export class FileSystemSkillStore {
|
|
|
133
189
|
const all = this.load();
|
|
134
190
|
return all.get(id) ?? null;
|
|
135
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Resources exist only for directory-layout skills (`<name>/SKILL.md`) —
|
|
194
|
+
* every sibling file of SKILL.md is addressable by its relative path.
|
|
195
|
+
* The REAL path must stay inside the skill directory: lexical
|
|
196
|
+
* containment alone would follow a symlink planted inside the skill dir
|
|
197
|
+
* to anywhere on the host.
|
|
198
|
+
*/
|
|
199
|
+
async getResource(id, resourcePath) {
|
|
200
|
+
this.load();
|
|
201
|
+
const dir = this.skillDirs.get(id);
|
|
202
|
+
if (!dir) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const realDir = fs.realpathSync(dir);
|
|
207
|
+
const realResolved = fs.realpathSync(path.resolve(dir, resourcePath));
|
|
208
|
+
if (realResolved !== realDir &&
|
|
209
|
+
!realResolved.startsWith(realDir + path.sep)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return fs.readFileSync(realResolved, "utf-8");
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const code = error.code;
|
|
216
|
+
if (code === "ENOENT" || code === "EISDIR" || code === "ELOOP") {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
136
222
|
async put(skill) {
|
|
137
223
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
138
224
|
const filePath = path.join(this.baseDir, `${skill.id}.json`);
|
|
@@ -164,6 +250,7 @@ export class FileSystemSkillStore {
|
|
|
164
250
|
return this.cache;
|
|
165
251
|
}
|
|
166
252
|
const skills = new Map();
|
|
253
|
+
this.skillDirs = new Map();
|
|
167
254
|
let entries;
|
|
168
255
|
try {
|
|
169
256
|
entries = fs.readdirSync(this.baseDir, { withFileTypes: true });
|
|
@@ -188,7 +275,9 @@ export class FileSystemSkillStore {
|
|
|
188
275
|
if (fs.existsSync(skillMd)) {
|
|
189
276
|
const parsed = parseSkillMarkdown(fs.readFileSync(skillMd, "utf-8"), entry.name);
|
|
190
277
|
if (parsed) {
|
|
191
|
-
|
|
278
|
+
const resources = listSkillResourceFiles(fullPath);
|
|
279
|
+
skills.set(parsed.id, resources.length > 0 ? { ...parsed, resources } : parsed);
|
|
280
|
+
this.skillDirs.set(parsed.id, path.resolve(fullPath));
|
|
192
281
|
}
|
|
193
282
|
}
|
|
194
283
|
}
|
|
@@ -236,7 +325,7 @@ export class FileSystemSkillStore {
|
|
|
236
325
|
/** Resolve a storage config to a concrete store. Defaults to memory. */
|
|
237
326
|
export function createSkillStore(config) {
|
|
238
327
|
if (!config || config.type === "memory") {
|
|
239
|
-
return new InMemorySkillStore(config?.type === "memory" ? config.skills : undefined);
|
|
328
|
+
return new InMemorySkillStore(config?.type === "memory" ? config.skills : undefined, config?.type === "memory" ? config.resources : undefined);
|
|
240
329
|
}
|
|
241
330
|
if (config.type === "filesystem") {
|
|
242
331
|
return new FileSystemSkillStore(config.path);
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in skill tools
|
|
3
|
-
*
|
|
2
|
+
* Built-in skill tools.
|
|
3
|
+
*
|
|
4
|
+
* Two factories, mirroring the two disclosure levels:
|
|
5
|
+
*
|
|
6
|
+
* createSkillTools — instance tools registered once at construction:
|
|
7
|
+
* list_skills (lightweight catalog) plus the gated skill_create /
|
|
8
|
+
* skill_update / skill_delete mutation tools.
|
|
9
|
+
*
|
|
10
|
+
* createSkillCallTools — per-call tools injected into options.tools by
|
|
11
|
+
* prepareGenerate/prepareStream (the RAG-tool pattern): use_skill, whose
|
|
12
|
+
* description embeds the `<available_skills>` listing so the model
|
|
13
|
+
* decides from context when a skill applies (no mandatory pre-flight
|
|
14
|
+
* call), and read_skill_resource for on-demand auxiliary files. The
|
|
15
|
+
* sessionId rides in by closure, so activations pin to the session
|
|
16
|
+
* without runtime tool-context plumbing.
|
|
4
17
|
*
|
|
5
18
|
* The manager is resolved lazily via a resolver callback so tools can be
|
|
6
19
|
* registered at construction time while the store initializes on first
|
|
7
20
|
* use (mirrors how retrieve_context resolves conversationMemory lazily).
|
|
8
|
-
*
|
|
9
|
-
* Tool descriptions are ported from curator's production skills tools —
|
|
10
|
-
* the "always check skills first / no_match is expected, not an error /
|
|
11
|
-
* pick the best of 2-5 or ask" prompt engineering is proven in prod.
|
|
12
21
|
*/
|
|
13
|
-
import type { SkillsManagerLike, SkillToolsOptions, Tool } from "../types/index.js";
|
|
22
|
+
import type { SkillCallToolsContext, SkillsManagerLike, SkillToolsOptions, Tool } from "../types/index.js";
|
|
23
|
+
/**
|
|
24
|
+
* Per-call skill tools: use_skill + read_skill_resource. Injected into
|
|
25
|
+
* options.tools for one generate()/stream() call; same-name entries shadow
|
|
26
|
+
* any registered tool, so the description always reflects this call's
|
|
27
|
+
* listing and scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createSkillCallTools(resolveManager: () => SkillsManagerLike | null, context: SkillCallToolsContext): Record<string, Tool>;
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
* Returns Vercel AI SDK tool() objects
|
|
17
|
-
* execute) keyed by tool name.
|
|
31
|
+
* Instance skill tools bound to a lazily-resolved manager: list_skills
|
|
32
|
+
* plus the gated mutation tools. Returns Vercel AI SDK tool() objects
|
|
33
|
+
* (description + Zod inputSchema + execute) keyed by tool name.
|
|
18
34
|
*/
|
|
19
35
|
export declare function createSkillTools(resolveManager: () => SkillsManagerLike | null, options?: SkillToolsOptions): Record<string, Tool>;
|
|
@@ -1,126 +1,240 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in skill tools
|
|
3
|
-
*
|
|
2
|
+
* Built-in skill tools.
|
|
3
|
+
*
|
|
4
|
+
* Two factories, mirroring the two disclosure levels:
|
|
5
|
+
*
|
|
6
|
+
* createSkillTools — instance tools registered once at construction:
|
|
7
|
+
* list_skills (lightweight catalog) plus the gated skill_create /
|
|
8
|
+
* skill_update / skill_delete mutation tools.
|
|
9
|
+
*
|
|
10
|
+
* createSkillCallTools — per-call tools injected into options.tools by
|
|
11
|
+
* prepareGenerate/prepareStream (the RAG-tool pattern): use_skill, whose
|
|
12
|
+
* description embeds the `<available_skills>` listing so the model
|
|
13
|
+
* decides from context when a skill applies (no mandatory pre-flight
|
|
14
|
+
* call), and read_skill_resource for on-demand auxiliary files. The
|
|
15
|
+
* sessionId rides in by closure, so activations pin to the session
|
|
16
|
+
* without runtime tool-context plumbing.
|
|
4
17
|
*
|
|
5
18
|
* The manager is resolved lazily via a resolver callback so tools can be
|
|
6
19
|
* registered at construction time while the store initializes on first
|
|
7
20
|
* use (mirrors how retrieve_context resolves conversationMemory lazily).
|
|
8
|
-
*
|
|
9
|
-
* Tool descriptions are ported from curator's production skills tools —
|
|
10
|
-
* the "always check skills first / no_match is expected, not an error /
|
|
11
|
-
* pick the best of 2-5 or ask" prompt engineering is proven in prod.
|
|
12
21
|
*/
|
|
22
|
+
import { trace } from "@opentelemetry/api";
|
|
13
23
|
import { z } from "zod";
|
|
14
24
|
import { logger } from "../utils/logger.js";
|
|
15
25
|
import { tool } from "../utils/tool.js";
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
import { isSkillVisibleInScope } from "./skillMatcher.js";
|
|
27
|
+
const NOT_CONFIGURED = {
|
|
28
|
+
success: false,
|
|
29
|
+
error: "Skills are not available — the skills store failed to initialize. " +
|
|
30
|
+
"Answer from general knowledge.",
|
|
31
|
+
};
|
|
32
|
+
/** Response payload for an activated skill — full instructions, never truncated. */
|
|
33
|
+
function toActivationPayload(skill) {
|
|
18
34
|
return {
|
|
19
|
-
id: skill.id,
|
|
20
35
|
name: skill.name,
|
|
21
36
|
...(skill.displayName ? { displayName: skill.displayName } : {}),
|
|
22
|
-
description: skill.description,
|
|
23
|
-
instructions: skill.instructions,
|
|
24
|
-
tags: skill.tags ?? [],
|
|
25
|
-
scope: skill.scope ?? "global",
|
|
26
37
|
version: skill.version ?? 1,
|
|
38
|
+
instructions: skill.instructions,
|
|
39
|
+
...(skill.resources && skill.resources.length > 0
|
|
40
|
+
? {
|
|
41
|
+
resources: skill.resources.map((r) => r.path),
|
|
42
|
+
resourcesHint: "Auxiliary files — read one with read_skill_resource only when the task needs it.",
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
note: "Follow these instructions for the current task. The skill stays loaded " +
|
|
46
|
+
"for this conversation — do not invoke use_skill for it again.",
|
|
27
47
|
};
|
|
28
48
|
}
|
|
49
|
+
/** Build the use_skill description for the call's discovery mode. */
|
|
50
|
+
function useSkillDescription(context) {
|
|
51
|
+
const base = "Load a team-defined skill (SOP, playbook, workflow) by name to get its full instructions. " +
|
|
52
|
+
"When the user's task matches a skill, invoke this tool, then follow the loaded instructions exactly. " +
|
|
53
|
+
"Do NOT invoke it for a skill that is already loaded in this conversation, and do not " +
|
|
54
|
+
"invoke it at all when no skill matches the task.";
|
|
55
|
+
if (context.discovery === "tool" && context.listing) {
|
|
56
|
+
return `${base}\n\nSkills available in this conversation:\n${context.listing}`;
|
|
57
|
+
}
|
|
58
|
+
if (context.discovery === "system-prompt") {
|
|
59
|
+
return `${base} The available skills are listed under "## Available Skills" in your instructions.`;
|
|
60
|
+
}
|
|
61
|
+
return `${base} Discover available skills with list_skills.`;
|
|
62
|
+
}
|
|
29
63
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
64
|
+
* Per-call skill tools: use_skill + read_skill_resource. Injected into
|
|
65
|
+
* options.tools for one generate()/stream() call; same-name entries shadow
|
|
66
|
+
* any registered tool, so the description always reflects this call's
|
|
67
|
+
* listing and scope.
|
|
33
68
|
*/
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
export function createSkillCallTools(resolveManager, context) {
|
|
70
|
+
/**
|
|
71
|
+
* Rebuild activation state from stored history before every dedup
|
|
72
|
+
* check. use_skill is rare, so one history read per attempt is cheap —
|
|
73
|
+
* and it keeps dedup truthful across restarts, other instances sharing
|
|
74
|
+
* the memory backend, and pins whose persistence failed. Fail-open: a
|
|
75
|
+
* hydration error only risks a duplicate pin, never a lost activation.
|
|
76
|
+
*/
|
|
77
|
+
const ensureHydrated = async (manager) => {
|
|
78
|
+
const { sessionId, getStoredMessages } = context;
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const stored = getStoredMessages
|
|
84
|
+
? await getStoredMessages(sessionId)
|
|
85
|
+
: [];
|
|
86
|
+
manager.sessions.hydrate(sessionId, stored);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logger.warn("[SkillTools] Session hydration failed — continuing", {
|
|
90
|
+
sessionId,
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
40
94
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
description:
|
|
44
|
-
"Searches team-defined skills (SOPs, playbooks, workflows) using an index-first approach — " +
|
|
45
|
-
"fast, low-cost, and does not load skills that do not match. " +
|
|
46
|
-
"You MUST provide at least one of: query (keyword from the user message) or tag (domain category). " +
|
|
47
|
-
"If 1 match is found, follow its instructions exactly. " +
|
|
48
|
-
"If 2-5 matches are found, use your judgment based on the user message to pick the best one, " +
|
|
49
|
-
"or ask the user to clarify if the instructions would lead to meaningfully different actions. " +
|
|
50
|
-
"Only skip this call for purely conversational messages like greetings. " +
|
|
51
|
-
'Returns `{ skills: [], reason: "no_match" }` when nothing matches — this is expected, NOT an ' +
|
|
52
|
-
"error. In that case, answer from general knowledge.",
|
|
95
|
+
return {
|
|
96
|
+
use_skill: tool({
|
|
97
|
+
description: useSkillDescription(context),
|
|
53
98
|
inputSchema: z.object({
|
|
54
|
-
|
|
55
|
-
.string()
|
|
56
|
-
.optional()
|
|
57
|
-
.describe("Keyword from the user message, matched against skill name, display name, and description. " +
|
|
58
|
-
'E.g. "deployment", "refund process", "on-call escalation".'),
|
|
59
|
-
tag: z
|
|
60
|
-
.string()
|
|
61
|
-
.optional()
|
|
62
|
-
.describe('Domain category tag to narrow results (e.g. "payments", "devops"). ' +
|
|
63
|
-
"Applied on top of the query filter when both are provided."),
|
|
64
|
-
scopeId: z
|
|
99
|
+
name: z
|
|
65
100
|
.string()
|
|
66
|
-
.
|
|
67
|
-
.describe(
|
|
68
|
-
"Usually omit — the host default applies."),
|
|
101
|
+
.min(1)
|
|
102
|
+
.describe('Exact skill name from the available skills listing, e.g. "refund_dispute_escalation".'),
|
|
69
103
|
}),
|
|
70
104
|
execute: async (args) => {
|
|
71
|
-
|
|
105
|
+
const manager = resolveManager();
|
|
106
|
+
if (!manager) {
|
|
107
|
+
return NOT_CONFIGURED;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const skill = await manager.get(args.name);
|
|
111
|
+
if (!skill || !isSkillVisibleInScope(skill, context.scopeId)) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: `No skill named "${args.name}" is available. Use the exact name ` +
|
|
115
|
+
"from the available skills listing (or list_skills).",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (context.sessionId && context.sessionPersistence) {
|
|
119
|
+
await ensureHydrated(manager);
|
|
120
|
+
if (manager.sessions.isActive(context.sessionId, skill.id, skill.name)) {
|
|
121
|
+
// Report the version the session actually follows — a
|
|
122
|
+
// mid-session update must not masquerade as loaded.
|
|
123
|
+
const activation = manager.sessions.getActivation(context.sessionId, skill.id, skill.name);
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
data: {
|
|
127
|
+
status: "already_loaded",
|
|
128
|
+
name: skill.name,
|
|
129
|
+
version: activation?.version ?? skill.version ?? 1,
|
|
130
|
+
message: "This skill is already loaded in the conversation — follow " +
|
|
131
|
+
"the instructions from when it was loaded.",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
manager.sessions.recordActivation(context.sessionId, skill);
|
|
136
|
+
}
|
|
137
|
+
const pinned = Boolean(context.sessionId && context.sessionPersistence);
|
|
138
|
+
trace.getActiveSpan()?.setAttributes({
|
|
139
|
+
"skill.name": skill.name,
|
|
140
|
+
"skill.version": skill.version ?? 1,
|
|
141
|
+
"skill.instructions_chars": skill.instructions.length,
|
|
142
|
+
"skill.pinned": pinned,
|
|
143
|
+
});
|
|
144
|
+
logger.debug("[SkillTools] Skill activated", {
|
|
145
|
+
skill: skill.name,
|
|
146
|
+
version: skill.version ?? 1,
|
|
147
|
+
sessionId: context.sessionId,
|
|
148
|
+
pinned,
|
|
149
|
+
});
|
|
150
|
+
return { success: true, data: toActivationPayload(skill) };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.warn("[SkillTools] use_skill failed", {
|
|
154
|
+
skill: args.name,
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
});
|
|
72
157
|
return {
|
|
73
158
|
success: false,
|
|
74
|
-
error:
|
|
75
|
-
data: { skills: [] },
|
|
159
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
160
|
};
|
|
77
161
|
}
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
read_skill_resource: tool({
|
|
165
|
+
description: "Read an auxiliary file bundled with a loaded skill (paths appear in the " +
|
|
166
|
+
"skill's `resources` list or are referenced from its instructions). Load the " +
|
|
167
|
+
"skill with use_skill first. Only read files the current task actually needs — " +
|
|
168
|
+
"each resource costs context.",
|
|
169
|
+
inputSchema: z.object({
|
|
170
|
+
skill: z.string().min(1).describe("Name of the loaded skill."),
|
|
171
|
+
path: z
|
|
172
|
+
.string()
|
|
173
|
+
.min(1)
|
|
174
|
+
.describe('Resource path relative to the skill, e.g. "references/forms.md".'),
|
|
175
|
+
}),
|
|
176
|
+
execute: async (args) => {
|
|
78
177
|
const manager = resolveManager();
|
|
79
178
|
if (!manager) {
|
|
80
|
-
return
|
|
179
|
+
return NOT_CONFIGURED;
|
|
81
180
|
}
|
|
82
181
|
try {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
182
|
+
const skill = await manager.get(args.skill);
|
|
183
|
+
if (!skill || !isSkillVisibleInScope(skill, context.scopeId)) {
|
|
85
184
|
return {
|
|
86
|
-
success:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
185
|
+
success: false,
|
|
186
|
+
error: `No skill named "${args.skill}" is available.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (context.sessionId && context.sessionPersistence) {
|
|
190
|
+
await ensureHydrated(manager);
|
|
191
|
+
if (!manager.sessions.isActive(context.sessionId, skill.id, skill.name)) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error: `Skill "${skill.name}" is not loaded — call use_skill first.`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const content = await manager.getResource(skill.id, args.path);
|
|
199
|
+
if (content === null) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: `Resource "${args.path}" not found on skill "${skill.name}". ` +
|
|
203
|
+
"Valid paths are listed in the skill's `resources`.",
|
|
92
204
|
};
|
|
93
205
|
}
|
|
94
206
|
return {
|
|
95
207
|
success: true,
|
|
96
|
-
data: {
|
|
97
|
-
skills: skills.map(toToolSkill),
|
|
98
|
-
count: skills.length,
|
|
99
|
-
...(skills.length > 1
|
|
100
|
-
? {
|
|
101
|
-
hint: "Multiple skills matched. Pick the most relevant one for the user message, or ask the user to clarify.",
|
|
102
|
-
}
|
|
103
|
-
: {}),
|
|
104
|
-
},
|
|
208
|
+
data: { skill: skill.name, path: args.path, content },
|
|
105
209
|
};
|
|
106
210
|
}
|
|
107
211
|
catch (error) {
|
|
108
|
-
logger.warn("[SkillTools]
|
|
212
|
+
logger.warn("[SkillTools] read_skill_resource failed", {
|
|
213
|
+
skill: args.skill,
|
|
214
|
+
path: args.path,
|
|
109
215
|
error: error instanceof Error ? error.message : String(error),
|
|
110
216
|
});
|
|
111
217
|
return {
|
|
112
218
|
success: false,
|
|
113
219
|
error: error instanceof Error ? error.message : String(error),
|
|
114
|
-
data: { skills: [] },
|
|
115
220
|
};
|
|
116
221
|
}
|
|
117
222
|
},
|
|
118
223
|
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Instance skill tools bound to a lazily-resolved manager: list_skills
|
|
228
|
+
* plus the gated mutation tools. Returns Vercel AI SDK tool() objects
|
|
229
|
+
* (description + Zod inputSchema + execute) keyed by tool name.
|
|
230
|
+
*/
|
|
231
|
+
export function createSkillTools(resolveManager, options) {
|
|
232
|
+
const tools = {
|
|
119
233
|
list_skills: tool({
|
|
120
234
|
description: "Returns a lightweight list of all available skills — name, display name, description, and " +
|
|
121
235
|
"tags only. No instructions are returned, keeping context cost minimal. " +
|
|
122
236
|
'Use this only when a user explicitly asks "what skills do you have?" or "what can you help ' +
|
|
123
|
-
'me with?".
|
|
237
|
+
'me with?". To load a skill for a task, use use_skill instead.',
|
|
124
238
|
inputSchema: z.object({
|
|
125
239
|
scopeId: z
|
|
126
240
|
.string()
|
|
@@ -130,7 +244,7 @@ export function createSkillTools(resolveManager, options) {
|
|
|
130
244
|
execute: async (args) => {
|
|
131
245
|
const manager = resolveManager();
|
|
132
246
|
if (!manager) {
|
|
133
|
-
return
|
|
247
|
+
return { ...NOT_CONFIGURED, data: { skills: [] } };
|
|
134
248
|
}
|
|
135
249
|
try {
|
|
136
250
|
const items = await manager.list(args.scopeId);
|
|
@@ -170,10 +284,7 @@ export function createSkillTools(resolveManager, options) {
|
|
|
170
284
|
async function runMutation(resolveManager, action) {
|
|
171
285
|
const manager = resolveManager();
|
|
172
286
|
if (!manager) {
|
|
173
|
-
return
|
|
174
|
-
success: false,
|
|
175
|
-
error: "Skills are not available — the skills store failed to initialize.",
|
|
176
|
-
};
|
|
287
|
+
return NOT_CONFIGURED;
|
|
177
288
|
}
|
|
178
289
|
try {
|
|
179
290
|
const result = await manager.requestMutation(action);
|
|
@@ -285,8 +396,8 @@ function createSkillMutationTools(resolveManager) {
|
|
|
285
396
|
skill_update: tool({
|
|
286
397
|
description: "Propose an update to an EXISTING skill. Only the provided fields change; the version is " +
|
|
287
398
|
"bumped. Depending on host configuration the change is applied immediately or queued for " +
|
|
288
|
-
"human approval. Look the skill up with
|
|
289
|
-
"
|
|
399
|
+
"human approval. Look the skill up with list_skills first to get its exact name. " +
|
|
400
|
+
"Do not paraphrase or expand the user's instructions.",
|
|
290
401
|
inputSchema: z.object({
|
|
291
402
|
skillId: z
|
|
292
403
|
.string()
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* write paths fail closed (errors reject the mutation).
|
|
8
8
|
*/
|
|
9
9
|
import type { SkillDefinition, SkillIndexItem, SkillMutationAction, SkillMutationResult, SkillSearchQuery, SkillsConfig } from "../types/index.js";
|
|
10
|
+
import { SkillSessionTracker } from "./skillSessionTracker.js";
|
|
10
11
|
export declare class SkillsManager {
|
|
11
12
|
private readonly config;
|
|
12
13
|
private readonly store;
|
|
@@ -14,8 +15,14 @@ export declare class SkillsManager {
|
|
|
14
15
|
private cachedIndexAt;
|
|
15
16
|
/** Serializes writes within this process — see requestMutation(). */
|
|
16
17
|
private mutationQueue;
|
|
18
|
+
/** Per-session activation state (pinned skills). */
|
|
19
|
+
readonly sessions: SkillSessionTracker;
|
|
17
20
|
constructor(config: SkillsConfig);
|
|
18
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Cached index read, sorted by name. TTL 0 disables caching. Sorting
|
|
23
|
+
* here (not per render) keeps every downstream listing byte-stable
|
|
24
|
+
* regardless of store enumeration order.
|
|
25
|
+
*/
|
|
19
26
|
getIndex(forceRefresh?: boolean): Promise<SkillIndexItem[]>;
|
|
20
27
|
private invalidateIndex;
|
|
21
28
|
/**
|
|
@@ -36,6 +43,23 @@ export declare class SkillsManager {
|
|
|
36
43
|
scopeId?: string;
|
|
37
44
|
tags?: string[];
|
|
38
45
|
}): Promise<string | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Render the `<available_skills>` block for the use_skill tool
|
|
48
|
+
* description ("tool" discovery mode), or null when nothing is visible.
|
|
49
|
+
* Bounded by listingBudgetChars; entries are never dropped.
|
|
50
|
+
*/
|
|
51
|
+
buildToolListing(options?: {
|
|
52
|
+
scopeId?: string;
|
|
53
|
+
tags?: string[];
|
|
54
|
+
}): Promise<string | null>;
|
|
55
|
+
/** Visible (active, scope- and tag-filtered) index entries for one call. */
|
|
56
|
+
private visibleItems;
|
|
57
|
+
/**
|
|
58
|
+
* Read an auxiliary resource file bundled with a skill. Paths are
|
|
59
|
+
* relative to the skill; traversal segments are rejected. Null when the
|
|
60
|
+
* skill, the resource, or store resource support is absent.
|
|
61
|
+
*/
|
|
62
|
+
getResource(idOrName: string, resourcePath: string): Promise<string | null>;
|
|
39
63
|
/**
|
|
40
64
|
* Whether skill create/update/delete is enabled on this instance. Gates the
|
|
41
65
|
* LLM-facing `skill_*` tools (registration) and the server REST mutation
|