@juspay/neurolink 9.84.2 → 9.85.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 (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +436 -401
  3. package/dist/cli/factories/commandFactory.d.ts +17 -0
  4. package/dist/cli/factories/commandFactory.js +254 -0
  5. package/dist/cli/parser.js +2 -0
  6. package/dist/cli/utils/skillsFlags.d.ts +10 -0
  7. package/dist/cli/utils/skillsFlags.js +22 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.js +6 -0
  10. package/dist/lib/index.d.ts +5 -0
  11. package/dist/lib/index.js +6 -0
  12. package/dist/lib/neurolink.d.ts +28 -0
  13. package/dist/lib/neurolink.js +117 -0
  14. package/dist/lib/server/routes/agentRoutes.js +156 -1
  15. package/dist/lib/server/utils/validation.d.ts +32 -0
  16. package/dist/lib/server/utils/validation.js +18 -0
  17. package/dist/lib/session/globalSessionState.d.ts +10 -1
  18. package/dist/lib/session/globalSessionState.js +18 -0
  19. package/dist/lib/skills/skillMatcher.d.ts +20 -0
  20. package/dist/lib/skills/skillMatcher.js +80 -0
  21. package/dist/lib/skills/skillStoreRedis.d.ts +22 -0
  22. package/dist/lib/skills/skillStoreRedis.js +98 -0
  23. package/dist/lib/skills/skillStoreS3.d.ts +41 -0
  24. package/dist/lib/skills/skillStoreS3.js +233 -0
  25. package/dist/lib/skills/skillStores.d.ts +44 -0
  26. package/dist/lib/skills/skillStores.js +252 -0
  27. package/dist/lib/skills/skillTools.d.ts +19 -0
  28. package/dist/lib/skills/skillTools.js +340 -0
  29. package/dist/lib/skills/skillsManager.d.ts +54 -0
  30. package/dist/lib/skills/skillsManager.js +220 -0
  31. package/dist/lib/types/config.d.ts +10 -0
  32. package/dist/lib/types/generate.d.ts +8 -0
  33. package/dist/lib/types/index.d.ts +1 -0
  34. package/dist/lib/types/index.js +1 -0
  35. package/dist/lib/types/skills.d.ts +296 -0
  36. package/dist/lib/types/skills.js +17 -0
  37. package/dist/lib/types/stream.d.ts +8 -0
  38. package/dist/neurolink.d.ts +28 -0
  39. package/dist/neurolink.js +117 -0
  40. package/dist/server/routes/agentRoutes.js +156 -1
  41. package/dist/server/utils/validation.d.ts +32 -0
  42. package/dist/server/utils/validation.js +18 -0
  43. package/dist/session/globalSessionState.d.ts +10 -1
  44. package/dist/session/globalSessionState.js +18 -0
  45. package/dist/skills/skillMatcher.d.ts +20 -0
  46. package/dist/skills/skillMatcher.js +79 -0
  47. package/dist/skills/skillStoreRedis.d.ts +22 -0
  48. package/dist/skills/skillStoreRedis.js +97 -0
  49. package/dist/skills/skillStoreS3.d.ts +41 -0
  50. package/dist/skills/skillStoreS3.js +232 -0
  51. package/dist/skills/skillStores.d.ts +44 -0
  52. package/dist/skills/skillStores.js +251 -0
  53. package/dist/skills/skillTools.d.ts +19 -0
  54. package/dist/skills/skillTools.js +339 -0
  55. package/dist/skills/skillsManager.d.ts +54 -0
  56. package/dist/skills/skillsManager.js +219 -0
  57. package/dist/types/config.d.ts +10 -0
  58. package/dist/types/generate.d.ts +8 -0
  59. package/dist/types/index.d.ts +1 -0
  60. package/dist/types/index.js +1 -0
  61. package/dist/types/skills.d.ts +296 -0
  62. package/dist/types/skills.js +16 -0
  63. package/dist/types/stream.d.ts +8 -0
  64. package/package.json +2 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Native skills support — versioned, discoverable instruction packs
3
+ * (SOPs, playbooks, workflows) exposed to the model via progressive
4
+ * disclosure: a cheap name+description index up front, full instructions
5
+ * loaded on demand through built-in skill tools.
6
+ *
7
+ * Architecture mirrors the Hippocampus memory subsystem: a `skills` config
8
+ * on the NeuroLink constructor lazily initializes a SkillsManager, which
9
+ * auto-registers built-in tools (search_skills / list_skills, plus gated
10
+ * mutation tools) and optionally injects a compact skills index into the
11
+ * system prompt of each generate()/stream() call.
12
+ *
13
+ * Naming: every exported type carries a `Skill` prefix to satisfy the
14
+ * `unique-type-names` ESLint rule.
15
+ */
16
+ /** Visibility of a skill: available everywhere, or only in specific scopes. */
17
+ export type SkillScopeKind = "global" | "scoped";
18
+ /** Lifecycle status. Deletes are soft — deprecated skills stay in storage. */
19
+ export type SkillLifecycleStatus = "active" | "deprecated";
20
+ /**
21
+ * A complete skill: index metadata plus the full `instructions` body.
22
+ * `instructions` is the expensive part — it is only hydrated for matched
23
+ * skills, never included in index listings or the prompt index.
24
+ */
25
+ export type SkillDefinition = {
26
+ /** Stable unique identifier (UUID for created skills, or derived from filename). */
27
+ id: string;
28
+ /** Machine-friendly unique name (snake_case recommended), used for matching. */
29
+ name: string;
30
+ /** Human-readable display name shown in listings. */
31
+ displayName?: string;
32
+ /** One or two sentences describing when the skill applies — the matching signal. */
33
+ description: string;
34
+ /** Full step-by-step instructions the model follows when the skill matches. */
35
+ instructions: string;
36
+ /** Domain tags for filtering (e.g. ["payments", "escalation"]). */
37
+ tags?: string[];
38
+ /** Visibility. Default: "global". */
39
+ scope?: SkillScopeKind;
40
+ /** Scope identifiers this skill is limited to when scope === "scoped" (e.g. channel/team/tenant ids). */
41
+ scopeIds?: string[];
42
+ /** Monotonic version, bumped on every approved update. Default: 1. */
43
+ version?: number;
44
+ /** Lifecycle status. Default: "active". */
45
+ status?: SkillLifecycleStatus;
46
+ /** ISO timestamp of creation. */
47
+ createdAt?: string;
48
+ /** ISO timestamp of last update. */
49
+ updatedAt?: string;
50
+ /** Free-form host metadata (audit fields, approval references, …). */
51
+ metadata?: Record<string, unknown>;
52
+ };
53
+ /** Lightweight index entry — everything except the instructions body. */
54
+ export type SkillIndexItem = Omit<SkillDefinition, "instructions">;
55
+ /**
56
+ * Pluggable persistence backend. NeuroLink ships memory and filesystem
57
+ * stores; hosts plug their own (S3, database, …) via the "custom" storage
58
+ * type. `index()` must be cheap relative to `get()` — it backs every
59
+ * search and prompt-index build.
60
+ */
61
+ export type SkillStore = {
62
+ /** Fetch one skill (with instructions) by id. Null when absent. */
63
+ get(id: string): Promise<SkillDefinition | null>;
64
+ /** Create or replace a skill. */
65
+ put(skill: SkillDefinition): Promise<void>;
66
+ /** Hard-remove a skill from storage. (Soft deletes go through put().) */
67
+ delete(id: string): Promise<void>;
68
+ /** List index entries (no instructions) for all stored skills. */
69
+ index(): Promise<SkillIndexItem[]>;
70
+ /** Optional: drop any internal caches (called after mutations). */
71
+ invalidate?(): void;
72
+ };
73
+ /** In-process store, optionally seeded. Good for tests and embedded use. */
74
+ export type SkillMemoryStorageConfig = {
75
+ type: "memory";
76
+ /** Initial skills to seed the store with. */
77
+ skills?: SkillDefinition[];
78
+ };
79
+ /**
80
+ * Directory-backed store. Reads three layouts:
81
+ * - `<dir>/<id>.json` — one JSON-serialized SkillDefinition per file
82
+ * - `<dir>/<name>.md` — markdown with YAML frontmatter; body = instructions
83
+ * - `<dir>/<name>/SKILL.md` — Claude-skills-style directory layout
84
+ * Mutations always write `<id>.json`; markdown sources are read-only.
85
+ */
86
+ export type SkillFilesystemStorageConfig = {
87
+ type: "filesystem";
88
+ /** Directory containing skill files. Created on first write if absent. */
89
+ path: string;
90
+ };
91
+ /** Host-provided store implementation (e.g. curator's S3 repository). */
92
+ export type SkillCustomStorageConfig = {
93
+ type: "custom";
94
+ store: SkillStore;
95
+ };
96
+ /**
97
+ * S3-backed store. Layout: `<prefix>skills/<id>.json` per skill plus a
98
+ * `<prefix>index.json` document that is upserted on writes and rebuilt
99
+ * from a bucket listing when missing or corrupt (self-healing).
100
+ *
101
+ * Requires the optional peer `@aws-sdk/client-s3` to be installed in the
102
+ * host application (same pattern as memory's optional @juspay/hippocampus).
103
+ * Credentials default to the standard AWS provider chain when omitted.
104
+ */
105
+ export type SkillS3StorageConfig = {
106
+ type: "s3";
107
+ bucket: string;
108
+ /** Key prefix inside the bucket. Default: "neurolink-skills/". */
109
+ prefix?: string;
110
+ region?: string;
111
+ /** Custom endpoint (MinIO, LocalStack, …). */
112
+ endpoint?: string;
113
+ /** Use path-style addressing (required by most S3-compatible stores). */
114
+ forcePathStyle?: boolean;
115
+ credentials?: {
116
+ accessKeyId: string;
117
+ secretAccessKey: string;
118
+ sessionToken?: string;
119
+ };
120
+ };
121
+ /**
122
+ * Redis-backed store using NeuroLink's pooled Redis client (`redis` v5,
123
+ * already a core dependency). One JSON value per skill under
124
+ * `<keyPrefix><id>`; the index is derived via SCAN + MGET. Skills are
125
+ * persistent — no TTL is applied.
126
+ */
127
+ export type SkillRedisStorageConfig = {
128
+ type: "redis";
129
+ url?: string;
130
+ host?: string;
131
+ port?: number;
132
+ username?: string;
133
+ password?: string;
134
+ db?: number;
135
+ /** Key prefix. Default: "neurolink:skills:". */
136
+ keyPrefix?: string;
137
+ };
138
+ /**
139
+ * Structural surface of the lazily-required @aws-sdk/client-s3 module —
140
+ * only the pieces the S3 skill store touches (same pattern as
141
+ * HippocampusModule for the optional memory peer).
142
+ */
143
+ export type SkillS3ModuleSurface = {
144
+ S3Client: new (config: Record<string, unknown>) => {
145
+ send: (command: unknown) => Promise<unknown>;
146
+ };
147
+ GetObjectCommand: new (input: Record<string, unknown>) => unknown;
148
+ PutObjectCommand: new (input: Record<string, unknown>) => unknown;
149
+ DeleteObjectCommand: new (input: Record<string, unknown>) => unknown;
150
+ ListObjectsV2Command: new (input: Record<string, unknown>) => unknown;
151
+ };
152
+ /** Shape of the `<prefix>index.json` document maintained by the S3 store. */
153
+ export type SkillS3IndexDocument = {
154
+ lastUpdated: string;
155
+ skills: SkillIndexItem[];
156
+ };
157
+ /**
158
+ * Minimal object-storage operations the S3 skill store runs on. The
159
+ * default implementation is created lazily from @aws-sdk/client-s3;
160
+ * tests and hosts with pre-built clients can inject their own.
161
+ */
162
+ export type SkillS3ObjectOps = {
163
+ /** Fetch an object's body as a UTF-8 string. Null when the key is absent. */
164
+ getObject(key: string): Promise<string | null>;
165
+ putObject(key: string, body: string): Promise<void>;
166
+ deleteObject(key: string): Promise<void>;
167
+ /** List all object keys under a prefix (paginated internally). */
168
+ listKeys(prefix: string): Promise<string[]>;
169
+ };
170
+ export type SkillsStorageConfig = SkillMemoryStorageConfig | SkillFilesystemStorageConfig | SkillS3StorageConfig | SkillRedisStorageConfig | SkillCustomStorageConfig;
171
+ /** Query accepted by SkillsManager.search() and the search_skills tool. */
172
+ export type SkillSearchQuery = {
173
+ /** Keyword matched (case-insensitive substring) against name, displayName, and description. */
174
+ query?: string;
175
+ /** Tag filter, applied on top of the keyword match. */
176
+ tag?: string;
177
+ /** Scope filter: include global skills plus skills scoped to this id. */
178
+ scopeId?: string;
179
+ /** Maximum matches to hydrate. Defaults to SkillsConfig.maxMatches. */
180
+ limit?: number;
181
+ };
182
+ /** Input for creating a skill (id/version/status/timestamps are assigned by the manager). */
183
+ export type SkillCreateInput = {
184
+ name: string;
185
+ displayName?: string;
186
+ description: string;
187
+ instructions: string;
188
+ tags?: string[];
189
+ scope?: SkillScopeKind;
190
+ scopeIds?: string[];
191
+ metadata?: Record<string, unknown>;
192
+ };
193
+ /** Patch for updating a skill. Only provided fields change; version is bumped. */
194
+ export type SkillUpdateInput = Partial<SkillCreateInput>;
195
+ /** A proposed mutation, passed to the host's onMutationRequest gate. */
196
+ export type SkillMutationAction = {
197
+ type: "create";
198
+ skill: SkillCreateInput;
199
+ requestedBy?: string;
200
+ } | {
201
+ type: "update";
202
+ skillId: string;
203
+ patch: SkillUpdateInput;
204
+ requestedBy?: string;
205
+ } | {
206
+ type: "delete";
207
+ skillId: string;
208
+ requestedBy?: string;
209
+ };
210
+ /**
211
+ * Host decision for a proposed mutation.
212
+ * - "approved": NeuroLink applies the mutation immediately.
213
+ * - "rejected": nothing is written; `reason` is surfaced to the model.
214
+ * - "pending": the host queued the action for out-of-band approval
215
+ * (e.g. a Slack maker-checker flow) and will apply it itself later;
216
+ * `reference` is surfaced to the model (e.g. an approval ticket id).
217
+ */
218
+ export type SkillMutationDecision = {
219
+ outcome: "approved";
220
+ } | {
221
+ outcome: "rejected";
222
+ reason?: string;
223
+ } | {
224
+ outcome: "pending";
225
+ reference?: string;
226
+ };
227
+ /** Result envelope returned by SkillsManager.requestMutation(). */
228
+ export type SkillMutationResult = {
229
+ decision: SkillMutationDecision;
230
+ /** The resulting skill after an applied create/update (absent for delete/pending/rejected). */
231
+ skill?: SkillDefinition;
232
+ };
233
+ /**
234
+ * Instance-level skills configuration (NeuroLink constructor `skills` option).
235
+ * Opt-in: nothing is registered or injected unless `enabled: true`.
236
+ */
237
+ export type SkillsConfig = {
238
+ enabled: boolean;
239
+ /** Persistence backend. Default: `{ type: "memory" }`. */
240
+ storage?: SkillsStorageConfig;
241
+ /**
242
+ * Inject a compact skills index (names + descriptions, never instructions)
243
+ * into the system prompt of each generate()/stream() call. Default: true.
244
+ * Set false for curator-style pure tool-driven disclosure.
245
+ */
246
+ promptIndex?: boolean;
247
+ /** Maximum skills hydrated (with instructions) per search. Default: 5. */
248
+ maxMatches?: number;
249
+ /** Maximum entries rendered in the prompt index before truncation. Default: 50. */
250
+ promptIndexMaxItems?: number;
251
+ /** Index cache TTL in milliseconds. Default: 30000. 0 disables caching. */
252
+ indexCacheTtlMs?: number;
253
+ /** Default scope filter applied when a call/tool provides none. */
254
+ defaultScopeId?: string;
255
+ /**
256
+ * Register skill_create / skill_update / skill_delete tools so the model
257
+ * can propose skill changes. Default: false. Combine with
258
+ * `onMutationRequest` to gate writes behind host approval.
259
+ */
260
+ allowMutations?: boolean;
261
+ /**
262
+ * Host approval gate invoked before any mutation is applied. When absent
263
+ * and allowMutations is true, mutations apply directly. Errors thrown
264
+ * here reject the mutation (fail closed for writes).
265
+ */
266
+ onMutationRequest?: (action: SkillMutationAction) => Promise<SkillMutationDecision>;
267
+ };
268
+ /**
269
+ * Structural view of SkillsManager consumed by the skill tools factory —
270
+ * keeps skillTools.ts decoupled from the concrete manager class.
271
+ */
272
+ export type SkillsManagerLike = {
273
+ search: (query: SkillSearchQuery) => Promise<SkillDefinition[]>;
274
+ list: (scopeId?: string) => Promise<SkillIndexItem[]>;
275
+ requestMutation: (action: SkillMutationAction) => Promise<SkillMutationResult>;
276
+ };
277
+ /** Options for the createSkillTools factory. */
278
+ export type SkillToolsOptions = {
279
+ /** Include skill_create / skill_update / skill_delete. Default: false. */
280
+ allowMutations?: boolean;
281
+ };
282
+ /**
283
+ * Per-call skills control on generate()/stream(). Only effective when the
284
+ * instance was constructed with skills enabled; per-call wins over instance
285
+ * config (same precedence convention as per-call credentials).
286
+ */
287
+ export type SkillsCallOptions = {
288
+ /** Master toggle for this call (prompt index only — tools stay registered). Default: true. */
289
+ enabled?: boolean;
290
+ /** Per-call override of SkillsConfig.promptIndex. */
291
+ promptIndex?: boolean;
292
+ /** Scope filter for the prompt index on this call. Overrides defaultScopeId. */
293
+ scopeId?: string;
294
+ /** Restrict the prompt index to skills carrying at least one of these tags. */
295
+ tags?: string[];
296
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Native skills support — versioned, discoverable instruction packs
3
+ * (SOPs, playbooks, workflows) exposed to the model via progressive
4
+ * disclosure: a cheap name+description index up front, full instructions
5
+ * loaded on demand through built-in skill tools.
6
+ *
7
+ * Architecture mirrors the Hippocampus memory subsystem: a `skills` config
8
+ * on the NeuroLink constructor lazily initializes a SkillsManager, which
9
+ * auto-registers built-in tools (search_skills / list_skills, plus gated
10
+ * mutation tools) and optionally injects a compact skills index into the
11
+ * system prompt of each generate()/stream() call.
12
+ *
13
+ * Naming: every exported type carries a `Skill` prefix to satisfy the
14
+ * `unique-type-names` ESLint rule.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=skills.js.map
@@ -1,6 +1,7 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
2
  import type { EvaluationData } from "./evaluation.js";
3
3
  import type { RAGConfig } from "./rag.js";
4
+ import type { SkillsCallOptions } from "./skills.js";
4
5
  import type { AnalyticsData, ToolExecutionEvent, ToolExecutionSummary } from "../types/index.js";
5
6
  import type { MiddlewareFactoryOptions, OnChunkCallback, OnErrorCallback, OnFinishCallback } from "../types/middleware.js";
6
7
  import type { TokenUsage } from "./analytics.js";
@@ -534,6 +535,13 @@ export type StreamOptions = {
534
535
  };
535
536
  /** @deprecated Use `piiDetection`, `responseValidation`, and `inputValidation` instead. */
536
537
  processors?: ProcessorPipelineConfig;
538
+ /**
539
+ * Per-call skills control. Only effective when the instance was
540
+ * constructed with `skills.enabled: true`. Lets a call disable the
541
+ * prompt index, or narrow it by scope/tags. Per-call wins over
542
+ * instance config.
543
+ */
544
+ skills?: SkillsCallOptions;
537
545
  };
538
546
  /**
539
547
  * Stream function result type - Primary output format for streaming
@@ -12,6 +12,7 @@ import type { RedisConversationMemoryManager } from "./core/redisConversationMem
12
12
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
13
13
  import { MCPToolRegistry } from "./mcp/toolRegistry.js";
14
14
  import type { DynamicOptions } from "./types/index.js";
15
+ import { SkillsManager } from "./skills/skillsManager.js";
15
16
  import { TaskManager } from "./tasks/taskManager.js";
16
17
  /**
17
18
  * Curator P2-4 dedup (concurrency-safe): native providers emit
@@ -140,6 +141,8 @@ export declare class NeuroLink {
140
141
  private cachedFileTools;
141
142
  private memoryInstance?;
142
143
  private memorySDKConfig?;
144
+ private skillsManagerInstance?;
145
+ private skillsConfig?;
143
146
  /**
144
147
  * Extract and set Langfuse context from options with proper async scoping
145
148
  */
@@ -264,6 +267,31 @@ export declare class NeuroLink {
264
267
  * Only registered when Redis conversation memory is active.
265
268
  */
266
269
  private registerMemoryRetrievalTools;
270
+ /**
271
+ * Lazy initialization for the skills subsystem — mirrors ensureMemoryReady().
272
+ * Returns null (and stays null) when skills are not configured or the
273
+ * store failed to initialize; read paths fail open on that null.
274
+ */
275
+ private ensureSkillsReady;
276
+ /**
277
+ * Register the built-in skill tools (search_skills / list_skills, plus
278
+ * mutation tools when allowMutations is set). Follows the
279
+ * registerMemoryRetrievalTools() pattern: registered via registerTool()
280
+ * so they land in the "user-defined" category that reaches the LLM tool
281
+ * schema, with the manager resolved lazily at execution time.
282
+ */
283
+ private registerSkillTools;
284
+ /**
285
+ * Append the compact skills index (names + descriptions, never
286
+ * instructions) to the system prompt for one generate()/stream() call.
287
+ * Fails open: any error leaves the prompt untouched.
288
+ */
289
+ private applySkillsPromptIndex;
290
+ /**
291
+ * Programmatic access to the skills subsystem (search/list/get/mutations).
292
+ * Returns null when skills are not configured or failed to initialize.
293
+ */
294
+ getSkillsManager(): SkillsManager | null;
267
295
  /** Format memory context for prompt inclusion */
268
296
  private formatMemoryContext;
269
297
  /**
package/dist/neurolink.js CHANGED
@@ -54,6 +54,8 @@ import { MCPToolRegistry } from "./mcp/toolRegistry.js";
54
54
  import { resolveDynamicArgument } from "./dynamic/dynamicResolver.js";
55
55
  import { initializeHippocampus } from "./memory/hippocampusInitializer.js";
56
56
  import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
57
+ import { SkillsManager } from "./skills/skillsManager.js";
58
+ import { createSkillTools } from "./skills/skillTools.js";
57
59
  import { getMetricsAggregator, MetricsAggregator, } from "./observability/metricsAggregator.js";
58
60
  import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, ModelAccessDeniedError, } from "./types/index.js";
59
61
  import { SpanSerializer } from "./observability/utils/spanSerializer.js";
@@ -458,6 +460,10 @@ export class NeuroLink {
458
460
  // Memory instance and config
459
461
  memoryInstance;
460
462
  memorySDKConfig;
463
+ // Skills subsystem — lazily initialized manager + instance config.
464
+ // `undefined` = not yet attempted, `null` = init failed (stay disabled).
465
+ skillsManagerInstance;
466
+ skillsConfig;
461
467
  /**
462
468
  * Extract and set Langfuse context from options with proper async scoping
463
469
  */
@@ -830,6 +836,10 @@ export class NeuroLink {
830
836
  this.initializeMCPEnhancements(config);
831
837
  this.registerFileTools();
832
838
  this.registerMemoryRetrievalTools();
839
+ if (config?.skills?.enabled) {
840
+ this.skillsConfig = config.skills;
841
+ this.registerSkillTools();
842
+ }
833
843
  this.initializeLangfuse(constructorId, constructorStartTime, constructorHrTimeStart);
834
844
  this.initializeMetricsListeners();
835
845
  this.logConstructorComplete(constructorId, constructorStartTime, constructorHrTimeStart);
@@ -1243,6 +1253,107 @@ export class NeuroLink {
1243
1253
  });
1244
1254
  logger.info("[NeuroLink] Memory retrieval tools registered");
1245
1255
  }
1256
+ /**
1257
+ * Lazy initialization for the skills subsystem — mirrors ensureMemoryReady().
1258
+ * Returns null (and stays null) when skills are not configured or the
1259
+ * store failed to initialize; read paths fail open on that null.
1260
+ */
1261
+ ensureSkillsReady() {
1262
+ if (this.skillsManagerInstance !== undefined) {
1263
+ return this.skillsManagerInstance;
1264
+ }
1265
+ if (!this.skillsConfig?.enabled) {
1266
+ this.skillsManagerInstance = null;
1267
+ return null;
1268
+ }
1269
+ try {
1270
+ this.skillsManagerInstance = new SkillsManager(this.skillsConfig);
1271
+ }
1272
+ catch (error) {
1273
+ logger.warn("[NeuroLink] Skills initialization failed — skills disabled for this instance", { error: error instanceof Error ? error.message : String(error) });
1274
+ this.skillsManagerInstance = null;
1275
+ }
1276
+ return this.skillsManagerInstance;
1277
+ }
1278
+ /**
1279
+ * Register the built-in skill tools (search_skills / list_skills, plus
1280
+ * mutation tools when allowMutations is set). Follows the
1281
+ * registerMemoryRetrievalTools() pattern: registered via registerTool()
1282
+ * so they land in the "user-defined" category that reaches the LLM tool
1283
+ * schema, with the manager resolved lazily at execution time.
1284
+ */
1285
+ registerSkillTools() {
1286
+ const canonicalTools = createSkillTools(() => this.ensureSkillsReady(), {
1287
+ allowMutations: this.skillsConfig?.allowMutations === true,
1288
+ });
1289
+ for (const [toolName, toolDef] of Object.entries(canonicalTools)) {
1290
+ this.registerTool(toolName, {
1291
+ name: toolName,
1292
+ description: toolDef.description ?? toolName,
1293
+ // Zod schema — registerTool() detects isZodSchema and preserves it
1294
+ // so ToolsManager gives the LLM full parameter types.
1295
+ inputSchema: toolDef
1296
+ .inputSchema,
1297
+ execute: async (params) => withTimeout(toolDef.execute(params, { toolCallId: "skill-tool", messages: [] }), TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS, ErrorFactory.toolTimeout(toolName, TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS)),
1298
+ });
1299
+ }
1300
+ logger.info(`[NeuroLink] Registered ${Object.keys(canonicalTools).length} skill tools`, { allowMutations: this.skillsConfig?.allowMutations === true });
1301
+ }
1302
+ /**
1303
+ * Append the compact skills index (names + descriptions, never
1304
+ * instructions) to the system prompt for one generate()/stream() call.
1305
+ * Fails open: any error leaves the prompt untouched.
1306
+ */
1307
+ async applySkillsPromptIndex(options) {
1308
+ if (!this.skillsConfig?.enabled || options.skills?.enabled === false) {
1309
+ return;
1310
+ }
1311
+ // Per-call promptIndex wins over instance config; default is on.
1312
+ const promptIndexEnabled = options.skills?.promptIndex ?? this.skillsConfig.promptIndex ?? true;
1313
+ if (!promptIndexEnabled) {
1314
+ return;
1315
+ }
1316
+ // Media-only modes have no meaningful text prompt to augment.
1317
+ const mode = options.output?.mode;
1318
+ if (mode === "avatar" ||
1319
+ mode === "music" ||
1320
+ mode === "video" ||
1321
+ mode === "ppt") {
1322
+ return;
1323
+ }
1324
+ try {
1325
+ const manager = this.ensureSkillsReady();
1326
+ if (!manager) {
1327
+ return;
1328
+ }
1329
+ const block = await manager.buildPromptIndex({
1330
+ ...(options.skills?.scopeId !== undefined
1331
+ ? { scopeId: options.skills.scopeId }
1332
+ : {}),
1333
+ ...(options.skills?.tags !== undefined
1334
+ ? { tags: options.skills.tags }
1335
+ : {}),
1336
+ });
1337
+ if (block) {
1338
+ options.systemPrompt = options.systemPrompt
1339
+ ? `${options.systemPrompt}\n\n${block}`
1340
+ : block;
1341
+ logger.debug("[NeuroLink] Skills prompt index injected", {
1342
+ blockLength: block.length,
1343
+ });
1344
+ }
1345
+ }
1346
+ catch (error) {
1347
+ logger.warn("[NeuroLink] Skills prompt index injection failed — continuing without it", { error: error instanceof Error ? error.message : String(error) });
1348
+ }
1349
+ }
1350
+ /**
1351
+ * Programmatic access to the skills subsystem (search/list/get/mutations).
1352
+ * Returns null when skills are not configured or failed to initialize.
1353
+ */
1354
+ getSkillsManager() {
1355
+ return this.ensureSkillsReady();
1356
+ }
1246
1357
  /** Format memory context for prompt inclusion */
1247
1358
  formatMemoryContext(memoryContext, currentInput) {
1248
1359
  return `Context from previous conversations:
@@ -3499,6 +3610,9 @@ Current user's request: ${currentInput}`;
3499
3610
  });
3500
3611
  }
3501
3612
  }
3613
+ // Skills: append the compact skills index to the system prompt so the
3614
+ // model knows which skills exist (bodies load via search_skills).
3615
+ await this.applySkillsPromptIndex(options);
3502
3616
  // Media-only modes (avatar, music, video, ppt) do not have a meaningful
3503
3617
  // text prompt to augment with memory — skip injection to avoid corrupting
3504
3618
  // the empty/synthesized input.text that was set for these modes.
@@ -7037,6 +7151,9 @@ Current user's request: ${currentInput}`;
7037
7151
  logger.warn("Memory retrieval failed:", error);
7038
7152
  }
7039
7153
  }
7154
+ // Skills: append the compact skills index to the system prompt so the
7155
+ // model knows which skills exist (bodies load via search_skills).
7156
+ await this.applySkillsPromptIndex(options);
7040
7157
  // Apply orchestration if enabled and no specific provider/model requested
7041
7158
  if (this.enableOrchestration && !options.provider && !options.model) {
7042
7159
  try {