@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
|
@@ -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
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
|
-
import { filterSkillIndex, formatSkillsPromptIndex } from "./skillMatcher.js";
|
|
11
|
+
import { filterSkillIndex, formatSkillsPromptIndex, renderSkillListing, sortSkillIndex, } from "./skillMatcher.js";
|
|
12
|
+
import { SkillSessionTracker } from "./skillSessionTracker.js";
|
|
12
13
|
import { createSkillStore } from "./skillStores.js";
|
|
13
14
|
const DEFAULT_MAX_MATCHES = 5;
|
|
14
15
|
const DEFAULT_PROMPT_INDEX_MAX_ITEMS = 50;
|
|
15
16
|
const DEFAULT_INDEX_CACHE_TTL_MS = 30_000;
|
|
17
|
+
const DEFAULT_LISTING_BUDGET_CHARS = 15_000;
|
|
16
18
|
export class SkillsManager {
|
|
17
19
|
config;
|
|
18
20
|
store;
|
|
@@ -20,11 +22,17 @@ export class SkillsManager {
|
|
|
20
22
|
cachedIndexAt = 0;
|
|
21
23
|
/** Serializes writes within this process — see requestMutation(). */
|
|
22
24
|
mutationQueue = Promise.resolve();
|
|
25
|
+
/** Per-session activation state (pinned skills). */
|
|
26
|
+
sessions = new SkillSessionTracker();
|
|
23
27
|
constructor(config) {
|
|
24
28
|
this.config = config;
|
|
25
29
|
this.store = createSkillStore(config.storage);
|
|
26
30
|
}
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Cached index read, sorted by name. TTL 0 disables caching. Sorting
|
|
33
|
+
* here (not per render) keeps every downstream listing byte-stable
|
|
34
|
+
* regardless of store enumeration order.
|
|
35
|
+
*/
|
|
28
36
|
async getIndex(forceRefresh = false) {
|
|
29
37
|
const ttl = this.config.indexCacheTtlMs ?? DEFAULT_INDEX_CACHE_TTL_MS;
|
|
30
38
|
const fresh = this.cachedIndex !== null &&
|
|
@@ -33,7 +41,7 @@ export class SkillsManager {
|
|
|
33
41
|
if (fresh && !forceRefresh && this.cachedIndex) {
|
|
34
42
|
return this.cachedIndex;
|
|
35
43
|
}
|
|
36
|
-
const index = await this.store.index();
|
|
44
|
+
const index = sortSkillIndex(await this.store.index());
|
|
37
45
|
this.cachedIndex = index;
|
|
38
46
|
this.cachedIndexAt = Date.now();
|
|
39
47
|
return index;
|
|
@@ -87,12 +95,46 @@ export class SkillsManager {
|
|
|
87
95
|
* nothing is visible. Never includes instructions.
|
|
88
96
|
*/
|
|
89
97
|
async buildPromptIndex(options) {
|
|
98
|
+
const items = await this.visibleItems(options);
|
|
99
|
+
return formatSkillsPromptIndex(items, this.config.promptIndexMaxItems ?? DEFAULT_PROMPT_INDEX_MAX_ITEMS);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Render the `<available_skills>` block for the use_skill tool
|
|
103
|
+
* description ("tool" discovery mode), or null when nothing is visible.
|
|
104
|
+
* Bounded by listingBudgetChars; entries are never dropped.
|
|
105
|
+
*/
|
|
106
|
+
async buildToolListing(options) {
|
|
107
|
+
const items = await this.visibleItems(options);
|
|
108
|
+
return renderSkillListing(items, this.config.listingBudgetChars ?? DEFAULT_LISTING_BUDGET_CHARS);
|
|
109
|
+
}
|
|
110
|
+
/** Visible (active, scope- and tag-filtered) index entries for one call. */
|
|
111
|
+
async visibleItems(options) {
|
|
90
112
|
let items = await this.list(options?.scopeId);
|
|
91
113
|
if (options?.tags && options.tags.length > 0) {
|
|
92
114
|
const wanted = options.tags.map((t) => t.toLowerCase());
|
|
93
115
|
items = items.filter((item) => (item.tags ?? []).some((t) => wanted.includes(t.toLowerCase())));
|
|
94
116
|
}
|
|
95
|
-
return
|
|
117
|
+
return items;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read an auxiliary resource file bundled with a skill. Paths are
|
|
121
|
+
* relative to the skill; traversal segments are rejected. Null when the
|
|
122
|
+
* skill, the resource, or store resource support is absent.
|
|
123
|
+
*/
|
|
124
|
+
async getResource(idOrName, resourcePath) {
|
|
125
|
+
const normalized = resourcePath.replace(/\\/g, "/");
|
|
126
|
+
if (normalized.startsWith("/") ||
|
|
127
|
+
normalized.split("/").some((segment) => segment === "..")) {
|
|
128
|
+
throw new Error(`Invalid resource path "${resourcePath}" — must be relative to the skill directory`);
|
|
129
|
+
}
|
|
130
|
+
if (!this.store.getResource) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const skill = await this.get(idOrName);
|
|
134
|
+
if (!skill) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return this.store.getResource(skill.id, normalized);
|
|
96
138
|
}
|
|
97
139
|
/**
|
|
98
140
|
* Whether skill create/update/delete is enabled on this instance. Gates the
|
|
@@ -117,11 +117,11 @@ export type NeurolinkConstructorConfig = {
|
|
|
117
117
|
classifierRouter?: ClassifierRouterConfig;
|
|
118
118
|
/**
|
|
119
119
|
* Native skills: versioned, discoverable instruction packs (SOPs,
|
|
120
|
-
* playbooks) with progressive disclosure. When enabled,
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* See {@link SkillsConfig}.
|
|
120
|
+
* playbooks) with progressive disclosure. When enabled, each
|
|
121
|
+
* generate()/stream() call gets a skills discovery listing plus
|
|
122
|
+
* use_skill / read_skill_resource tools; activated skill instructions
|
|
123
|
+
* pin to the session so they are loaded once and replayed from history.
|
|
124
|
+
* Opt-in and fails open on read paths. See {@link SkillsConfig}.
|
|
125
125
|
*/
|
|
126
126
|
skills?: SkillsConfig;
|
|
127
127
|
};
|
|
@@ -250,6 +250,20 @@ export type ChatMessageMetadata = {
|
|
|
250
250
|
* payload from the local artifact store.
|
|
251
251
|
*/
|
|
252
252
|
artifactId?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Marks a pinned skill-activation message: the full instructions of a
|
|
255
|
+
* skill loaded via use_skill, persisted into session history so later
|
|
256
|
+
* turns replay it verbatim instead of re-fetching the skill. Pinned
|
|
257
|
+
* skill messages are protected from sliding-window truncation and are
|
|
258
|
+
* re-included after memory summarization.
|
|
259
|
+
*/
|
|
260
|
+
isSkill?: boolean;
|
|
261
|
+
/** Skill id of a pinned skill-activation message. */
|
|
262
|
+
skillId?: string;
|
|
263
|
+
/** Skill name of a pinned skill-activation message. */
|
|
264
|
+
skillName?: string;
|
|
265
|
+
/** Skill version captured at activation (sessions pin the activated version). */
|
|
266
|
+
skillVersion?: number;
|
|
253
267
|
};
|
|
254
268
|
/**
|
|
255
269
|
* Chat message format for conversation history
|
|
@@ -383,6 +397,19 @@ export type StoreConversationTurnOptions = {
|
|
|
383
397
|
};
|
|
384
398
|
/** Gemini 3 thought signature for reasoning continuity across turns */
|
|
385
399
|
thoughtSignature?: string;
|
|
400
|
+
/**
|
|
401
|
+
* Pinned skill-activation messages (skills v2) recorded during this turn.
|
|
402
|
+
* Inserted between the user and assistant messages so replayed history
|
|
403
|
+
* mirrors the actual order: ask → skill loaded → answer. Stored verbatim —
|
|
404
|
+
* skill instructions are never truncated.
|
|
405
|
+
*
|
|
406
|
+
* Invariant for history consumers: a skill-bearing turn is a
|
|
407
|
+
* user → skill(user-role, metadata.isSkill) → assistant triplet, so
|
|
408
|
+
* stored history is NOT strictly pair-wise alternating. Pair-based
|
|
409
|
+
* logic must filter `metadata.isSkill` first (see slidingWindowTruncator
|
|
410
|
+
* for the canonical partition-and-reanchor pattern).
|
|
411
|
+
*/
|
|
412
|
+
skillMessages?: ChatMessage[];
|
|
386
413
|
};
|
|
387
414
|
/**
|
|
388
415
|
* Lightweight session metadata for efficient session listing
|