@juspay/neurolink 12.9.5 → 12.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/dist/artifacts/artifactBanking.d.ts +6 -2
- package/dist/artifacts/artifactBanking.js +9 -14
- package/dist/artifacts/artifactReader.d.ts +69 -0
- package/dist/artifacts/artifactReader.js +157 -0
- package/dist/artifacts/artifactStore.d.ts +7 -3
- package/dist/artifacts/artifactStore.js +10 -21
- package/dist/artifacts/artifactStoreFactory.d.ts +39 -0
- package/dist/artifacts/artifactStoreFactory.js +101 -0
- package/dist/artifacts/redisArtifactStore.d.ts +84 -0
- package/dist/artifacts/redisArtifactStore.js +270 -0
- package/dist/browser/neurolink.min.js +378 -382
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/memory/memoryRetrievalTools.js +104 -35
- package/dist/neurolink.d.ts +74 -2
- package/dist/neurolink.js +197 -19
- package/dist/types/artifact.d.ts +124 -3
- package/dist/types/config.d.ts +7 -0
- package/dist/types/mcp.d.ts +16 -0
- package/dist/utils/redis.d.ts +15 -0
- package/dist/utils/redis.js +64 -6
- package/dist/utils/toolCallRepair.d.ts +42 -0
- package/dist/utils/toolCallRepair.js +78 -15
- package/package.json +7 -6
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export { PROVIDER_DESCRIPTORS, PROVIDER_DESCRIPTORS_BY_NAME, PROVIDER_ALIAS_INDE
|
|
|
38
38
|
export { NeuroLinkConfigManager as ConfigManager } from "./config/configManager.js";
|
|
39
39
|
export { BaseFactory, BaseRegistry, NeuroLinkFeatureError, createErrorFactory, withRetry, TypedEventEmitter, } from "./core/infrastructure/index.js";
|
|
40
40
|
export { NeuroLinkClient, createClient, NeuroLinkApiError, } from "./client/httpClient.js";
|
|
41
|
+
export { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
|
|
42
|
+
export { RedisArtifactStore } from "./artifacts/redisArtifactStore.js";
|
|
43
|
+
export { createArtifactStore, resolveArtifactStorageType, } from "./artifacts/artifactStoreFactory.js";
|
|
41
44
|
export { NeuroLinkLanguageModel, NeuroLinkProvider as NeuroLinkAIProvider, createNeuroLinkProvider, createNeuroLinkModel, createStreamingResponse, neurolink as neuroLinkAIInstance, } from "./client/aiSdkAdapter.js";
|
|
42
45
|
export { createApiKeyAuthInterceptor, createBearerAuthInterceptor, createDynamicAuthInterceptor, createLoggingInterceptor, createRetryInterceptor, createRateLimitInterceptor, createRequestTransformInterceptor, createResponseTransformInterceptor, createCacheInterceptor, createTimeoutInterceptor, createErrorHandlerInterceptor, composeMiddleware, conditionalMiddleware, } from "./client/interceptors.js";
|
|
43
46
|
export { SSEClient, WebSocketStreamingClient, createStreamingClient, createAsyncStream, collectStream, } from "./client/streamingClient.js";
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,13 @@ export { BaseFactory, BaseRegistry, NeuroLinkFeatureError, createErrorFactory, w
|
|
|
51
51
|
export {
|
|
52
52
|
// HTTP Client
|
|
53
53
|
NeuroLinkClient, createClient, NeuroLinkApiError, } from "./client/httpClient.js";
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// ARTIFACT STORAGE — backends for externalized tool outputs and banked payloads.
|
|
56
|
+
// Pick one with `artifacts.storage` / STORAGE_TYPE, or implement ArtifactStore.
|
|
57
|
+
// ============================================================================
|
|
58
|
+
export { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
|
|
59
|
+
export { RedisArtifactStore } from "./artifacts/redisArtifactStore.js";
|
|
60
|
+
export { createArtifactStore, resolveArtifactStorageType, } from "./artifacts/artifactStoreFactory.js";
|
|
54
61
|
export {
|
|
55
62
|
// AI SDK Adapter
|
|
56
63
|
NeuroLinkLanguageModel, NeuroLinkProvider as NeuroLinkAIProvider, createNeuroLinkProvider, createNeuroLinkModel, createStreamingResponse, neurolink as neuroLinkAIInstance, } from "./client/aiSdkAdapter.js";
|
|
@@ -6,12 +6,15 @@ import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../
|
|
|
6
6
|
import { withSpan } from "../telemetry/withSpan.js";
|
|
7
7
|
import { tracers } from "../telemetry/tracers.js";
|
|
8
8
|
import { tool } from "../utils/tool.js";
|
|
9
|
+
import { MAX_ARTIFACT_SEARCH_MATCHES, readArtifactWindow, searchArtifactContent, validateSearchPattern, } from "../artifacts/artifactReader.js";
|
|
9
10
|
/** Maximum characters returned per retrieval request */
|
|
10
11
|
const DEFAULT_RETRIEVAL_LIMIT = 50_000;
|
|
11
12
|
/** Hard maximum for user/LLM-supplied limit to prevent massive tool outputs */
|
|
12
13
|
const MAX_RETRIEVAL_LIMIT = 200_000;
|
|
13
14
|
/** Maximum number of search matches returned */
|
|
14
15
|
const MAX_SEARCH_MATCHES = 50;
|
|
16
|
+
/** Bound on one artifact backend round trip, so a stalled store never hangs the tool. */
|
|
17
|
+
const ARTIFACT_READ_TIMEOUT_MS = 10_000;
|
|
15
18
|
/**
|
|
16
19
|
* Factory function that creates memory retrieval tools bound to a memory manager.
|
|
17
20
|
*
|
|
@@ -27,13 +30,16 @@ const MAX_SEARCH_MATCHES = 50;
|
|
|
27
30
|
export function createMemoryRetrievalTools(memoryManager, artifactStore) {
|
|
28
31
|
return {
|
|
29
32
|
retrieve_context: tool({
|
|
30
|
-
description: "Retrieve messages from conversation memory, or
|
|
31
|
-
"
|
|
33
|
+
description: "Retrieve messages from conversation memory, or read an externalized " +
|
|
34
|
+
"tool output / banked payload by artifact ID. Use this to:\n" +
|
|
32
35
|
"• Access full tool outputs when a result was truncated or externalized\n" +
|
|
33
36
|
"• Review previous assistant responses\n" +
|
|
34
|
-
"• Search
|
|
35
|
-
"Supports filtering by role, pagination for large content,
|
|
36
|
-
"
|
|
37
|
+
"• Search a session's history, or an artifact, for literal text\n" +
|
|
38
|
+
"Supports filtering by role, offset/limit pagination for large content, " +
|
|
39
|
+
"and case-insensitive literal search (not regex).\n" +
|
|
40
|
+
"To read an artifact, provide `artifactId` (omit sessionId): pass " +
|
|
41
|
+
"`offset`/`limit` to page, or `search` to get match offsets and jump " +
|
|
42
|
+
"straight to them instead of paging.",
|
|
37
43
|
inputSchema: z.object({
|
|
38
44
|
sessionId: z
|
|
39
45
|
.string()
|
|
@@ -44,8 +50,9 @@ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
|
|
|
44
50
|
.string()
|
|
45
51
|
.optional()
|
|
46
52
|
.describe("Artifact ID from an externalized MCP tool output " +
|
|
47
|
-
"(visible in the tool output as neurolinkArtifactId=<id>)
|
|
48
|
-
"When provided,
|
|
53
|
+
"(visible in the tool output as neurolinkArtifactId=<id>) or a " +
|
|
54
|
+
"banked payload. When provided, reads the stored payload: a " +
|
|
55
|
+
"window at `offset`/`limit`, or with `search`, the matches."),
|
|
49
56
|
messageId: z
|
|
50
57
|
.string()
|
|
51
58
|
.optional()
|
|
@@ -75,8 +82,13 @@ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
|
|
|
75
82
|
search: z
|
|
76
83
|
.string()
|
|
77
84
|
.optional()
|
|
78
|
-
.describe("
|
|
79
|
-
"
|
|
85
|
+
.describe("Case-insensitive literal text to find (regex metacharacters are " +
|
|
86
|
+
"matched literally). Session history: returns matching lines " +
|
|
87
|
+
"with line numbers. Artifact: returns up to " +
|
|
88
|
+
`${MAX_ARTIFACT_SEARCH_MATCHES} matches, each with the character ` +
|
|
89
|
+
"`offset` of the hit and a short snippet — pass that offset back " +
|
|
90
|
+
"as `offset` to read around it. With `offset`, the artifact " +
|
|
91
|
+
"search starts there; use `nextSearchOffset` to continue."),
|
|
80
92
|
}),
|
|
81
93
|
execute: async (args) => withSpan({
|
|
82
94
|
name: "neurolink.memory.retrieve_context",
|
|
@@ -108,35 +120,12 @@ async function executeRetrieveContext(args, memoryManager, artifactStore, otelSp
|
|
|
108
120
|
message: "Artifact store not configured",
|
|
109
121
|
});
|
|
110
122
|
return {
|
|
111
|
-
error: "Artifact store not configured — " +
|
|
112
|
-
"
|
|
123
|
+
error: "Artifact store not configured — this instance has never banked " +
|
|
124
|
+
"or externalized anything, so there is no artifact to read",
|
|
113
125
|
artifactId: args.artifactId,
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
|
-
|
|
117
|
-
if (content === null) {
|
|
118
|
-
otelSpan.setStatus({
|
|
119
|
-
code: SpanStatusCode.ERROR,
|
|
120
|
-
message: "Artifact not found or has expired",
|
|
121
|
-
});
|
|
122
|
-
return {
|
|
123
|
-
error: "Artifact not found or has expired",
|
|
124
|
-
artifactId: args.artifactId,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
|
|
128
|
-
const start = args.offset ?? 0;
|
|
129
|
-
const slice = content.slice(start, start + charLimit);
|
|
130
|
-
otelSpan.setAttribute("memory.artifact_size", content.length);
|
|
131
|
-
otelSpan.setAttribute("memory.returned_bytes", slice.length);
|
|
132
|
-
return {
|
|
133
|
-
artifactId: args.artifactId,
|
|
134
|
-
content: slice,
|
|
135
|
-
totalSize: content.length,
|
|
136
|
-
hasMore: start + charLimit < content.length,
|
|
137
|
-
offset: start,
|
|
138
|
-
limit: charLimit,
|
|
139
|
-
};
|
|
128
|
+
return executeArtifactRetrieval(args.artifactId, args, artifactStore, otelSpan);
|
|
140
129
|
}
|
|
141
130
|
// ── End artifact resolution ─────────────────────────────────────────
|
|
142
131
|
if (!args.sessionId) {
|
|
@@ -272,3 +261,83 @@ async function executeRetrieveContext(args, memoryManager, artifactStore, otelSp
|
|
|
272
261
|
return { error: "Failed to retrieve context" };
|
|
273
262
|
}
|
|
274
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* The artifact branch of `retrieve_context`.
|
|
266
|
+
*
|
|
267
|
+
* Two modes, chosen by `search`:
|
|
268
|
+
* - Paged read: one window through `readArtifactWindow`, which lets a backend
|
|
269
|
+
* with range reads move only the window. `hasMore` comes from the window's
|
|
270
|
+
* `totalLength`, so it never needs the payload either.
|
|
271
|
+
* - Search: the whole payload is read once and scanned for the literal
|
|
272
|
+
* pattern; the model gets match offsets and bounded snippets and can jump
|
|
273
|
+
* straight to the hit with `offset` instead of paging to it. Before this
|
|
274
|
+
* branch existed `search` was accepted and silently ignored here, and a
|
|
275
|
+
* model could not tell — an unfiltered window looks like "no matches".
|
|
276
|
+
*
|
|
277
|
+
* Backend failures (a Redis outage, a timeout) are reported as errors, never
|
|
278
|
+
* as "not found": those are different facts and the model acts differently on
|
|
279
|
+
* each.
|
|
280
|
+
*/
|
|
281
|
+
async function executeArtifactRetrieval(artifactId, args, artifactStore, otelSpan) {
|
|
282
|
+
const notFound = () => {
|
|
283
|
+
otelSpan.setStatus({
|
|
284
|
+
code: SpanStatusCode.ERROR,
|
|
285
|
+
message: "Artifact not found or has expired",
|
|
286
|
+
});
|
|
287
|
+
return { error: "Artifact not found or has expired", artifactId };
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
if (args.search !== undefined) {
|
|
291
|
+
const invalid = validateSearchPattern(args.search);
|
|
292
|
+
if (invalid) {
|
|
293
|
+
otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: invalid });
|
|
294
|
+
return { error: invalid, artifactId };
|
|
295
|
+
}
|
|
296
|
+
// A search has to see the whole payload; the window contract is for reads.
|
|
297
|
+
const content = await withTimeout(artifactStore.retrieve(artifactId), ARTIFACT_READ_TIMEOUT_MS, new Error(`ArtifactStore.retrieve() timed out for artifact "${artifactId}"`));
|
|
298
|
+
if (content === null) {
|
|
299
|
+
return notFound();
|
|
300
|
+
}
|
|
301
|
+
const result = searchArtifactContent(content, args.search, {
|
|
302
|
+
from: args.offset,
|
|
303
|
+
});
|
|
304
|
+
otelSpan.setAttribute("memory.artifact_size", content.length);
|
|
305
|
+
otelSpan.setAttribute("memory.search_matches", result.totalMatches);
|
|
306
|
+
return {
|
|
307
|
+
artifactId,
|
|
308
|
+
search: args.search,
|
|
309
|
+
totalSize: content.length,
|
|
310
|
+
...result,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
|
|
314
|
+
const start = Math.max(0, args.offset ?? 0);
|
|
315
|
+
const window = await withTimeout(readArtifactWindow(artifactStore, artifactId, {
|
|
316
|
+
offset: start,
|
|
317
|
+
limit: charLimit,
|
|
318
|
+
}), ARTIFACT_READ_TIMEOUT_MS, new Error(`Artifact read timed out for artifact "${artifactId}"`));
|
|
319
|
+
if (window === null) {
|
|
320
|
+
return notFound();
|
|
321
|
+
}
|
|
322
|
+
otelSpan.setAttribute("memory.artifact_size", window.totalLength);
|
|
323
|
+
otelSpan.setAttribute("memory.returned_bytes", window.content.length);
|
|
324
|
+
otelSpan.setAttribute("memory.artifact_range_read", typeof artifactStore.retrieveRange === "function");
|
|
325
|
+
return {
|
|
326
|
+
artifactId,
|
|
327
|
+
content: window.content,
|
|
328
|
+
totalSize: window.totalLength,
|
|
329
|
+
hasMore: window.offset + window.content.length < window.totalLength,
|
|
330
|
+
offset: window.offset,
|
|
331
|
+
limit: charLimit,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
336
|
+
logger.error("[MemoryRetrievalTools] Artifact read failed", {
|
|
337
|
+
artifactId,
|
|
338
|
+
error: message,
|
|
339
|
+
});
|
|
340
|
+
otelSpan.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
341
|
+
return { error: `Artifact read failed: ${message}`, artifactId };
|
|
342
|
+
}
|
|
343
|
+
}
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -82,8 +82,19 @@ export declare class NeuroLink {
|
|
|
82
82
|
private mcpToolBatcher?;
|
|
83
83
|
private mcpEnhancedDiscovery?;
|
|
84
84
|
private mcpToolMiddlewares;
|
|
85
|
-
/** Artifact store for externalized MCP tool outputs
|
|
85
|
+
/** Artifact store for externalized MCP tool outputs and banked payloads. */
|
|
86
86
|
private mcpArtifactStore?;
|
|
87
|
+
/** `artifacts` constructor config — consulted whenever a store is created. */
|
|
88
|
+
private artifactsConfig?;
|
|
89
|
+
/**
|
|
90
|
+
* True when this instance built `mcpArtifactStore` itself (local or Redis
|
|
91
|
+
* from config / STORAGE_TYPE). NeuroLink closes only stores it built — on
|
|
92
|
+
* replacement and on shutdown; a store handed in through `artifacts.store`
|
|
93
|
+
* or `setArtifactStore()` is the caller's to close.
|
|
94
|
+
*/
|
|
95
|
+
private ownsArtifactStore;
|
|
96
|
+
/** Normalizer settings, kept so `setArtifactStore()` can rebuild it. */
|
|
97
|
+
private mcpOutputNormalizerConfig?;
|
|
87
98
|
private _disableToolCacheForCurrentRequest;
|
|
88
99
|
/**
|
|
89
100
|
* (toolName + args) keys already served during the CURRENT request.
|
|
@@ -1943,9 +1954,37 @@ export declare class NeuroLink {
|
|
|
1943
1954
|
* @param options - Execution options
|
|
1944
1955
|
* @returns Tool execution result
|
|
1945
1956
|
*/
|
|
1946
|
-
executeExternalMCPTool(serverId: string,
|
|
1957
|
+
executeExternalMCPTool(serverId: string, requestedToolName: string, parameters: JsonObject, options?: {
|
|
1947
1958
|
timeout?: number;
|
|
1948
1959
|
}): Promise<unknown>;
|
|
1960
|
+
/**
|
|
1961
|
+
* Resolve a tool name for a direct external MCP execution
|
|
1962
|
+
* (`executeExternalMCPTool`) against the server's currently discovered
|
|
1963
|
+
* tools.
|
|
1964
|
+
*
|
|
1965
|
+
* `experimental_repairToolCall` only runs inside the AI-SDK's own
|
|
1966
|
+
* streamText/generateText loop (see toolCallRepair.ts), so a near-miss
|
|
1967
|
+
* tool name reaching `executeExternalMCPTool` directly previously had no
|
|
1968
|
+
* recovery at all — just the generic `Tool 'x' not found for server 'y'`
|
|
1969
|
+
* `Error` that `ToolDiscoveryService.executeTool` throws deeper in the
|
|
1970
|
+
* stack. This reuses the same name-matching policy
|
|
1971
|
+
* (`resolveToolName`: case-insensitive exact → unambiguous substring →
|
|
1972
|
+
* Levenshtein) so a repair here is accepted under exactly the rules
|
|
1973
|
+
* already proven for the generation path.
|
|
1974
|
+
*
|
|
1975
|
+
* Exact match is a zero-risk fast path: it is returned unchanged before
|
|
1976
|
+
* any resolution attempt, so every existing caller that already sends a
|
|
1977
|
+
* valid name — including the AI-SDK path's `createExternalMCPTool`, which
|
|
1978
|
+
* always executes with a name it just discovered — sees no behaviour
|
|
1979
|
+
* change.
|
|
1980
|
+
*
|
|
1981
|
+
* Deliberately does NOT attempt a repair when the server is unknown or
|
|
1982
|
+
* not connected: `ExternalServerManager.executeTool` throws distinct,
|
|
1983
|
+
* more accurate errors for those states ("Server 'x' not found" /
|
|
1984
|
+
* "not in connected state"), and resolving against an empty tool list
|
|
1985
|
+
* here would replace those with a misleading "tool not found" instead.
|
|
1986
|
+
*/
|
|
1987
|
+
private resolveDirectMcpToolName;
|
|
1949
1988
|
/**
|
|
1950
1989
|
* Get all tools from external MCP servers
|
|
1951
1990
|
* @returns Array of external tool information
|
|
@@ -2707,6 +2746,39 @@ export declare class NeuroLink {
|
|
|
2707
2746
|
* @returns The artifact store backing {@link bankArtifact} / {@link readArtifact}
|
|
2708
2747
|
*/
|
|
2709
2748
|
getArtifactStore(): ArtifactStore;
|
|
2749
|
+
/**
|
|
2750
|
+
* Replace this instance's artifact store.
|
|
2751
|
+
*
|
|
2752
|
+
* Everything that writes or reads artifacts follows the swap: banking,
|
|
2753
|
+
* `retrieve_context`, host-side `readArtifact`, and the MCP output
|
|
2754
|
+
* normalizer — which is rebuilt here because it captured the previous store
|
|
2755
|
+
* at construction. Assigning the field alone would miss it, and
|
|
2756
|
+
* externalized tool outputs would keep landing in the old backend while
|
|
2757
|
+
* read-backs looked in the new one.
|
|
2758
|
+
*
|
|
2759
|
+
* Call it before the first bank or externalized tool output: artifacts
|
|
2760
|
+
* already in the previous store are not migrated, and their ids stop
|
|
2761
|
+
* resolving through this instance. `artifacts.store` in the constructor
|
|
2762
|
+
* config is the same thing without the ordering concern.
|
|
2763
|
+
*
|
|
2764
|
+
* Ownership: a store you hand in — here or via `artifacts.store` — stays
|
|
2765
|
+
* yours to close. NeuroLink closes only the stores it built itself, when
|
|
2766
|
+
* they are replaced here and on `shutdown()`.
|
|
2767
|
+
*
|
|
2768
|
+
* @param store - Any {@link ArtifactStore}
|
|
2769
|
+
*/
|
|
2770
|
+
setArtifactStore(store: ArtifactStore): void;
|
|
2771
|
+
/**
|
|
2772
|
+
* Build the store the `artifacts` config (or `STORAGE_TYPE`) asks for. The
|
|
2773
|
+
* Redis connection falls back to conversation memory's, so one
|
|
2774
|
+
* `STORAGE_TYPE=redis` moves sessions and artifacts together.
|
|
2775
|
+
*/
|
|
2776
|
+
private createConfiguredArtifactStore;
|
|
2777
|
+
/**
|
|
2778
|
+
* (Re)build the MCP output normalizer over `artifactStore`. A no-op unless
|
|
2779
|
+
* `mcp.outputLimits` was configured — without it nothing is externalized.
|
|
2780
|
+
*/
|
|
2781
|
+
private installOutputNormalizer;
|
|
2710
2782
|
/**
|
|
2711
2783
|
* Bank a payload to a file and get back a pointer to it.
|
|
2712
2784
|
*
|
package/dist/neurolink.js
CHANGED
|
@@ -47,7 +47,7 @@ import { ToolResultCache } from "./mcp/caching/index.js";
|
|
|
47
47
|
import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
|
|
48
48
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
49
49
|
import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
|
|
50
|
-
import {
|
|
50
|
+
import { createArtifactStore, resolveArtifactStorageType, } from "./artifacts/artifactStoreFactory.js";
|
|
51
51
|
import { bankArtifact as bankArtifactPayload, readArtifact as readBankedArtifact, } from "./artifacts/artifactBanking.js";
|
|
52
52
|
import { ToolRouter } from "./mcp/routing/index.js";
|
|
53
53
|
// Import direct tools server for automatic registration
|
|
@@ -89,6 +89,7 @@ import { logger, mcpLogger } from "./utils/logger.js";
|
|
|
89
89
|
import { redactUrlCredentials, safeDebugSerialize, sanitizeRecord, stringifyContentSafe, } from "./utils/logSanitize.js";
|
|
90
90
|
import { extractMcpErrorText } from "./utils/mcpErrorText.js";
|
|
91
91
|
import { createCustomToolServerInfo, detectCategory, } from "./utils/mcpDefaults.js";
|
|
92
|
+
import { ExternalMcpToolNotFoundError, rankToolNameCandidates, resolveToolName, } from "./utils/toolCallRepair.js";
|
|
92
93
|
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
93
94
|
// Import orchestration components
|
|
94
95
|
import { ModelRouter } from "./utils/modelRouter.js";
|
|
@@ -379,8 +380,19 @@ export class NeuroLink {
|
|
|
379
380
|
mcpToolBatcher;
|
|
380
381
|
mcpEnhancedDiscovery;
|
|
381
382
|
mcpToolMiddlewares = [];
|
|
382
|
-
/** Artifact store for externalized MCP tool outputs
|
|
383
|
+
/** Artifact store for externalized MCP tool outputs and banked payloads. */
|
|
383
384
|
mcpArtifactStore;
|
|
385
|
+
/** `artifacts` constructor config — consulted whenever a store is created. */
|
|
386
|
+
artifactsConfig;
|
|
387
|
+
/**
|
|
388
|
+
* True when this instance built `mcpArtifactStore` itself (local or Redis
|
|
389
|
+
* from config / STORAGE_TYPE). NeuroLink closes only stores it built — on
|
|
390
|
+
* replacement and on shutdown; a store handed in through `artifacts.store`
|
|
391
|
+
* or `setArtifactStore()` is the caller's to close.
|
|
392
|
+
*/
|
|
393
|
+
ownsArtifactStore = false;
|
|
394
|
+
/** Normalizer settings, kept so `setArtifactStore()` can rebuild it. */
|
|
395
|
+
mcpOutputNormalizerConfig;
|
|
384
396
|
_disableToolCacheForCurrentRequest = false;
|
|
385
397
|
/**
|
|
386
398
|
* (toolName + args) keys already served during the CURRENT request.
|
|
@@ -1197,24 +1209,22 @@ export class NeuroLink {
|
|
|
1197
1209
|
});
|
|
1198
1210
|
}
|
|
1199
1211
|
// ToolRouter — lazy-initialized when 2+ external servers exist (see addExternalMCPServer)
|
|
1212
|
+
// Artifact storage — where externalized MCP outputs and banked payloads
|
|
1213
|
+
// live. Kept so both creation sites (externalize here, and
|
|
1214
|
+
// getArtifactStore() on demand) resolve the same backend.
|
|
1215
|
+
this.artifactsConfig = config?.artifacts;
|
|
1200
1216
|
// McpOutputNormalizer — active when mcp.outputLimits is configured
|
|
1201
1217
|
if (mcpConfig?.outputLimits) {
|
|
1202
1218
|
const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
|
|
1203
1219
|
const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
|
|
1204
1220
|
const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
|
|
1221
|
+
this.mcpOutputNormalizerConfig = { strategy, maxBytes, warnBytes };
|
|
1205
1222
|
let artifactStore;
|
|
1206
1223
|
if (strategy === "externalize") {
|
|
1207
|
-
artifactStore =
|
|
1224
|
+
artifactStore = this.createConfiguredArtifactStore();
|
|
1208
1225
|
this.mcpArtifactStore = artifactStore;
|
|
1209
|
-
logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
|
|
1210
1226
|
}
|
|
1211
|
-
|
|
1212
|
-
this.externalServerManager.setOutputNormalizer(normalizer);
|
|
1213
|
-
logger.debug("[NeuroLink] MCP output normalizer initialized", {
|
|
1214
|
-
strategy,
|
|
1215
|
-
maxBytes,
|
|
1216
|
-
warnBytes,
|
|
1217
|
-
});
|
|
1227
|
+
this.installOutputNormalizer(artifactStore);
|
|
1218
1228
|
}
|
|
1219
1229
|
}
|
|
1220
1230
|
/**
|
|
@@ -2805,6 +2815,17 @@ Current user's request: ${currentInput}`;
|
|
|
2805
2815
|
this._taskManager = undefined;
|
|
2806
2816
|
}
|
|
2807
2817
|
}
|
|
2818
|
+
// Release the artifact store this instance built (a pooled Redis
|
|
2819
|
+
// connection, for one). An injected store is the caller's to close.
|
|
2820
|
+
if (this.ownsArtifactStore && this.mcpArtifactStore?.close) {
|
|
2821
|
+
try {
|
|
2822
|
+
await this.mcpArtifactStore.close();
|
|
2823
|
+
logger.debug("[NeuroLink] Artifact store closed");
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
logger.warn("[NeuroLink] Artifact store close failed:", error);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2808
2829
|
// Close conversation memory manager (release Redis connections, etc.)
|
|
2809
2830
|
if (this.conversationMemory?.close) {
|
|
2810
2831
|
try {
|
|
@@ -11732,8 +11753,16 @@ Current user's request: ${currentInput}`;
|
|
|
11732
11753
|
* @param options - Execution options
|
|
11733
11754
|
* @returns Tool execution result
|
|
11734
11755
|
*/
|
|
11735
|
-
async executeExternalMCPTool(serverId,
|
|
11756
|
+
async executeExternalMCPTool(serverId, requestedToolName, parameters, options) {
|
|
11757
|
+
// Falls back to the requested name so the catch block below still has
|
|
11758
|
+
// something meaningful to log if resolution itself is what throws.
|
|
11759
|
+
let toolName = requestedToolName;
|
|
11736
11760
|
try {
|
|
11761
|
+
// Direct-boundary name repair (see resolveDirectMcpToolName): an exact
|
|
11762
|
+
// match returns requestedToolName unchanged, so every existing caller
|
|
11763
|
+
// — including the AI-SDK generation path's createExternalMCPTool,
|
|
11764
|
+
// which always calls with an already-discovered name — is unaffected.
|
|
11765
|
+
toolName = this.resolveDirectMcpToolName(serverId, requestedToolName);
|
|
11737
11766
|
mcpLogger.debug(`[NeuroLink] Executing external MCP tool: ${toolName} on ${serverId}`);
|
|
11738
11767
|
// BZ-664: Check existing ToolResultCache before executing to avoid
|
|
11739
11768
|
// duplicate identical calls within the same session.
|
|
@@ -11803,6 +11832,65 @@ Current user's request: ${currentInput}`;
|
|
|
11803
11832
|
throw error;
|
|
11804
11833
|
}
|
|
11805
11834
|
}
|
|
11835
|
+
/**
|
|
11836
|
+
* Resolve a tool name for a direct external MCP execution
|
|
11837
|
+
* (`executeExternalMCPTool`) against the server's currently discovered
|
|
11838
|
+
* tools.
|
|
11839
|
+
*
|
|
11840
|
+
* `experimental_repairToolCall` only runs inside the AI-SDK's own
|
|
11841
|
+
* streamText/generateText loop (see toolCallRepair.ts), so a near-miss
|
|
11842
|
+
* tool name reaching `executeExternalMCPTool` directly previously had no
|
|
11843
|
+
* recovery at all — just the generic `Tool 'x' not found for server 'y'`
|
|
11844
|
+
* `Error` that `ToolDiscoveryService.executeTool` throws deeper in the
|
|
11845
|
+
* stack. This reuses the same name-matching policy
|
|
11846
|
+
* (`resolveToolName`: case-insensitive exact → unambiguous substring →
|
|
11847
|
+
* Levenshtein) so a repair here is accepted under exactly the rules
|
|
11848
|
+
* already proven for the generation path.
|
|
11849
|
+
*
|
|
11850
|
+
* Exact match is a zero-risk fast path: it is returned unchanged before
|
|
11851
|
+
* any resolution attempt, so every existing caller that already sends a
|
|
11852
|
+
* valid name — including the AI-SDK path's `createExternalMCPTool`, which
|
|
11853
|
+
* always executes with a name it just discovered — sees no behaviour
|
|
11854
|
+
* change.
|
|
11855
|
+
*
|
|
11856
|
+
* Deliberately does NOT attempt a repair when the server is unknown or
|
|
11857
|
+
* not connected: `ExternalServerManager.executeTool` throws distinct,
|
|
11858
|
+
* more accurate errors for those states ("Server 'x' not found" /
|
|
11859
|
+
* "not in connected state"), and resolving against an empty tool list
|
|
11860
|
+
* here would replace those with a misleading "tool not found" instead.
|
|
11861
|
+
*/
|
|
11862
|
+
resolveDirectMcpToolName(serverId, requestedName) {
|
|
11863
|
+
const server = this.getExternalMCPServer(serverId);
|
|
11864
|
+
if (!server || server.status !== "connected" || !server.client) {
|
|
11865
|
+
return requestedName;
|
|
11866
|
+
}
|
|
11867
|
+
const availableNames = this.getExternalMCPServerTools(serverId).map((tool) => tool.name);
|
|
11868
|
+
if (availableNames.includes(requestedName)) {
|
|
11869
|
+
return requestedName;
|
|
11870
|
+
}
|
|
11871
|
+
const resolution = resolveToolName(requestedName, availableNames);
|
|
11872
|
+
if (!resolution) {
|
|
11873
|
+
throw new ExternalMcpToolNotFoundError(requestedName, serverId, rankToolNameCandidates(requestedName, availableNames));
|
|
11874
|
+
}
|
|
11875
|
+
// Recorded on a short-lived span rather than the method's return value:
|
|
11876
|
+
// executeExternalMCPTool returns the raw upstream tool result verbatim
|
|
11877
|
+
// (widely consumed as-is, e.g. by createExternalMCPTool's execute()),
|
|
11878
|
+
// so wrapping it to carry resolution metadata would be a breaking
|
|
11879
|
+
// change for every existing direct caller.
|
|
11880
|
+
tracers.mcp.startActiveSpan("neurolink.mcp.toolNameRepair", {
|
|
11881
|
+
attributes: {
|
|
11882
|
+
"mcp.server_id": serverId,
|
|
11883
|
+
"mcp.tool_name.requested": requestedName,
|
|
11884
|
+
"mcp.tool_name.resolved": resolution.name,
|
|
11885
|
+
"mcp.tool_name.repair_strategy": resolution.strategy,
|
|
11886
|
+
...(resolution.score !== undefined
|
|
11887
|
+
? { "mcp.tool_name.repair_score": resolution.score }
|
|
11888
|
+
: {}),
|
|
11889
|
+
},
|
|
11890
|
+
}, (span) => span.end());
|
|
11891
|
+
mcpLogger.info(`[NeuroLink] Repaired external MCP tool name at direct execution boundary: "${requestedName}" → "${resolution.name}" (${resolution.strategy}) on server '${serverId}'`);
|
|
11892
|
+
return resolution.name;
|
|
11893
|
+
}
|
|
11806
11894
|
/**
|
|
11807
11895
|
* Get all tools from external MCP servers
|
|
11808
11896
|
* @returns Array of external tool information
|
|
@@ -12942,13 +13030,86 @@ Current user's request: ${currentInput}`;
|
|
|
12942
13030
|
*/
|
|
12943
13031
|
getArtifactStore() {
|
|
12944
13032
|
if (!this.mcpArtifactStore) {
|
|
12945
|
-
this.mcpArtifactStore =
|
|
12946
|
-
logger.debug("[NeuroLink] Artifact store created on demand (local-temp) for banking");
|
|
13033
|
+
this.mcpArtifactStore = this.createConfiguredArtifactStore();
|
|
12947
13034
|
}
|
|
12948
13035
|
// A no-op when the constructor already registered it.
|
|
12949
13036
|
this.registerMemoryRetrievalTools();
|
|
12950
13037
|
return this.mcpArtifactStore;
|
|
12951
13038
|
}
|
|
13039
|
+
/**
|
|
13040
|
+
* Replace this instance's artifact store.
|
|
13041
|
+
*
|
|
13042
|
+
* Everything that writes or reads artifacts follows the swap: banking,
|
|
13043
|
+
* `retrieve_context`, host-side `readArtifact`, and the MCP output
|
|
13044
|
+
* normalizer — which is rebuilt here because it captured the previous store
|
|
13045
|
+
* at construction. Assigning the field alone would miss it, and
|
|
13046
|
+
* externalized tool outputs would keep landing in the old backend while
|
|
13047
|
+
* read-backs looked in the new one.
|
|
13048
|
+
*
|
|
13049
|
+
* Call it before the first bank or externalized tool output: artifacts
|
|
13050
|
+
* already in the previous store are not migrated, and their ids stop
|
|
13051
|
+
* resolving through this instance. `artifacts.store` in the constructor
|
|
13052
|
+
* config is the same thing without the ordering concern.
|
|
13053
|
+
*
|
|
13054
|
+
* Ownership: a store you hand in — here or via `artifacts.store` — stays
|
|
13055
|
+
* yours to close. NeuroLink closes only the stores it built itself, when
|
|
13056
|
+
* they are replaced here and on `shutdown()`.
|
|
13057
|
+
*
|
|
13058
|
+
* @param store - Any {@link ArtifactStore}
|
|
13059
|
+
*/
|
|
13060
|
+
setArtifactStore(store) {
|
|
13061
|
+
const previous = this.mcpArtifactStore;
|
|
13062
|
+
if (previous === store) {
|
|
13063
|
+
return;
|
|
13064
|
+
}
|
|
13065
|
+
if (previous) {
|
|
13066
|
+
logger.warn("[NeuroLink] Artifact store replaced — artifacts already written to " +
|
|
13067
|
+
"the previous store will not resolve through this instance");
|
|
13068
|
+
if (this.ownsArtifactStore && previous.close) {
|
|
13069
|
+
void previous.close().catch((error) => {
|
|
13070
|
+
logger.warn("[NeuroLink] Previous artifact store close failed", {
|
|
13071
|
+
error: error instanceof Error ? error.message : String(error),
|
|
13072
|
+
});
|
|
13073
|
+
});
|
|
13074
|
+
}
|
|
13075
|
+
}
|
|
13076
|
+
this.mcpArtifactStore = store;
|
|
13077
|
+
this.ownsArtifactStore = false;
|
|
13078
|
+
this.installOutputNormalizer(store);
|
|
13079
|
+
this.registerMemoryRetrievalTools();
|
|
13080
|
+
}
|
|
13081
|
+
/**
|
|
13082
|
+
* Build the store the `artifacts` config (or `STORAGE_TYPE`) asks for. The
|
|
13083
|
+
* Redis connection falls back to conversation memory's, so one
|
|
13084
|
+
* `STORAGE_TYPE=redis` moves sessions and artifacts together.
|
|
13085
|
+
*/
|
|
13086
|
+
createConfiguredArtifactStore() {
|
|
13087
|
+
const injected = this.artifactsConfig?.store !== undefined;
|
|
13088
|
+
const store = createArtifactStore(this.artifactsConfig, this.conversationMemoryConfig?.conversationMemory?.redisConfig);
|
|
13089
|
+
// An injected store is returned as-is by the factory and stays the
|
|
13090
|
+
// caller's; anything else was built here and is this instance's to close.
|
|
13091
|
+
this.ownsArtifactStore = !injected;
|
|
13092
|
+
logger.debug("[NeuroLink] Artifact store initialized", {
|
|
13093
|
+
backend: injected
|
|
13094
|
+
? "custom"
|
|
13095
|
+
: resolveArtifactStorageType(this.artifactsConfig),
|
|
13096
|
+
});
|
|
13097
|
+
return store;
|
|
13098
|
+
}
|
|
13099
|
+
/**
|
|
13100
|
+
* (Re)build the MCP output normalizer over `artifactStore`. A no-op unless
|
|
13101
|
+
* `mcp.outputLimits` was configured — without it nothing is externalized.
|
|
13102
|
+
*/
|
|
13103
|
+
installOutputNormalizer(artifactStore) {
|
|
13104
|
+
if (!this.mcpOutputNormalizerConfig) {
|
|
13105
|
+
return;
|
|
13106
|
+
}
|
|
13107
|
+
this.externalServerManager.setOutputNormalizer(new McpOutputNormalizer(this.mcpOutputNormalizerConfig, artifactStore));
|
|
13108
|
+
logger.debug("[NeuroLink] MCP output normalizer initialized", {
|
|
13109
|
+
...this.mcpOutputNormalizerConfig,
|
|
13110
|
+
hasArtifactStore: artifactStore !== undefined,
|
|
13111
|
+
});
|
|
13112
|
+
}
|
|
12952
13113
|
/**
|
|
12953
13114
|
* Bank a payload to a file and get back a pointer to it.
|
|
12954
13115
|
*
|
|
@@ -13287,7 +13448,24 @@ Current user's request: ${currentInput}`;
|
|
|
13287
13448
|
logger.warn("[NeuroLink] Error shutting down external MCP servers:", error);
|
|
13288
13449
|
}
|
|
13289
13450
|
}
|
|
13290
|
-
// 3.
|
|
13451
|
+
// 3. Release the artifact store this instance built (a pooled Redis
|
|
13452
|
+
// connection, for one). Same ownership rule as shutdown(): an injected
|
|
13453
|
+
// store is the caller's to close. Without this step every
|
|
13454
|
+
// construct / use / dispose cycle left the pooled reference acquired.
|
|
13455
|
+
if (this.ownsArtifactStore && this.mcpArtifactStore?.close) {
|
|
13456
|
+
try {
|
|
13457
|
+
await this.mcpArtifactStore.close();
|
|
13458
|
+
logger.debug("[NeuroLink] Artifact store closed");
|
|
13459
|
+
}
|
|
13460
|
+
catch (error) {
|
|
13461
|
+
const err = error instanceof Error
|
|
13462
|
+
? error
|
|
13463
|
+
: new Error(`Artifact store close error: ${String(error)}`);
|
|
13464
|
+
cleanupErrors.push(err);
|
|
13465
|
+
logger.warn("[NeuroLink] Error closing artifact store:", error);
|
|
13466
|
+
}
|
|
13467
|
+
}
|
|
13468
|
+
// 4. Clear all event listeners to prevent memory leaks
|
|
13291
13469
|
if (this.emitter) {
|
|
13292
13470
|
try {
|
|
13293
13471
|
logger.debug("[NeuroLink] Removing all event listeners...");
|
|
@@ -13305,7 +13483,7 @@ Current user's request: ${currentInput}`;
|
|
|
13305
13483
|
logger.warn("[NeuroLink] Error removing event listeners:", error);
|
|
13306
13484
|
}
|
|
13307
13485
|
}
|
|
13308
|
-
//
|
|
13486
|
+
// 5. Clear all circuit breakers
|
|
13309
13487
|
if (this.toolCircuitBreakers && this.toolCircuitBreakers.size > 0) {
|
|
13310
13488
|
try {
|
|
13311
13489
|
logger.debug(`[NeuroLink] Clearing ${this.toolCircuitBreakers.size} circuit breakers...`);
|
|
@@ -13320,7 +13498,7 @@ Current user's request: ${currentInput}`;
|
|
|
13320
13498
|
logger.warn("[NeuroLink] Error clearing circuit breakers:", error);
|
|
13321
13499
|
}
|
|
13322
13500
|
}
|
|
13323
|
-
//
|
|
13501
|
+
// 6. Clear all Maps and caches
|
|
13324
13502
|
try {
|
|
13325
13503
|
logger.debug("[NeuroLink] Clearing maps and caches...");
|
|
13326
13504
|
if (this.toolExecutionMetrics) {
|
|
@@ -13371,7 +13549,7 @@ Current user's request: ${currentInput}`;
|
|
|
13371
13549
|
this._taskManager = undefined;
|
|
13372
13550
|
}
|
|
13373
13551
|
}
|
|
13374
|
-
//
|
|
13552
|
+
// 7. Reset initialization flags
|
|
13375
13553
|
try {
|
|
13376
13554
|
logger.debug("[NeuroLink] Resetting initialization state...");
|
|
13377
13555
|
this.mcpInitialized = false;
|
|
@@ -13387,7 +13565,7 @@ Current user's request: ${currentInput}`;
|
|
|
13387
13565
|
cleanupErrors.push(err);
|
|
13388
13566
|
logger.warn("[NeuroLink] Error resetting state:", error);
|
|
13389
13567
|
}
|
|
13390
|
-
//
|
|
13568
|
+
// 7. Log completion
|
|
13391
13569
|
if (cleanupErrors.length === 0) {
|
|
13392
13570
|
logger.debug("[NeuroLink] ✅ Resource disposal completed successfully");
|
|
13393
13571
|
}
|