@juspay/neurolink 9.51.1 → 9.51.3

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.
@@ -19,6 +19,7 @@ import { LoopSession } from "../loop/session.js";
19
19
  import { initializeCliParser } from "../parser.js";
20
20
  import { formatFileSize, saveAudioToFile } from "../utils/audioFileUtils.js";
21
21
  import { resolveFilePaths } from "../utils/pathResolver.js";
22
+ import { animatedWrite } from "../utils/typewriter.js";
22
23
  import { formatVideoFileSize, getVideoMetadataSummary, saveVideoToFile, } from "../utils/videoFileUtils.js";
23
24
  import { OllamaCommandFactory } from "./ollamaCommandFactory.js";
24
25
  import { SageMakerCommandFactory } from "./sagemakerCommandFactory.js";
@@ -1995,7 +1996,7 @@ export class CLICommandFactory {
1995
1996
  ];
1996
1997
  let fullContent = "";
1997
1998
  for (const chunk of chunks) {
1998
- process.stdout.write(chunk);
1999
+ await animatedWrite(chunk);
1999
2000
  fullContent += chunk;
2000
2001
  await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate streaming delay
2001
2002
  }
@@ -2247,7 +2248,7 @@ export class CLICommandFactory {
2247
2248
  "string");
2248
2249
  };
2249
2250
  if (isText(evt)) {
2250
- process.stdout.write(evt.content);
2251
+ await animatedWrite(evt.content);
2251
2252
  fullContent += evt.content;
2252
2253
  }
2253
2254
  else if (isAudio(evt)) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Typewriter animation for CLI streaming output.
3
+ * Writes text character-by-character with a configurable delay.
4
+ * @module cli/utils/typewriter
5
+ */
6
+ /**
7
+ * Write text to stdout with a per-character typewriter animation.
8
+ * Falls back to raw write when delay is 0 or negative.
9
+ */
10
+ export declare function typewriterWrite(text: string, delayMs?: number): Promise<void>;
11
+ /** Whether typewriter animation should be used for the current process. */
12
+ export declare function shouldAnimate(): boolean;
13
+ /**
14
+ * Write text to stdout with animation when stdout is a TTY, otherwise
15
+ * fall back to a raw write. Use this from CLI streaming code paths to
16
+ * keep the behaviour consistent in one place.
17
+ */
18
+ export declare function animatedWrite(text: string): Promise<void>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Typewriter animation for CLI streaming output.
3
+ * Writes text character-by-character with a configurable delay.
4
+ * @module cli/utils/typewriter
5
+ */
6
+ const DEFAULT_CHAR_DELAY_MS = 8;
7
+ /**
8
+ * Write text to stdout with a per-character typewriter animation.
9
+ * Falls back to raw write when delay is 0 or negative.
10
+ */
11
+ export async function typewriterWrite(text, delayMs = DEFAULT_CHAR_DELAY_MS) {
12
+ if (!text) {
13
+ return;
14
+ }
15
+ if (delayMs <= 0) {
16
+ process.stdout.write(text);
17
+ return;
18
+ }
19
+ // Use Array.from to handle surrogate pairs / emoji correctly
20
+ for (const ch of Array.from(text)) {
21
+ process.stdout.write(ch);
22
+ await new Promise((r) => setTimeout(r, delayMs));
23
+ }
24
+ }
25
+ /** Whether typewriter animation should be used for the current process. */
26
+ export function shouldAnimate() {
27
+ return Boolean(process.stdout.isTTY);
28
+ }
29
+ /**
30
+ * Write text to stdout with animation when stdout is a TTY, otherwise
31
+ * fall back to a raw write. Use this from CLI streaming code paths to
32
+ * keep the behaviour consistent in one place.
33
+ */
34
+ export async function animatedWrite(text) {
35
+ if (shouldAnimate()) {
36
+ await typewriterWrite(text);
37
+ }
38
+ else {
39
+ process.stdout.write(text);
40
+ }
41
+ }
42
+ //# sourceMappingURL=typewriter.js.map
@@ -31,6 +31,16 @@ export declare class ToolsManager {
31
31
  protected sessionId?: string;
32
32
  protected userId?: string;
33
33
  constructor(providerName: AIProviderName, directTools: Record<string, unknown>, neurolink?: NeuroLink | undefined, utilities?: ToolUtilities | undefined);
34
+ /**
35
+ * BZ-666: Wrap tool execute with output truncation to prevent
36
+ * context overflow when large results flow into the AI SDK accumulator.
37
+ */
38
+ private wrapExecuteWithTruncation;
39
+ /**
40
+ * BZ-666: Apply generateToolOutputPreview to tool results to prevent
41
+ * context overflow when large results flow into the AI SDK accumulator.
42
+ */
43
+ private truncateToolResult;
34
44
  /**
35
45
  * Set session context for MCP tools
36
46
  */
@@ -22,6 +22,7 @@ import { SpanStatusCode } from "@opentelemetry/api";
22
22
  import { logger } from "../../utils/logger.js";
23
23
  import { getKeyCount } from "../../utils/transformationUtils.js";
24
24
  import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
25
+ import { generateToolOutputPreview } from "../../context/toolOutputLimits.js";
25
26
  /**
26
27
  * ToolsManager class - Handles all tool management operations
27
28
  */
@@ -44,6 +45,79 @@ export class ToolsManager {
44
45
  this.utilities = utilities;
45
46
  this.mcpTools = {};
46
47
  }
48
+ /**
49
+ * BZ-666: Wrap tool execute with output truncation to prevent
50
+ * context overflow when large results flow into the AI SDK accumulator.
51
+ */
52
+ wrapExecuteWithTruncation(toolName, originalExecute) {
53
+ return async (params) => {
54
+ const result = await originalExecute(params);
55
+ return this.truncateToolResult(toolName, result);
56
+ };
57
+ }
58
+ /**
59
+ * BZ-666: Apply generateToolOutputPreview to tool results to prevent
60
+ * context overflow when large results flow into the AI SDK accumulator.
61
+ */
62
+ truncateToolResult(toolName, result) {
63
+ if (result === null || result === undefined) {
64
+ return result;
65
+ }
66
+ // Handle string results directly
67
+ if (typeof result === "string") {
68
+ const { preview, truncated, originalSize } = generateToolOutputPreview(result);
69
+ if (truncated) {
70
+ logger.debug(`[ToolsManager] Truncated '${toolName}' string output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
71
+ }
72
+ return truncated ? preview : result;
73
+ }
74
+ // Handle object results (e.g. readFile returns { content, ... })
75
+ if (typeof result === "object") {
76
+ const obj = result;
77
+ let nextObj = null;
78
+ // Truncate "content" if present and oversized
79
+ if (typeof obj.content === "string") {
80
+ const { preview, truncated, originalSize } = generateToolOutputPreview(obj.content);
81
+ if (truncated) {
82
+ logger.debug(`[ToolsManager] Truncated '${toolName}' content field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
83
+ nextObj = { ...(nextObj ?? obj), content: preview };
84
+ }
85
+ }
86
+ // Truncate "data" if present and oversized — both fields can coexist
87
+ if (typeof obj.data === "string") {
88
+ const { preview, truncated, originalSize } = generateToolOutputPreview(obj.data);
89
+ if (truncated) {
90
+ logger.debug(`[ToolsManager] Truncated '${toolName}' data field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
91
+ nextObj = { ...(nextObj ?? obj), data: preview };
92
+ }
93
+ }
94
+ if (nextObj) {
95
+ return nextObj;
96
+ }
97
+ // For other objects, check if their JSON serialization is too large.
98
+ // Use UTF-8 byte length, not string length, to match the 50KB budget.
99
+ try {
100
+ const jsonStr = JSON.stringify(result);
101
+ if (Buffer.byteLength(jsonStr, "utf-8") > 51_200) {
102
+ const { preview, truncated, originalSize } = generateToolOutputPreview(jsonStr);
103
+ if (truncated) {
104
+ logger.debug(`[ToolsManager] Truncated '${toolName}' JSON output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
105
+ // Preserve object shape so callers reading structured fields don't
106
+ // get a type surprise. Attach the preview under a sentinel field.
107
+ return {
108
+ _truncated: true,
109
+ _originalSize: originalSize,
110
+ _preview: preview,
111
+ };
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // JSON serialization failed — return as-is
117
+ }
118
+ }
119
+ return result;
120
+ }
47
121
  /**
48
122
  * Set session context for MCP tools
49
123
  */
@@ -179,14 +253,15 @@ export class ToolsManager {
179
253
  typeof directTool === "object" &&
180
254
  "execute" in directTool) {
181
255
  const originalExecute = directTool.execute;
182
- // Create a new tool with wrapped execute function
256
+ // Create a new tool with wrapped execute function (BZ-666/BZ-664 guards applied)
257
+ const guardedExecute = this.wrapExecuteWithTruncation(toolName, originalExecute);
183
258
  tools[toolName] = {
184
259
  ...directTool,
185
260
  execute: async (params) => {
186
261
  const startTime = Date.now();
187
262
  this.emitToolEvent("tool:start", toolName, { input: params });
188
263
  try {
189
- const result = await originalExecute(params);
264
+ const result = await guardedExecute(params);
190
265
  this.emitToolEvent("tool:end", toolName, {
191
266
  result,
192
267
  success: true,
@@ -228,6 +303,12 @@ export class ToolsManager {
228
303
  if (toolInfo && typeof toolInfo.execute === "function") {
229
304
  const tool = await this.createCustomToolFromDefinition(toolName, toolInfo);
230
305
  if (tool && !tools[toolName]) {
306
+ // BZ-666/BZ-664: Wrap custom tool execute with guards
307
+ const origExec = tool.execute;
308
+ if (origExec) {
309
+ const guarded = this.wrapExecuteWithTruncation(toolName, origExec);
310
+ tool.execute = guarded;
311
+ }
231
312
  tools[toolName] = tool;
232
313
  }
233
314
  }
@@ -444,47 +525,42 @@ export class ToolsManager {
444
525
  ? this.utilities.createPermissiveZodSchema()
445
526
  : z.object({});
446
527
  }
528
+ // BZ-666/BZ-664: Wrap the raw MCP execute with guards before event wrapping
529
+ const rawExecute = async (params) => {
530
+ if (this.neurolink &&
531
+ typeof this.neurolink.executeExternalMCPTool === "function") {
532
+ return this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params);
533
+ }
534
+ throw new Error(`Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`);
535
+ };
536
+ const guardedExecute = this.wrapExecuteWithTruncation(tool.name, rawExecute);
447
537
  return createAISDKTool({
448
538
  description: tool.description || `External MCP tool ${tool.name}`,
449
539
  inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
450
540
  execute: async (params) => {
451
541
  const startTime = Date.now();
452
542
  this.emitToolEvent("tool:start", tool.name, { input: params });
453
- // Execute via NeuroLink's direct tool execution
454
- if (this.neurolink &&
455
- typeof this.neurolink.executeExternalMCPTool === "function") {
456
- try {
457
- const result = await this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params);
458
- this.emitToolEvent("tool:end", tool.name, {
459
- result,
460
- success: true,
461
- responseTime: Date.now() - startTime,
462
- });
463
- return result;
464
- }
465
- catch (mcpError) {
466
- const errorMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
467
- this.emitToolEvent("tool:end", tool.name, {
468
- error: errorMsg,
469
- success: false,
470
- responseTime: Date.now() - startTime,
471
- });
472
- logger.error(`External MCP tool failed: ${tool.name}`, {
473
- serverId: tool.serverId,
474
- error: errorMsg,
475
- });
476
- throw mcpError;
477
- }
543
+ try {
544
+ const result = await guardedExecute(params);
545
+ this.emitToolEvent("tool:end", tool.name, {
546
+ result,
547
+ success: true,
548
+ responseTime: Date.now() - startTime,
549
+ });
550
+ return result;
478
551
  }
479
- else {
480
- const error = `Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`;
552
+ catch (mcpError) {
553
+ const errorMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
481
554
  this.emitToolEvent("tool:end", tool.name, {
482
- error,
555
+ error: errorMsg,
483
556
  success: false,
484
557
  responseTime: Date.now() - startTime,
485
558
  });
486
- logger.error(error);
487
- throw new Error(error);
559
+ logger.error(`External MCP tool failed: ${tool.name}`, {
560
+ serverId: tool.serverId,
561
+ error: errorMsg,
562
+ });
563
+ throw mcpError;
488
564
  }
489
565
  },
490
566
  });
@@ -31,6 +31,16 @@ export declare class ToolsManager {
31
31
  protected sessionId?: string;
32
32
  protected userId?: string;
33
33
  constructor(providerName: AIProviderName, directTools: Record<string, unknown>, neurolink?: NeuroLink | undefined, utilities?: ToolUtilities | undefined);
34
+ /**
35
+ * BZ-666: Wrap tool execute with output truncation to prevent
36
+ * context overflow when large results flow into the AI SDK accumulator.
37
+ */
38
+ private wrapExecuteWithTruncation;
39
+ /**
40
+ * BZ-666: Apply generateToolOutputPreview to tool results to prevent
41
+ * context overflow when large results flow into the AI SDK accumulator.
42
+ */
43
+ private truncateToolResult;
34
44
  /**
35
45
  * Set session context for MCP tools
36
46
  */
@@ -22,6 +22,7 @@ import { SpanStatusCode } from "@opentelemetry/api";
22
22
  import { logger } from "../../utils/logger.js";
23
23
  import { getKeyCount } from "../../utils/transformationUtils.js";
24
24
  import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
25
+ import { generateToolOutputPreview } from "../../context/toolOutputLimits.js";
25
26
  /**
26
27
  * ToolsManager class - Handles all tool management operations
27
28
  */
@@ -44,6 +45,79 @@ export class ToolsManager {
44
45
  this.utilities = utilities;
45
46
  this.mcpTools = {};
46
47
  }
48
+ /**
49
+ * BZ-666: Wrap tool execute with output truncation to prevent
50
+ * context overflow when large results flow into the AI SDK accumulator.
51
+ */
52
+ wrapExecuteWithTruncation(toolName, originalExecute) {
53
+ return async (params) => {
54
+ const result = await originalExecute(params);
55
+ return this.truncateToolResult(toolName, result);
56
+ };
57
+ }
58
+ /**
59
+ * BZ-666: Apply generateToolOutputPreview to tool results to prevent
60
+ * context overflow when large results flow into the AI SDK accumulator.
61
+ */
62
+ truncateToolResult(toolName, result) {
63
+ if (result === null || result === undefined) {
64
+ return result;
65
+ }
66
+ // Handle string results directly
67
+ if (typeof result === "string") {
68
+ const { preview, truncated, originalSize } = generateToolOutputPreview(result);
69
+ if (truncated) {
70
+ logger.debug(`[ToolsManager] Truncated '${toolName}' string output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
71
+ }
72
+ return truncated ? preview : result;
73
+ }
74
+ // Handle object results (e.g. readFile returns { content, ... })
75
+ if (typeof result === "object") {
76
+ const obj = result;
77
+ let nextObj = null;
78
+ // Truncate "content" if present and oversized
79
+ if (typeof obj.content === "string") {
80
+ const { preview, truncated, originalSize } = generateToolOutputPreview(obj.content);
81
+ if (truncated) {
82
+ logger.debug(`[ToolsManager] Truncated '${toolName}' content field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
83
+ nextObj = { ...(nextObj ?? obj), content: preview };
84
+ }
85
+ }
86
+ // Truncate "data" if present and oversized — both fields can coexist
87
+ if (typeof obj.data === "string") {
88
+ const { preview, truncated, originalSize } = generateToolOutputPreview(obj.data);
89
+ if (truncated) {
90
+ logger.debug(`[ToolsManager] Truncated '${toolName}' data field: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
91
+ nextObj = { ...(nextObj ?? obj), data: preview };
92
+ }
93
+ }
94
+ if (nextObj) {
95
+ return nextObj;
96
+ }
97
+ // For other objects, check if their JSON serialization is too large.
98
+ // Use UTF-8 byte length, not string length, to match the 50KB budget.
99
+ try {
100
+ const jsonStr = JSON.stringify(result);
101
+ if (Buffer.byteLength(jsonStr, "utf-8") > 51_200) {
102
+ const { preview, truncated, originalSize } = generateToolOutputPreview(jsonStr);
103
+ if (truncated) {
104
+ logger.debug(`[ToolsManager] Truncated '${toolName}' JSON output: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes`);
105
+ // Preserve object shape so callers reading structured fields don't
106
+ // get a type surprise. Attach the preview under a sentinel field.
107
+ return {
108
+ _truncated: true,
109
+ _originalSize: originalSize,
110
+ _preview: preview,
111
+ };
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // JSON serialization failed — return as-is
117
+ }
118
+ }
119
+ return result;
120
+ }
47
121
  /**
48
122
  * Set session context for MCP tools
49
123
  */
@@ -179,14 +253,15 @@ export class ToolsManager {
179
253
  typeof directTool === "object" &&
180
254
  "execute" in directTool) {
181
255
  const originalExecute = directTool.execute;
182
- // Create a new tool with wrapped execute function
256
+ // Create a new tool with wrapped execute function (BZ-666/BZ-664 guards applied)
257
+ const guardedExecute = this.wrapExecuteWithTruncation(toolName, originalExecute);
183
258
  tools[toolName] = {
184
259
  ...directTool,
185
260
  execute: async (params) => {
186
261
  const startTime = Date.now();
187
262
  this.emitToolEvent("tool:start", toolName, { input: params });
188
263
  try {
189
- const result = await originalExecute(params);
264
+ const result = await guardedExecute(params);
190
265
  this.emitToolEvent("tool:end", toolName, {
191
266
  result,
192
267
  success: true,
@@ -228,6 +303,12 @@ export class ToolsManager {
228
303
  if (toolInfo && typeof toolInfo.execute === "function") {
229
304
  const tool = await this.createCustomToolFromDefinition(toolName, toolInfo);
230
305
  if (tool && !tools[toolName]) {
306
+ // BZ-666/BZ-664: Wrap custom tool execute with guards
307
+ const origExec = tool.execute;
308
+ if (origExec) {
309
+ const guarded = this.wrapExecuteWithTruncation(toolName, origExec);
310
+ tool.execute = guarded;
311
+ }
231
312
  tools[toolName] = tool;
232
313
  }
233
314
  }
@@ -444,47 +525,42 @@ export class ToolsManager {
444
525
  ? this.utilities.createPermissiveZodSchema()
445
526
  : z.object({});
446
527
  }
528
+ // BZ-666/BZ-664: Wrap the raw MCP execute with guards before event wrapping
529
+ const rawExecute = async (params) => {
530
+ if (this.neurolink &&
531
+ typeof this.neurolink.executeExternalMCPTool === "function") {
532
+ return this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params);
533
+ }
534
+ throw new Error(`Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`);
535
+ };
536
+ const guardedExecute = this.wrapExecuteWithTruncation(tool.name, rawExecute);
447
537
  return createAISDKTool({
448
538
  description: tool.description || `External MCP tool ${tool.name}`,
449
539
  inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
450
540
  execute: async (params) => {
451
541
  const startTime = Date.now();
452
542
  this.emitToolEvent("tool:start", tool.name, { input: params });
453
- // Execute via NeuroLink's direct tool execution
454
- if (this.neurolink &&
455
- typeof this.neurolink.executeExternalMCPTool === "function") {
456
- try {
457
- const result = await this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params);
458
- this.emitToolEvent("tool:end", tool.name, {
459
- result,
460
- success: true,
461
- responseTime: Date.now() - startTime,
462
- });
463
- return result;
464
- }
465
- catch (mcpError) {
466
- const errorMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
467
- this.emitToolEvent("tool:end", tool.name, {
468
- error: errorMsg,
469
- success: false,
470
- responseTime: Date.now() - startTime,
471
- });
472
- logger.error(`External MCP tool failed: ${tool.name}`, {
473
- serverId: tool.serverId,
474
- error: errorMsg,
475
- });
476
- throw mcpError;
477
- }
543
+ try {
544
+ const result = await guardedExecute(params);
545
+ this.emitToolEvent("tool:end", tool.name, {
546
+ result,
547
+ success: true,
548
+ responseTime: Date.now() - startTime,
549
+ });
550
+ return result;
478
551
  }
479
- else {
480
- const error = `Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`;
552
+ catch (mcpError) {
553
+ const errorMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
481
554
  this.emitToolEvent("tool:end", tool.name, {
482
- error,
555
+ error: errorMsg,
483
556
  success: false,
484
557
  responseTime: Date.now() - startTime,
485
558
  });
486
- logger.error(error);
487
- throw new Error(error);
559
+ logger.error(`External MCP tool failed: ${tool.name}`, {
560
+ serverId: tool.serverId,
561
+ error: errorMsg,
562
+ });
563
+ throw mcpError;
488
564
  }
489
565
  },
490
566
  });
@@ -775,17 +775,18 @@ export class NeuroLink {
775
775
  initializeMCPEnhancements(config) {
776
776
  const mcpConfig = config?.mcp;
777
777
  this.mcpEnhancementsConfig = mcpConfig;
778
- // ToolCache — disabled by default, opt-in
779
- if (mcpConfig?.cache?.enabled) {
778
+ // BZ-664: ToolCache — enabled by default to prevent duplicate tool calls.
779
+ // Callers can explicitly opt out via mcp.cache.enabled = false.
780
+ if (mcpConfig?.cache?.enabled !== false) {
780
781
  this.mcpToolResultCache = new ToolResultCache({
781
- ttl: mcpConfig.cache.ttl ?? 300_000,
782
- maxSize: mcpConfig.cache.maxSize ?? 500,
783
- strategy: mcpConfig.cache.strategy ?? "lru",
782
+ ttl: mcpConfig?.cache?.ttl ?? 300_000,
783
+ maxSize: mcpConfig?.cache?.maxSize ?? 500,
784
+ strategy: mcpConfig?.cache?.strategy ?? "lru",
784
785
  });
785
786
  logger.debug("[NeuroLink] MCP tool result cache initialized", {
786
- ttl: mcpConfig.cache.ttl ?? 300_000,
787
- maxSize: mcpConfig.cache.maxSize ?? 500,
788
- strategy: mcpConfig.cache.strategy ?? "lru",
787
+ ttl: mcpConfig?.cache?.ttl ?? 300_000,
788
+ maxSize: mcpConfig?.cache?.maxSize ?? 500,
789
+ strategy: mcpConfig?.cache?.strategy ?? "lru",
789
790
  });
790
791
  }
791
792
  // ToolCallBatcher — disabled by default, opt-in
@@ -7628,7 +7629,36 @@ Current user's request: ${currentInput}`;
7628
7629
  async executeExternalMCPTool(serverId, toolName, parameters, options) {
7629
7630
  try {
7630
7631
  mcpLogger.debug(`[NeuroLink] Executing external MCP tool: ${toolName} on ${serverId}`);
7632
+ // BZ-664: Check existing ToolResultCache before executing to avoid
7633
+ // duplicate identical calls within the same session.
7634
+ //
7635
+ // Safety guards aligned with executeToolInternal():
7636
+ // - Skip destructive tools (destructiveHint annotation)
7637
+ // - Scope cache key by serverId (two servers can expose same tool name)
7638
+ // and toolExecutionContext (prevents cross-session/user leaks)
7639
+ const toolAnnotations = this.getToolAnnotationsForExecution(toolName);
7640
+ const cacheEnabled = !!this.mcpToolResultCache &&
7641
+ !this._disableToolCacheForCurrentRequest &&
7642
+ !toolAnnotations?.destructiveHint;
7643
+ const cacheKeyArgs = {
7644
+ __serverId: serverId,
7645
+ __args: parameters,
7646
+ ...(this.toolExecutionContext
7647
+ ? { __ctx: this.toolExecutionContext }
7648
+ : {}),
7649
+ };
7650
+ if (cacheEnabled && this.mcpToolResultCache) {
7651
+ const cached = this.mcpToolResultCache.getCachedResult(toolName, cacheKeyArgs);
7652
+ if (cached !== undefined) {
7653
+ mcpLogger.debug(`[NeuroLink] Tool result cache HIT: ${toolName} on ${serverId}`);
7654
+ return cached;
7655
+ }
7656
+ }
7631
7657
  const result = await this.externalServerManager.executeTool(serverId, toolName, parameters, options);
7658
+ // BZ-664: Store result in cache after successful execution
7659
+ if (cacheEnabled && this.mcpToolResultCache) {
7660
+ this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result);
7661
+ }
7632
7662
  mcpLogger.debug(`[NeuroLink] External MCP tool executed successfully: ${toolName}`);
7633
7663
  return result;
7634
7664
  }
@@ -4,7 +4,7 @@ import { stepCountIs, streamText } from "ai";
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
- import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, } from "../auth/anthropicOAuth.js";
7
+ import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, CLAUDE_CODE_OAUTH_BETAS, } from "../auth/anthropicOAuth.js";
8
8
  import { AnthropicModels, TOKEN_EXPIRY_BUFFER_MS, } from "../constants/enums.js";
9
9
  import { BaseProvider } from "../core/baseProvider.js";
10
10
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
@@ -310,6 +310,9 @@ export class AnthropicProvider extends BaseProvider {
310
310
  anthropic = createAnthropic({
311
311
  apiKey: apiKeyToUse,
312
312
  headers,
313
+ ...(process.env.ANTHROPIC_BASE_URL && {
314
+ baseURL: process.env.ANTHROPIC_BASE_URL,
315
+ }),
313
316
  fetch: createProxyFetch(),
314
317
  });
315
318
  logger.debug("Anthropic Provider initialized with API key", {
@@ -354,9 +357,23 @@ export class AnthropicProvider extends BaseProvider {
354
357
  */
355
358
  getAuthHeaders() {
356
359
  const headers = {};
357
- // Add beta headers if enabled
360
+ // When routing through proxy (ANTHROPIC_BASE_URL set), use the full
361
+ // OAuth beta set so the proxy forwards them upstream. Without these,
362
+ // Anthropic treats the request with tighter non-subscription rate limits.
363
+ const usingProxy = !!process.env.ANTHROPIC_BASE_URL;
358
364
  if (this.enableBetaFeatures) {
359
- headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
365
+ if (usingProxy) {
366
+ headers["anthropic-beta"] = [
367
+ ...CLAUDE_CODE_OAUTH_BETAS,
368
+ "fine-grained-tool-streaming-2025-05-14",
369
+ "context-1m-2025-08-07",
370
+ "interleaved-thinking-2025-05-14",
371
+ "redact-thinking-2026-02-12",
372
+ ].join(",");
373
+ }
374
+ else {
375
+ headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
376
+ }
360
377
  }
361
378
  // Add subscription-specific headers if applicable
362
379
  if (this.subscriptionTier !== "api") {
@@ -2,7 +2,7 @@ const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
2
2
  const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
3
3
  const HIGH_TOOL_COUNT_THRESHOLD = 24;
4
4
  const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
5
- const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 120_000;
5
+ const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 10_000;
6
6
  const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
7
7
  export function inferClaudeProxyModelTier(modelName) {
8
8
  const normalized = modelName.toLowerCase();
@@ -221,10 +221,15 @@ export function applyRateLimitCooldownScope(args) {
221
221
  const rcBackoffLevels = args.state.requestClassBackoffLevels ?? {};
222
222
  const mtBackoffLevels = args.state.modelTierBackoffLevels ?? {};
223
223
  const scopedBackoffLevel = Math.max(rcBackoffLevels[requestClassKey] ?? 0, mtBackoffLevels[modelTierKey] ?? 0);
224
- const floorMs = args.profile.modelTier === "opus" || args.profile.requiresStrongToolFidelity
225
- ? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
226
- : args.profile.isHighToolCountNonStream
227
- ? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
224
+ // High-tool-count-non-stream gets its own (lower) floor so that requests
225
+ // recover faster once proper OAuth betas are forwarded. Check it first
226
+ // because every >=24-tool request also satisfies requiresStrongToolFidelity
227
+ // (threshold 8), which would otherwise shadow this branch.
228
+ const floorMs = args.profile.isHighToolCountNonStream
229
+ ? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
230
+ : args.profile.modelTier === "opus" ||
231
+ args.profile.requiresStrongToolFidelity
232
+ ? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
228
233
  : DEFAULT_COOLDOWN_FLOOR_MS;
229
234
  const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
230
235
  const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);