@juspay/neurolink 9.85.0 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +420 -412
  3. package/dist/context/contextCompactor.js +16 -2
  4. package/dist/context/stages/slidingWindowTruncator.js +76 -30
  5. package/dist/core/conversationMemoryManager.js +13 -2
  6. package/dist/core/redisConversationMemoryManager.js +10 -1
  7. package/dist/lib/context/contextCompactor.js +16 -2
  8. package/dist/lib/context/stages/slidingWindowTruncator.js +76 -30
  9. package/dist/lib/core/conversationMemoryManager.js +13 -2
  10. package/dist/lib/core/redisConversationMemoryManager.js +10 -1
  11. package/dist/lib/neurolink.d.ts +31 -6
  12. package/dist/lib/neurolink.js +163 -33
  13. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +39 -1
  14. package/dist/lib/server/routes/claudeProxyRoutes.js +300 -41
  15. package/dist/lib/skills/skillMatcher.d.ts +33 -4
  16. package/dist/lib/skills/skillMatcher.js +81 -6
  17. package/dist/lib/skills/skillSessionTracker.d.ts +52 -0
  18. package/dist/lib/skills/skillSessionTracker.js +150 -0
  19. package/dist/lib/skills/skillStoreRedis.d.ts +8 -0
  20. package/dist/lib/skills/skillStoreRedis.js +18 -2
  21. package/dist/lib/skills/skillStoreS3.d.ts +17 -2
  22. package/dist/lib/skills/skillStoreS3.js +78 -6
  23. package/dist/lib/skills/skillStores.d.ts +16 -2
  24. package/dist/lib/skills/skillStores.js +94 -5
  25. package/dist/lib/skills/skillTools.d.ts +26 -10
  26. package/dist/lib/skills/skillTools.js +190 -79
  27. package/dist/lib/skills/skillsManager.d.ts +25 -1
  28. package/dist/lib/skills/skillsManager.js +46 -4
  29. package/dist/lib/types/config.d.ts +5 -5
  30. package/dist/lib/types/conversation.d.ts +27 -0
  31. package/dist/lib/types/proxy.d.ts +30 -2
  32. package/dist/lib/types/skills.d.ts +145 -14
  33. package/dist/lib/types/skills.js +3 -3
  34. package/dist/lib/utils/conversationMemory.d.ts +1 -1
  35. package/dist/lib/utils/conversationMemory.js +15 -2
  36. package/dist/neurolink.d.ts +31 -6
  37. package/dist/neurolink.js +163 -33
  38. package/dist/server/routes/claudeProxyRoutes.d.ts +39 -1
  39. package/dist/server/routes/claudeProxyRoutes.js +300 -41
  40. package/dist/skills/skillMatcher.d.ts +33 -4
  41. package/dist/skills/skillMatcher.js +81 -6
  42. package/dist/skills/skillSessionTracker.d.ts +52 -0
  43. package/dist/skills/skillSessionTracker.js +149 -0
  44. package/dist/skills/skillStoreRedis.d.ts +8 -0
  45. package/dist/skills/skillStoreRedis.js +18 -2
  46. package/dist/skills/skillStoreS3.d.ts +17 -2
  47. package/dist/skills/skillStoreS3.js +78 -6
  48. package/dist/skills/skillStores.d.ts +16 -2
  49. package/dist/skills/skillStores.js +94 -5
  50. package/dist/skills/skillTools.d.ts +26 -10
  51. package/dist/skills/skillTools.js +190 -79
  52. package/dist/skills/skillsManager.d.ts +25 -1
  53. package/dist/skills/skillsManager.js +46 -4
  54. package/dist/types/config.d.ts +5 -5
  55. package/dist/types/conversation.d.ts +27 -0
  56. package/dist/types/proxy.d.ts +30 -2
  57. package/dist/types/skills.d.ts +145 -14
  58. package/dist/types/skills.js +3 -3
  59. package/dist/utils/conversationMemory.d.ts +1 -1
  60. package/dist/utils/conversationMemory.js +15 -2
  61. 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
- constructor(seed) {
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.skills.set(skill.id, normalizeSkill(skill));
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
- skills.set(parsed.id, parsed);
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, following the createMemoryRetrievalTools /
3
- * createFileTools / createTaskTools factory pattern.
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
- * Create the built-in skill tools bound to a lazily-resolved manager.
16
- * Returns Vercel AI SDK tool() objects (description + Zod inputSchema +
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, following the createMemoryRetrievalTools /
3
- * createFileTools / createTaskTools factory pattern.
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
- /** Trim a hydrated skill to the fields the model needs. */
17
- function toToolSkill(skill) {
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
- * Create the built-in skill tools bound to a lazily-resolved manager.
31
- * Returns Vercel AI SDK tool() objects (description + Zod inputSchema +
32
- * execute) keyed by tool name.
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 createSkillTools(resolveManager, options) {
35
- const notConfigured = {
36
- success: false,
37
- error: "Skills are not available the skills store failed to initialize. " +
38
- "Answer from general knowledge.",
39
- data: { skills: [] },
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
- const tools = {
42
- search_skills: tool({
43
- description: "ALWAYS call this at the start of every user request before answering from general knowledge. " +
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
- query: z
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
- .optional()
67
- .describe("Scope identifier (channel/team/tenant id) to include scoped skills alongside global ones. " +
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
- if (!args.query && !args.tag) {
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: 'At least one of "query" or "tag" must be provided to search_skills.',
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 notConfigured;
179
+ return NOT_CONFIGURED;
81
180
  }
82
181
  try {
83
- const skills = await manager.search(args);
84
- if (skills.length === 0) {
182
+ const skill = await manager.get(args.skill);
183
+ if (!skill || !isSkillVisibleInScope(skill, context.scopeId)) {
85
184
  return {
86
- success: true,
87
- data: {
88
- skills: [],
89
- reason: "no_match",
90
- message: `No skills found${args.query ? ` matching "${args.query}"` : ""}${args.tag ? ` with tag "${args.tag}"` : ""}. Answer from general knowledge.`,
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] search_skills failed", {
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?". Do NOT use this for skill lookup before answering — use search_skills instead.',
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 notConfigured;
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 search_skills or list_skills first to get its id or " +
289
- "exact name. Do not paraphrase or expand the user's instructions.",
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
- /** Cached index read. TTL 0 disables caching. */
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