@juspay/neurolink 10.10.5 → 10.10.6

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.
@@ -80,6 +80,31 @@ export declare class ConversationMemoryManager implements IConversationMemoryMan
80
80
  * Resets summary pointers since old pointers may reference messages that no longer exist.
81
81
  */
82
82
  setSessionMessages(sessionId: string, messages: ChatMessage[], userId?: string): Promise<void>;
83
+ /**
84
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
85
+ * messages, mirroring the Redis manager.
86
+ *
87
+ * Parity fix: this used to exist only on the Redis backend, so an in-memory
88
+ * session never turned tool activity into messages and every downstream path
89
+ * that reasons about tool batches (compaction, pruning, pair repair) saw a
90
+ * different history shape depending on `STORAGE_TYPE`.
91
+ *
92
+ * Calls are written before results — the same order the Redis flush uses —
93
+ * and `toolCallId` is carried on BOTH sides so `repairToolPairs` can match by
94
+ * id rather than adjacency (a parallel batch has no positional pairing).
95
+ */
96
+ storeToolExecution(sessionId: string, userId: string | undefined, toolCalls: Array<{
97
+ toolCallId?: string;
98
+ toolName?: string;
99
+ args?: Record<string, unknown>;
100
+ [key: string]: unknown;
101
+ }>, toolResults: Array<{
102
+ toolCallId?: string;
103
+ output?: unknown;
104
+ result?: unknown;
105
+ error?: string;
106
+ [key: string]: unknown;
107
+ }>, currentTime?: Date): Promise<void>;
83
108
  /** Close/shutdown — no-op for in-memory manager (no external connections to release) */
84
109
  close(): Promise<void>;
85
110
  }
@@ -392,6 +392,77 @@ export class ConversationMemoryManager {
392
392
  session.lastCountedAt = undefined;
393
393
  session.lastActivity = Date.now();
394
394
  }
395
+ /**
396
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
397
+ * messages, mirroring the Redis manager.
398
+ *
399
+ * Parity fix: this used to exist only on the Redis backend, so an in-memory
400
+ * session never turned tool activity into messages and every downstream path
401
+ * that reasons about tool batches (compaction, pruning, pair repair) saw a
402
+ * different history shape depending on `STORAGE_TYPE`.
403
+ *
404
+ * Calls are written before results — the same order the Redis flush uses —
405
+ * and `toolCallId` is carried on BOTH sides so `repairToolPairs` can match by
406
+ * id rather than adjacency (a parallel batch has no positional pairing).
407
+ */
408
+ async storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime) {
409
+ await this.ensureInitialized();
410
+ let session = this.sessions.get(sessionId);
411
+ if (!session) {
412
+ session = this.createNewSession(sessionId, userId);
413
+ this.sessions.set(sessionId, session);
414
+ this.enforceSessionLimit();
415
+ }
416
+ const timestamp = (currentTime ?? new Date()).toISOString();
417
+ const toolNameById = new Map();
418
+ for (const toolCall of toolCalls ?? []) {
419
+ const toolCallId = toolCall.toolCallId ?? "";
420
+ const toolName = toolCall.toolName ?? "unknown";
421
+ if (toolCallId) {
422
+ toolNameById.set(toolCallId, toolName);
423
+ }
424
+ session.messages.push({
425
+ id: randomUUID(),
426
+ role: "tool_call",
427
+ content: "", // Tool calls carry their payload in `args`, not content.
428
+ tool: toolName,
429
+ ...(toolCallId ? { toolCallId } : {}),
430
+ args: (toolCall.args ?? {}),
431
+ timestamp,
432
+ });
433
+ }
434
+ for (const toolResult of toolResults ?? []) {
435
+ const toolCallId = toolResult.toolCallId ?? "";
436
+ const toolName = (toolCallId ? toolNameById.get(toolCallId) : undefined) ??
437
+ String(toolResult.toolName ?? "unknown");
438
+ const rawOutput = "output" in toolResult ? toolResult.output : toolResult.result;
439
+ let content;
440
+ if (typeof rawOutput === "string") {
441
+ content = rawOutput;
442
+ }
443
+ else {
444
+ try {
445
+ content = JSON.stringify(rawOutput ?? null) ?? "null";
446
+ }
447
+ catch (error) {
448
+ content = `[Serialization failed: ${error instanceof Error ? error.message : String(error)}]`;
449
+ }
450
+ }
451
+ session.messages.push({
452
+ id: randomUUID(),
453
+ role: "tool_result",
454
+ content,
455
+ tool: toolName,
456
+ ...(toolCallId ? { toolCallId } : {}),
457
+ result: {
458
+ success: !toolResult.error,
459
+ ...(toolResult.error ? { error: String(toolResult.error) } : {}),
460
+ },
461
+ timestamp,
462
+ });
463
+ }
464
+ session.lastActivity = Date.now();
465
+ }
395
466
  /** Close/shutdown — no-op for in-memory manager (no external connections to release) */
396
467
  async close() {
397
468
  // In-memory manager has nothing to close
@@ -80,6 +80,31 @@ export declare class ConversationMemoryManager implements IConversationMemoryMan
80
80
  * Resets summary pointers since old pointers may reference messages that no longer exist.
81
81
  */
82
82
  setSessionMessages(sessionId: string, messages: ChatMessage[], userId?: string): Promise<void>;
83
+ /**
84
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
85
+ * messages, mirroring the Redis manager.
86
+ *
87
+ * Parity fix: this used to exist only on the Redis backend, so an in-memory
88
+ * session never turned tool activity into messages and every downstream path
89
+ * that reasons about tool batches (compaction, pruning, pair repair) saw a
90
+ * different history shape depending on `STORAGE_TYPE`.
91
+ *
92
+ * Calls are written before results — the same order the Redis flush uses —
93
+ * and `toolCallId` is carried on BOTH sides so `repairToolPairs` can match by
94
+ * id rather than adjacency (a parallel batch has no positional pairing).
95
+ */
96
+ storeToolExecution(sessionId: string, userId: string | undefined, toolCalls: Array<{
97
+ toolCallId?: string;
98
+ toolName?: string;
99
+ args?: Record<string, unknown>;
100
+ [key: string]: unknown;
101
+ }>, toolResults: Array<{
102
+ toolCallId?: string;
103
+ output?: unknown;
104
+ result?: unknown;
105
+ error?: string;
106
+ [key: string]: unknown;
107
+ }>, currentTime?: Date): Promise<void>;
83
108
  /** Close/shutdown — no-op for in-memory manager (no external connections to release) */
84
109
  close(): Promise<void>;
85
110
  }
@@ -392,6 +392,77 @@ export class ConversationMemoryManager {
392
392
  session.lastCountedAt = undefined;
393
393
  session.lastActivity = Date.now();
394
394
  }
395
+ /**
396
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
397
+ * messages, mirroring the Redis manager.
398
+ *
399
+ * Parity fix: this used to exist only on the Redis backend, so an in-memory
400
+ * session never turned tool activity into messages and every downstream path
401
+ * that reasons about tool batches (compaction, pruning, pair repair) saw a
402
+ * different history shape depending on `STORAGE_TYPE`.
403
+ *
404
+ * Calls are written before results — the same order the Redis flush uses —
405
+ * and `toolCallId` is carried on BOTH sides so `repairToolPairs` can match by
406
+ * id rather than adjacency (a parallel batch has no positional pairing).
407
+ */
408
+ async storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime) {
409
+ await this.ensureInitialized();
410
+ let session = this.sessions.get(sessionId);
411
+ if (!session) {
412
+ session = this.createNewSession(sessionId, userId);
413
+ this.sessions.set(sessionId, session);
414
+ this.enforceSessionLimit();
415
+ }
416
+ const timestamp = (currentTime ?? new Date()).toISOString();
417
+ const toolNameById = new Map();
418
+ for (const toolCall of toolCalls ?? []) {
419
+ const toolCallId = toolCall.toolCallId ?? "";
420
+ const toolName = toolCall.toolName ?? "unknown";
421
+ if (toolCallId) {
422
+ toolNameById.set(toolCallId, toolName);
423
+ }
424
+ session.messages.push({
425
+ id: randomUUID(),
426
+ role: "tool_call",
427
+ content: "", // Tool calls carry their payload in `args`, not content.
428
+ tool: toolName,
429
+ ...(toolCallId ? { toolCallId } : {}),
430
+ args: (toolCall.args ?? {}),
431
+ timestamp,
432
+ });
433
+ }
434
+ for (const toolResult of toolResults ?? []) {
435
+ const toolCallId = toolResult.toolCallId ?? "";
436
+ const toolName = (toolCallId ? toolNameById.get(toolCallId) : undefined) ??
437
+ String(toolResult.toolName ?? "unknown");
438
+ const rawOutput = "output" in toolResult ? toolResult.output : toolResult.result;
439
+ let content;
440
+ if (typeof rawOutput === "string") {
441
+ content = rawOutput;
442
+ }
443
+ else {
444
+ try {
445
+ content = JSON.stringify(rawOutput ?? null) ?? "null";
446
+ }
447
+ catch (error) {
448
+ content = `[Serialization failed: ${error instanceof Error ? error.message : String(error)}]`;
449
+ }
450
+ }
451
+ session.messages.push({
452
+ id: randomUUID(),
453
+ role: "tool_result",
454
+ content,
455
+ tool: toolName,
456
+ ...(toolCallId ? { toolCallId } : {}),
457
+ result: {
458
+ success: !toolResult.error,
459
+ ...(toolResult.error ? { error: String(toolResult.error) } : {}),
460
+ },
461
+ timestamp,
462
+ });
463
+ }
464
+ session.lastActivity = Date.now();
465
+ }
395
466
  /** Close/shutdown — no-op for in-memory manager (no external connections to release) */
396
467
  async close() {
397
468
  // In-memory manager has nothing to close
@@ -1787,8 +1787,14 @@ export declare class NeuroLink {
1787
1787
  [key: string]: unknown;
1788
1788
  }>, currentTime?: Date): Promise<void>;
1789
1789
  /**
1790
- * Check if tool execution storage is available
1791
- * @returns boolean indicating if Redis storage is configured and available
1790
+ * Check if tool execution storage is available.
1791
+ *
1792
+ * Now capability-based rather than Redis-specific: any configured memory
1793
+ * backend implementing `storeToolExecution` qualifies. The old check
1794
+ * required `STORAGE_TYPE === "redis"` AND a Redis manager by class name, so
1795
+ * in-memory sessions reported false and silently skipped tool persistence.
1796
+ *
1797
+ * @returns whether the active memory backend can persist tool executions
1792
1798
  */
1793
1799
  isToolExecutionStorageAvailable(): boolean;
1794
1800
  /**
@@ -11129,11 +11129,16 @@ Current user's request: ${currentInput}`;
11129
11129
  });
11130
11130
  return;
11131
11131
  }
11132
- // Type guard to ensure it's Redis conversation memory manager
11133
- const redisMemory = this
11134
- .conversationMemory;
11132
+ // Any backend that implements storeToolExecution no longer a Redis cast.
11133
+ // The in-memory manager implements it too, so tool activity becomes
11134
+ // tool_call/tool_result messages regardless of STORAGE_TYPE.
11135
+ const memory = this.conversationMemory;
11136
+ if (!memory?.storeToolExecution) {
11137
+ logger.debug("Tool execution storage not supported by this memory backend");
11138
+ return;
11139
+ }
11135
11140
  try {
11136
- await redisMemory.storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime);
11141
+ await memory.storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime);
11137
11142
  }
11138
11143
  catch (error) {
11139
11144
  logger.warn("Failed to store tool executions", {
@@ -11145,15 +11150,17 @@ Current user's request: ${currentInput}`;
11145
11150
  }
11146
11151
  }
11147
11152
  /**
11148
- * Check if tool execution storage is available
11149
- * @returns boolean indicating if Redis storage is configured and available
11153
+ * Check if tool execution storage is available.
11154
+ *
11155
+ * Now capability-based rather than Redis-specific: any configured memory
11156
+ * backend implementing `storeToolExecution` qualifies. The old check
11157
+ * required `STORAGE_TYPE === "redis"` AND a Redis manager by class name, so
11158
+ * in-memory sessions reported false and silently skipped tool persistence.
11159
+ *
11160
+ * @returns whether the active memory backend can persist tool executions
11150
11161
  */
11151
11162
  isToolExecutionStorageAvailable() {
11152
- const isRedisStorage = process.env.STORAGE_TYPE === "redis";
11153
- const hasRedisConversationMemory = this.conversationMemory &&
11154
- this.conversationMemory.constructor.name ===
11155
- "RedisConversationMemoryManager";
11156
- return !!(isRedisStorage && hasRedisConversationMemory);
11163
+ return typeof this.conversationMemory?.storeToolExecution === "function";
11157
11164
  }
11158
11165
  /**
11159
11166
  * Get the raw messages array for a session.
@@ -32,6 +32,28 @@ export type IConversationMemoryManager = {
32
32
  getSessionMessages(sessionId: string, userId?: string): Promise<ChatMessage[]>;
33
33
  /** Replace the entire messages array for a session */
34
34
  setSessionMessages(sessionId: string, messages: ChatMessage[], userId?: string): Promise<void>;
35
+ /**
36
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
37
+ * messages on the session.
38
+ *
39
+ * Declared on the interface so every backend can implement it. Previously
40
+ * only the Redis manager had it, and the caller reached it by casting — so
41
+ * on in-memory storage tool activity never became messages at all, and the
42
+ * compaction, pruning and pair-repair paths saw a different history shape
43
+ * depending on `STORAGE_TYPE`.
44
+ */
45
+ storeToolExecution?(sessionId: string, userId: string | undefined, toolCalls: Array<{
46
+ toolCallId?: string;
47
+ toolName?: string;
48
+ args?: Record<string, unknown>;
49
+ [key: string]: unknown;
50
+ }>, toolResults: Array<{
51
+ toolCallId?: string;
52
+ output?: unknown;
53
+ result?: unknown;
54
+ error?: string;
55
+ [key: string]: unknown;
56
+ }>, currentTime?: Date): Promise<void>;
35
57
  /** Close/shutdown the memory manager and release resources (e.g., Redis connections) */
36
58
  close?(): Promise<void>;
37
59
  };
@@ -1787,8 +1787,14 @@ export declare class NeuroLink {
1787
1787
  [key: string]: unknown;
1788
1788
  }>, currentTime?: Date): Promise<void>;
1789
1789
  /**
1790
- * Check if tool execution storage is available
1791
- * @returns boolean indicating if Redis storage is configured and available
1790
+ * Check if tool execution storage is available.
1791
+ *
1792
+ * Now capability-based rather than Redis-specific: any configured memory
1793
+ * backend implementing `storeToolExecution` qualifies. The old check
1794
+ * required `STORAGE_TYPE === "redis"` AND a Redis manager by class name, so
1795
+ * in-memory sessions reported false and silently skipped tool persistence.
1796
+ *
1797
+ * @returns whether the active memory backend can persist tool executions
1792
1798
  */
1793
1799
  isToolExecutionStorageAvailable(): boolean;
1794
1800
  /**
package/dist/neurolink.js CHANGED
@@ -11129,11 +11129,16 @@ Current user's request: ${currentInput}`;
11129
11129
  });
11130
11130
  return;
11131
11131
  }
11132
- // Type guard to ensure it's Redis conversation memory manager
11133
- const redisMemory = this
11134
- .conversationMemory;
11132
+ // Any backend that implements storeToolExecution no longer a Redis cast.
11133
+ // The in-memory manager implements it too, so tool activity becomes
11134
+ // tool_call/tool_result messages regardless of STORAGE_TYPE.
11135
+ const memory = this.conversationMemory;
11136
+ if (!memory?.storeToolExecution) {
11137
+ logger.debug("Tool execution storage not supported by this memory backend");
11138
+ return;
11139
+ }
11135
11140
  try {
11136
- await redisMemory.storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime);
11141
+ await memory.storeToolExecution(sessionId, userId, toolCalls, toolResults, currentTime);
11137
11142
  }
11138
11143
  catch (error) {
11139
11144
  logger.warn("Failed to store tool executions", {
@@ -11145,15 +11150,17 @@ Current user's request: ${currentInput}`;
11145
11150
  }
11146
11151
  }
11147
11152
  /**
11148
- * Check if tool execution storage is available
11149
- * @returns boolean indicating if Redis storage is configured and available
11153
+ * Check if tool execution storage is available.
11154
+ *
11155
+ * Now capability-based rather than Redis-specific: any configured memory
11156
+ * backend implementing `storeToolExecution` qualifies. The old check
11157
+ * required `STORAGE_TYPE === "redis"` AND a Redis manager by class name, so
11158
+ * in-memory sessions reported false and silently skipped tool persistence.
11159
+ *
11160
+ * @returns whether the active memory backend can persist tool executions
11150
11161
  */
11151
11162
  isToolExecutionStorageAvailable() {
11152
- const isRedisStorage = process.env.STORAGE_TYPE === "redis";
11153
- const hasRedisConversationMemory = this.conversationMemory &&
11154
- this.conversationMemory.constructor.name ===
11155
- "RedisConversationMemoryManager";
11156
- return !!(isRedisStorage && hasRedisConversationMemory);
11163
+ return typeof this.conversationMemory?.storeToolExecution === "function";
11157
11164
  }
11158
11165
  /**
11159
11166
  * Get the raw messages array for a session.
@@ -32,6 +32,28 @@ export type IConversationMemoryManager = {
32
32
  getSessionMessages(sessionId: string, userId?: string): Promise<ChatMessage[]>;
33
33
  /** Replace the entire messages array for a session */
34
34
  setSessionMessages(sessionId: string, messages: ChatMessage[], userId?: string): Promise<void>;
35
+ /**
36
+ * Persist a step's tool calls and results as `tool_call` / `tool_result`
37
+ * messages on the session.
38
+ *
39
+ * Declared on the interface so every backend can implement it. Previously
40
+ * only the Redis manager had it, and the caller reached it by casting — so
41
+ * on in-memory storage tool activity never became messages at all, and the
42
+ * compaction, pruning and pair-repair paths saw a different history shape
43
+ * depending on `STORAGE_TYPE`.
44
+ */
45
+ storeToolExecution?(sessionId: string, userId: string | undefined, toolCalls: Array<{
46
+ toolCallId?: string;
47
+ toolName?: string;
48
+ args?: Record<string, unknown>;
49
+ [key: string]: unknown;
50
+ }>, toolResults: Array<{
51
+ toolCallId?: string;
52
+ output?: unknown;
53
+ result?: unknown;
54
+ error?: string;
55
+ [key: string]: unknown;
56
+ }>, currentTime?: Date): Promise<void>;
35
57
  /** Close/shutdown the memory manager and release resources (e.g., Redis connections) */
36
58
  close?(): Promise<void>;
37
59
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.10.5",
3
+ "version": "10.10.6",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -88,6 +88,7 @@
88
88
  "test:loop-guard-core": "npx tsx test/continuous-test-suite-loop-guard-core.ts",
89
89
  "test:openai-compat-guard": "npx tsx test/continuous-test-suite-openai-compat-guard.ts",
90
90
  "test:anthropic-guard": "npx tsx test/continuous-test-suite-anthropic-guard.ts",
91
+ "test:tool-storage-parity": "npx tsx test/continuous-test-suite-tool-storage-parity.ts",
91
92
  "test:middleware": "npx tsx test/continuous-test-suite-middleware.ts",
92
93
  "test:observability": "npx tsx test/continuous-test-suite-observability.ts",
93
94
  "test:ppt": "npx tsx test/continuous-test-suite-ppt.ts",