@juspay/neurolink 12.9.6 → 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 +367 -371
- 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 +45 -1
- package/dist/neurolink.js +128 -18
- package/dist/types/artifact.d.ts +124 -3
- package/dist/types/config.d.ts +7 -0
- package/dist/utils/redis.d.ts +15 -0
- package/dist/utils/redis.js +64 -6
- package/package.json +5 -5
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.
|
|
@@ -2735,6 +2746,39 @@ export declare class NeuroLink {
|
|
|
2735
2746
|
* @returns The artifact store backing {@link bankArtifact} / {@link readArtifact}
|
|
2736
2747
|
*/
|
|
2737
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;
|
|
2738
2782
|
/**
|
|
2739
2783
|
* Bank a payload to a file and get back a pointer to it.
|
|
2740
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
|
|
@@ -380,8 +380,19 @@ export class NeuroLink {
|
|
|
380
380
|
mcpToolBatcher;
|
|
381
381
|
mcpEnhancedDiscovery;
|
|
382
382
|
mcpToolMiddlewares = [];
|
|
383
|
-
/** Artifact store for externalized MCP tool outputs
|
|
383
|
+
/** Artifact store for externalized MCP tool outputs and banked payloads. */
|
|
384
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;
|
|
385
396
|
_disableToolCacheForCurrentRequest = false;
|
|
386
397
|
/**
|
|
387
398
|
* (toolName + args) keys already served during the CURRENT request.
|
|
@@ -1198,24 +1209,22 @@ export class NeuroLink {
|
|
|
1198
1209
|
});
|
|
1199
1210
|
}
|
|
1200
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;
|
|
1201
1216
|
// McpOutputNormalizer — active when mcp.outputLimits is configured
|
|
1202
1217
|
if (mcpConfig?.outputLimits) {
|
|
1203
1218
|
const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
|
|
1204
1219
|
const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
|
|
1205
1220
|
const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
|
|
1221
|
+
this.mcpOutputNormalizerConfig = { strategy, maxBytes, warnBytes };
|
|
1206
1222
|
let artifactStore;
|
|
1207
1223
|
if (strategy === "externalize") {
|
|
1208
|
-
artifactStore =
|
|
1224
|
+
artifactStore = this.createConfiguredArtifactStore();
|
|
1209
1225
|
this.mcpArtifactStore = artifactStore;
|
|
1210
|
-
logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
|
|
1211
1226
|
}
|
|
1212
|
-
|
|
1213
|
-
this.externalServerManager.setOutputNormalizer(normalizer);
|
|
1214
|
-
logger.debug("[NeuroLink] MCP output normalizer initialized", {
|
|
1215
|
-
strategy,
|
|
1216
|
-
maxBytes,
|
|
1217
|
-
warnBytes,
|
|
1218
|
-
});
|
|
1227
|
+
this.installOutputNormalizer(artifactStore);
|
|
1219
1228
|
}
|
|
1220
1229
|
}
|
|
1221
1230
|
/**
|
|
@@ -2806,6 +2815,17 @@ Current user's request: ${currentInput}`;
|
|
|
2806
2815
|
this._taskManager = undefined;
|
|
2807
2816
|
}
|
|
2808
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
|
+
}
|
|
2809
2829
|
// Close conversation memory manager (release Redis connections, etc.)
|
|
2810
2830
|
if (this.conversationMemory?.close) {
|
|
2811
2831
|
try {
|
|
@@ -13010,13 +13030,86 @@ Current user's request: ${currentInput}`;
|
|
|
13010
13030
|
*/
|
|
13011
13031
|
getArtifactStore() {
|
|
13012
13032
|
if (!this.mcpArtifactStore) {
|
|
13013
|
-
this.mcpArtifactStore =
|
|
13014
|
-
logger.debug("[NeuroLink] Artifact store created on demand (local-temp) for banking");
|
|
13033
|
+
this.mcpArtifactStore = this.createConfiguredArtifactStore();
|
|
13015
13034
|
}
|
|
13016
13035
|
// A no-op when the constructor already registered it.
|
|
13017
13036
|
this.registerMemoryRetrievalTools();
|
|
13018
13037
|
return this.mcpArtifactStore;
|
|
13019
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
|
+
}
|
|
13020
13113
|
/**
|
|
13021
13114
|
* Bank a payload to a file and get back a pointer to it.
|
|
13022
13115
|
*
|
|
@@ -13355,7 +13448,24 @@ Current user's request: ${currentInput}`;
|
|
|
13355
13448
|
logger.warn("[NeuroLink] Error shutting down external MCP servers:", error);
|
|
13356
13449
|
}
|
|
13357
13450
|
}
|
|
13358
|
-
// 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
|
|
13359
13469
|
if (this.emitter) {
|
|
13360
13470
|
try {
|
|
13361
13471
|
logger.debug("[NeuroLink] Removing all event listeners...");
|
|
@@ -13373,7 +13483,7 @@ Current user's request: ${currentInput}`;
|
|
|
13373
13483
|
logger.warn("[NeuroLink] Error removing event listeners:", error);
|
|
13374
13484
|
}
|
|
13375
13485
|
}
|
|
13376
|
-
//
|
|
13486
|
+
// 5. Clear all circuit breakers
|
|
13377
13487
|
if (this.toolCircuitBreakers && this.toolCircuitBreakers.size > 0) {
|
|
13378
13488
|
try {
|
|
13379
13489
|
logger.debug(`[NeuroLink] Clearing ${this.toolCircuitBreakers.size} circuit breakers...`);
|
|
@@ -13388,7 +13498,7 @@ Current user's request: ${currentInput}`;
|
|
|
13388
13498
|
logger.warn("[NeuroLink] Error clearing circuit breakers:", error);
|
|
13389
13499
|
}
|
|
13390
13500
|
}
|
|
13391
|
-
//
|
|
13501
|
+
// 6. Clear all Maps and caches
|
|
13392
13502
|
try {
|
|
13393
13503
|
logger.debug("[NeuroLink] Clearing maps and caches...");
|
|
13394
13504
|
if (this.toolExecutionMetrics) {
|
|
@@ -13439,7 +13549,7 @@ Current user's request: ${currentInput}`;
|
|
|
13439
13549
|
this._taskManager = undefined;
|
|
13440
13550
|
}
|
|
13441
13551
|
}
|
|
13442
|
-
//
|
|
13552
|
+
// 7. Reset initialization flags
|
|
13443
13553
|
try {
|
|
13444
13554
|
logger.debug("[NeuroLink] Resetting initialization state...");
|
|
13445
13555
|
this.mcpInitialized = false;
|
|
@@ -13455,7 +13565,7 @@ Current user's request: ${currentInput}`;
|
|
|
13455
13565
|
cleanupErrors.push(err);
|
|
13456
13566
|
logger.warn("[NeuroLink] Error resetting state:", error);
|
|
13457
13567
|
}
|
|
13458
|
-
//
|
|
13568
|
+
// 7. Log completion
|
|
13459
13569
|
if (cleanupErrors.length === 0) {
|
|
13460
13570
|
logger.debug("[NeuroLink] ✅ Resource disposal completed successfully");
|
|
13461
13571
|
}
|
package/dist/types/artifact.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module types/artifactTypes
|
|
9
9
|
*/
|
|
10
|
+
import type { RedisStorageConfig } from "./conversation.js";
|
|
10
11
|
/** Metadata recorded alongside a stored artifact. */
|
|
11
12
|
export type ArtifactMeta = {
|
|
12
13
|
/** Tool name that produced the output. */
|
|
@@ -83,10 +84,63 @@ export type ArtifactPageRequest = {
|
|
|
83
84
|
limit?: number;
|
|
84
85
|
};
|
|
85
86
|
/**
|
|
86
|
-
*
|
|
87
|
+
* One window of an artifact, as returned by `ArtifactStore.retrieveRange` or
|
|
88
|
+
* by the shared reader when the store only supports whole-payload reads.
|
|
87
89
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
+
* Offsets and lengths are CHARACTERS (UTF-16 code units, the unit
|
|
91
|
+
* `String.prototype.slice` and `retrieve_context`'s `offset` / `limit` use),
|
|
92
|
+
* never bytes — so a model advancing `offset` by the characters it received
|
|
93
|
+
* lands exactly where the previous window ended.
|
|
94
|
+
*/
|
|
95
|
+
export type ArtifactWindow = {
|
|
96
|
+
/** The characters in `[offset, offset + content.length)`. */
|
|
97
|
+
content: string;
|
|
98
|
+
/** Character offset this window starts at. */
|
|
99
|
+
offset: number;
|
|
100
|
+
/** Total character length of the whole payload. */
|
|
101
|
+
totalLength: number;
|
|
102
|
+
};
|
|
103
|
+
/** One hit from a literal search over an artifact. */
|
|
104
|
+
export type ArtifactSearchMatch = {
|
|
105
|
+
/** Character offset of the match — pass it back as `offset` to read there. */
|
|
106
|
+
offset: number;
|
|
107
|
+
/** 1-based line number the match sits on. */
|
|
108
|
+
line: number;
|
|
109
|
+
/** Character offset the snippet starts at (≤ `offset`). */
|
|
110
|
+
snippetOffset: number;
|
|
111
|
+
/**
|
|
112
|
+
* Bounded context around the match. Bounded on purpose: an MCP artifact is
|
|
113
|
+
* usually one compact JSON line, so "the matching line" would be the whole
|
|
114
|
+
* payload.
|
|
115
|
+
*/
|
|
116
|
+
snippet: string;
|
|
117
|
+
};
|
|
118
|
+
/** Result of a literal search over an artifact. */
|
|
119
|
+
export type ArtifactSearchResult = {
|
|
120
|
+
/** Matches returned, in payload order. */
|
|
121
|
+
matches: ArtifactSearchMatch[];
|
|
122
|
+
/** `matches.length`. */
|
|
123
|
+
matchCount: number;
|
|
124
|
+
/** Every match in the payload, including the ones not returned. */
|
|
125
|
+
totalMatches: number;
|
|
126
|
+
/** True when `totalMatches > matchCount`. */
|
|
127
|
+
truncated: boolean;
|
|
128
|
+
/** Character offset to pass as `offset` to search for the next matches. */
|
|
129
|
+
nextSearchOffset?: number;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Pluggable storage contract for externalized MCP tool outputs and banked
|
|
133
|
+
* payloads.
|
|
134
|
+
*
|
|
135
|
+
* Shipped backends: `LocalTempArtifactStore` (filesystem, per-process index
|
|
136
|
+
* with a cross-process sidecar) and `RedisArtifactStore` (TTL-expired, shared
|
|
137
|
+
* across replicas, range reads). Pick one with `artifacts.storage` or the
|
|
138
|
+
* `STORAGE_TYPE` environment variable, or inject any implementation via
|
|
139
|
+
* `artifacts.store` / `setArtifactStore()`.
|
|
140
|
+
*
|
|
141
|
+
* Only `store`, `retrieve`, `delete`, `cleanup` and `generatePreview` are
|
|
142
|
+
* required. `retrieveRange` and `close` are optional capabilities: NeuroLink
|
|
143
|
+
* uses them when present and falls back cleanly when absent.
|
|
90
144
|
*/
|
|
91
145
|
export type ArtifactStore = {
|
|
92
146
|
/**
|
|
@@ -100,6 +154,22 @@ export type ArtifactStore = {
|
|
|
100
154
|
* Returns `null` if the artifact is not found or has been cleaned up.
|
|
101
155
|
*/
|
|
102
156
|
retrieve(id: string): Promise<string | null>;
|
|
157
|
+
/**
|
|
158
|
+
* Retrieve one character window without materialising the whole payload.
|
|
159
|
+
*
|
|
160
|
+
* Optional. When present, `retrieve_context` and `readArtifact` call it for
|
|
161
|
+
* every paged read instead of `retrieve()` + slice, so a backend with native
|
|
162
|
+
* range reads (Redis `GETRANGE`, S3 `Range`) moves only the window. The
|
|
163
|
+
* result carries `totalLength` so `hasMore` never needs the payload.
|
|
164
|
+
*
|
|
165
|
+
* `offset` and `limit` are characters. A backend that can only address
|
|
166
|
+
* bytes must either know the payload is single-byte (ASCII) or fall back to
|
|
167
|
+
* a full read and slice — it must never return a window that starts at the
|
|
168
|
+
* wrong character. `limit` omitted means "to the end".
|
|
169
|
+
*
|
|
170
|
+
* Returns `null` if the artifact is not found or has expired.
|
|
171
|
+
*/
|
|
172
|
+
retrieveRange?(id: string, range: ArtifactPageRequest): Promise<ArtifactWindow | null>;
|
|
103
173
|
/** Delete a single artifact. No-op if the ID does not exist. */
|
|
104
174
|
delete(id: string): Promise<void>;
|
|
105
175
|
/**
|
|
@@ -109,6 +179,13 @@ export type ArtifactStore = {
|
|
|
109
179
|
cleanup(olderThanMs: number): Promise<number>;
|
|
110
180
|
/** Generate a short preview string from a serialized payload. */
|
|
111
181
|
generatePreview(payload: string): string;
|
|
182
|
+
/**
|
|
183
|
+
* Release whatever the store holds open (a pooled connection, a file
|
|
184
|
+
* handle). Optional. NeuroLink calls it — from `shutdown()`, and when
|
|
185
|
+
* `setArtifactStore()` replaces the store — only for stores it built
|
|
186
|
+
* itself; a store you inject is yours to close.
|
|
187
|
+
*/
|
|
188
|
+
close?(): Promise<void>;
|
|
112
189
|
};
|
|
113
190
|
/**
|
|
114
191
|
* In-memory index row tracked by LocalTempArtifactStore.
|
|
@@ -122,3 +199,47 @@ export type IndexEntry = ArtifactMeta & {
|
|
|
122
199
|
*/
|
|
123
200
|
rehydrated?: boolean;
|
|
124
201
|
};
|
|
202
|
+
/**
|
|
203
|
+
* Where artifacts live. Mirrors conversation memory's `STORAGE_TYPE`:
|
|
204
|
+
* - "local" OS temp directory, per-process index with a cross-process
|
|
205
|
+
* sidecar. Fine for one machine; artifacts do not survive a pod.
|
|
206
|
+
* - "redis" Shared across replicas, expired by TTL, range reads via
|
|
207
|
+
* `GETRANGE`. Uses the same connection pool as Redis conversation
|
|
208
|
+
* memory.
|
|
209
|
+
*/
|
|
210
|
+
export type ArtifactStorageType = "local" | "redis";
|
|
211
|
+
/**
|
|
212
|
+
* Artifact storage configuration (`new NeuroLink({ artifacts })`).
|
|
213
|
+
*
|
|
214
|
+
* Resolution order for the backend: `store` → `storage` → `STORAGE_TYPE`
|
|
215
|
+
* environment variable → `"local"`. Resolution order for the Redis connection:
|
|
216
|
+
* `redisConfig` → `conversationMemory.redisConfig` → `REDIS_URL` / `REDIS_HOST`
|
|
217
|
+
* environment variables — so a deployment already running conversation memory
|
|
218
|
+
* on Redis keeps its artifacts on the same Redis without new settings.
|
|
219
|
+
*/
|
|
220
|
+
export type ArtifactStorageConfig = {
|
|
221
|
+
/** Backend to use. Default: `STORAGE_TYPE` env var, else `"local"`. */
|
|
222
|
+
storage?: ArtifactStorageType;
|
|
223
|
+
/**
|
|
224
|
+
* Redis connection for `storage: "redis"`. `keyPrefix` defaults to
|
|
225
|
+
* `neurolink:artifact:` (NOT the conversation prefix). `ttl` is seconds,
|
|
226
|
+
* must be positive, and defaults to 86400; zero or negative is replaced by
|
|
227
|
+
* the default with a warning — artifacts in Redis always expire.
|
|
228
|
+
* `userSessionsKeyPrefix` is ignored.
|
|
229
|
+
*/
|
|
230
|
+
redisConfig?: RedisStorageConfig;
|
|
231
|
+
/**
|
|
232
|
+
* A ready-made backend. Wins over `storage`. Use this for S3, a database,
|
|
233
|
+
* or a wrapped store; `setArtifactStore()` does the same after construction.
|
|
234
|
+
*/
|
|
235
|
+
store?: ArtifactStore;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* What `RedisArtifactStore` keeps beside each payload. `charLength` is what
|
|
239
|
+
* makes range reads honest: when it equals `sizeBytes` the payload is pure
|
|
240
|
+
* ASCII and a byte range IS a character range.
|
|
241
|
+
*/
|
|
242
|
+
export type RedisArtifactRecord = ArtifactMeta & {
|
|
243
|
+
/** `payload.length` at store time — characters, not bytes. */
|
|
244
|
+
charLength: number;
|
|
245
|
+
};
|