@juspay/neurolink 9.51.3 → 9.52.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 (118) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/artifacts/artifactStore.d.ts +56 -0
  3. package/dist/artifacts/artifactStore.js +143 -0
  4. package/dist/browser/neurolink.min.js +311 -298
  5. package/dist/cli/commands/mcp.d.ts +6 -0
  6. package/dist/cli/commands/mcp.js +128 -86
  7. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  8. package/dist/core/factory.d.ts +2 -2
  9. package/dist/core/factory.js +4 -4
  10. package/dist/core/redisConversationMemoryManager.js +20 -0
  11. package/dist/factories/providerFactory.d.ts +4 -4
  12. package/dist/factories/providerFactory.js +20 -7
  13. package/dist/factories/providerRegistry.d.ts +5 -0
  14. package/dist/factories/providerRegistry.js +45 -26
  15. package/dist/lib/artifacts/artifactStore.d.ts +56 -0
  16. package/dist/lib/artifacts/artifactStore.js +144 -0
  17. package/dist/lib/core/factory.d.ts +2 -2
  18. package/dist/lib/core/factory.js +4 -4
  19. package/dist/lib/core/redisConversationMemoryManager.js +20 -0
  20. package/dist/lib/factories/providerFactory.d.ts +4 -4
  21. package/dist/lib/factories/providerFactory.js +20 -7
  22. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  23. package/dist/lib/factories/providerRegistry.js +45 -26
  24. package/dist/lib/mcp/externalServerManager.d.ts +6 -0
  25. package/dist/lib/mcp/externalServerManager.js +9 -0
  26. package/dist/lib/mcp/mcpOutputNormalizer.d.ts +49 -0
  27. package/dist/lib/mcp/mcpOutputNormalizer.js +182 -0
  28. package/dist/lib/mcp/toolDiscoveryService.d.ts +10 -0
  29. package/dist/lib/mcp/toolDiscoveryService.js +32 -1
  30. package/dist/lib/memory/memoryRetrievalTools.d.ts +64 -9
  31. package/dist/lib/memory/memoryRetrievalTools.js +77 -9
  32. package/dist/lib/neurolink.d.ts +23 -0
  33. package/dist/lib/neurolink.js +128 -86
  34. package/dist/lib/providers/amazonBedrock.d.ts +6 -1
  35. package/dist/lib/providers/amazonBedrock.js +14 -2
  36. package/dist/lib/providers/amazonSagemaker.d.ts +7 -1
  37. package/dist/lib/providers/amazonSagemaker.js +21 -3
  38. package/dist/lib/providers/anthropic.d.ts +4 -1
  39. package/dist/lib/providers/anthropic.js +18 -5
  40. package/dist/lib/providers/azureOpenai.d.ts +2 -1
  41. package/dist/lib/providers/azureOpenai.js +10 -5
  42. package/dist/lib/providers/googleAiStudio.d.ts +4 -1
  43. package/dist/lib/providers/googleAiStudio.js +6 -7
  44. package/dist/lib/providers/googleVertex.d.ts +3 -1
  45. package/dist/lib/providers/googleVertex.js +96 -17
  46. package/dist/lib/providers/huggingFace.d.ts +2 -1
  47. package/dist/lib/providers/huggingFace.js +4 -4
  48. package/dist/lib/providers/litellm.d.ts +5 -1
  49. package/dist/lib/providers/litellm.js +14 -9
  50. package/dist/lib/providers/mistral.d.ts +2 -1
  51. package/dist/lib/providers/mistral.js +2 -2
  52. package/dist/lib/providers/ollama.d.ts +3 -1
  53. package/dist/lib/providers/ollama.js +2 -2
  54. package/dist/lib/providers/openAI.d.ts +5 -1
  55. package/dist/lib/providers/openAI.js +15 -5
  56. package/dist/lib/providers/openRouter.d.ts +5 -1
  57. package/dist/lib/providers/openRouter.js +17 -5
  58. package/dist/lib/providers/openaiCompatible.d.ts +4 -1
  59. package/dist/lib/providers/openaiCompatible.js +15 -3
  60. package/dist/lib/session/globalSessionState.js +44 -1
  61. package/dist/lib/types/artifactTypes.d.ts +63 -0
  62. package/dist/lib/types/artifactTypes.js +11 -0
  63. package/dist/lib/types/configTypes.d.ts +39 -0
  64. package/dist/lib/types/conversation.d.ts +7 -0
  65. package/dist/lib/types/generateTypes.d.ts +13 -0
  66. package/dist/lib/types/index.d.ts +2 -0
  67. package/dist/lib/types/mcpOutputTypes.d.ts +40 -0
  68. package/dist/lib/types/mcpOutputTypes.js +9 -0
  69. package/dist/lib/types/providers.d.ts +75 -0
  70. package/dist/lib/types/streamTypes.d.ts +7 -1
  71. package/dist/mcp/externalServerManager.d.ts +6 -0
  72. package/dist/mcp/externalServerManager.js +9 -0
  73. package/dist/mcp/mcpOutputNormalizer.d.ts +49 -0
  74. package/dist/mcp/mcpOutputNormalizer.js +181 -0
  75. package/dist/mcp/toolDiscoveryService.d.ts +10 -0
  76. package/dist/mcp/toolDiscoveryService.js +32 -1
  77. package/dist/memory/memoryRetrievalTools.d.ts +64 -9
  78. package/dist/memory/memoryRetrievalTools.js +77 -9
  79. package/dist/neurolink.d.ts +23 -0
  80. package/dist/neurolink.js +128 -86
  81. package/dist/providers/amazonBedrock.d.ts +6 -1
  82. package/dist/providers/amazonBedrock.js +14 -2
  83. package/dist/providers/amazonSagemaker.d.ts +7 -1
  84. package/dist/providers/amazonSagemaker.js +21 -3
  85. package/dist/providers/anthropic.d.ts +4 -1
  86. package/dist/providers/anthropic.js +18 -5
  87. package/dist/providers/azureOpenai.d.ts +2 -1
  88. package/dist/providers/azureOpenai.js +10 -5
  89. package/dist/providers/googleAiStudio.d.ts +4 -1
  90. package/dist/providers/googleAiStudio.js +6 -7
  91. package/dist/providers/googleVertex.d.ts +3 -1
  92. package/dist/providers/googleVertex.js +96 -17
  93. package/dist/providers/huggingFace.d.ts +2 -1
  94. package/dist/providers/huggingFace.js +4 -4
  95. package/dist/providers/litellm.d.ts +5 -1
  96. package/dist/providers/litellm.js +14 -9
  97. package/dist/providers/mistral.d.ts +2 -1
  98. package/dist/providers/mistral.js +2 -2
  99. package/dist/providers/ollama.d.ts +3 -1
  100. package/dist/providers/ollama.js +2 -2
  101. package/dist/providers/openAI.d.ts +5 -1
  102. package/dist/providers/openAI.js +15 -5
  103. package/dist/providers/openRouter.d.ts +5 -1
  104. package/dist/providers/openRouter.js +17 -5
  105. package/dist/providers/openaiCompatible.d.ts +4 -1
  106. package/dist/providers/openaiCompatible.js +15 -3
  107. package/dist/session/globalSessionState.js +44 -1
  108. package/dist/types/artifactTypes.d.ts +63 -0
  109. package/dist/types/artifactTypes.js +10 -0
  110. package/dist/types/configTypes.d.ts +39 -0
  111. package/dist/types/conversation.d.ts +7 -0
  112. package/dist/types/generateTypes.d.ts +13 -0
  113. package/dist/types/index.d.ts +2 -0
  114. package/dist/types/mcpOutputTypes.d.ts +40 -0
  115. package/dist/types/mcpOutputTypes.js +8 -0
  116. package/dist/types/providers.d.ts +75 -0
  117. package/dist/types/streamTypes.d.ts +7 -1
  118. package/package.json +3 -2
@@ -26,9 +26,20 @@ export class ToolDiscoveryService extends EventEmitter {
26
26
  toolRegistry = new Map();
27
27
  serverTools = new Map();
28
28
  discoveryInProgress = new Set();
29
+ /** Optional normalizer applied to every tool output before it is returned. */
30
+ outputNormalizer;
29
31
  constructor() {
30
32
  super();
31
33
  }
34
+ /**
35
+ * Attach a McpOutputNormalizer.
36
+ * When set, every raw callTool() result is passed through the normalizer
37
+ * before being returned. Oversized outputs are replaced with compact
38
+ * surrogates according to the configured strategy.
39
+ */
40
+ setOutputNormalizer(normalizer) {
41
+ this.outputNormalizer = normalizer;
42
+ }
32
43
  /**
33
44
  * Discover tools from an external MCP server
34
45
  */
@@ -361,6 +372,26 @@ export class ToolDiscoveryService extends EventEmitter {
361
372
  arguments: parameters,
362
373
  }), timeout, new Error(`Tool execution timeout: ${toolName}`));
363
374
  callSpan.setStatus({ code: SpanStatusCode.OK });
375
+ // ── MCP output normalization ──────────────────────────────────
376
+ // Intercept here — after receive, before cache, before memory,
377
+ // before LLM context injection. Returns a compact surrogate when
378
+ // the payload exceeds mcp.outputLimits.maxBytes.
379
+ if (this.outputNormalizer) {
380
+ try {
381
+ const normalized = await this.outputNormalizer.normalize(callResult, { toolName, serverId });
382
+ callSpan.setAttribute("mcp.output.strategy", normalized.isExternalized ? "externalize" : "inline");
383
+ if (normalized.isExternalized) {
384
+ callSpan.setAttribute("mcp.output.original_bytes", normalized.originalBytes);
385
+ }
386
+ return normalized.result;
387
+ }
388
+ catch (normErr) {
389
+ mcpLogger.warn(`[ToolDiscoveryService] McpOutputNormalizer failed for ` +
390
+ `${toolName}: ${normErr instanceof Error ? normErr.message : String(normErr)} ` +
391
+ `— returning raw result`);
392
+ }
393
+ }
394
+ // ── end normalization ─────────────────────────────────────────
364
395
  return callResult;
365
396
  }
366
397
  catch (err) {
@@ -385,7 +416,7 @@ export class ToolDiscoveryService extends EventEmitter {
385
416
  }
386
417
  mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
387
418
  duration,
388
- hasContent: !!result.content,
419
+ hasContent: !!result?.content,
389
420
  });
390
421
  return {
391
422
  success: true,
@@ -7,14 +7,21 @@
7
7
  * @module
8
8
  */
9
9
  import type { RedisConversationMemoryManager } from "../core/redisConversationMemoryManager.js";
10
+ import type { ArtifactStore } from "../artifacts/artifactStore.js";
10
11
  /**
11
12
  * Factory function that creates memory retrieval tools bound to a memory manager.
12
- * @param memoryManager - The Redis conversation memory manager instance
13
+ *
14
+ * @param memoryManager Redis conversation memory manager instance.
15
+ * @param artifactStore Optional artifact store for externalized MCP outputs.
16
+ * When provided, retrieve_context gains an `artifactId`
17
+ * parameter that fetches the full payload written by
18
+ * McpOutputNormalizer under strategy="externalize".
13
19
  * @returns Record of tool name to Vercel AI SDK tool definition
14
20
  */
15
- export declare function createMemoryRetrievalTools(memoryManager: RedisConversationMemoryManager): {
21
+ export declare function createMemoryRetrievalTools(memoryManager: RedisConversationMemoryManager | undefined, artifactStore?: ArtifactStore): {
16
22
  retrieve_context: import("ai").Tool<{
17
- sessionId: string;
23
+ sessionId?: string | undefined;
24
+ artifactId?: string | undefined;
18
25
  messageId?: string | undefined;
19
26
  role?: "system" | "user" | "assistant" | "tool_call" | "tool_result" | undefined;
20
27
  lastN?: number | undefined;
@@ -22,14 +29,62 @@ export declare function createMemoryRetrievalTools(memoryManager: RedisConversat
22
29
  limit?: number | undefined;
23
30
  search?: string | undefined;
24
31
  }, {
32
+ error: string;
33
+ artifactId: string;
34
+ content?: undefined;
35
+ totalSize?: undefined;
36
+ hasMore?: undefined;
37
+ offset?: undefined;
38
+ limit?: undefined;
39
+ sessionId?: undefined;
40
+ messageId?: undefined;
41
+ messages?: undefined;
42
+ totalMessages?: undefined;
43
+ } | {
44
+ artifactId: string;
45
+ content: string;
46
+ totalSize: number;
47
+ hasMore: boolean;
48
+ offset: number;
49
+ limit: number;
50
+ error?: undefined;
51
+ sessionId?: undefined;
52
+ messageId?: undefined;
53
+ messages?: undefined;
54
+ totalMessages?: undefined;
55
+ } | {
56
+ error: string;
57
+ artifactId?: undefined;
58
+ content?: undefined;
59
+ totalSize?: undefined;
60
+ hasMore?: undefined;
61
+ offset?: undefined;
62
+ limit?: undefined;
63
+ sessionId?: undefined;
64
+ messageId?: undefined;
65
+ messages?: undefined;
66
+ totalMessages?: undefined;
67
+ } | {
25
68
  error: string;
26
69
  sessionId: string;
70
+ artifactId?: undefined;
71
+ content?: undefined;
72
+ totalSize?: undefined;
73
+ hasMore?: undefined;
74
+ offset?: undefined;
75
+ limit?: undefined;
27
76
  messageId?: undefined;
28
77
  messages?: undefined;
29
78
  totalMessages?: undefined;
30
79
  } | {
31
80
  error: string;
32
81
  messageId: string;
82
+ artifactId?: undefined;
83
+ content?: undefined;
84
+ totalSize?: undefined;
85
+ hasMore?: undefined;
86
+ offset?: undefined;
87
+ limit?: undefined;
33
88
  sessionId?: undefined;
34
89
  messages?: undefined;
35
90
  totalMessages?: undefined;
@@ -70,13 +125,13 @@ export declare function createMemoryRetrievalTools(memoryManager: RedisConversat
70
125
  })[];
71
126
  totalMessages: number;
72
127
  error?: undefined;
128
+ artifactId?: undefined;
129
+ content?: undefined;
130
+ totalSize?: undefined;
131
+ hasMore?: undefined;
132
+ offset?: undefined;
133
+ limit?: undefined;
73
134
  sessionId?: undefined;
74
135
  messageId?: undefined;
75
- } | {
76
- error: string;
77
- sessionId?: undefined;
78
- messageId?: undefined;
79
- messages?: undefined;
80
- totalMessages?: undefined;
81
136
  }>;
82
137
  };
@@ -9,6 +9,7 @@
9
9
  import { tool } from "ai";
10
10
  import { z } from "zod";
11
11
  import { logger } from "../utils/logger.js";
12
+ import { withTimeout } from "../utils/errorHandling.js";
12
13
  import { SpanSerializer, SpanType, SpanStatus, } from "../observability/index.js";
13
14
  import { getMetricsAggregator } from "../observability/index.js";
14
15
  /** Maximum characters returned per retrieval request */
@@ -19,18 +20,36 @@ const MAX_RETRIEVAL_LIMIT = 200_000;
19
20
  const MAX_SEARCH_MATCHES = 50;
20
21
  /**
21
22
  * Factory function that creates memory retrieval tools bound to a memory manager.
22
- * @param memoryManager - The Redis conversation memory manager instance
23
+ *
24
+ * @param memoryManager Redis conversation memory manager instance.
25
+ * @param artifactStore Optional artifact store for externalized MCP outputs.
26
+ * When provided, retrieve_context gains an `artifactId`
27
+ * parameter that fetches the full payload written by
28
+ * McpOutputNormalizer under strategy="externalize".
23
29
  * @returns Record of tool name to Vercel AI SDK tool definition
24
30
  */
25
- export function createMemoryRetrievalTools(memoryManager) {
31
+ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
26
32
  return {
27
33
  retrieve_context: tool({
28
- description: "Retrieve messages from conversation memory. Use this to access full tool " +
29
- "outputs when a result was truncated, review previous assistant responses, " +
30
- "or search through conversation history. Supports filtering by role, " +
31
- "pagination for large content, and regex search within messages.",
34
+ description: "Retrieve messages from conversation memory, or fetch the full payload of " +
35
+ "an externalized MCP tool output by artifact ID. Use this to:\n" +
36
+ " Access full tool outputs when a result was truncated or externalized\n" +
37
+ " Review previous assistant responses\n" +
38
+ "• Search through conversation history\n" +
39
+ "Supports filtering by role, pagination for large content, and regex search.\n" +
40
+ "To fetch an externalized artifact, provide `artifactId` (omit sessionId).",
32
41
  inputSchema: z.object({
33
- sessionId: z.string().describe("Session ID for the conversation"),
42
+ sessionId: z
43
+ .string()
44
+ .optional()
45
+ .describe("Session ID for conversation history retrieval. " +
46
+ "Required unless artifactId is provided."),
47
+ artifactId: z
48
+ .string()
49
+ .optional()
50
+ .describe("Artifact ID from an externalized MCP tool output " +
51
+ "(visible in the tool output as neurolinkArtifactId=<id>). " +
52
+ "When provided, returns the full stored payload directly."),
34
53
  messageId: z
35
54
  .string()
36
55
  .optional()
@@ -64,19 +83,68 @@ export function createMemoryRetrievalTools(memoryManager) {
64
83
  "Returns matching lines with line numbers."),
65
84
  }),
66
85
  execute: async (args) => {
86
+ // ── Artifact resolution path ────────────────────────────────────────
87
+ // When the caller supplies an artifactId we short-circuit to the
88
+ // artifact store (bypassing Redis) and return the full payload with
89
+ // optional offset/limit pagination.
90
+ if (args.artifactId) {
91
+ if (!artifactStore) {
92
+ logger.warn("[MemoryRetrievalTools] retrieve_context called with artifactId " +
93
+ "but no ArtifactStore is configured");
94
+ return {
95
+ error: "Artifact store not configured — " +
96
+ "mcp.outputLimits.strategy must be set to 'externalize' to use artifactId retrieval",
97
+ artifactId: args.artifactId,
98
+ };
99
+ }
100
+ const content = await withTimeout(artifactStore.retrieve(args.artifactId), 10_000, new Error(`ArtifactStore.retrieve() timed out for artifact "${args.artifactId}"`));
101
+ if (content === null) {
102
+ return {
103
+ error: "Artifact not found or has expired",
104
+ artifactId: args.artifactId,
105
+ };
106
+ }
107
+ const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
108
+ const start = args.offset ?? 0;
109
+ const slice = content.slice(start, start + charLimit);
110
+ return {
111
+ artifactId: args.artifactId,
112
+ content: slice,
113
+ totalSize: content.length,
114
+ hasMore: start + charLimit < content.length,
115
+ offset: start,
116
+ limit: charLimit,
117
+ };
118
+ }
119
+ // ── End artifact resolution ─────────────────────────────────────────
120
+ if (!args.sessionId) {
121
+ return {
122
+ error: "sessionId is required when artifactId is not provided",
123
+ };
124
+ }
125
+ if (!memoryManager) {
126
+ return {
127
+ error: "Session history retrieval requires Redis conversation memory — " +
128
+ "enable mcp.conversationMemory with a Redis backend, or use " +
129
+ "artifactId to retrieve an externalized MCP tool output.",
130
+ };
131
+ }
67
132
  const span = SpanSerializer.createSpan(SpanType.MEMORY, "memory.retrieve", {
68
133
  "memory.operation": "retrieve",
69
134
  "memory.store": "redis",
70
135
  "memory.query": args.search || args.messageId || `lastN:${args.lastN ?? "all"}`,
71
136
  });
72
137
  const startTime = Date.now();
138
+ // args.sessionId is guaranteed non-null here — we returned early above
139
+ // when it was missing. Cast via string coercion to satisfy eslint.
140
+ const sessionId = String(args.sessionId);
73
141
  try {
74
- const conversation = await memoryManager.getSessionRaw(args.sessionId);
142
+ const conversation = await withTimeout(memoryManager.getSessionRaw(sessionId), 10_000, new Error(`getSessionRaw() timed out for session "${sessionId}"`));
75
143
  if (!conversation) {
76
144
  span.durationMs = Date.now() - startTime;
77
145
  const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK);
78
146
  getMetricsAggregator().recordSpan(endedSpan);
79
- return { error: "Session not found", sessionId: args.sessionId };
147
+ return { error: "Session not found", sessionId };
80
148
  }
81
149
  let messages = conversation.messages;
82
150
  // Filter by specific messageId
@@ -47,6 +47,8 @@ export declare class NeuroLink {
47
47
  private mcpToolBatcher?;
48
48
  private mcpEnhancedDiscovery?;
49
49
  private mcpToolMiddlewares;
50
+ /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
51
+ private mcpArtifactStore?;
50
52
  private _disableToolCacheForCurrentRequest;
51
53
  private mcpEnhancementsConfig?;
52
54
  private toolCircuitBreakers;
@@ -71,6 +73,27 @@ export declare class NeuroLink {
71
73
  private authProvider?;
72
74
  private pendingAuthConfig?;
73
75
  private authInitPromise?;
76
+ private credentials?;
77
+ /**
78
+ * Merge instance-level credentials with per-call credentials.
79
+ *
80
+ * Semantics: **deep merge at the provider level.** For each provider key
81
+ * present in both `this.credentials` and `callCredentials`, the per-call
82
+ * fields are merged ON TOP of the instance-level fields, so fields not
83
+ * mentioned in the per-call slice are preserved.
84
+ *
85
+ * Example:
86
+ * ```
87
+ * instance: { openai: { apiKey: "key1", baseURL: "url1" } }
88
+ * per-call: { openai: { apiKey: "key2" } }
89
+ * merged: { openai: { apiKey: "key2", baseURL: "url1" } } // baseURL preserved
90
+ * ```
91
+ *
92
+ * Providers present only in one source are carried through unchanged.
93
+ * Unrelated providers (not overridden in callCredentials) are carried through
94
+ * from instance credentials unchanged.
95
+ */
96
+ private resolveCredentials;
74
97
  private hitlManager?;
75
98
  private _sessionCostUsd;
76
99
  private fileRegistry;
package/dist/neurolink.js CHANGED
@@ -40,6 +40,8 @@ import { ToolCallBatcher } from "./mcp/batching/index.js";
40
40
  import { ToolResultCache } from "./mcp/caching/index.js";
41
41
  import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
42
42
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
43
+ import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
44
+ import { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
43
45
  import { ToolRouter } from "./mcp/routing/index.js";
44
46
  // Import direct tools server for automatic registration
45
47
  import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
@@ -216,6 +218,8 @@ export class NeuroLink {
216
218
  mcpToolBatcher;
217
219
  mcpEnhancedDiscovery;
218
220
  mcpToolMiddlewares = [];
221
+ /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
222
+ mcpArtifactStore;
219
223
  _disableToolCacheForCurrentRequest = false;
220
224
  mcpEnhancementsConfig;
221
225
  // Enhanced error handling support
@@ -255,6 +259,60 @@ export class NeuroLink {
255
259
  authProvider;
256
260
  pendingAuthConfig;
257
261
  authInitPromise;
262
+ // Per-provider credential overrides (instance-level default)
263
+ credentials;
264
+ /**
265
+ * Merge instance-level credentials with per-call credentials.
266
+ *
267
+ * Semantics: **deep merge at the provider level.** For each provider key
268
+ * present in both `this.credentials` and `callCredentials`, the per-call
269
+ * fields are merged ON TOP of the instance-level fields, so fields not
270
+ * mentioned in the per-call slice are preserved.
271
+ *
272
+ * Example:
273
+ * ```
274
+ * instance: { openai: { apiKey: "key1", baseURL: "url1" } }
275
+ * per-call: { openai: { apiKey: "key2" } }
276
+ * merged: { openai: { apiKey: "key2", baseURL: "url1" } } // baseURL preserved
277
+ * ```
278
+ *
279
+ * Providers present only in one source are carried through unchanged.
280
+ * Unrelated providers (not overridden in callCredentials) are carried through
281
+ * from instance credentials unchanged.
282
+ */
283
+ resolveCredentials(callCredentials) {
284
+ if (!this.credentials && !callCredentials) {
285
+ return undefined;
286
+ }
287
+ if (!this.credentials) {
288
+ return callCredentials;
289
+ }
290
+ if (!callCredentials) {
291
+ return this.credentials;
292
+ }
293
+ // Per-provider deep merge: for each provider key in the per-call
294
+ // override, merge its fields on top of the instance-level slice so
295
+ // individual fields (e.g. baseURL) are preserved when only apiKey
296
+ // is overridden per-call.
297
+ const merged = { ...this.credentials };
298
+ for (const key of Object.keys(callCredentials)) {
299
+ const instanceSlice = this.credentials[key];
300
+ const callSlice = callCredentials[key];
301
+ if (instanceSlice &&
302
+ callSlice &&
303
+ typeof instanceSlice === "object" &&
304
+ typeof callSlice === "object") {
305
+ merged[key] = {
306
+ ...instanceSlice,
307
+ ...callSlice,
308
+ };
309
+ }
310
+ else {
311
+ merged[key] = callSlice ?? instanceSlice;
312
+ }
313
+ }
314
+ return merged;
315
+ }
258
316
  // HITL (Human-in-the-Loop) support
259
317
  hitlManager;
260
318
  // Accumulated cost in USD across all generate() calls on this instance
@@ -595,6 +653,10 @@ export class NeuroLink {
595
653
  if (config?.auth) {
596
654
  this.pendingAuthConfig = config.auth;
597
655
  }
656
+ // Store per-provider credential overrides
657
+ if (config?.credentials) {
658
+ this.credentials = config.credentials;
659
+ }
598
660
  // Store task config for lazy initialization
599
661
  this._taskManagerConfig = config?.tasks;
600
662
  // Eagerly create TaskManager and register tools if config is provided
@@ -818,6 +880,25 @@ export class NeuroLink {
818
880
  });
819
881
  }
820
882
  // ToolRouter — lazy-initialized when 2+ external servers exist (see addExternalMCPServer)
883
+ // McpOutputNormalizer — active when mcp.outputLimits is configured
884
+ if (mcpConfig?.outputLimits) {
885
+ const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
886
+ const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
887
+ const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
888
+ let artifactStore;
889
+ if (strategy === "externalize") {
890
+ artifactStore = new LocalTempArtifactStore();
891
+ this.mcpArtifactStore = artifactStore;
892
+ logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
893
+ }
894
+ const normalizer = new McpOutputNormalizer({ strategy, maxBytes, warnBytes }, artifactStore);
895
+ this.externalServerManager.setOutputNormalizer(normalizer);
896
+ logger.debug("[NeuroLink] MCP output normalizer initialized", {
897
+ strategy,
898
+ maxBytes,
899
+ warnBytes,
900
+ });
901
+ }
821
902
  }
822
903
  /**
823
904
  * Register file reference tools with the MCP tool registry.
@@ -937,90 +1018,46 @@ export class NeuroLink {
937
1018
  "redis" in memConfig &&
938
1019
  !!memConfig.redis) ||
939
1020
  process.env.STORAGE_TYPE === "redis";
940
- if (!memConfig?.enabled || !hasRedisConfig) {
941
- logger.debug("[NeuroLink] Skipping memory retrieval tools requires Redis conversation memory");
1021
+ const hasArtifactStore = !!this.mcpArtifactStore;
1022
+ // Register when Redis is configured OR when an artifact store exists.
1023
+ // Artifact store alone is sufficient for the artifactId retrieval path —
1024
+ // session history retrieval just returns a clear error when Redis is absent.
1025
+ if ((!memConfig?.enabled || !hasRedisConfig) && !hasArtifactStore) {
1026
+ logger.debug("[NeuroLink] Skipping memory retrieval tools — requires Redis conversation memory or an artifact store");
942
1027
  return;
943
1028
  }
944
- const tools = {
945
- retrieve_context: {
946
- description: "Retrieve messages from conversation memory. Use this to access full tool " +
947
- "outputs when a result was truncated, review previous assistant responses, " +
948
- "or search through conversation history.",
949
- execute: async (params) => {
950
- // Lazy access: conversationMemory is initialized on first generate() call
951
- const memoryManager = this.conversationMemory;
952
- if (!memoryManager || !("getSessionRaw" in memoryManager)) {
953
- return {
954
- success: false,
955
- error: "Memory retrieval not available — Redis memory manager not initialized",
956
- metadata: {
957
- toolName: "retrieve_context",
958
- serverId: "direct",
959
- executionTime: 0,
960
- },
961
- };
962
- }
963
- const actualTools = createMemoryRetrievalTools(memoryManager);
964
- const result = await actualTools.retrieve_context.execute(params, {
965
- toolCallId: "memory-retrieval",
966
- messages: [],
967
- });
968
- // Check if the tool itself reported an error
969
- const hasError = result &&
970
- typeof result === "object" &&
971
- "error" in result &&
972
- !("messages" in result);
973
- const errorMsg = hasError
974
- ? result.error
975
- : undefined;
976
- return {
977
- success: !hasError,
978
- data: result,
979
- ...(errorMsg ? { error: errorMsg } : {}),
980
- metadata: {
981
- toolName: "retrieve_context",
982
- serverId: "direct",
983
- executionTime: 0,
984
- },
985
- };
986
- },
1029
+ // Extract the canonical tool definition (schema + description) from the
1030
+ // memoryRetrievalTools factory. We pass undefined as the memoryManager here
1031
+ // because we only need the Zod inputSchema and description at registration
1032
+ // time the actual manager is resolved lazily at execution time.
1033
+ const canonicalTools = createMemoryRetrievalTools(undefined, this.mcpArtifactStore);
1034
+ const retrieveContextDef = canonicalTools.retrieve_context;
1035
+ // Register via this.registerTool() so the tool ends up in the "user-defined"
1036
+ // category inside toolRegistry. getCustomTools() returns that category, which
1037
+ // is what ToolsManager reads to build the tool schema sent to the LLM.
1038
+ // (Tools registered via toolRegistry.registerTool() directly land in the
1039
+ // "built-in" category and are never included in the LLM's tool schema.)
1040
+ this.registerTool("retrieve_context", {
1041
+ name: "retrieve_context",
1042
+ description: retrieveContextDef.description ?? "Retrieve context or artifacts",
1043
+ // Pass the Zod schema so ToolsManager gives the LLM full parameter types.
1044
+ // registerTool() detects isZodSchema on inputSchema and preserves it.
1045
+ inputSchema: retrieveContextDef
1046
+ .inputSchema,
1047
+ execute: async (params) => {
1048
+ // Lazy: conversationMemory is initialized on the first generate() call.
1049
+ // When only an artifact store is present (no Redis), memoryManager is
1050
+ // undefined — createMemoryRetrievalTools handles that via an explicit guard.
1051
+ const memoryManager = this.conversationMemory;
1052
+ const tools = createMemoryRetrievalTools(memoryManager, this.mcpArtifactStore);
1053
+ // Return the result directly so the LLM receives clean output instead
1054
+ // of a nested { success, data, metadata } wrapper.
1055
+ // Bounded by TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS so a stalled Redis or
1056
+ // filesystem backend never hangs the tool call indefinitely.
1057
+ return await withTimeout(tools.retrieve_context.execute(params, { toolCallId: "memory-retrieval", messages: [] }), TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS, ErrorFactory.toolTimeout("retrieve_context", TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS));
987
1058
  },
988
- };
989
- const registrations = Object.entries(tools).map(async ([toolName, toolDef]) => {
990
- const toolId = `direct.${toolName}`;
991
- const toolInfo = {
992
- name: toolName,
993
- description: toolDef.description,
994
- inputSchema: {},
995
- serverId: "direct",
996
- category: "built-in",
997
- };
998
- await this.toolRegistry.registerTool(toolId, toolInfo, {
999
- execute: async (params) => {
1000
- try {
1001
- return await toolDef.execute(params);
1002
- }
1003
- catch (error) {
1004
- // Known limitation: this non-throwing error path returns
1005
- // { success: false } without recording errorCategories in
1006
- // toolExecutionMetrics. These are internal memory-tool failures
1007
- // (low frequency), so the risk of metric gaps is minimal.
1008
- // A full fix would require access to the metrics map here,
1009
- // which is not available in the registration closure.
1010
- return {
1011
- success: false,
1012
- error: error instanceof Error ? error.message : String(error),
1013
- metadata: { toolName, serverId: "direct", executionTime: 0 },
1014
- };
1015
- }
1016
- },
1017
- description: toolDef.description,
1018
- inputSchema: {},
1019
- });
1020
- });
1021
- void Promise.all(registrations).then(() => {
1022
- logger.info("[NeuroLink] Memory retrieval tools registered");
1023
1059
  });
1060
+ logger.info("[NeuroLink] Memory retrieval tools registered");
1024
1061
  }
1025
1062
  /** Format memory context for prompt inclusion */
1026
1063
  formatMemoryContext(memoryContext, currentInput) {
@@ -2143,6 +2180,7 @@ Current user's request: ${currentInput}`;
2143
2180
  }
2144
2181
  }
2145
2182
  logger.debug("[NeuroLink] Graceful shutdown completed");
2183
+ this.credentials = undefined;
2146
2184
  }
2147
2185
  catch (error) {
2148
2186
  logger.error("[NeuroLink] Shutdown failed:", error);
@@ -2671,6 +2709,7 @@ Current user's request: ${currentInput}`;
2671
2709
  skipToolPromptInjection: options.skipToolPromptInjection,
2672
2710
  middleware: options.middleware,
2673
2711
  conversationMessages: options.conversationMessages,
2712
+ credentials: options.credentials,
2674
2713
  };
2675
2714
  const extraContext = options;
2676
2715
  if (extraContext.sessionId || extraContext.userId) {
@@ -2805,7 +2844,7 @@ Current user's request: ${currentInput}`;
2805
2844
  const { extractPPTContext, getEffectivePPTProvider } = await import("./features/ppt/utils.js");
2806
2845
  // Get provider instance for content planning
2807
2846
  const requestedProvider = (options.provider || "vertex");
2808
- const provider = await AIProviderFactory.createProvider(requestedProvider, options.model, true, this);
2847
+ const provider = await AIProviderFactory.createProvider(requestedProvider, options.model, true, this, undefined, this.resolveCredentials(options.credentials));
2809
2848
  // Resolve effective PPT provider (may auto-select if current is not PPT-compatible)
2810
2849
  const effectiveProvider = await getEffectivePPTProvider(provider, requestedProvider, options.model || "default", this);
2811
2850
  // Extract PPT context from options
@@ -3839,7 +3878,7 @@ Current user's request: ${currentInput}`;
3839
3878
  }
3840
3879
  async generateWithMCPProvider(context) {
3841
3880
  const { options, requestId, functionTag, tryMCPStartTime, providerName, availableTools, enhancedSystemPrompt, conversationMessages, } = context;
3842
- const provider = await AIProviderFactory.createProvider(providerName, options.model, !options.disableTools, this, options.region);
3881
+ const provider = await AIProviderFactory.createProvider(providerName, options.model, !options.disableTools, this, options.region, this.resolveCredentials(options.credentials));
3843
3882
  provider.setTraceContext(this._metricsTraceContext);
3844
3883
  this.emitter.emit("connected");
3845
3884
  this.emitter.emit("message", `${providerName} provider initialized successfully`);
@@ -4035,7 +4074,8 @@ Current user's request: ${currentInput}`;
4035
4074
  }
4036
4075
  const provider = await AIProviderFactory.createProvider(providerName, options.model, !options.disableTools, // Pass disableTools as inverse of enableMCP
4037
4076
  this, // Pass SDK instance
4038
- options.region);
4077
+ options.region, // Pass region parameter
4078
+ this.resolveCredentials(options.credentials));
4039
4079
  // Propagate trace context for parent-child span hierarchy
4040
4080
  provider.setTraceContext(this._metricsTraceContext);
4041
4081
  // ADD: Emit connection events for successful provider creation (Bedrock-compatible)
@@ -4783,7 +4823,7 @@ Current user's request: ${currentInput}`;
4783
4823
  reason: errorMsg,
4784
4824
  });
4785
4825
  try {
4786
- const fallbackProvider = await AIProviderFactory.createProvider(fallbackRoute.provider, fallbackRoute.model);
4826
+ const fallbackProvider = await AIProviderFactory.createProvider(fallbackRoute.provider, fallbackRoute.model, true, undefined, undefined, this.resolveCredentials(enhancedOptions.credentials));
4787
4827
  // Ensure fallback provider can execute tools
4788
4828
  fallbackProvider.setupToolExecutor({
4789
4829
  customTools: this.getCustomTools(),
@@ -4946,7 +4986,8 @@ Current user's request: ${currentInput}`;
4946
4986
  const providerName = await getBestProvider(options.provider);
4947
4987
  const provider = await AIProviderFactory.createProvider(providerName, options.model, !options.disableTools, // Pass disableTools as inverse of enableMCP
4948
4988
  this, // Pass SDK instance
4949
- options.region);
4989
+ options.region, // Pass region parameter
4990
+ this.resolveCredentials(options.credentials));
4950
4991
  // Propagate trace context for parent-child span hierarchy
4951
4992
  provider.setTraceContext(this._metricsTraceContext);
4952
4993
  // Enable tool execution for the provider using BaseProvider method
@@ -5161,7 +5202,7 @@ Current user's request: ${currentInput}`;
5161
5202
  const originalPrompt = options.input.text;
5162
5203
  const responseTime = Date.now() - startTime;
5163
5204
  const providerName = await getBestProvider(options.provider);
5164
- const provider = await AIProviderFactory.createProvider(providerName, options.model);
5205
+ const provider = await AIProviderFactory.createProvider(providerName, options.model, true, undefined, undefined, this.resolveCredentials(options.credentials));
5165
5206
  const fallbackStreamResult = await provider.stream({
5166
5207
  input: { text: options.input.text },
5167
5208
  model: options.model,
@@ -8523,6 +8564,7 @@ Current user's request: ${currentInput}`;
8523
8564
  this.mcpInitialized = false;
8524
8565
  this.mcpInitPromise = null;
8525
8566
  this.conversationMemoryNeedsInit = false;
8567
+ this.credentials = undefined;
8526
8568
  logger.debug("[NeuroLink] Initialization state reset successfully");
8527
8569
  }
8528
8570
  catch (error) {