@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
|
@@ -6,15 +6,28 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Architecture mirrors the Hippocampus memory subsystem: a `skills` config
|
|
8
8
|
* on the NeuroLink constructor lazily initializes a SkillsManager, which
|
|
9
|
-
* auto-registers built-in tools (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* auto-registers built-in tools (list_skills, plus gated mutation tools)
|
|
10
|
+
* and augments each generate()/stream() call with the discovery listing
|
|
11
|
+
* plus per-call use_skill / read_skill_resource tools.
|
|
12
12
|
*
|
|
13
13
|
* Naming: every exported type carries a `Skill` prefix to satisfy the
|
|
14
14
|
* `unique-type-names` ESLint rule.
|
|
15
15
|
*/
|
|
16
|
+
import type { ChatMessage } from "./conversation.js";
|
|
16
17
|
/** Visibility of a skill: available everywhere, or only in specific scopes. */
|
|
17
18
|
export type SkillScopeKind = "global" | "scoped";
|
|
19
|
+
/**
|
|
20
|
+
* Reference to an auxiliary file bundled with a skill (progressive
|
|
21
|
+
* disclosure level 3). Resources are read into context on demand via the
|
|
22
|
+
* read_skill_resource tool — a skill's SKILL.md should stay lean and point
|
|
23
|
+
* to resources for rarely-needed detail.
|
|
24
|
+
*/
|
|
25
|
+
export type SkillResourceRef = {
|
|
26
|
+
/** Path relative to the skill's directory, e.g. "references/edge-cases.md". */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Size in bytes when known (listing hint only). */
|
|
29
|
+
size?: number;
|
|
30
|
+
};
|
|
18
31
|
/** Lifecycle status. Deletes are soft — deprecated skills stay in storage. */
|
|
19
32
|
export type SkillLifecycleStatus = "active" | "deprecated";
|
|
20
33
|
/**
|
|
@@ -47,6 +60,12 @@ export type SkillDefinition = {
|
|
|
47
60
|
createdAt?: string;
|
|
48
61
|
/** ISO timestamp of last update. */
|
|
49
62
|
updatedAt?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Auxiliary files bundled with the skill, readable on demand through
|
|
65
|
+
* read_skill_resource. Populated by stores that support resources
|
|
66
|
+
* (directory-layout filesystem skills, S3, Redis).
|
|
67
|
+
*/
|
|
68
|
+
resources?: SkillResourceRef[];
|
|
50
69
|
/** Free-form host metadata (audit fields, approval references, …). */
|
|
51
70
|
metadata?: Record<string, unknown>;
|
|
52
71
|
};
|
|
@@ -69,12 +88,24 @@ export type SkillStore = {
|
|
|
69
88
|
index(): Promise<SkillIndexItem[]>;
|
|
70
89
|
/** Optional: drop any internal caches (called after mutations). */
|
|
71
90
|
invalidate?(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Optional: fetch an auxiliary resource file bundled with a skill.
|
|
93
|
+
* `resourcePath` is relative to the skill (e.g. "references/forms.md").
|
|
94
|
+
* Null when the skill or resource is absent. Stores without resource
|
|
95
|
+
* support simply omit this method.
|
|
96
|
+
*/
|
|
97
|
+
getResource?(id: string, resourcePath: string): Promise<string | null>;
|
|
72
98
|
};
|
|
73
99
|
/** In-process store, optionally seeded. Good for tests and embedded use. */
|
|
74
100
|
export type SkillMemoryStorageConfig = {
|
|
75
101
|
type: "memory";
|
|
76
102
|
/** Initial skills to seed the store with. */
|
|
77
103
|
skills?: SkillDefinition[];
|
|
104
|
+
/**
|
|
105
|
+
* Resource file contents keyed by skill id → relative path.
|
|
106
|
+
* E.g. `{ "my-skill": { "references/forms.md": "..." } }`.
|
|
107
|
+
*/
|
|
108
|
+
resources?: Record<string, Record<string, string>>;
|
|
78
109
|
};
|
|
79
110
|
/**
|
|
80
111
|
* Directory-backed store. Reads three layouts:
|
|
@@ -154,6 +185,15 @@ export type SkillS3IndexDocument = {
|
|
|
154
185
|
lastUpdated: string;
|
|
155
186
|
skills: SkillIndexItem[];
|
|
156
187
|
};
|
|
188
|
+
/** Result of a conditional (ETag) object read. */
|
|
189
|
+
export type SkillS3ConditionalGetResult = {
|
|
190
|
+
/** Object body; null when the key is absent. */
|
|
191
|
+
body: string | null;
|
|
192
|
+
/** ETag of the returned body, for the next conditional read. */
|
|
193
|
+
etag?: string;
|
|
194
|
+
/** True when the object is unchanged since the supplied ETag (no body). */
|
|
195
|
+
notModified?: boolean;
|
|
196
|
+
};
|
|
157
197
|
/**
|
|
158
198
|
* Minimal object-storage operations the S3 skill store runs on. The
|
|
159
199
|
* default implementation is created lazily from @aws-sdk/client-s3;
|
|
@@ -166,9 +206,15 @@ export type SkillS3ObjectOps = {
|
|
|
166
206
|
deleteObject(key: string): Promise<void>;
|
|
167
207
|
/** List all object keys under a prefix (paginated internally). */
|
|
168
208
|
listKeys(prefix: string): Promise<string[]>;
|
|
209
|
+
/**
|
|
210
|
+
* Optional: ETag-conditional read (If-None-Match). Used for index.json
|
|
211
|
+
* refreshes so an unchanged index costs a 304 instead of a full download.
|
|
212
|
+
* Ops without it fall back to plain getObject.
|
|
213
|
+
*/
|
|
214
|
+
getObjectConditional?(key: string, etag?: string): Promise<SkillS3ConditionalGetResult>;
|
|
169
215
|
};
|
|
170
216
|
export type SkillsStorageConfig = SkillMemoryStorageConfig | SkillFilesystemStorageConfig | SkillS3StorageConfig | SkillRedisStorageConfig | SkillCustomStorageConfig;
|
|
171
|
-
/** Query accepted by SkillsManager.search()
|
|
217
|
+
/** Query accepted by SkillsManager.search() (programmatic + CLI search). */
|
|
172
218
|
export type SkillSearchQuery = {
|
|
173
219
|
/** Keyword matched (case-insensitive substring) against name, displayName, and description. */
|
|
174
220
|
query?: string;
|
|
@@ -239,14 +285,38 @@ export type SkillsConfig = {
|
|
|
239
285
|
/** Persistence backend. Default: `{ type: "memory" }`. */
|
|
240
286
|
storage?: SkillsStorageConfig;
|
|
241
287
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
288
|
+
* Where the skills listing (names + descriptions, never instructions)
|
|
289
|
+
* surfaces for model-driven discovery:
|
|
290
|
+
* - "tool" (default): an `<available_skills>` block embedded in the
|
|
291
|
+
* use_skill tool description — the Claude Code pattern. Keeps the
|
|
292
|
+
* host's system prompt untouched and the listing cache-stable.
|
|
293
|
+
* - "system-prompt": a "## Available Skills" index appended to the
|
|
294
|
+
* system prompt instead.
|
|
295
|
+
* - "none": no listing anywhere; discovery only via list_skills.
|
|
296
|
+
*/
|
|
297
|
+
discovery?: SkillDiscoveryMode;
|
|
298
|
+
/**
|
|
299
|
+
* Character budget for the "tool" discovery listing. When the full
|
|
300
|
+
* listing exceeds it, every description is shortened uniformly (first
|
|
301
|
+
* sentence, then a hard cap) so the render stays a pure function of the
|
|
302
|
+
* index — byte-stable across calls; names are never dropped.
|
|
303
|
+
* Default: 15000.
|
|
245
304
|
*/
|
|
246
|
-
|
|
305
|
+
listingBudgetChars?: number;
|
|
306
|
+
/**
|
|
307
|
+
* Pin activated skill instructions into session history so later turns
|
|
308
|
+
* replay them verbatim (byte-stable, provider-cacheable) instead of
|
|
309
|
+
* re-fetching the skill. Requires conversation memory + a sessionId on
|
|
310
|
+
* the call. Default: true.
|
|
311
|
+
*/
|
|
312
|
+
sessionPersistence?: boolean;
|
|
247
313
|
/** Maximum skills hydrated (with instructions) per search. Default: 5. */
|
|
248
314
|
maxMatches?: number;
|
|
249
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Maximum entries rendered by the "system-prompt" discovery mode before
|
|
317
|
+
* truncation. Default: 50. The "tool" mode is bounded by
|
|
318
|
+
* listingBudgetChars instead and never drops entries.
|
|
319
|
+
*/
|
|
250
320
|
promptIndexMaxItems?: number;
|
|
251
321
|
/** Index cache TTL in milliseconds. Default: 30000. 0 disables caching. */
|
|
252
322
|
indexCacheTtlMs?: number;
|
|
@@ -265,6 +335,30 @@ export type SkillsConfig = {
|
|
|
265
335
|
*/
|
|
266
336
|
onMutationRequest?: (action: SkillMutationAction) => Promise<SkillMutationDecision>;
|
|
267
337
|
};
|
|
338
|
+
/** Where the skills discovery listing surfaces. See SkillsConfig.discovery. */
|
|
339
|
+
export type SkillDiscoveryMode = "tool" | "system-prompt" | "none";
|
|
340
|
+
/**
|
|
341
|
+
* One activated skill in a session: which skill, at which version, when.
|
|
342
|
+
* Sessions pin the version active at activation time — a mid-session skill
|
|
343
|
+
* update never mutates instructions the model has already loaded.
|
|
344
|
+
*/
|
|
345
|
+
export type SkillActivationRecord = {
|
|
346
|
+
skillId: string;
|
|
347
|
+
name: string;
|
|
348
|
+
version: number;
|
|
349
|
+
/** ISO timestamp of activation. */
|
|
350
|
+
activatedAt: string;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Structural view of the per-session activation tracker consumed by the
|
|
354
|
+
* skill tools factory.
|
|
355
|
+
*/
|
|
356
|
+
export type SkillSessionStateLike = {
|
|
357
|
+
isActive: (sessionId: string, skillId: string, name: string) => boolean;
|
|
358
|
+
getActivation: (sessionId: string, skillId: string, name?: string) => SkillActivationRecord | undefined;
|
|
359
|
+
recordActivation: (sessionId: string, skill: SkillDefinition) => ChatMessage;
|
|
360
|
+
hydrate: (sessionId: string, storedMessages: ChatMessage[]) => void;
|
|
361
|
+
};
|
|
268
362
|
/**
|
|
269
363
|
* Structural view of SkillsManager consumed by the skill tools factory —
|
|
270
364
|
* keeps skillTools.ts decoupled from the concrete manager class.
|
|
@@ -272,8 +366,38 @@ export type SkillsConfig = {
|
|
|
272
366
|
export type SkillsManagerLike = {
|
|
273
367
|
search: (query: SkillSearchQuery) => Promise<SkillDefinition[]>;
|
|
274
368
|
list: (scopeId?: string) => Promise<SkillIndexItem[]>;
|
|
369
|
+
get: (idOrName: string) => Promise<SkillDefinition | null>;
|
|
370
|
+
getResource: (idOrName: string, resourcePath: string) => Promise<string | null>;
|
|
371
|
+
sessions: SkillSessionStateLike;
|
|
275
372
|
requestMutation: (action: SkillMutationAction) => Promise<SkillMutationResult>;
|
|
276
373
|
};
|
|
374
|
+
/**
|
|
375
|
+
* Per-call context bound into the use_skill / read_skill_resource tools at
|
|
376
|
+
* injection time (prepareGenerate/prepareStream). The sessionId is captured
|
|
377
|
+
* by closure so activation state is tracked without relying on runtime tool
|
|
378
|
+
* context plumbing.
|
|
379
|
+
*/
|
|
380
|
+
export type SkillCallToolsContext = {
|
|
381
|
+
/** Session the call belongs to; absent → activation state is per-turn only. */
|
|
382
|
+
sessionId?: string;
|
|
383
|
+
/** Scope filter applied when resolving skills for this call. */
|
|
384
|
+
scopeId?: string;
|
|
385
|
+
/**
|
|
386
|
+
* Pin activated instructions into session history after the turn.
|
|
387
|
+
* Mirrors SkillsConfig.sessionPersistence resolved for this call.
|
|
388
|
+
*/
|
|
389
|
+
sessionPersistence: boolean;
|
|
390
|
+
/** Discovery mode resolved for this call — shapes the use_skill description. */
|
|
391
|
+
discovery: SkillDiscoveryMode;
|
|
392
|
+
/** Rendered `<available_skills>` block for "tool" discovery; null when empty. */
|
|
393
|
+
listing?: string | null;
|
|
394
|
+
/**
|
|
395
|
+
* Stored session history loader used to hydrate activation state before
|
|
396
|
+
* every dedup check (restart/multi-instance/failed-persistence safety).
|
|
397
|
+
* Invoked once per use_skill / read_skill_resource attempt.
|
|
398
|
+
*/
|
|
399
|
+
getStoredMessages?: (sessionId: string) => Promise<ChatMessage[]>;
|
|
400
|
+
};
|
|
277
401
|
/** Options for the createSkillTools factory. */
|
|
278
402
|
export type SkillToolsOptions = {
|
|
279
403
|
/** Include skill_create / skill_update / skill_delete. Default: false. */
|
|
@@ -285,12 +409,19 @@ export type SkillToolsOptions = {
|
|
|
285
409
|
* config (same precedence convention as per-call credentials).
|
|
286
410
|
*/
|
|
287
411
|
export type SkillsCallOptions = {
|
|
288
|
-
/** Master toggle for this call (
|
|
412
|
+
/** Master toggle for this call (listing + per-call tools). Default: true. */
|
|
289
413
|
enabled?: boolean;
|
|
290
|
-
/** Per-call override of SkillsConfig.
|
|
291
|
-
|
|
292
|
-
/** Scope filter for the
|
|
414
|
+
/** Per-call override of SkillsConfig.discovery. */
|
|
415
|
+
discovery?: SkillDiscoveryMode;
|
|
416
|
+
/** Scope filter for the listing and skill resolution on this call. Overrides defaultScopeId. */
|
|
293
417
|
scopeId?: string;
|
|
294
|
-
/** Restrict the
|
|
418
|
+
/** Restrict the listing to skills carrying at least one of these tags. */
|
|
295
419
|
tags?: string[];
|
|
420
|
+
/**
|
|
421
|
+
* Skill names to activate at the start of this call: their full
|
|
422
|
+
* instructions are injected up front (and pinned to the session when
|
|
423
|
+
* sessionPersistence is on), without waiting for the model to invoke
|
|
424
|
+
* use_skill. Already-active skills are skipped.
|
|
425
|
+
*/
|
|
426
|
+
preload?: string[];
|
|
296
427
|
};
|
package/dist/lib/types/skills.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Architecture mirrors the Hippocampus memory subsystem: a `skills` config
|
|
8
8
|
* on the NeuroLink constructor lazily initializes a SkillsManager, which
|
|
9
|
-
* auto-registers built-in tools (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* auto-registers built-in tools (list_skills, plus gated mutation tools)
|
|
10
|
+
* and augments each generate()/stream() call with the discovery listing
|
|
11
|
+
* plus per-call use_skill / read_skill_resource tools.
|
|
12
12
|
*
|
|
13
13
|
* Naming: every exported type carries a `Skill` prefix to satisfy the
|
|
14
14
|
* `unique-type-names` ESLint rule.
|
|
@@ -28,7 +28,7 @@ export declare function getConversationMessages(conversationMemory: Conversation
|
|
|
28
28
|
* Store conversation turn for future context
|
|
29
29
|
* Saves user messages and AI responses for conversation memory
|
|
30
30
|
*/
|
|
31
|
-
export declare function storeConversationTurn(conversationMemory: ConversationMemoryManager | RedisConversationMemoryManager | null | undefined, originalOptions: TextGenerationOptions, result: TextGenerationResult, startTimeStamp?: Date | undefined, requestId?: string): Promise<void>;
|
|
31
|
+
export declare function storeConversationTurn(conversationMemory: ConversationMemoryManager | RedisConversationMemoryManager | null | undefined, originalOptions: TextGenerationOptions, result: TextGenerationResult, startTimeStamp?: Date | undefined, requestId?: string, skillMessages?: ChatMessage[]): Promise<void>;
|
|
32
32
|
/**
|
|
33
33
|
* Build context messages from pointer onwards (token-based memory)
|
|
34
34
|
* Returns summary message (if exists) + all messages after the summarized pointer
|
|
@@ -227,7 +227,7 @@ export async function getConversationMessages(conversationMemory, options) {
|
|
|
227
227
|
* Store conversation turn for future context
|
|
228
228
|
* Saves user messages and AI responses for conversation memory
|
|
229
229
|
*/
|
|
230
|
-
export async function storeConversationTurn(conversationMemory, originalOptions, result, startTimeStamp, requestId) {
|
|
230
|
+
export async function storeConversationTurn(conversationMemory, originalOptions, result, startTimeStamp, requestId, skillMessages) {
|
|
231
231
|
logger.debug("[conversationMemoryUtils] storeConversationTurn called", {
|
|
232
232
|
requestId,
|
|
233
233
|
hasMemory: !!conversationMemory,
|
|
@@ -369,6 +369,9 @@ export async function storeConversationTurn(conversationMemory, originalOptions,
|
|
|
369
369
|
enableSummarization: originalOptions.enableSummarization,
|
|
370
370
|
requestId,
|
|
371
371
|
events: toolActivityEvents,
|
|
372
|
+
...(skillMessages && skillMessages.length > 0
|
|
373
|
+
? { skillMessages }
|
|
374
|
+
: {}),
|
|
372
375
|
tokenUsage: result.usage
|
|
373
376
|
? {
|
|
374
377
|
inputTokens: result.usage.input,
|
|
@@ -450,6 +453,12 @@ export function buildContextFromPointer(session, requestId) {
|
|
|
450
453
|
return session.messages;
|
|
451
454
|
}
|
|
452
455
|
const messagesAfterPointer = session.messages.slice(pointerIndex + 1);
|
|
456
|
+
// Pinned skill instructions must survive summarization: any skill message
|
|
457
|
+
// that fell behind the pointer is re-included verbatim after the summary,
|
|
458
|
+
// so an activated skill keeps its full instructions for the whole session.
|
|
459
|
+
const pinnedSkillMessages = session.messages
|
|
460
|
+
.slice(0, pointerIndex + 1)
|
|
461
|
+
.filter((msg) => msg.metadata?.isSkill);
|
|
453
462
|
// Construct context: summary message + recent messages
|
|
454
463
|
const summaryMessage = {
|
|
455
464
|
id: `summary-${session.summarizedUpToMessageId}`,
|
|
@@ -468,7 +477,11 @@ export function buildContextFromPointer(session, requestId) {
|
|
|
468
477
|
totalMessages: session.messages.length,
|
|
469
478
|
summaryLength: session.summarizedMessage.length,
|
|
470
479
|
});
|
|
471
|
-
const contextMessages = [
|
|
480
|
+
const contextMessages = [
|
|
481
|
+
summaryMessage,
|
|
482
|
+
...pinnedSkillMessages,
|
|
483
|
+
...messagesAfterPointer,
|
|
484
|
+
];
|
|
472
485
|
// Log context built for LLM with structural metadata
|
|
473
486
|
const totalChars = contextMessages.reduce((sum, msg) => sum + msg.content.length, 0);
|
|
474
487
|
logger.info("[ConversationMemory] Context built for LLM", {
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -274,7 +274,7 @@ export declare class NeuroLink {
|
|
|
274
274
|
*/
|
|
275
275
|
private ensureSkillsReady;
|
|
276
276
|
/**
|
|
277
|
-
* Register the built-in skill tools (
|
|
277
|
+
* Register the built-in skill tools (list_skills, plus
|
|
278
278
|
* mutation tools when allowMutations is set). Follows the
|
|
279
279
|
* registerMemoryRetrievalTools() pattern: registered via registerTool()
|
|
280
280
|
* so they land in the "user-defined" category that reaches the LLM tool
|
|
@@ -282,11 +282,36 @@ export declare class NeuroLink {
|
|
|
282
282
|
*/
|
|
283
283
|
private registerSkillTools;
|
|
284
284
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
* Skills augmentation for one generate()/stream() call:
|
|
286
|
+
* 1. Discovery — surface the skills listing per the resolved mode:
|
|
287
|
+
* embedded in the use_skill tool description ("tool", default) or
|
|
288
|
+
* appended to the system prompt ("system-prompt").
|
|
289
|
+
* 2. Per-call tools — inject use_skill + read_skill_resource into
|
|
290
|
+
* options.tools (the RAG-tool pattern; same-name entries shadow
|
|
291
|
+
* registered tools) with the sessionId closure-bound so activations
|
|
292
|
+
* pin to the session.
|
|
293
|
+
* 3. Preload — activate host-requested skills up front, injecting their
|
|
294
|
+
* instructions into this call's system prompt and pinning them.
|
|
295
|
+
* Fails open: any error leaves the call untouched.
|
|
296
|
+
*/
|
|
297
|
+
private applySkillsAugmentation;
|
|
298
|
+
/** Session id for skill activation tracking, from the call context. */
|
|
299
|
+
private resolveSkillSessionId;
|
|
300
|
+
/** User id for skill-session hydration (Redis keys sessions by userId). */
|
|
301
|
+
private resolveSkillUserId;
|
|
302
|
+
/**
|
|
303
|
+
* Activate host-requested skills before the model runs: instructions go
|
|
304
|
+
* into this call's system prompt; when persisting, the activation is
|
|
305
|
+
* pinned so later turns replay it from history instead. Unknown or
|
|
306
|
+
* already-active names are skipped with a warn/debug log (fail-open).
|
|
307
|
+
*/
|
|
308
|
+
private preloadSkills;
|
|
309
|
+
/**
|
|
310
|
+
* Pinned skill messages recorded during this turn, ready for
|
|
311
|
+
* StoreConversationTurnOptions.skillMessages. Empty when skills are off
|
|
312
|
+
* or nothing was activated.
|
|
313
|
+
*/
|
|
314
|
+
private drainPendingSkillMessages;
|
|
290
315
|
/**
|
|
291
316
|
* Programmatic access to the skills subsystem (search/list/get/mutations).
|
|
292
317
|
* Returns null when skills are not configured or failed to initialize.
|
package/dist/neurolink.js
CHANGED
|
@@ -54,8 +54,10 @@ 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 { isSkillVisibleInScope } from "./skills/skillMatcher.js";
|
|
58
|
+
import { buildSkillActivationMessage } from "./skills/skillSessionTracker.js";
|
|
57
59
|
import { SkillsManager } from "./skills/skillsManager.js";
|
|
58
|
-
import { createSkillTools } from "./skills/skillTools.js";
|
|
60
|
+
import { createSkillCallTools, createSkillTools } from "./skills/skillTools.js";
|
|
59
61
|
import { getMetricsAggregator, MetricsAggregator, } from "./observability/metricsAggregator.js";
|
|
60
62
|
import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, ModelAccessDeniedError, } from "./types/index.js";
|
|
61
63
|
import { SpanSerializer } from "./observability/utils/spanSerializer.js";
|
|
@@ -1276,7 +1278,7 @@ export class NeuroLink {
|
|
|
1276
1278
|
return this.skillsManagerInstance;
|
|
1277
1279
|
}
|
|
1278
1280
|
/**
|
|
1279
|
-
* Register the built-in skill tools (
|
|
1281
|
+
* Register the built-in skill tools (list_skills, plus
|
|
1280
1282
|
* mutation tools when allowMutations is set). Follows the
|
|
1281
1283
|
* registerMemoryRetrievalTools() pattern: registered via registerTool()
|
|
1282
1284
|
* so they land in the "user-defined" category that reaches the LLM tool
|
|
@@ -1300,19 +1302,22 @@ export class NeuroLink {
|
|
|
1300
1302
|
logger.info(`[NeuroLink] Registered ${Object.keys(canonicalTools).length} skill tools`, { allowMutations: this.skillsConfig?.allowMutations === true });
|
|
1301
1303
|
}
|
|
1302
1304
|
/**
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1305
|
+
* Skills augmentation for one generate()/stream() call:
|
|
1306
|
+
* 1. Discovery — surface the skills listing per the resolved mode:
|
|
1307
|
+
* embedded in the use_skill tool description ("tool", default) or
|
|
1308
|
+
* appended to the system prompt ("system-prompt").
|
|
1309
|
+
* 2. Per-call tools — inject use_skill + read_skill_resource into
|
|
1310
|
+
* options.tools (the RAG-tool pattern; same-name entries shadow
|
|
1311
|
+
* registered tools) with the sessionId closure-bound so activations
|
|
1312
|
+
* pin to the session.
|
|
1313
|
+
* 3. Preload — activate host-requested skills up front, injecting their
|
|
1314
|
+
* instructions into this call's system prompt and pinning them.
|
|
1315
|
+
* Fails open: any error leaves the call untouched.
|
|
1306
1316
|
*/
|
|
1307
|
-
async
|
|
1317
|
+
async applySkillsAugmentation(options) {
|
|
1308
1318
|
if (!this.skillsConfig?.enabled || options.skills?.enabled === false) {
|
|
1309
1319
|
return;
|
|
1310
1320
|
}
|
|
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
1321
|
// Media-only modes have no meaningful text prompt to augment.
|
|
1317
1322
|
const mode = options.output?.mode;
|
|
1318
1323
|
if (mode === "avatar" ||
|
|
@@ -1326,26 +1331,142 @@ export class NeuroLink {
|
|
|
1326
1331
|
if (!manager) {
|
|
1327
1332
|
return;
|
|
1328
1333
|
}
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1334
|
+
const discovery = options.skills?.discovery ?? this.skillsConfig.discovery ?? "tool";
|
|
1335
|
+
const scopeId = options.skills?.scopeId ?? this.skillsConfig.defaultScopeId;
|
|
1336
|
+
const tags = options.skills?.tags;
|
|
1337
|
+
const sessionId = this.resolveSkillSessionId(options.context) ??
|
|
1338
|
+
this.resolveSkillSessionId(options);
|
|
1339
|
+
const userId = this.resolveSkillUserId(options.context) ??
|
|
1340
|
+
this.resolveSkillUserId(options);
|
|
1341
|
+
// Pinning requires somewhere to pin: without conversation memory the
|
|
1342
|
+
// drained messages would be silently discarded while dedup reports
|
|
1343
|
+
// already_loaded — so persistence is only on when memory is
|
|
1344
|
+
// configured (or already initialized).
|
|
1345
|
+
const memoryAvailable = Boolean(this.conversationMemory) ||
|
|
1346
|
+
Boolean(this.conversationMemoryConfig?.conversationMemory?.enabled);
|
|
1347
|
+
const sessionPersistence = (this.skillsConfig.sessionPersistence ?? true) &&
|
|
1348
|
+
Boolean(sessionId) &&
|
|
1349
|
+
memoryAvailable;
|
|
1350
|
+
const visibility = {
|
|
1351
|
+
...(scopeId !== undefined ? { scopeId } : {}),
|
|
1352
|
+
...(tags !== undefined ? { tags } : {}),
|
|
1353
|
+
};
|
|
1354
|
+
if (discovery === "system-prompt") {
|
|
1355
|
+
const block = await manager.buildPromptIndex(visibility);
|
|
1356
|
+
if (block) {
|
|
1357
|
+
options.systemPrompt = options.systemPrompt
|
|
1358
|
+
? `${options.systemPrompt}\n\n${block}`
|
|
1359
|
+
: block;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const listing = discovery === "tool"
|
|
1363
|
+
? await manager.buildToolListing(visibility)
|
|
1364
|
+
: null;
|
|
1365
|
+
const callTools = createSkillCallTools(() => this.ensureSkillsReady(), {
|
|
1366
|
+
...(sessionId ? { sessionId } : {}),
|
|
1367
|
+
...(scopeId !== undefined ? { scopeId } : {}),
|
|
1368
|
+
sessionPersistence,
|
|
1369
|
+
discovery,
|
|
1370
|
+
listing,
|
|
1371
|
+
// userId rides by closure: Redis memory keys sessions by
|
|
1372
|
+
// userId:sessionId, so hydration must read the same key the
|
|
1373
|
+
// store-turn path writes.
|
|
1374
|
+
getStoredMessages: async (sid) => this.conversationMemory
|
|
1375
|
+
? await this.conversationMemory.getSessionMessages(sid, userId)
|
|
1376
|
+
: [],
|
|
1336
1377
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1378
|
+
// Caller-supplied per-call tools win over the injected ones (a host
|
|
1379
|
+
// may bring its own use_skill); the injected tools still shadow any
|
|
1380
|
+
// registered base tool of the same name via the provider merge.
|
|
1381
|
+
options.tools = {
|
|
1382
|
+
...callTools,
|
|
1383
|
+
...(options.tools ?? {}),
|
|
1384
|
+
};
|
|
1385
|
+
if (options.skills?.preload?.length) {
|
|
1386
|
+
await this.preloadSkills(manager, options, options.skills.preload, {
|
|
1387
|
+
...(sessionId ? { sessionId } : {}),
|
|
1388
|
+
...(userId ? { userId } : {}),
|
|
1389
|
+
sessionPersistence,
|
|
1390
|
+
...(scopeId !== undefined ? { scopeId } : {}),
|
|
1343
1391
|
});
|
|
1344
1392
|
}
|
|
1393
|
+
logger.debug("[NeuroLink] Skills augmentation applied", {
|
|
1394
|
+
discovery,
|
|
1395
|
+
listingLength: listing?.length ?? 0,
|
|
1396
|
+
sessionPersistence,
|
|
1397
|
+
preloadCount: options.skills?.preload?.length ?? 0,
|
|
1398
|
+
});
|
|
1345
1399
|
}
|
|
1346
1400
|
catch (error) {
|
|
1347
|
-
logger.warn("[NeuroLink] Skills
|
|
1401
|
+
logger.warn("[NeuroLink] Skills augmentation failed — continuing without skills", { error: error instanceof Error ? error.message : String(error) });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
/** Session id for skill activation tracking, from the call context. */
|
|
1405
|
+
resolveSkillSessionId(context) {
|
|
1406
|
+
const fromContext = context
|
|
1407
|
+
?.sessionId;
|
|
1408
|
+
return typeof fromContext === "string" && fromContext
|
|
1409
|
+
? fromContext
|
|
1410
|
+
: undefined;
|
|
1411
|
+
}
|
|
1412
|
+
/** User id for skill-session hydration (Redis keys sessions by userId). */
|
|
1413
|
+
resolveSkillUserId(context) {
|
|
1414
|
+
const fromContext = context?.userId;
|
|
1415
|
+
return typeof fromContext === "string" && fromContext
|
|
1416
|
+
? fromContext
|
|
1417
|
+
: undefined;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Activate host-requested skills before the model runs: instructions go
|
|
1421
|
+
* into this call's system prompt; when persisting, the activation is
|
|
1422
|
+
* pinned so later turns replay it from history instead. Unknown or
|
|
1423
|
+
* already-active names are skipped with a warn/debug log (fail-open).
|
|
1424
|
+
*/
|
|
1425
|
+
async preloadSkills(manager, options, names, call) {
|
|
1426
|
+
const { sessionId, userId, sessionPersistence, scopeId } = call;
|
|
1427
|
+
for (const name of names) {
|
|
1428
|
+
const skill = await manager.get(name);
|
|
1429
|
+
if (!skill || !isSkillVisibleInScope(skill, scopeId)) {
|
|
1430
|
+
logger.warn("[NeuroLink] Preload skill not found — skipping", {
|
|
1431
|
+
skill: name,
|
|
1432
|
+
});
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
let block;
|
|
1436
|
+
if (sessionId && sessionPersistence) {
|
|
1437
|
+
// Always hydrate (empty history when memory isn't initialized yet):
|
|
1438
|
+
// the tracker derives truth from stored pins + this turn's pending,
|
|
1439
|
+
// never from stale in-process records.
|
|
1440
|
+
manager.sessions.hydrate(sessionId, this.conversationMemory
|
|
1441
|
+
? await this.conversationMemory.getSessionMessages(sessionId, userId)
|
|
1442
|
+
: []);
|
|
1443
|
+
if (manager.sessions.isActive(sessionId, skill.id, skill.name)) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
block = manager.sessions.recordActivation(sessionId, skill).content;
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
block = buildSkillActivationMessage(skill).content;
|
|
1450
|
+
}
|
|
1451
|
+
options.systemPrompt = options.systemPrompt
|
|
1452
|
+
? `${options.systemPrompt}\n\n${block}`
|
|
1453
|
+
: block;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Pinned skill messages recorded during this turn, ready for
|
|
1458
|
+
* StoreConversationTurnOptions.skillMessages. Empty when skills are off
|
|
1459
|
+
* or nothing was activated.
|
|
1460
|
+
*/
|
|
1461
|
+
drainPendingSkillMessages(sessionId) {
|
|
1462
|
+
if (typeof sessionId !== "string" || !sessionId) {
|
|
1463
|
+
return [];
|
|
1464
|
+
}
|
|
1465
|
+
const manager = this.skillsManagerInstance;
|
|
1466
|
+
if (!manager) {
|
|
1467
|
+
return [];
|
|
1348
1468
|
}
|
|
1469
|
+
return manager.sessions.drainPending(sessionId);
|
|
1349
1470
|
}
|
|
1350
1471
|
/**
|
|
1351
1472
|
* Programmatic access to the skills subsystem (search/list/get/mutations).
|
|
@@ -3610,9 +3731,9 @@ Current user's request: ${currentInput}`;
|
|
|
3610
3731
|
});
|
|
3611
3732
|
}
|
|
3612
3733
|
}
|
|
3613
|
-
// Skills:
|
|
3614
|
-
//
|
|
3615
|
-
await this.
|
|
3734
|
+
// Skills: surface the discovery listing and inject the per-call
|
|
3735
|
+
// use_skill / read_skill_resource tools (bodies load on activation).
|
|
3736
|
+
await this.applySkillsAugmentation(options);
|
|
3616
3737
|
// Media-only modes (avatar, music, video, ppt) do not have a meaningful
|
|
3617
3738
|
// text prompt to augment with memory — skip injection to avoid corrupting
|
|
3618
3739
|
// the empty/synthesized input.text that was set for these modes.
|
|
@@ -4431,7 +4552,7 @@ Current user's request: ${currentInput}`;
|
|
|
4431
4552
|
});
|
|
4432
4553
|
const memStoreStart = Date.now();
|
|
4433
4554
|
try {
|
|
4434
|
-
await storeConversationTurn(this.conversationMemory, options, result, new Date(startTime), requestId);
|
|
4555
|
+
await storeConversationTurn(this.conversationMemory, options, result, new Date(startTime), requestId, this.drainPendingSkillMessages(options.context?.sessionId));
|
|
4435
4556
|
this.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": path }, Date.now() - memStoreStart, SpanStatus.OK);
|
|
4436
4557
|
}
|
|
4437
4558
|
catch (memoryError) {
|
|
@@ -7151,9 +7272,9 @@ Current user's request: ${currentInput}`;
|
|
|
7151
7272
|
logger.warn("Memory retrieval failed:", error);
|
|
7152
7273
|
}
|
|
7153
7274
|
}
|
|
7154
|
-
// Skills:
|
|
7155
|
-
//
|
|
7156
|
-
await this.
|
|
7275
|
+
// Skills: surface the discovery listing and inject the per-call
|
|
7276
|
+
// use_skill / read_skill_resource tools (bodies load on activation).
|
|
7277
|
+
await this.applySkillsAugmentation(options);
|
|
7157
7278
|
// Apply orchestration if enabled and no specific provider/model requested
|
|
7158
7279
|
if (this.enableOrchestration && !options.provider && !options.model) {
|
|
7159
7280
|
try {
|
|
@@ -7526,6 +7647,7 @@ Current user's request: ${currentInput}`;
|
|
|
7526
7647
|
}
|
|
7527
7648
|
const memStoreStart = Date.now();
|
|
7528
7649
|
try {
|
|
7650
|
+
const pendingSkillMessages = this.drainPendingSkillMessages(sessionId);
|
|
7529
7651
|
await this.conversationMemory.storeConversationTurn({
|
|
7530
7652
|
sessionId,
|
|
7531
7653
|
userId,
|
|
@@ -7537,6 +7659,9 @@ Current user's request: ${currentInput}`;
|
|
|
7537
7659
|
events: eventSequence.length > 0 ? eventSequence : undefined,
|
|
7538
7660
|
requestId: enhancedOptions.context
|
|
7539
7661
|
?.requestId,
|
|
7662
|
+
...(pendingSkillMessages.length > 0
|
|
7663
|
+
? { skillMessages: pendingSkillMessages }
|
|
7664
|
+
: {}),
|
|
7540
7665
|
});
|
|
7541
7666
|
this.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": "stream" }, Date.now() - memStoreStart, SpanStatus.OK);
|
|
7542
7667
|
logger.debug("[NeuroLink.stream] Stored conversation turn with events", {
|
|
@@ -8075,8 +8200,10 @@ Current user's request: ${currentInput}`;
|
|
|
8075
8200
|
}
|
|
8076
8201
|
const memStoreStart = Date.now();
|
|
8077
8202
|
try {
|
|
8203
|
+
const fallbackSessionId = sessionId || options.context?.sessionId;
|
|
8204
|
+
const pendingSkillMessages = self.drainPendingSkillMessages(fallbackSessionId);
|
|
8078
8205
|
await self.conversationMemory.storeConversationTurn({
|
|
8079
|
-
sessionId:
|
|
8206
|
+
sessionId: fallbackSessionId,
|
|
8080
8207
|
userId: userId || options.context?.userId,
|
|
8081
8208
|
userMessage: originalPrompt ?? "",
|
|
8082
8209
|
aiResponse: fallbackAccumulatedContent,
|
|
@@ -8086,6 +8213,9 @@ Current user's request: ${currentInput}`;
|
|
|
8086
8213
|
requestId: enhancedOptions?.context?.requestId ||
|
|
8087
8214
|
options.context
|
|
8088
8215
|
?.requestId,
|
|
8216
|
+
...(pendingSkillMessages.length > 0
|
|
8217
|
+
? { skillMessages: pendingSkillMessages }
|
|
8218
|
+
: {}),
|
|
8089
8219
|
});
|
|
8090
8220
|
self.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": "fallback-stream" }, Date.now() - memStoreStart, SpanStatus.OK);
|
|
8091
8221
|
}
|