@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +272 -272
- package/dist/cli/factories/commandFactory.js +3 -2
- package/dist/cli/utils/typewriter.d.ts +18 -0
- package/dist/cli/utils/typewriter.js +42 -0
- package/dist/core/modules/ToolsManager.d.ts +10 -0
- package/dist/core/modules/ToolsManager.js +108 -32
- package/dist/lib/core/modules/ToolsManager.d.ts +10 -0
- package/dist/lib/core/modules/ToolsManager.js +108 -32
- package/dist/lib/neurolink.js +38 -8
- package/dist/lib/providers/anthropic.js +20 -3
- package/dist/lib/proxy/routingPolicy.js +10 -5
- package/dist/lib/types/configTypes.d.ts +2 -2
- package/dist/neurolink.js +38 -8
- package/dist/providers/anthropic.js +20 -3
- package/dist/proxy/routingPolicy.js +10 -5
- package/dist/types/configTypes.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
480
|
-
const
|
|
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(
|
|
487
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
480
|
-
const
|
|
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(
|
|
487
|
-
|
|
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
|
});
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -775,17 +775,18 @@ export class NeuroLink {
|
|
|
775
775
|
initializeMCPEnhancements(config) {
|
|
776
776
|
const mcpConfig = config?.mcp;
|
|
777
777
|
this.mcpEnhancementsConfig = mcpConfig;
|
|
778
|
-
// ToolCache —
|
|
779
|
-
|
|
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
|
|
782
|
-
maxSize: mcpConfig
|
|
783
|
-
strategy: mcpConfig
|
|
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
|
|
787
|
-
maxSize: mcpConfig
|
|
788
|
-
strategy: mcpConfig
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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);
|