@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
@@ -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 (search_skills / list_skills, plus
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
- * 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.
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 applySkillsPromptIndex(options) {
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 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
- : {}),
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
- 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,
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 prompt index injection failed — continuing without it", { error: error instanceof Error ? error.message : String(error) });
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: 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);
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: 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);
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: sessionId || options.context?.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
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import type { ModelRouter } from "../../proxy/modelRouter.js";
14
- import type { ParsedClaudeError, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState } from "../../types/index.js";
14
+ import type { AccountCooldownPlan, AccountQuota, ParsedClaudeError, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState } from "../../types/index.js";
15
15
  /** Resolve the configured primary's stable key to its current index in the
16
16
  * request's enabledAccounts list. Returns 0 (insertion-order fallback) when
17
17
  * no key is configured or the key cannot be matched (account disabled/
@@ -23,6 +23,41 @@ declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[]): n
23
23
  * account once its rate limit window expires. Called at the start of each
24
24
  * request. Home is resolved fresh per call via resolveHomeIndex. */
25
25
  declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[]): void;
26
+ /** Convert an Anthropic unified-window reset (Unix epoch SECONDS, per the
27
+ * `anthropic-ratelimit-unified-*-reset` headers) into epoch-ms. Tolerates a
28
+ * value already expressed in ms (some intermediaries normalise it). Returns
29
+ * undefined for absent/zero/past-or-garbage timestamps so callers can fall
30
+ * back to retry-after. */
31
+ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): number | undefined;
32
+ /**
33
+ * Decide how to cool an account after a genuine (non-anti-abuse) 429.
34
+ *
35
+ * The unified subscription limits expose per-window status + reset:
36
+ * - weekly (7d) "rejected" → hard cap for the week; cool until the 7d reset.
37
+ * - session (5h) "rejected" → paced out for this session; cool until the 5h reset.
38
+ * Both mean "retrying this account is futile until its window resets" → rotate
39
+ * immediately (no same-account retries) and park the account until the ACTUAL
40
+ * reset — never the legacy 60s hardcap that let us re-hammer a spent account.
41
+ *
42
+ * Anything else (window still "allowed" but momentarily 429'd — a per-minute
43
+ * burst / acceleration limit) is transient: honor retry-after as a floor,
44
+ * allow a couple of jittered same-account retries, then a short cooldown.
45
+ */
46
+ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number): AccountCooldownPlan;
47
+ /**
48
+ * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
49
+ * spend the account whose window refreshes SOONEST first, so its about-to-reset
50
+ * allowance isn't wasted, then move to accounts with longer-dated resets.
51
+ *
52
+ * Priority among usable accounts:
53
+ * 1. soonest WEEKLY (7d) reset — the scarce, use-it-or-lose-it ceiling
54
+ * 2. soonest SESSION (5h) reset
55
+ * 3. highest weekly utilization — finish off the one closest to done
56
+ * Accounts with no quota data yet keep insertion order (stable sort) and sit
57
+ * after those with a known soonest reset. Cooling/rejected accounts sort last,
58
+ * soonest-back-to-service first, as last resort.
59
+ */
60
+ declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number): ProxyPassthroughAccount[];
26
61
  /**
27
62
  * Create Claude-compatible proxy routes.
28
63
  *
@@ -57,6 +92,9 @@ export declare function isTransientHttpFailure(status: number, errBody: string):
57
92
  export declare const __testHooks: {
58
93
  resolveHomeIndex: typeof resolveHomeIndex;
59
94
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
95
+ planCooldownFor429: typeof planCooldownFor429;
96
+ orderAccountsByQuota: typeof orderAccountsByQuota;
97
+ resetEpochToMs: typeof resetEpochToMs;
60
98
  setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
61
99
  getConfiguredPrimaryAccountKey: () => string | undefined;
62
100
  setPrimaryAccountIndex: (index: number) => void;