@juspay/neurolink 9.80.3 → 9.80.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/browser/neurolink.min.js +346 -346
- package/dist/context/stages/structuredSummarizer.js +15 -4
- package/dist/core/modules/GenerationHandler.js +28 -10
- package/dist/core/modules/structuredOutputPolicy.d.ts +19 -0
- package/dist/core/modules/structuredOutputPolicy.js +26 -0
- package/dist/lib/context/stages/structuredSummarizer.js +15 -4
- package/dist/lib/core/modules/GenerationHandler.js +28 -10
- package/dist/lib/core/modules/structuredOutputPolicy.d.ts +19 -0
- package/dist/lib/core/modules/structuredOutputPolicy.js +26 -0
- package/dist/lib/mcp/externalServerManager.js +21 -6
- package/dist/lib/mcp/mcpClientFactory.js +5 -1
- package/dist/lib/neurolink.js +58 -25
- package/dist/lib/providers/googleVertex.js +116 -19
- package/dist/lib/types/generate.d.ts +13 -0
- package/dist/lib/utils/conversationMemory.js +19 -8
- package/dist/lib/utils/logSanitize.d.ts +26 -0
- package/dist/lib/utils/logSanitize.js +56 -0
- package/dist/mcp/externalServerManager.js +21 -6
- package/dist/mcp/mcpClientFactory.js +5 -1
- package/dist/neurolink.js +58 -25
- package/dist/providers/googleVertex.js +116 -19
- package/dist/types/generate.d.ts +13 -0
- package/dist/utils/conversationMemory.js +19 -8
- package/dist/utils/logSanitize.d.ts +26 -0
- package/dist/utils/logSanitize.js +56 -0
- package/package.json +1 -1
|
@@ -17,10 +17,21 @@ function findSplitIndexByTokens(messages, targetRecentTokens, provider) {
|
|
|
17
17
|
let recentTokens = 0;
|
|
18
18
|
let splitIndex = messages.length;
|
|
19
19
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Guarded: JSON.stringify on a giant single message can throw RangeError
|
|
21
|
+
// (V8 max string length). A message that big cannot be "recent-window"
|
|
22
|
+
// material anyway — treat it as effectively infinite tokens so it (and
|
|
23
|
+
// everything older) lands on the summarized side instead of aborting
|
|
24
|
+
// compaction.
|
|
25
|
+
let msgTokens;
|
|
26
|
+
try {
|
|
27
|
+
const content = typeof messages[i].content === "string"
|
|
28
|
+
? messages[i].content
|
|
29
|
+
: (JSON.stringify(messages[i].content) ?? "");
|
|
30
|
+
msgTokens = estimateTokens(content, provider);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
msgTokens = Number.MAX_SAFE_INTEGER;
|
|
34
|
+
}
|
|
24
35
|
if (recentTokens + msgTokens > targetRecentTokens) {
|
|
25
36
|
splitIndex = i + 1;
|
|
26
37
|
break;
|
|
@@ -21,7 +21,7 @@ import { calculateCost } from "../../utils/pricing.js";
|
|
|
21
21
|
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
22
22
|
import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
|
|
23
23
|
import { DEFAULT_MAX_STEPS } from "../constants.js";
|
|
24
|
-
import { isTemperatureDeprecatedError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
|
|
24
|
+
import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
|
|
25
25
|
import { coerceJsonToSchema } from "../../utils/json/coerce.js";
|
|
26
26
|
import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
|
|
27
27
|
import { Output, stepCountIs } from "../../utils/tool.js";
|
|
@@ -284,15 +284,20 @@ export class GenerationHandler {
|
|
|
284
284
|
}
|
|
285
285
|
catch (error) {
|
|
286
286
|
// Fall back to text-mode (no experimental_output) when structured
|
|
287
|
-
// output + tools failed, in
|
|
287
|
+
// output + tools failed, in three cases:
|
|
288
288
|
// 1. NoObjectGeneratedError — the SDK couldn't coerce the object.
|
|
289
289
|
// 2. The provider rejected json-mode-with-tools outright (e.g. Groq:
|
|
290
290
|
// "json mode cannot be combined with tool/function calling").
|
|
291
|
-
//
|
|
291
|
+
// 3. The provider rejected the schema as too complex for its
|
|
292
|
+
// constrained decoding (Vertex Gemini 400 "too many states") —
|
|
293
|
+
// deterministic, so re-sending the schema can never succeed.
|
|
294
|
+
// In all cases we retry without structured output and let
|
|
292
295
|
// formatEnhancedResult coerce the text response into valid JSON.
|
|
296
|
+
const schemaTooComplex = useStructuredOutput && isSchemaComplexityError(error);
|
|
293
297
|
const isStructuredOutputConflict = useStructuredOutput &&
|
|
294
298
|
(error instanceof NoObjectGeneratedError ||
|
|
295
|
-
isToolsSchemaConflictError(error)
|
|
299
|
+
isToolsSchemaConflictError(error) ||
|
|
300
|
+
schemaTooComplex);
|
|
296
301
|
if (isStructuredOutputConflict) {
|
|
297
302
|
span.setAttribute("neurolink.has_fallback", true);
|
|
298
303
|
// NLK-GAP-007: Record initial failure event before fallback retry
|
|
@@ -301,13 +306,26 @@ export class GenerationHandler {
|
|
|
301
306
|
"retry.attempt": 1,
|
|
302
307
|
"retry.reason": error instanceof NoObjectGeneratedError
|
|
303
308
|
? "NoObjectGeneratedError_structured_output_fallback"
|
|
304
|
-
:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
provider: this.providerName,
|
|
308
|
-
model: this.modelName,
|
|
309
|
-
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
: schemaTooComplex
|
|
310
|
+
? "schema_complexity_structured_output_fallback"
|
|
311
|
+
: "tools_schema_conflict_structured_output_fallback",
|
|
310
312
|
});
|
|
313
|
+
if (schemaTooComplex) {
|
|
314
|
+
// warn (not debug): callers should simplify their schema — the
|
|
315
|
+
// fallback keeps the turn alive but skips native enforcement.
|
|
316
|
+
logger.warn("[GenerationHandler] schema too complex for provider constrained decoding — retrying with prompt-based JSON", {
|
|
317
|
+
provider: this.providerName,
|
|
318
|
+
model: this.modelName,
|
|
319
|
+
error: error instanceof Error ? error.message : String(error),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
logger.debug("[GenerationHandler] structured-output conflict caught - falling back to manual JSON extraction", {
|
|
324
|
+
provider: this.providerName,
|
|
325
|
+
model: this.modelName,
|
|
326
|
+
error: error instanceof Error ? error.message : String(error),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
311
329
|
// Retry without experimental_output - the formatEnhancedResult method
|
|
312
330
|
// will extract JSON from the text response
|
|
313
331
|
const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, shouldUseTools, false), span, "generateText(fallback)");
|
|
@@ -41,6 +41,25 @@ export declare function isToolsSchemaExclusionInForce(providerName: string, mode
|
|
|
41
41
|
* and retried without structured output instead of failing the call.
|
|
42
42
|
*/
|
|
43
43
|
export declare function isToolsSchemaConflictError(error: unknown): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* True when a provider error indicates the request was rejected because the
|
|
46
|
+
* JSON schema is too complex for the provider's constrained decoding. Vertex
|
|
47
|
+
* Gemini enforces response schemas as a state machine and rejects
|
|
48
|
+
* "complex" schemas (regex patterns, string min/max lengths, nested array
|
|
49
|
+
* caps, long enums) with a deterministic 400 INVALID_ARGUMENT:
|
|
50
|
+
*
|
|
51
|
+
* "The specified schema produces a constraint that has too many states
|
|
52
|
+
* for serving. Typical causes of this error are schemas with lots of
|
|
53
|
+
* text …, schemas with long array length limits …, or schemas using
|
|
54
|
+
* complex value matchers …"
|
|
55
|
+
*
|
|
56
|
+
* The error arrives wrapped (provider error formatter + upstream JSON
|
|
57
|
+
* envelope), so match on the distinctive substring. Retrying with the same
|
|
58
|
+
* schema can never succeed — the correct recovery is to drop native schema
|
|
59
|
+
* enforcement and coerce the text response into the schema instead
|
|
60
|
+
* (same flow as isToolsSchemaConflictError).
|
|
61
|
+
*/
|
|
62
|
+
export declare function isSchemaComplexityError(error: unknown): boolean;
|
|
44
63
|
/**
|
|
45
64
|
* True when a provider error indicates the request was rejected because the
|
|
46
65
|
* `temperature` parameter is deprecated / unsupported for the model. The newest
|
|
@@ -68,6 +68,32 @@ export function isToolsSchemaConflictError(error) {
|
|
|
68
68
|
/(tool|function)[\s-]?call[^.]{0,60}json[\s_-]?(mode|schema)/i.test(message) ||
|
|
69
69
|
/response_format[^.]{0,60}(tool|function)/i.test(message));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* True when a provider error indicates the request was rejected because the
|
|
73
|
+
* JSON schema is too complex for the provider's constrained decoding. Vertex
|
|
74
|
+
* Gemini enforces response schemas as a state machine and rejects
|
|
75
|
+
* "complex" schemas (regex patterns, string min/max lengths, nested array
|
|
76
|
+
* caps, long enums) with a deterministic 400 INVALID_ARGUMENT:
|
|
77
|
+
*
|
|
78
|
+
* "The specified schema produces a constraint that has too many states
|
|
79
|
+
* for serving. Typical causes of this error are schemas with lots of
|
|
80
|
+
* text …, schemas with long array length limits …, or schemas using
|
|
81
|
+
* complex value matchers …"
|
|
82
|
+
*
|
|
83
|
+
* The error arrives wrapped (provider error formatter + upstream JSON
|
|
84
|
+
* envelope), so match on the distinctive substring. Retrying with the same
|
|
85
|
+
* schema can never succeed — the correct recovery is to drop native schema
|
|
86
|
+
* enforcement and coerce the text response into the schema instead
|
|
87
|
+
* (same flow as isToolsSchemaConflictError).
|
|
88
|
+
*/
|
|
89
|
+
export function isSchemaComplexityError(error) {
|
|
90
|
+
const message = error instanceof Error
|
|
91
|
+
? error.message
|
|
92
|
+
: typeof error === "string"
|
|
93
|
+
? error
|
|
94
|
+
: "";
|
|
95
|
+
return /too many states/i.test(message);
|
|
96
|
+
}
|
|
71
97
|
/**
|
|
72
98
|
* True when a provider error indicates the request was rejected because the
|
|
73
99
|
* `temperature` parameter is deprecated / unsupported for the model. The newest
|
|
@@ -17,10 +17,21 @@ function findSplitIndexByTokens(messages, targetRecentTokens, provider) {
|
|
|
17
17
|
let recentTokens = 0;
|
|
18
18
|
let splitIndex = messages.length;
|
|
19
19
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Guarded: JSON.stringify on a giant single message can throw RangeError
|
|
21
|
+
// (V8 max string length). A message that big cannot be "recent-window"
|
|
22
|
+
// material anyway — treat it as effectively infinite tokens so it (and
|
|
23
|
+
// everything older) lands on the summarized side instead of aborting
|
|
24
|
+
// compaction.
|
|
25
|
+
let msgTokens;
|
|
26
|
+
try {
|
|
27
|
+
const content = typeof messages[i].content === "string"
|
|
28
|
+
? messages[i].content
|
|
29
|
+
: (JSON.stringify(messages[i].content) ?? "");
|
|
30
|
+
msgTokens = estimateTokens(content, provider);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
msgTokens = Number.MAX_SAFE_INTEGER;
|
|
34
|
+
}
|
|
24
35
|
if (recentTokens + msgTokens > targetRecentTokens) {
|
|
25
36
|
splitIndex = i + 1;
|
|
26
37
|
break;
|
|
@@ -21,7 +21,7 @@ import { calculateCost } from "../../utils/pricing.js";
|
|
|
21
21
|
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
22
22
|
import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
|
|
23
23
|
import { DEFAULT_MAX_STEPS } from "../constants.js";
|
|
24
|
-
import { isTemperatureDeprecatedError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
|
|
24
|
+
import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
|
|
25
25
|
import { coerceJsonToSchema } from "../../utils/json/coerce.js";
|
|
26
26
|
import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
|
|
27
27
|
import { Output, stepCountIs } from "../../utils/tool.js";
|
|
@@ -284,15 +284,20 @@ export class GenerationHandler {
|
|
|
284
284
|
}
|
|
285
285
|
catch (error) {
|
|
286
286
|
// Fall back to text-mode (no experimental_output) when structured
|
|
287
|
-
// output + tools failed, in
|
|
287
|
+
// output + tools failed, in three cases:
|
|
288
288
|
// 1. NoObjectGeneratedError — the SDK couldn't coerce the object.
|
|
289
289
|
// 2. The provider rejected json-mode-with-tools outright (e.g. Groq:
|
|
290
290
|
// "json mode cannot be combined with tool/function calling").
|
|
291
|
-
//
|
|
291
|
+
// 3. The provider rejected the schema as too complex for its
|
|
292
|
+
// constrained decoding (Vertex Gemini 400 "too many states") —
|
|
293
|
+
// deterministic, so re-sending the schema can never succeed.
|
|
294
|
+
// In all cases we retry without structured output and let
|
|
292
295
|
// formatEnhancedResult coerce the text response into valid JSON.
|
|
296
|
+
const schemaTooComplex = useStructuredOutput && isSchemaComplexityError(error);
|
|
293
297
|
const isStructuredOutputConflict = useStructuredOutput &&
|
|
294
298
|
(error instanceof NoObjectGeneratedError ||
|
|
295
|
-
isToolsSchemaConflictError(error)
|
|
299
|
+
isToolsSchemaConflictError(error) ||
|
|
300
|
+
schemaTooComplex);
|
|
296
301
|
if (isStructuredOutputConflict) {
|
|
297
302
|
span.setAttribute("neurolink.has_fallback", true);
|
|
298
303
|
// NLK-GAP-007: Record initial failure event before fallback retry
|
|
@@ -301,13 +306,26 @@ export class GenerationHandler {
|
|
|
301
306
|
"retry.attempt": 1,
|
|
302
307
|
"retry.reason": error instanceof NoObjectGeneratedError
|
|
303
308
|
? "NoObjectGeneratedError_structured_output_fallback"
|
|
304
|
-
:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
provider: this.providerName,
|
|
308
|
-
model: this.modelName,
|
|
309
|
-
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
: schemaTooComplex
|
|
310
|
+
? "schema_complexity_structured_output_fallback"
|
|
311
|
+
: "tools_schema_conflict_structured_output_fallback",
|
|
310
312
|
});
|
|
313
|
+
if (schemaTooComplex) {
|
|
314
|
+
// warn (not debug): callers should simplify their schema — the
|
|
315
|
+
// fallback keeps the turn alive but skips native enforcement.
|
|
316
|
+
logger.warn("[GenerationHandler] schema too complex for provider constrained decoding — retrying with prompt-based JSON", {
|
|
317
|
+
provider: this.providerName,
|
|
318
|
+
model: this.modelName,
|
|
319
|
+
error: error instanceof Error ? error.message : String(error),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
logger.debug("[GenerationHandler] structured-output conflict caught - falling back to manual JSON extraction", {
|
|
324
|
+
provider: this.providerName,
|
|
325
|
+
model: this.modelName,
|
|
326
|
+
error: error instanceof Error ? error.message : String(error),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
311
329
|
// Retry without experimental_output - the formatEnhancedResult method
|
|
312
330
|
// will extract JSON from the text response
|
|
313
331
|
const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, shouldUseTools, false), span, "generateText(fallback)");
|
|
@@ -41,6 +41,25 @@ export declare function isToolsSchemaExclusionInForce(providerName: string, mode
|
|
|
41
41
|
* and retried without structured output instead of failing the call.
|
|
42
42
|
*/
|
|
43
43
|
export declare function isToolsSchemaConflictError(error: unknown): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* True when a provider error indicates the request was rejected because the
|
|
46
|
+
* JSON schema is too complex for the provider's constrained decoding. Vertex
|
|
47
|
+
* Gemini enforces response schemas as a state machine and rejects
|
|
48
|
+
* "complex" schemas (regex patterns, string min/max lengths, nested array
|
|
49
|
+
* caps, long enums) with a deterministic 400 INVALID_ARGUMENT:
|
|
50
|
+
*
|
|
51
|
+
* "The specified schema produces a constraint that has too many states
|
|
52
|
+
* for serving. Typical causes of this error are schemas with lots of
|
|
53
|
+
* text …, schemas with long array length limits …, or schemas using
|
|
54
|
+
* complex value matchers …"
|
|
55
|
+
*
|
|
56
|
+
* The error arrives wrapped (provider error formatter + upstream JSON
|
|
57
|
+
* envelope), so match on the distinctive substring. Retrying with the same
|
|
58
|
+
* schema can never succeed — the correct recovery is to drop native schema
|
|
59
|
+
* enforcement and coerce the text response into the schema instead
|
|
60
|
+
* (same flow as isToolsSchemaConflictError).
|
|
61
|
+
*/
|
|
62
|
+
export declare function isSchemaComplexityError(error: unknown): boolean;
|
|
44
63
|
/**
|
|
45
64
|
* True when a provider error indicates the request was rejected because the
|
|
46
65
|
* `temperature` parameter is deprecated / unsupported for the model. The newest
|
|
@@ -68,6 +68,32 @@ export function isToolsSchemaConflictError(error) {
|
|
|
68
68
|
/(tool|function)[\s-]?call[^.]{0,60}json[\s_-]?(mode|schema)/i.test(message) ||
|
|
69
69
|
/response_format[^.]{0,60}(tool|function)/i.test(message));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* True when a provider error indicates the request was rejected because the
|
|
73
|
+
* JSON schema is too complex for the provider's constrained decoding. Vertex
|
|
74
|
+
* Gemini enforces response schemas as a state machine and rejects
|
|
75
|
+
* "complex" schemas (regex patterns, string min/max lengths, nested array
|
|
76
|
+
* caps, long enums) with a deterministic 400 INVALID_ARGUMENT:
|
|
77
|
+
*
|
|
78
|
+
* "The specified schema produces a constraint that has too many states
|
|
79
|
+
* for serving. Typical causes of this error are schemas with lots of
|
|
80
|
+
* text …, schemas with long array length limits …, or schemas using
|
|
81
|
+
* complex value matchers …"
|
|
82
|
+
*
|
|
83
|
+
* The error arrives wrapped (provider error formatter + upstream JSON
|
|
84
|
+
* envelope), so match on the distinctive substring. Retrying with the same
|
|
85
|
+
* schema can never succeed — the correct recovery is to drop native schema
|
|
86
|
+
* enforcement and coerce the text response into the schema instead
|
|
87
|
+
* (same flow as isToolsSchemaConflictError).
|
|
88
|
+
*/
|
|
89
|
+
export function isSchemaComplexityError(error) {
|
|
90
|
+
const message = error instanceof Error
|
|
91
|
+
? error.message
|
|
92
|
+
: typeof error === "string"
|
|
93
|
+
? error
|
|
94
|
+
: "";
|
|
95
|
+
return /too many states/i.test(message);
|
|
96
|
+
}
|
|
71
97
|
/**
|
|
72
98
|
* True when a provider error indicates the request was rejected because the
|
|
73
99
|
* `temperature` parameter is deprecated / unsupported for the model. The newest
|
|
@@ -348,7 +348,9 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
348
348
|
}
|
|
349
349
|
catch (error) {
|
|
350
350
|
const errorMsg = `Failed to load MCP server ${serverId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
351
|
-
|
|
351
|
+
// No log here: the result-processing loop below owns the single
|
|
352
|
+
// ERROR record for this failure (logging in both places
|
|
353
|
+
// double-counted every parallel-load failure).
|
|
352
354
|
return { serverId, error: errorMsg };
|
|
353
355
|
}
|
|
354
356
|
});
|
|
@@ -366,11 +368,16 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
366
368
|
}
|
|
367
369
|
else if (error) {
|
|
368
370
|
errors.push(error);
|
|
371
|
+
// Config-loaded servers never pass through
|
|
372
|
+
// NeuroLink.addExternalMCPServer, so this is the outermost point
|
|
373
|
+
// for them — it owns the single ERROR record per failed server.
|
|
374
|
+
mcpLogger.error(`[ExternalServerManager] Failed to load server ${serverId}: ${error}`);
|
|
369
375
|
}
|
|
370
376
|
else if (serverResult && !serverResult.success) {
|
|
371
377
|
const errorMsg = `Failed to load server ${serverId}: ${serverResult.error}`;
|
|
372
378
|
errors.push(errorMsg);
|
|
373
|
-
|
|
379
|
+
// See above: outermost point for config-loaded servers.
|
|
380
|
+
mcpLogger.error(`[ExternalServerManager] ${errorMsg}`);
|
|
374
381
|
}
|
|
375
382
|
}
|
|
376
383
|
else {
|
|
@@ -482,13 +489,17 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
482
489
|
else {
|
|
483
490
|
const error = `Failed to load server ${serverId}: ${result.error}`;
|
|
484
491
|
errors.push(error);
|
|
485
|
-
|
|
492
|
+
// Config-loaded servers never pass through
|
|
493
|
+
// NeuroLink.addExternalMCPServer — this sequential-load branch is
|
|
494
|
+
// their outermost point and owns the single ERROR record.
|
|
495
|
+
mcpLogger.error(`[ExternalServerManager] ${error}`);
|
|
486
496
|
}
|
|
487
497
|
}
|
|
488
498
|
catch (error) {
|
|
489
499
|
const errorMsg = `Failed to load MCP server ${serverId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
490
500
|
errors.push(errorMsg);
|
|
491
|
-
|
|
501
|
+
// See above: outermost point for config-loaded servers.
|
|
502
|
+
mcpLogger.error(`[ExternalServerManager] ${errorMsg}`);
|
|
492
503
|
// Continue with other servers - don't let one failure break everything
|
|
493
504
|
}
|
|
494
505
|
}
|
|
@@ -708,7 +719,9 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
708
719
|
};
|
|
709
720
|
}
|
|
710
721
|
catch (error) {
|
|
711
|
-
|
|
722
|
+
// debug, not error: NeuroLink.addExternalMCPServer (the public entry
|
|
723
|
+
// point) emits the single ERROR record for a failed registration.
|
|
724
|
+
mcpLogger.debug(`[ExternalServerManager] Failed to add server ${serverId}:`, error);
|
|
712
725
|
// Clean up if instance was created
|
|
713
726
|
this.servers.delete(serverId);
|
|
714
727
|
return {
|
|
@@ -851,7 +864,9 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
851
864
|
mcpLogger.info(`[ExternalServerManager] Server started successfully: ${serverId}`);
|
|
852
865
|
}
|
|
853
866
|
catch (error) {
|
|
854
|
-
|
|
867
|
+
// debug, not error: the failure is rethrown below and surfaces once at
|
|
868
|
+
// the NeuroLink.addExternalMCPServer entry point.
|
|
869
|
+
mcpLogger.debug(`[ExternalServerManager] Failed to start server ${serverId}:`, error);
|
|
855
870
|
this.updateServerStatus(serverId, "failed");
|
|
856
871
|
instance.lastError =
|
|
857
872
|
error instanceof Error ? error.message : String(error);
|
|
@@ -142,7 +142,11 @@ export class MCPClientFactory {
|
|
|
142
142
|
duration: Date.now() - startTime,
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
|
-
|
|
145
|
+
// debug, not error: this failure propagates up through
|
|
146
|
+
// ExternalServerManager to NeuroLink.addExternalMCPServer, which emits
|
|
147
|
+
// the single ERROR record. Logging at every layer turned one failed
|
|
148
|
+
// server registration into 5 ERROR lines in production.
|
|
149
|
+
mcpLogger.debug(`[MCPClientFactory] Failed to create client for ${config.id}:`, error);
|
|
146
150
|
obsSpan.durationMs = Date.now() - startTime;
|
|
147
151
|
const endedObsSpan = SpanSerializer.endSpan(obsSpan, SpanStatus.ERROR);
|
|
148
152
|
endedObsSpan.statusMessage = errorMessage;
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -72,6 +72,7 @@ import { coerceJsonToSchema } from "./utils/json/coerce.js";
|
|
|
72
72
|
// Factory processing imports
|
|
73
73
|
import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
|
|
74
74
|
import { logger, mcpLogger } from "./utils/logger.js";
|
|
75
|
+
import { redactUrlCredentials, safeDebugSerialize, sanitizeRecord, stringifyContentSafe, } from "./utils/logSanitize.js";
|
|
75
76
|
import { extractMcpErrorText } from "./utils/mcpErrorText.js";
|
|
76
77
|
import { createCustomToolServerInfo, detectCategory, } from "./utils/mcpDefaults.js";
|
|
77
78
|
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
@@ -2156,9 +2157,7 @@ Current user's request: ${currentInput}`;
|
|
|
2156
2157
|
const anyOptions = optionsOrPrompt;
|
|
2157
2158
|
if (anyOptions.messages && anyOptions.messages.length > 0) {
|
|
2158
2159
|
const lastMessage = anyOptions.messages[anyOptions.messages.length - 1];
|
|
2159
|
-
return
|
|
2160
|
-
? lastMessage.content
|
|
2161
|
-
: JSON.stringify(lastMessage.content);
|
|
2160
|
+
return stringifyContentSafe(lastMessage.content);
|
|
2162
2161
|
}
|
|
2163
2162
|
// Handle input.text format
|
|
2164
2163
|
return optionsOrPrompt.input?.text || "";
|
|
@@ -3879,9 +3878,7 @@ Current user's request: ${currentInput}`;
|
|
|
3879
3878
|
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
3880
3879
|
.map((m) => ({
|
|
3881
3880
|
role: m.role,
|
|
3882
|
-
content:
|
|
3883
|
-
? m.content
|
|
3884
|
-
: JSON.stringify(m.content),
|
|
3881
|
+
content: stringifyContentSafe(m.content),
|
|
3885
3882
|
})) ??
|
|
3886
3883
|
options.conversationHistory,
|
|
3887
3884
|
timeout: options.timeout,
|
|
@@ -3978,9 +3975,7 @@ Current user's request: ${currentInput}`;
|
|
|
3978
3975
|
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
3979
3976
|
.map((m) => ({
|
|
3980
3977
|
role: m.role,
|
|
3981
|
-
content:
|
|
3982
|
-
? m.content
|
|
3983
|
-
: JSON.stringify(m.content),
|
|
3978
|
+
content: stringifyContentSafe(m.content),
|
|
3984
3979
|
})) ??
|
|
3985
3980
|
options.conversationHistory,
|
|
3986
3981
|
timeout: options.timeout,
|
|
@@ -4603,6 +4598,11 @@ Current user's request: ${currentInput}`;
|
|
|
4603
4598
|
*/
|
|
4604
4599
|
async performMCPGenerationRetries(options, generateInternalId, generateInternalStartTime, generateInternalHrTimeStart, functionTag) {
|
|
4605
4600
|
const maxMcpRetries = RETRY_ATTEMPTS.QUICK;
|
|
4601
|
+
// Captured BEFORE the first attempt: ensureMCPGenerationBudget (inside
|
|
4602
|
+
// tryMCPGeneration) auto-scales options.timeout for >100K-token contexts
|
|
4603
|
+
// when the caller left it unset, so checking options.timeout inside the
|
|
4604
|
+
// catch below could not tell caller-set apart from auto-scaled.
|
|
4605
|
+
const callerSetTimeout = options.timeout !== undefined;
|
|
4606
4606
|
// NL-007: Track retry metadata for observability
|
|
4607
4607
|
const retryErrors = [];
|
|
4608
4608
|
let retryCount = 0;
|
|
@@ -4645,6 +4645,20 @@ Current user's request: ${currentInput}`;
|
|
|
4645
4645
|
logger.debug(`[${functionTag}] AbortError detected on attempt ${attempt}, stopping retries`);
|
|
4646
4646
|
throw error;
|
|
4647
4647
|
}
|
|
4648
|
+
// A TimeoutError against an explicit caller-set options.timeout is
|
|
4649
|
+
// deterministic-by-construction: every remaining retry AND the
|
|
4650
|
+
// direct-generation fallback would re-run the same provider+model
|
|
4651
|
+
// under the same per-step budget. Observed in production as ~650s
|
|
4652
|
+
// of doomed follow-ups after a 90s step timeout. Surface it now.
|
|
4653
|
+
// Name check (not instanceof): two TimeoutError classes exist
|
|
4654
|
+
// (utils/timeout.ts and utils/async/withTimeout.ts) and both stamp
|
|
4655
|
+
// name = "TimeoutError".
|
|
4656
|
+
if (callerSetTimeout &&
|
|
4657
|
+
error instanceof Error &&
|
|
4658
|
+
error.name === "TimeoutError") {
|
|
4659
|
+
logger.warn(`[${functionTag}] Per-step timeout (${String(options.timeout)}) exhausted on attempt ${attempt} — surfacing instead of retrying with the same budget`);
|
|
4660
|
+
throw error;
|
|
4661
|
+
}
|
|
4648
4662
|
// NL-007: Record retry error for observability
|
|
4649
4663
|
retryCount++;
|
|
4650
4664
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -7220,11 +7234,18 @@ Current user's request: ${currentInput}`;
|
|
|
7220
7234
|
*/
|
|
7221
7235
|
async storeStreamConversationMemory(params) {
|
|
7222
7236
|
const { enhancedOptions, providerName, originalPrompt, accumulatedContent, startTime, eventSequence, } = params;
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7237
|
+
// Logger Guard: the full options object (history + tool outputs) can be
|
|
7238
|
+
// enormous — an eager JSON.stringify here threw RangeError: Invalid
|
|
7239
|
+
// string length in production and killed the turn. Serialize lazily,
|
|
7240
|
+
// bounded, and only when debug is actually enabled; sanitizeRecord
|
|
7241
|
+
// redacts credentials/PII keys before anything reaches the sink.
|
|
7242
|
+
if (logger.shouldLog("debug")) {
|
|
7243
|
+
logger.debug("[NeuroLink.stream] Preparing to store conversation turn in memory", {
|
|
7244
|
+
options: safeDebugSerialize(sanitizeRecord(enhancedOptions)),
|
|
7245
|
+
sessionId: enhancedOptions.context
|
|
7246
|
+
?.sessionId,
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7228
7249
|
// Guard: skip storing if no meaningful content was produced (no text AND no tool activity)
|
|
7229
7250
|
const hasToolEvents = eventSequence.some((e) => e.type === "tool:start" || e.type === "tool:end");
|
|
7230
7251
|
if (!accumulatedContent.trim() && !hasToolEvents) {
|
|
@@ -7234,12 +7255,16 @@ Current user's request: ${currentInput}`;
|
|
|
7234
7255
|
});
|
|
7235
7256
|
return;
|
|
7236
7257
|
}
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7258
|
+
// Logger Guard: see the matching block above — never eagerly stringify
|
|
7259
|
+
// the full options object, and always redact before serializing.
|
|
7260
|
+
if (logger.shouldLog("debug")) {
|
|
7261
|
+
logger.debug("[NeuroLink.stream] Storing conversation turn in memory", {
|
|
7262
|
+
options: safeDebugSerialize(sanitizeRecord(enhancedOptions)),
|
|
7263
|
+
sessionId: enhancedOptions.context
|
|
7264
|
+
?.sessionId,
|
|
7265
|
+
conversationMemoryExists: this.conversationMemory ? true : false,
|
|
7266
|
+
});
|
|
7267
|
+
}
|
|
7243
7268
|
// Store memory after stream consumption is complete
|
|
7244
7269
|
if (this.conversationMemory && enhancedOptions.context?.sessionId) {
|
|
7245
7270
|
const sessionId = enhancedOptions.context
|
|
@@ -8703,11 +8728,7 @@ Current user's request: ${currentInput}`;
|
|
|
8703
8728
|
: this.getCustomTools().has(toolName)
|
|
8704
8729
|
? "custom"
|
|
8705
8730
|
: "external";
|
|
8706
|
-
const inputStr =
|
|
8707
|
-
? params
|
|
8708
|
-
: params
|
|
8709
|
-
? JSON.stringify(params)
|
|
8710
|
-
: "";
|
|
8731
|
+
const inputStr = params ? stringifyContentSafe(params) : "";
|
|
8711
8732
|
const executionStartTime = Date.now();
|
|
8712
8733
|
// Per-invocation id so consumers can correlate a tool:start with its matching
|
|
8713
8734
|
// tool:end even when the same tool runs multiple times concurrently.
|
|
@@ -10271,8 +10292,15 @@ Current user's request: ${currentInput}`;
|
|
|
10271
10292
|
});
|
|
10272
10293
|
}
|
|
10273
10294
|
else {
|
|
10295
|
+
// Single ERROR record for a failed registration — the inner layers
|
|
10296
|
+
// (client factory, server manager) log at debug so one root cause no
|
|
10297
|
+
// longer fans out into 5 ERROR lines. Carry enough context here to
|
|
10298
|
+
// diagnose without the inner lines.
|
|
10274
10299
|
mcpLogger.error(`[NeuroLink] Failed to add external MCP server: ${serverId}`, {
|
|
10275
10300
|
error: result.error,
|
|
10301
|
+
transport: config.transport,
|
|
10302
|
+
command: config.command,
|
|
10303
|
+
url: config.url ? redactUrlCredentials(config.url) : undefined,
|
|
10276
10304
|
});
|
|
10277
10305
|
}
|
|
10278
10306
|
return result;
|
|
@@ -10304,6 +10332,11 @@ Current user's request: ${currentInput}`;
|
|
|
10304
10332
|
timestamp: Date.now(),
|
|
10305
10333
|
});
|
|
10306
10334
|
}
|
|
10335
|
+
else if (result.error?.includes("not found")) {
|
|
10336
|
+
// Expected no-op: consumers commonly call remove as cleanup after a
|
|
10337
|
+
// failed add, when the server never registered. Not an error.
|
|
10338
|
+
mcpLogger.debug(`[NeuroLink] Remove skipped — external MCP server not registered: ${serverId}`);
|
|
10339
|
+
}
|
|
10307
10340
|
else {
|
|
10308
10341
|
mcpLogger.error(`[NeuroLink] Failed to remove external MCP server: ${serverId}`, {
|
|
10309
10342
|
error: result.error,
|