@juspay/neurolink 9.80.3 → 9.81.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 +12 -0
- package/dist/browser/neurolink.min.js +352 -352
- package/dist/cli/loop/optionsSchema.js +16 -0
- package/dist/context/stages/structuredSummarizer.js +15 -4
- package/dist/core/analytics.js +22 -0
- package/dist/core/constants.d.ts +13 -0
- package/dist/core/constants.js +13 -0
- 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/analytics.js +22 -0
- package/dist/lib/core/constants.d.ts +13 -0
- package/dist/lib/core/constants.js +13 -0
- 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 +88 -28
- package/dist/lib/providers/googleNativeGemini3.d.ts +92 -4
- package/dist/lib/providers/googleNativeGemini3.js +186 -4
- package/dist/lib/providers/googleVertex.d.ts +15 -0
- package/dist/lib/providers/googleVertex.js +719 -139
- package/dist/lib/types/analytics.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +88 -0
- package/dist/lib/types/stream.d.ts +21 -1
- 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 +88 -28
- package/dist/providers/googleNativeGemini3.d.ts +92 -4
- package/dist/providers/googleNativeGemini3.js +186 -4
- package/dist/providers/googleVertex.d.ts +15 -0
- package/dist/providers/googleVertex.js +719 -139
- package/dist/types/analytics.d.ts +10 -0
- package/dist/types/generate.d.ts +88 -0
- package/dist/types/stream.d.ts +21 -1
- 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
|
@@ -35,6 +35,16 @@ export type AnalyticsData = {
|
|
|
35
35
|
timestamp: string;
|
|
36
36
|
cost?: number;
|
|
37
37
|
context?: JsonValue;
|
|
38
|
+
/** Number of agentic steps (model calls) the turn used. */
|
|
39
|
+
stepsUsed?: number;
|
|
40
|
+
/** Number of external tool calls the turn made (final_result excluded). */
|
|
41
|
+
toolCallCount?: number;
|
|
42
|
+
/** Why the turn ended — see GenerateStopReason. */
|
|
43
|
+
stopReason?: string;
|
|
44
|
+
/** Wall-clock duration of the turn in milliseconds. */
|
|
45
|
+
elapsedMs?: number;
|
|
46
|
+
/** Verbatim provider finish/stop reason for the terminal model call. */
|
|
47
|
+
rawFinishReason?: string;
|
|
38
48
|
};
|
|
39
49
|
/**
|
|
40
50
|
* Stream Analytics Data - Enhanced for performance tracking
|
|
@@ -296,7 +296,52 @@ export type GenerateOptions = {
|
|
|
296
296
|
* ```
|
|
297
297
|
*/
|
|
298
298
|
enabledToolNames?: string[];
|
|
299
|
+
/**
|
|
300
|
+
* Request timeout (e.g. 30000, '30s', '2m').
|
|
301
|
+
*
|
|
302
|
+
* PER-STEP semantics in agentic loops: on providers that run a native
|
|
303
|
+
* multi-step tool loop (Vertex Gemini / Vertex Claude), this bounds EACH
|
|
304
|
+
* model call in the loop, not the whole turn — a tool-heavy turn may run
|
|
305
|
+
* far longer than this value in total. Size it for the slowest single
|
|
306
|
+
* step (default 300s), and use `abortSignal` for a total-turn deadline.
|
|
307
|
+
*
|
|
308
|
+
* When set explicitly, a step timeout is surfaced immediately instead of
|
|
309
|
+
* burning internal retries/fallbacks that would re-run the same
|
|
310
|
+
* provider+model with the same doomed budget.
|
|
311
|
+
*/
|
|
299
312
|
timeout?: number | string;
|
|
313
|
+
/**
|
|
314
|
+
* Hard wall-clock cap for the WHOLE agentic turn (all model calls + tool
|
|
315
|
+
* executions), in milliseconds. When the deadline passes the turn ends
|
|
316
|
+
* gracefully with `stopReason: "time-limit"` and an honest time message —
|
|
317
|
+
* never the step-cap text. Unset = no turn-level deadline (the library
|
|
318
|
+
* imposes no product policy).
|
|
319
|
+
*
|
|
320
|
+
* Enforced by the native Vertex loops (Gemini + Claude); AI-SDK-driven
|
|
321
|
+
* providers currently ignore it.
|
|
322
|
+
*/
|
|
323
|
+
turnTimeoutMs?: number;
|
|
324
|
+
/**
|
|
325
|
+
* Maximum time with NO progress — no stream chunk received, no tool
|
|
326
|
+
* execution started or finished, no step started — before the turn ends
|
|
327
|
+
* with `stopReason: "stalled"`. Catches wedged tools and hung model calls
|
|
328
|
+
* that a whole-turn deadline would let run to the bitter end.
|
|
329
|
+
* Unset = disabled.
|
|
330
|
+
*/
|
|
331
|
+
stallTimeoutMs?: number;
|
|
332
|
+
/**
|
|
333
|
+
* When the remaining turn time drops below this, a wrap-up nudge rides the
|
|
334
|
+
* next tool-result turn telling the model to consolidate what it has and
|
|
335
|
+
* produce its final answer. Defaults to 120_000 when `turnTimeoutMs` is
|
|
336
|
+
* set; ignored when it is not.
|
|
337
|
+
*/
|
|
338
|
+
wrapupTimeLeadMs?: number;
|
|
339
|
+
/**
|
|
340
|
+
* Per-tool-execution timeout in milliseconds (default 300_000). A tool
|
|
341
|
+
* that exceeds it fails with an error tool_result and costs one step —
|
|
342
|
+
* the turn continues instead of hanging on a wedged tool.
|
|
343
|
+
*/
|
|
344
|
+
toolTimeoutMs?: number;
|
|
300
345
|
/** AbortSignal for external cancellation of the AI call */
|
|
301
346
|
abortSignal?: AbortSignal;
|
|
302
347
|
/**
|
|
@@ -549,6 +594,21 @@ export type AdditionalMemoryUser = {
|
|
|
549
594
|
/** Max words for this user's condensed memory. Overrides the default maxWords. */
|
|
550
595
|
maxWords?: number;
|
|
551
596
|
};
|
|
597
|
+
/**
|
|
598
|
+
* Why an agentic turn ended — the discriminator consumers should branch on
|
|
599
|
+
* instead of sniffing the provider-shaped `finishReason` (whose values are
|
|
600
|
+
* overloaded: e.g. "tool-calls" historically covered both step-cap exits and
|
|
601
|
+
* Gemini MALFORMED_FUNCTION_CALL provider errors).
|
|
602
|
+
*
|
|
603
|
+
* - `completed` — the model finished on its own (text answer or final_result)
|
|
604
|
+
* - `step-cap` — the `maxSteps` budget ran out while the model still wanted tools
|
|
605
|
+
* - `time-limit` — the `turnTimeoutMs` wall-clock deadline passed
|
|
606
|
+
* - `stalled` — no progress (no chunk, no tool start/finish) for `stallTimeoutMs`
|
|
607
|
+
* - `aborted` — the caller's `abortSignal` ended the turn
|
|
608
|
+
* - `provider-error` — the provider/model failed the turn (e.g. persistent
|
|
609
|
+
* MALFORMED_FUNCTION_CALL after retry); usually worth a caller-side retry
|
|
610
|
+
*/
|
|
611
|
+
export type GenerateStopReason = "completed" | "step-cap" | "time-limit" | "stalled" | "aborted" | "provider-error";
|
|
552
612
|
/**
|
|
553
613
|
* Generate function result type - Primary output format
|
|
554
614
|
* Future-ready for multi-modal outputs while maintaining text focus
|
|
@@ -649,6 +709,20 @@ export type GenerateResult = {
|
|
|
649
709
|
provider?: string;
|
|
650
710
|
model?: string;
|
|
651
711
|
finishReason?: string;
|
|
712
|
+
/**
|
|
713
|
+
* Why the agentic turn ended, independent of the provider-shaped
|
|
714
|
+
* `finishReason`. Populated by the native Vertex loops (Gemini + Claude);
|
|
715
|
+
* undefined on providers that don't run a native loop — fall back to
|
|
716
|
+
* `finishReason` heuristics there.
|
|
717
|
+
*/
|
|
718
|
+
stopReason?: GenerateStopReason;
|
|
719
|
+
/**
|
|
720
|
+
* Verbatim provider finish/stop reason for the turn's terminal model call
|
|
721
|
+
* (e.g. "MALFORMED_FUNCTION_CALL", "MAX_TOKENS", "max_tokens", "tool_use").
|
|
722
|
+
*/
|
|
723
|
+
rawFinishReason?: string;
|
|
724
|
+
/** Number of agentic steps (model calls) the turn used. */
|
|
725
|
+
stepsUsed?: number;
|
|
652
726
|
/**
|
|
653
727
|
* True when the schema JSON in `content`/`structuredData` was repaired from
|
|
654
728
|
* malformed model text (jsonrepair ran). The result is still valid JSON.
|
|
@@ -869,6 +943,14 @@ export type TextGenerationOptions = {
|
|
|
869
943
|
*/
|
|
870
944
|
enabledToolNames?: string[];
|
|
871
945
|
timeout?: number | string;
|
|
946
|
+
/** Wall-clock cap for the whole agentic turn (ms). See GenerateOptions.turnTimeoutMs. */
|
|
947
|
+
turnTimeoutMs?: number;
|
|
948
|
+
/** Max time with no progress before the turn ends as "stalled" (ms). See GenerateOptions.stallTimeoutMs. */
|
|
949
|
+
stallTimeoutMs?: number;
|
|
950
|
+
/** Remaining-time threshold that triggers the wrap-up nudge (ms). See GenerateOptions.wrapupTimeLeadMs. */
|
|
951
|
+
wrapupTimeLeadMs?: number;
|
|
952
|
+
/** Per-tool-execution timeout (ms, default 300_000). See GenerateOptions.toolTimeoutMs. */
|
|
953
|
+
toolTimeoutMs?: number;
|
|
872
954
|
/** AbortSignal for external cancellation of the AI call */
|
|
873
955
|
abortSignal?: AbortSignal;
|
|
874
956
|
disableTools?: boolean;
|
|
@@ -1115,6 +1197,12 @@ export type TextGenerationResult = {
|
|
|
1115
1197
|
/** Parsed structured object when a `schema` was requested (see GenerateResult.structuredData). */
|
|
1116
1198
|
structuredData?: unknown;
|
|
1117
1199
|
finishReason?: string;
|
|
1200
|
+
/** Turn-exit discriminator from native agentic loops (see GenerateStopReason). */
|
|
1201
|
+
stopReason?: GenerateStopReason;
|
|
1202
|
+
/** Verbatim provider finish/stop reason for the turn's terminal model call. */
|
|
1203
|
+
rawFinishReason?: string;
|
|
1204
|
+
/** Number of agentic steps (model calls) the turn used. */
|
|
1205
|
+
stepsUsed?: number;
|
|
1118
1206
|
/** True when the schema JSON was repaired from malformed model text. */
|
|
1119
1207
|
jsonRepaired?: boolean;
|
|
1120
1208
|
/** True when the schema JSON appears truncated (output hit the token cap). */
|
|
@@ -8,7 +8,7 @@ import type { JsonValue, UnknownRecord } from "./common.js";
|
|
|
8
8
|
import type { Content, ImageWithAltText } from "./content.js";
|
|
9
9
|
import type { ChatMessage } from "./conversation.js";
|
|
10
10
|
import type { StreamNoOutputSentinel } from "./noOutputSentinel.js";
|
|
11
|
-
import type { AdditionalMemoryUser } from "./generate.js";
|
|
11
|
+
import type { AdditionalMemoryUser, GenerateStopReason } from "./generate.js";
|
|
12
12
|
import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js";
|
|
13
13
|
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
|
|
14
14
|
import type { STTOptions, STTResult } from "./stt.js";
|
|
@@ -326,6 +326,14 @@ export type StreamOptions = {
|
|
|
326
326
|
schema?: ValidationSchema;
|
|
327
327
|
tools?: Record<string, Tool>;
|
|
328
328
|
timeout?: number | string;
|
|
329
|
+
/** Wall-clock cap for the whole agentic turn (ms). See GenerateOptions.turnTimeoutMs. */
|
|
330
|
+
turnTimeoutMs?: number;
|
|
331
|
+
/** Max time with no progress before the turn ends as "stalled" (ms). See GenerateOptions.stallTimeoutMs. */
|
|
332
|
+
stallTimeoutMs?: number;
|
|
333
|
+
/** Remaining-time threshold that triggers the wrap-up nudge (ms). See GenerateOptions.wrapupTimeLeadMs. */
|
|
334
|
+
wrapupTimeLeadMs?: number;
|
|
335
|
+
/** Per-tool-execution timeout (ms, default 300_000). See GenerateOptions.toolTimeoutMs. */
|
|
336
|
+
toolTimeoutMs?: number;
|
|
329
337
|
/** AbortSignal for external cancellation of the AI call */
|
|
330
338
|
abortSignal?: AbortSignal;
|
|
331
339
|
disableTools?: boolean;
|
|
@@ -518,6 +526,15 @@ export type StreamResult = {
|
|
|
518
526
|
model?: string;
|
|
519
527
|
usage?: TokenUsage;
|
|
520
528
|
finishReason?: string;
|
|
529
|
+
/**
|
|
530
|
+
* Why the agentic turn ended (see GenerateStopReason). For background-loop
|
|
531
|
+
* streams (native Vertex paths) prefer `metadata.stopReason` after draining
|
|
532
|
+
* the stream — this top-level field may be a getter that resolves late, and
|
|
533
|
+
* wrapper spreads can snapshot it before the loop finishes.
|
|
534
|
+
*/
|
|
535
|
+
stopReason?: GenerateStopReason;
|
|
536
|
+
/** Verbatim provider finish/stop reason for the turn's terminal model call. */
|
|
537
|
+
rawFinishReason?: string;
|
|
521
538
|
toolCalls?: StreamToolCall[];
|
|
522
539
|
toolResults?: StreamToolResult[];
|
|
523
540
|
toolEvents?: AsyncIterable<ToolExecutionEvent>;
|
|
@@ -537,6 +554,9 @@ export type StreamResult = {
|
|
|
537
554
|
guardrailsBlocked?: boolean;
|
|
538
555
|
error?: string;
|
|
539
556
|
finishReason?: string;
|
|
557
|
+
stopReason?: GenerateStopReason;
|
|
558
|
+
rawFinishReason?: string;
|
|
559
|
+
stepsUsed?: number;
|
|
540
560
|
thoughtSignature?: string;
|
|
541
561
|
thoughts?: Array<{
|
|
542
562
|
id?: string;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
6
6
|
import { tracers } from "../telemetry/tracers.js";
|
|
7
7
|
import { withTimeout } from "./errorHandling.js";
|
|
8
|
+
import { safeDebugSerialize, sanitizeRecord } from "./logSanitize.js";
|
|
8
9
|
import { DEFAULT_FALLBACK_THRESHOLD, getConversationMemoryDefaults, MEMORY_THRESHOLD_PERCENTAGE, } from "../config/conversationMemory.js";
|
|
9
10
|
import { getAvailableInputTokens } from "../constants/contextWindows.js";
|
|
10
11
|
import { buildSummarizationPrompt } from "../context/prompts/summarizationPrompt.js";
|
|
@@ -106,20 +107,30 @@ export function applyConversationMemoryDefaults(userConfig) {
|
|
|
106
107
|
* Get conversation history as message array, summarizing if needed.
|
|
107
108
|
*/
|
|
108
109
|
export async function getConversationMessages(conversationMemory, options) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
// Logger Guard: options carries the full conversation history + tool
|
|
111
|
+
// outputs; eager JSON.stringify of it can throw RangeError: Invalid string
|
|
112
|
+
// length and abort the turn. Serialize lazily, bounded, never-throwing,
|
|
113
|
+
// and redacted (sanitizeRecord strips credential/PII keys).
|
|
114
|
+
if (logger.shouldLog("debug")) {
|
|
115
|
+
logger.debug("[conversationMemoryUtils] getConversationMessages called", {
|
|
116
|
+
hasMemory: !!conversationMemory,
|
|
117
|
+
memoryType: conversationMemory?.constructor?.name || "NONE",
|
|
118
|
+
hasContext: !!options.context,
|
|
119
|
+
enableSummarization: options.enableSummarization ?? false,
|
|
120
|
+
options: safeDebugSerialize(sanitizeRecord(options)),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
116
123
|
if (!conversationMemory || !options.context) {
|
|
117
124
|
logger.warn("[conversationMemoryUtils] No memory or context, returning empty messages", {
|
|
118
125
|
hasMemory: !!conversationMemory,
|
|
119
126
|
memoryType: conversationMemory?.constructor?.name || "NONE",
|
|
120
127
|
hasContext: !!options.context,
|
|
121
128
|
enableSummarization: options.enableSummarization ?? false,
|
|
122
|
-
options
|
|
129
|
+
// The options dump is debug-grade detail — don't pay for (or leak)
|
|
130
|
+
// the serialization on every memory-disabled call at warn level.
|
|
131
|
+
...(logger.shouldLog("debug")
|
|
132
|
+
? { options: safeDebugSerialize(sanitizeRecord(options)) }
|
|
133
|
+
: {}),
|
|
123
134
|
});
|
|
124
135
|
return [];
|
|
125
136
|
}
|
|
@@ -26,6 +26,32 @@
|
|
|
26
26
|
* @param maxLen - Maximum number of characters to keep (default 500).
|
|
27
27
|
*/
|
|
28
28
|
export declare function sanitizeForLog(text: string, maxLen?: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Stringify non-string message/tool content without ever throwing.
|
|
31
|
+
* JSON.stringify on a giant multimodal/tool-result payload can exceed V8's
|
|
32
|
+
* maximum string length and throw `RangeError: Invalid string length` — a
|
|
33
|
+
* generation turn must degrade to a placeholder, not abort, when that
|
|
34
|
+
* happens. Shared by the SDK core and providers (single source of truth).
|
|
35
|
+
*/
|
|
36
|
+
export declare function stringifyContentSafe(content: unknown): string;
|
|
37
|
+
/**
|
|
38
|
+
* Serialize an arbitrary value for a debug log without ever throwing or
|
|
39
|
+
* producing an unbounded string.
|
|
40
|
+
*
|
|
41
|
+
* `JSON.stringify` on a full options object (conversation history + tool
|
|
42
|
+
* outputs) can exceed V8's maximum string length and throw
|
|
43
|
+
* `RangeError: Invalid string length`, which — when evaluated eagerly inside
|
|
44
|
+
* a logger call — aborts the surrounding generation turn (observed in
|
|
45
|
+
* production). This helper caps the output and converts any stringify
|
|
46
|
+
* failure (RangeError, circular structure, BigInt, …) into a placeholder.
|
|
47
|
+
*
|
|
48
|
+
* Callers MUST still gate on `logger.shouldLog("debug")` per the Logger
|
|
49
|
+
* Guard rule — this helper makes serialization safe, not free.
|
|
50
|
+
*
|
|
51
|
+
* @param value - Arbitrary value to serialize.
|
|
52
|
+
* @param maxLen - Maximum number of characters to keep (default 10 000).
|
|
53
|
+
*/
|
|
54
|
+
export declare function safeDebugSerialize(value: unknown, maxLen?: number): string;
|
|
29
55
|
/**
|
|
30
56
|
* Strip embedded `user:pass@` credentials from a URL's authority component
|
|
31
57
|
* before logging it or surfacing it in a user-facing error.
|
|
@@ -74,6 +74,13 @@ const SENSITIVE_OBJECT_KEYS = [
|
|
|
74
74
|
"oauth",
|
|
75
75
|
"oauthToken",
|
|
76
76
|
"credentials",
|
|
77
|
+
"authContext",
|
|
78
|
+
"authToken",
|
|
79
|
+
"sessionToken",
|
|
80
|
+
"serviceAccountKey",
|
|
81
|
+
"secretAccessKey",
|
|
82
|
+
// PII, not a secret — but it has no business in debug log dumps.
|
|
83
|
+
"userEmail",
|
|
77
84
|
];
|
|
78
85
|
/**
|
|
79
86
|
* Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
|
|
@@ -91,6 +98,55 @@ export function sanitizeForLog(text, maxLen = 500) {
|
|
|
91
98
|
}
|
|
92
99
|
return text.slice(0, maxLen).replace(SECRET_PATTERN, "***");
|
|
93
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Stringify non-string message/tool content without ever throwing.
|
|
103
|
+
* JSON.stringify on a giant multimodal/tool-result payload can exceed V8's
|
|
104
|
+
* maximum string length and throw `RangeError: Invalid string length` — a
|
|
105
|
+
* generation turn must degrade to a placeholder, not abort, when that
|
|
106
|
+
* happens. Shared by the SDK core and providers (single source of truth).
|
|
107
|
+
*/
|
|
108
|
+
export function stringifyContentSafe(content) {
|
|
109
|
+
if (typeof content === "string") {
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
return JSON.stringify(content) ?? String(content);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return "[content too large to serialize]";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Serialize an arbitrary value for a debug log without ever throwing or
|
|
121
|
+
* producing an unbounded string.
|
|
122
|
+
*
|
|
123
|
+
* `JSON.stringify` on a full options object (conversation history + tool
|
|
124
|
+
* outputs) can exceed V8's maximum string length and throw
|
|
125
|
+
* `RangeError: Invalid string length`, which — when evaluated eagerly inside
|
|
126
|
+
* a logger call — aborts the surrounding generation turn (observed in
|
|
127
|
+
* production). This helper caps the output and converts any stringify
|
|
128
|
+
* failure (RangeError, circular structure, BigInt, …) into a placeholder.
|
|
129
|
+
*
|
|
130
|
+
* Callers MUST still gate on `logger.shouldLog("debug")` per the Logger
|
|
131
|
+
* Guard rule — this helper makes serialization safe, not free.
|
|
132
|
+
*
|
|
133
|
+
* @param value - Arbitrary value to serialize.
|
|
134
|
+
* @param maxLen - Maximum number of characters to keep (default 10 000).
|
|
135
|
+
*/
|
|
136
|
+
export function safeDebugSerialize(value, maxLen = 10_000) {
|
|
137
|
+
try {
|
|
138
|
+
const json = JSON.stringify(value);
|
|
139
|
+
if (json === undefined) {
|
|
140
|
+
return String(value);
|
|
141
|
+
}
|
|
142
|
+
return json.length > maxLen
|
|
143
|
+
? `${json.slice(0, maxLen)}…[truncated ${json.length - maxLen} chars]`
|
|
144
|
+
: json;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return `[unserializable: ${error instanceof Error ? error.message : String(error)}]`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
94
150
|
/**
|
|
95
151
|
* Strip embedded `user:pass@` credentials from a URL's authority component
|
|
96
152
|
* before logging it or surfacing it in a user-facing error.
|
|
@@ -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;
|