@juspay/neurolink 9.51.2 → 9.51.4
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/artifacts/artifactStore.d.ts +56 -0
- package/dist/artifacts/artifactStore.js +143 -0
- package/dist/browser/neurolink.min.js +314 -303
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +128 -86
- 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/core/redisConversationMemoryManager.js +20 -0
- package/dist/lib/artifacts/artifactStore.d.ts +56 -0
- package/dist/lib/artifacts/artifactStore.js +144 -0
- package/dist/lib/core/modules/ToolsManager.d.ts +10 -0
- package/dist/lib/core/modules/ToolsManager.js +108 -32
- package/dist/lib/core/redisConversationMemoryManager.js +20 -0
- package/dist/lib/mcp/externalServerManager.d.ts +6 -0
- package/dist/lib/mcp/externalServerManager.js +9 -0
- package/dist/lib/mcp/mcpOutputNormalizer.d.ts +49 -0
- package/dist/lib/mcp/mcpOutputNormalizer.js +182 -0
- package/dist/lib/mcp/toolDiscoveryService.d.ts +10 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -1
- package/dist/lib/memory/memoryRetrievalTools.d.ts +64 -9
- package/dist/lib/memory/memoryRetrievalTools.js +77 -9
- package/dist/lib/neurolink.d.ts +2 -0
- package/dist/lib/neurolink.js +97 -88
- package/dist/lib/session/globalSessionState.js +44 -1
- package/dist/lib/types/artifactTypes.d.ts +63 -0
- package/dist/lib/types/artifactTypes.js +11 -0
- package/dist/lib/types/configTypes.d.ts +34 -2
- package/dist/lib/types/conversation.d.ts +7 -0
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/mcpOutputTypes.d.ts +40 -0
- package/dist/lib/types/mcpOutputTypes.js +9 -0
- package/dist/mcp/externalServerManager.d.ts +6 -0
- package/dist/mcp/externalServerManager.js +9 -0
- package/dist/mcp/mcpOutputNormalizer.d.ts +49 -0
- package/dist/mcp/mcpOutputNormalizer.js +181 -0
- package/dist/mcp/toolDiscoveryService.d.ts +10 -0
- package/dist/mcp/toolDiscoveryService.js +32 -1
- package/dist/memory/memoryRetrievalTools.d.ts +64 -9
- package/dist/memory/memoryRetrievalTools.js +77 -9
- package/dist/neurolink.d.ts +2 -0
- package/dist/neurolink.js +97 -88
- package/dist/session/globalSessionState.js +44 -1
- package/dist/types/artifactTypes.d.ts +63 -0
- package/dist/types/artifactTypes.js +10 -0
- package/dist/types/configTypes.d.ts +34 -2
- package/dist/types/conversation.d.ts +7 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/mcpOutputTypes.d.ts +40 -0
- package/dist/types/mcpOutputTypes.js +8 -0
- package/package.json +1 -1
|
@@ -67,6 +67,12 @@ export declare class MCPCommandFactory {
|
|
|
67
67
|
* Execute exec command
|
|
68
68
|
*/
|
|
69
69
|
private static executeExec;
|
|
70
|
+
/**
|
|
71
|
+
* Run the actual MCP tool execution and format/write the output.
|
|
72
|
+
* Extracted to keep executeExec nesting within ESLint max-depth limit.
|
|
73
|
+
* Returns 0 on success, 1 on failure.
|
|
74
|
+
*/
|
|
75
|
+
private static runExecTool;
|
|
70
76
|
/**
|
|
71
77
|
* Execute remove command
|
|
72
78
|
*/
|
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -656,104 +656,146 @@ export class MCPCommandFactory {
|
|
|
656
656
|
}
|
|
657
657
|
}
|
|
658
658
|
const sdk = new NeuroLink();
|
|
659
|
-
|
|
660
|
-
// Check if server exists and is connected
|
|
661
|
-
const allServers = await sdk.listMCPServers();
|
|
662
|
-
const server = allServers.find((s) => s.name === serverName);
|
|
663
|
-
if (!server) {
|
|
664
|
-
if (spinner) {
|
|
665
|
-
spinner.fail();
|
|
666
|
-
}
|
|
667
|
-
logger.error(chalk.red(`❌ Server not found: ${serverName}`));
|
|
668
|
-
process.exit(1);
|
|
669
|
-
}
|
|
670
|
-
if (server.status !== "connected") {
|
|
671
|
-
if (spinner) {
|
|
672
|
-
spinner.fail();
|
|
673
|
-
}
|
|
674
|
-
logger.error(chalk.red(`❌ Server not connected: ${serverName}`));
|
|
675
|
-
logger.always(chalk.yellow("💡 Try: neurolink mcp test " + serverName));
|
|
676
|
-
process.exit(1);
|
|
677
|
-
}
|
|
678
|
-
// Check if tool exists
|
|
679
|
-
const tool = server.tools?.find((t) => t.name === toolName);
|
|
680
|
-
if (!tool) {
|
|
681
|
-
if (spinner) {
|
|
682
|
-
spinner.fail();
|
|
683
|
-
}
|
|
684
|
-
logger.error(chalk.red(`❌ Tool not found: ${toolName}`));
|
|
685
|
-
if (server.tools?.length) {
|
|
686
|
-
logger.always(chalk.blue("Available tools:"));
|
|
687
|
-
server.tools.forEach((t) => {
|
|
688
|
-
logger.always(` • ${t.name}: ${t.description}`);
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
process.exit(1);
|
|
692
|
-
}
|
|
693
|
-
// Execute the tool using the NeuroLink MCP tool registry
|
|
659
|
+
let exitCode = 0;
|
|
694
660
|
try {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
tool: toolName,
|
|
706
|
-
server: serverName,
|
|
707
|
-
params,
|
|
708
|
-
result: executionResult,
|
|
709
|
-
success: true,
|
|
710
|
-
timestamp: new Date().toISOString(),
|
|
711
|
-
};
|
|
712
|
-
if (spinner) {
|
|
713
|
-
spinner.succeed(chalk.green("✅ Tool executed successfully"));
|
|
714
|
-
}
|
|
715
|
-
// Display results
|
|
716
|
-
if (argv.format === "json") {
|
|
717
|
-
logger.always(JSON.stringify(result, null, 2));
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
logger.always(chalk.green("🔧 Tool Execution Results:"));
|
|
721
|
-
logger.always(` Tool: ${chalk.cyan(toolName)}`);
|
|
722
|
-
logger.always(` Server: ${chalk.cyan(serverName)}`);
|
|
723
|
-
logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`);
|
|
724
|
-
logger.always(` Timestamp: ${result.timestamp}`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
catch (toolError) {
|
|
728
|
-
const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
|
|
729
|
-
if (spinner) {
|
|
730
|
-
spinner.fail(chalk.red("❌ Tool execution failed"));
|
|
661
|
+
await this.getMCPStatusWithTimeout(sdk);
|
|
662
|
+
// Check if server exists and is connected
|
|
663
|
+
const allServers = await sdk.listMCPServers();
|
|
664
|
+
const server = allServers.find((s) => s.name === serverName);
|
|
665
|
+
if (!server) {
|
|
666
|
+
if (spinner) {
|
|
667
|
+
spinner.fail();
|
|
668
|
+
}
|
|
669
|
+
logger.error(chalk.red(`❌ Server not found: ${serverName}`));
|
|
670
|
+
exitCode = 1;
|
|
731
671
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
};
|
|
740
|
-
if (argv.format === "json") {
|
|
741
|
-
logger.always(JSON.stringify(result, null, 2));
|
|
672
|
+
else if (server.status !== "connected") {
|
|
673
|
+
if (spinner) {
|
|
674
|
+
spinner.fail();
|
|
675
|
+
}
|
|
676
|
+
logger.error(chalk.red(`❌ Server not connected: ${serverName}`));
|
|
677
|
+
logger.always(chalk.yellow("💡 Try: neurolink mcp test " + serverName));
|
|
678
|
+
exitCode = 1;
|
|
742
679
|
}
|
|
743
680
|
else {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
681
|
+
// Check if tool exists
|
|
682
|
+
const tool = server.tools?.find((t) => t.name === toolName);
|
|
683
|
+
if (!tool) {
|
|
684
|
+
if (spinner) {
|
|
685
|
+
spinner.fail();
|
|
686
|
+
}
|
|
687
|
+
logger.error(chalk.red(`❌ Tool not found: ${toolName}`));
|
|
688
|
+
if (server.tools?.length) {
|
|
689
|
+
logger.always(chalk.blue("Available tools:"));
|
|
690
|
+
server.tools.forEach((t) => {
|
|
691
|
+
logger.always(` • ${t.name}: ${t.description}`);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
exitCode = 1;
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
exitCode = await this.runExecTool(toolName, serverName, params, argv, spinner);
|
|
698
|
+
}
|
|
748
699
|
}
|
|
749
|
-
process.exit(1);
|
|
750
700
|
}
|
|
701
|
+
finally {
|
|
702
|
+
// Always shut down MCP connections to prevent lingering child processes
|
|
703
|
+
await sdk.shutdown().catch(() => undefined);
|
|
704
|
+
}
|
|
705
|
+
// Flush stdout/stderr before exit so buffered output (spinner lines,
|
|
706
|
+
// result text) is not truncated by process.exit(). process.exit() is
|
|
707
|
+
// still required because MCP stdio connections and health-check timers
|
|
708
|
+
// keep the Node.js event loop alive even after sdk.shutdown().
|
|
709
|
+
await new Promise((resolve) => {
|
|
710
|
+
process.stdout.write("", () => {
|
|
711
|
+
process.stderr.write("", () => resolve());
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
process.exit(exitCode);
|
|
751
715
|
}
|
|
752
716
|
catch (_error) {
|
|
753
717
|
logger.error(chalk.red(`❌ Exec command failed: ${_error.message}`));
|
|
754
718
|
process.exit(1);
|
|
755
719
|
}
|
|
756
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Run the actual MCP tool execution and format/write the output.
|
|
723
|
+
* Extracted to keep executeExec nesting within ESLint max-depth limit.
|
|
724
|
+
* Returns 0 on success, 1 on failure.
|
|
725
|
+
*/
|
|
726
|
+
static async runExecTool(toolName, serverName, params, argv, spinner) {
|
|
727
|
+
const WARN_BYTES = 50 * 1024; // 50 KB
|
|
728
|
+
try {
|
|
729
|
+
const { toolRegistry } = await import("../../lib/mcp/toolRegistry.js");
|
|
730
|
+
const executionResult = await withTimeout(toolRegistry.executeTool(toolName, params, {
|
|
731
|
+
sessionId: `cli-${Date.now()}`,
|
|
732
|
+
userId: process.env.USER || "cli-user",
|
|
733
|
+
config: { domainType: "cli-execution", customData: { serverName } },
|
|
734
|
+
}), MCP_STATUS_TIMEOUT_MS, ErrorFactory.toolTimeout(toolName, MCP_STATUS_TIMEOUT_MS));
|
|
735
|
+
const result = {
|
|
736
|
+
tool: toolName,
|
|
737
|
+
server: serverName,
|
|
738
|
+
params,
|
|
739
|
+
result: executionResult,
|
|
740
|
+
success: true,
|
|
741
|
+
timestamp: new Date().toISOString(),
|
|
742
|
+
};
|
|
743
|
+
if (spinner) {
|
|
744
|
+
spinner.succeed(chalk.green("✅ Tool executed successfully"));
|
|
745
|
+
}
|
|
746
|
+
const resultJson = JSON.stringify(result, null, 2);
|
|
747
|
+
const resultBytes = Buffer.byteLength(resultJson, "utf-8");
|
|
748
|
+
const outputFile = argv.output;
|
|
749
|
+
if (outputFile) {
|
|
750
|
+
await fs.promises.writeFile(outputFile, resultJson, "utf-8");
|
|
751
|
+
logger.always(chalk.green(`✅ Result saved to: ${chalk.cyan(outputFile)}`));
|
|
752
|
+
logger.always(` Size: ${resultBytes >= 1024 ? `${(resultBytes / 1024).toFixed(1)} KB` : `${resultBytes} B`}`);
|
|
753
|
+
}
|
|
754
|
+
else if (argv.format === "json") {
|
|
755
|
+
if (resultBytes > WARN_BYTES) {
|
|
756
|
+
logger.warn(chalk.yellow(`⚠ Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
|
|
757
|
+
`Use --output <file> to save to disk instead.`));
|
|
758
|
+
}
|
|
759
|
+
logger.always(resultJson);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
if (resultBytes > WARN_BYTES) {
|
|
763
|
+
logger.warn(chalk.yellow(`⚠ Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
|
|
764
|
+
`Use --output <file> to save to disk instead.`));
|
|
765
|
+
}
|
|
766
|
+
logger.always(chalk.green("🔧 Tool Execution Results:"));
|
|
767
|
+
logger.always(` Tool: ${chalk.cyan(toolName)}`);
|
|
768
|
+
logger.always(` Server: ${chalk.cyan(serverName)}`);
|
|
769
|
+
logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`);
|
|
770
|
+
logger.always(` Timestamp: ${result.timestamp}`);
|
|
771
|
+
}
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
catch (toolError) {
|
|
775
|
+
const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
|
|
776
|
+
if (spinner) {
|
|
777
|
+
spinner.fail(chalk.red("❌ Tool execution failed"));
|
|
778
|
+
}
|
|
779
|
+
const result = {
|
|
780
|
+
tool: toolName,
|
|
781
|
+
server: serverName,
|
|
782
|
+
params,
|
|
783
|
+
_error: errorMessage,
|
|
784
|
+
success: false,
|
|
785
|
+
timestamp: new Date().toISOString(),
|
|
786
|
+
};
|
|
787
|
+
if (argv.format === "json") {
|
|
788
|
+
logger.always(JSON.stringify(result, null, 2));
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
logger.error(chalk.red("🔧 Tool Execution Failed:"));
|
|
792
|
+
logger.error(` Tool: ${chalk.cyan(toolName)}`);
|
|
793
|
+
logger.error(` Server: ${chalk.cyan(serverName)}`);
|
|
794
|
+
logger.error(` Error: ${chalk.red(errorMessage)}`);
|
|
795
|
+
}
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
757
799
|
/**
|
|
758
800
|
* Execute remove command
|
|
759
801
|
*/
|
|
@@ -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
|
});
|
|
@@ -7,6 +7,7 @@ import { tracers } from "../telemetry/tracers.js";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
|
|
9
9
|
import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
|
|
10
|
+
import { NEUROLINK_ARTIFACT_ID_KEY } from "../mcp/mcpOutputNormalizer.js";
|
|
10
11
|
import { SummarizationEngine } from "../context/summarizationEngine.js";
|
|
11
12
|
import { NeuroLink } from "../neurolink.js";
|
|
12
13
|
import { ConversationMemoryError } from "../types/conversation.js";
|
|
@@ -1320,11 +1321,30 @@ User message: "${userMessage}"`;
|
|
|
1320
1321
|
maxBytes: this.config?.contextCompaction?.maxToolOutputBytes,
|
|
1321
1322
|
maxLines: this.config?.contextCompaction?.maxToolOutputLines,
|
|
1322
1323
|
});
|
|
1324
|
+
// Extract artifact ID if this result was externalized by McpOutputNormalizer.
|
|
1325
|
+
// The surrogate carries `_meta.neurolinkArtifactId` on the raw result object.
|
|
1326
|
+
let artifactId;
|
|
1327
|
+
try {
|
|
1328
|
+
const rawResult = toolResult.result;
|
|
1329
|
+
if (rawResult && typeof rawResult === "object") {
|
|
1330
|
+
const meta = rawResult._meta;
|
|
1331
|
+
if (meta && typeof meta === "object") {
|
|
1332
|
+
const idValue = meta[NEUROLINK_ARTIFACT_ID_KEY];
|
|
1333
|
+
if (typeof idValue === "string") {
|
|
1334
|
+
artifactId = idValue;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
// Ignore extraction errors — artifact ID is best-effort metadata
|
|
1341
|
+
}
|
|
1323
1342
|
// Build metadata — only store preview when truncation occurred (no duplication)
|
|
1324
1343
|
const metadata = {
|
|
1325
1344
|
truncated,
|
|
1326
1345
|
...(truncated && { toolOutputPreview: preview }),
|
|
1327
1346
|
...(truncated && { originalSize }),
|
|
1347
|
+
...(artifactId && { artifactId }),
|
|
1328
1348
|
};
|
|
1329
1349
|
// Build result — success/error metadata only, NOT the output data
|
|
1330
1350
|
const result = {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact Store
|
|
3
|
+
*
|
|
4
|
+
* Pluggable storage for externalized MCP tool outputs.
|
|
5
|
+
*
|
|
6
|
+
* When `mcp.outputLimits.strategy = "externalize"` the full tool payload is
|
|
7
|
+
* written here instead of being sent inline to the LLM. The model receives a
|
|
8
|
+
* compact surrogate with a preview and an artifact ID. The full payload can be
|
|
9
|
+
* retrieved on demand via the `retrieve_context` tool.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* ArtifactStore (interface) — canonical types in src/lib/types/artifactTypes.ts
|
|
13
|
+
* LocalTempArtifactStore — single-process, filesystem-backed implementation
|
|
14
|
+
*
|
|
15
|
+
* Distributed backends (S3, Redis blobs) can be added later by implementing
|
|
16
|
+
* ArtifactStore from types/artifactTypes.ts.
|
|
17
|
+
*
|
|
18
|
+
* @module artifacts/artifactStore
|
|
19
|
+
*/
|
|
20
|
+
import type { ArtifactMeta, ArtifactRef, ArtifactStore } from "../types/artifactTypes.js";
|
|
21
|
+
export type { ArtifactMeta, ArtifactRef, ArtifactStore, } from "../types/artifactTypes.js";
|
|
22
|
+
/**
|
|
23
|
+
* Filesystem-backed artifact store using the OS temp directory.
|
|
24
|
+
*
|
|
25
|
+
* Files are written with mode 0o600 (owner read/write only).
|
|
26
|
+
* An in-memory index tracks metadata without a separate index file.
|
|
27
|
+
*
|
|
28
|
+
* Suitable for:
|
|
29
|
+
* - CLI usage
|
|
30
|
+
* - Single-process SDK deployments
|
|
31
|
+
* - Multi-process deployments where each process manages its own artifacts
|
|
32
|
+
* (artifacts created in one process are not visible to others)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const store = new LocalTempArtifactStore();
|
|
37
|
+
* const ref = await store.store(largeJson, {
|
|
38
|
+
* toolName: "list_files",
|
|
39
|
+
* serverId: "filesystem-server",
|
|
40
|
+
* sizeBytes: Buffer.byteLength(largeJson),
|
|
41
|
+
* contentType: "json",
|
|
42
|
+
* });
|
|
43
|
+
* // Later, via retrieve_context:
|
|
44
|
+
* const full = await store.retrieve(ref.id);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class LocalTempArtifactStore implements ArtifactStore {
|
|
48
|
+
private readonly dir;
|
|
49
|
+
private readonly index;
|
|
50
|
+
constructor(dir?: string);
|
|
51
|
+
generatePreview(payload: string): string;
|
|
52
|
+
store(payload: string, meta: Omit<ArtifactMeta, "createdAt">): Promise<ArtifactRef>;
|
|
53
|
+
retrieve(id: string): Promise<string | null>;
|
|
54
|
+
delete(id: string): Promise<void>;
|
|
55
|
+
cleanup(olderThanMs: number): Promise<number>;
|
|
56
|
+
}
|