@juspay/neurolink 9.51.3 → 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 +6 -0
- package/dist/artifacts/artifactStore.d.ts +56 -0
- package/dist/artifacts/artifactStore.js +143 -0
- package/dist/browser/neurolink.min.js +307 -296
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +128 -86
- 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/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 +59 -80
- 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 +32 -0
- 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 +59 -80
- 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 +32 -0
- 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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Output Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Single responsibility: intercept a raw MCP `CallToolResult`, measure it,
|
|
5
|
+
* and apply the configured strategy so that oversized payloads never reach
|
|
6
|
+
* caches, Redis, or LLM context windows raw.
|
|
7
|
+
*
|
|
8
|
+
* Two strategies:
|
|
9
|
+
* - "inline" Pass through unchanged. The full payload enters the LLM
|
|
10
|
+
* context as-is. Emit a warning above warnBytes.
|
|
11
|
+
* - "externalize" Write the full payload to the ArtifactStore, return a
|
|
12
|
+
* compact surrogate with a head/tail preview and an artifact
|
|
13
|
+
* ID. The model uses `retrieve_context` with that ID to read
|
|
14
|
+
* the full output on demand, with offset/limit pagination.
|
|
15
|
+
*
|
|
16
|
+
* The surrogate result is shaped as an MCP `CallToolResult` so it passes
|
|
17
|
+
* transparently through any downstream code that expects that format.
|
|
18
|
+
* A `_meta` extension carries the artifact ID for structured extraction in
|
|
19
|
+
* `redisConversationMemoryManager`.
|
|
20
|
+
*
|
|
21
|
+
* @module mcp/mcpOutputNormalizer
|
|
22
|
+
*/
|
|
23
|
+
import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
|
|
24
|
+
import { logger } from "../utils/logger.js";
|
|
25
|
+
import { withTimeout } from "../utils/errorHandling.js";
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Public constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/** Default byte ceiling above which externalize fires (100 KB). */
|
|
30
|
+
export const DEFAULT_MAX_MCP_OUTPUT_BYTES = 100 * 1024;
|
|
31
|
+
/** Default byte threshold for emitting a warning while still inline (50 KB). */
|
|
32
|
+
export const DEFAULT_WARN_MCP_OUTPUT_BYTES = 50 * 1024;
|
|
33
|
+
/** Metadata key embedded in surrogate `_meta` and used by memory manager. */
|
|
34
|
+
export const NEUROLINK_ARTIFACT_ID_KEY = "neurolinkArtifactId";
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// McpOutputNormalizer
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Stateless normalizer (state lives in the injected ArtifactStore).
|
|
40
|
+
*
|
|
41
|
+
* Construct once per NeuroLink instance and set via
|
|
42
|
+
* `ToolDiscoveryService.setOutputNormalizer()`.
|
|
43
|
+
*/
|
|
44
|
+
export class McpOutputNormalizer {
|
|
45
|
+
config;
|
|
46
|
+
artifactStore;
|
|
47
|
+
constructor(config, artifactStore) {
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.artifactStore = artifactStore;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Measure `callResult`, apply strategy if oversized, return normalized output.
|
|
53
|
+
*
|
|
54
|
+
* Never throws: on any internal failure the raw result is returned unchanged
|
|
55
|
+
* with a warning log so tool execution is never broken by the normalizer.
|
|
56
|
+
*/
|
|
57
|
+
async normalize(callResult, context) {
|
|
58
|
+
const serialized = serialize(callResult);
|
|
59
|
+
const originalBytes = Buffer.byteLength(serialized, "utf-8");
|
|
60
|
+
// Fast path: below warn threshold — always inline, no logging
|
|
61
|
+
if (originalBytes <= this.config.warnBytes) {
|
|
62
|
+
return { result: callResult, isExternalized: false, originalBytes };
|
|
63
|
+
}
|
|
64
|
+
// Between warn and max: emit a warning but keep inline regardless of strategy
|
|
65
|
+
if (originalBytes <= this.config.maxBytes) {
|
|
66
|
+
logger.warn(`[McpOutputNormalizer] Large MCP output from "${context.toolName}" on ` +
|
|
67
|
+
`"${context.serverId}" (${formatBytes(originalBytes)}). ` +
|
|
68
|
+
`Approaching limit of ${formatBytes(this.config.maxBytes)}.`, {
|
|
69
|
+
toolName: context.toolName,
|
|
70
|
+
serverId: context.serverId,
|
|
71
|
+
originalBytes,
|
|
72
|
+
});
|
|
73
|
+
return { result: callResult, isExternalized: false, originalBytes };
|
|
74
|
+
}
|
|
75
|
+
// Above max — apply strategy
|
|
76
|
+
logger.warn(`[McpOutputNormalizer] MCP output from "${context.toolName}" on ` +
|
|
77
|
+
`"${context.serverId}" exceeds limit ` +
|
|
78
|
+
`(${formatBytes(originalBytes)} > ${formatBytes(this.config.maxBytes)}). ` +
|
|
79
|
+
`Applying strategy "${this.config.strategy}".`, { toolName: context.toolName, serverId: context.serverId, originalBytes });
|
|
80
|
+
if (this.config.strategy === "inline") {
|
|
81
|
+
// Caller explicitly opted in to inline regardless of size
|
|
82
|
+
return { result: callResult, isExternalized: false, originalBytes };
|
|
83
|
+
}
|
|
84
|
+
// strategy === "externalize"
|
|
85
|
+
if (!this.artifactStore) {
|
|
86
|
+
// Misconfiguration: externalize was chosen but no store was provided.
|
|
87
|
+
// Pass through inline so execution is never broken, but log loudly.
|
|
88
|
+
logger.error(`[McpOutputNormalizer] strategy="externalize" but no ArtifactStore ` +
|
|
89
|
+
`configured — passing through raw result for "${context.toolName}". ` +
|
|
90
|
+
`Set mcp.outputLimits.strategy="externalize" and ensure the NeuroLink ` +
|
|
91
|
+
`constructor creates a LocalTempArtifactStore.`);
|
|
92
|
+
return { result: callResult, isExternalized: false, originalBytes };
|
|
93
|
+
}
|
|
94
|
+
let ref;
|
|
95
|
+
try {
|
|
96
|
+
ref = await withTimeout(this.artifactStore.store(serialized, {
|
|
97
|
+
toolName: context.toolName,
|
|
98
|
+
serverId: context.serverId,
|
|
99
|
+
sessionId: context.sessionId,
|
|
100
|
+
sizeBytes: originalBytes,
|
|
101
|
+
contentType: isJsonLike(callResult) ? "json" : "text",
|
|
102
|
+
}), 10_000, new Error(`ArtifactStore.store() timed out for "${context.toolName}"`));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// Storage failure or timeout — pass through inline so the call doesn't break.
|
|
106
|
+
logger.error(`[McpOutputNormalizer] ArtifactStore.store() failed for ` +
|
|
107
|
+
`"${context.toolName}": ${err instanceof Error ? err.message : String(err)} ` +
|
|
108
|
+
`— passing through raw result.`);
|
|
109
|
+
return { result: callResult, isExternalized: false, originalBytes };
|
|
110
|
+
}
|
|
111
|
+
// Generate a compact head/tail preview for the surrogate.
|
|
112
|
+
// Cap at warnBytes so the surrogate itself is always well within limits.
|
|
113
|
+
const { preview } = generateToolOutputPreview(serialized, {
|
|
114
|
+
maxBytes: Math.min(this.config.warnBytes, DEFAULT_WARN_MCP_OUTPUT_BYTES),
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
result: buildSurrogate(preview, ref.id, context, originalBytes),
|
|
118
|
+
isExternalized: true,
|
|
119
|
+
artifactId: ref.id,
|
|
120
|
+
originalBytes,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Helpers
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Build a compact MCP-shaped surrogate the LLM receives instead of the
|
|
129
|
+
* raw oversized payload.
|
|
130
|
+
*
|
|
131
|
+
* Shape mirrors `CallToolResult` so downstream code that inspects
|
|
132
|
+
* `result.content` keeps working unchanged.
|
|
133
|
+
* `_meta` carries the artifact ID for structured extraction in
|
|
134
|
+
* `redisConversationMemoryManager`.
|
|
135
|
+
*/
|
|
136
|
+
function buildSurrogate(preview, artifactId, context, originalBytes) {
|
|
137
|
+
const text = `[MCP Tool Output — ${context.toolName} | ${context.serverId}]\n` +
|
|
138
|
+
`Original size: ${formatBytes(originalBytes)} | ` +
|
|
139
|
+
`Externalized — use retrieve_context with artifactId="${artifactId}" ` +
|
|
140
|
+
`to read the full output (supports offset + limit pagination)\n` +
|
|
141
|
+
`\n--- Preview (head + tail) ---\n` +
|
|
142
|
+
preview +
|
|
143
|
+
`\n--- End Preview ---\n` +
|
|
144
|
+
`[${NEUROLINK_ARTIFACT_ID_KEY}=${artifactId}]`;
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text }],
|
|
147
|
+
_meta: {
|
|
148
|
+
[NEUROLINK_ARTIFACT_ID_KEY]: artifactId,
|
|
149
|
+
originalBytes,
|
|
150
|
+
toolName: context.toolName,
|
|
151
|
+
serverId: context.serverId,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function serialize(value) {
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// Compact JSON keeps byte measurement accurate and size enforcement honest.
|
|
161
|
+
// Pretty-printing with null, 2 inflates every object by ~30–50 % and would
|
|
162
|
+
// shift the externalization threshold relative to what the LLM actually
|
|
163
|
+
// receives if the payload is ever inlined.
|
|
164
|
+
return JSON.stringify(value);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return String(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function isJsonLike(value) {
|
|
171
|
+
return typeof value === "object" && value !== null;
|
|
172
|
+
}
|
|
173
|
+
function formatBytes(bytes) {
|
|
174
|
+
if (bytes < 1024) {
|
|
175
|
+
return `${bytes} B`;
|
|
176
|
+
}
|
|
177
|
+
if (bytes < 1024 * 1024) {
|
|
178
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
179
|
+
}
|
|
180
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=mcpOutputNormalizer.js.map
|
|
@@ -8,6 +8,7 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
8
8
|
import type { ExternalMCPToolInfo, ExternalMCPToolResult } from "../types/externalMcp.js";
|
|
9
9
|
import type { ToolDiscoveryResult, ExternalToolExecutionOptions } from "../types/mcpTypes.js";
|
|
10
10
|
import type { JsonObject } from "../types/common.js";
|
|
11
|
+
import type { McpOutputNormalizer } from "./mcpOutputNormalizer.js";
|
|
11
12
|
/**
|
|
12
13
|
* ToolDiscoveryService
|
|
13
14
|
* Handles automatic tool discovery and registration from external MCP servers
|
|
@@ -17,7 +18,16 @@ export declare class ToolDiscoveryService extends EventEmitter {
|
|
|
17
18
|
private toolRegistry;
|
|
18
19
|
private serverTools;
|
|
19
20
|
private discoveryInProgress;
|
|
21
|
+
/** Optional normalizer applied to every tool output before it is returned. */
|
|
22
|
+
private outputNormalizer?;
|
|
20
23
|
constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Attach a McpOutputNormalizer.
|
|
26
|
+
* When set, every raw callTool() result is passed through the normalizer
|
|
27
|
+
* before being returned. Oversized outputs are replaced with compact
|
|
28
|
+
* surrogates according to the configured strategy.
|
|
29
|
+
*/
|
|
30
|
+
setOutputNormalizer(normalizer: McpOutputNormalizer): void;
|
|
21
31
|
/**
|
|
22
32
|
* Discover tools from an external MCP server
|
|
23
33
|
*/
|
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
|
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(
|
|
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
|
|
147
|
+
return { error: "Session not found", sessionId };
|
|
80
148
|
}
|
|
81
149
|
let messages = conversation.messages;
|
|
82
150
|
// Filter by specific messageId
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -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;
|