@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
|
@@ -49,6 +49,22 @@ export const textGenerationOptionsSchema = {
|
|
|
49
49
|
type: "number",
|
|
50
50
|
description: "Timeout for the generation request in milliseconds.",
|
|
51
51
|
},
|
|
52
|
+
turnTimeoutMs: {
|
|
53
|
+
type: "number",
|
|
54
|
+
description: "Wall-clock cap for the whole agentic turn in milliseconds (all steps + tool executions).",
|
|
55
|
+
},
|
|
56
|
+
stallTimeoutMs: {
|
|
57
|
+
type: "number",
|
|
58
|
+
description: "Maximum time with no progress (no chunk, no tool activity) before the turn ends as stalled, in milliseconds.",
|
|
59
|
+
},
|
|
60
|
+
wrapupTimeLeadMs: {
|
|
61
|
+
type: "number",
|
|
62
|
+
description: "Remaining-turn-time threshold that triggers a wrap-up nudge to the model, in milliseconds.",
|
|
63
|
+
},
|
|
64
|
+
toolTimeoutMs: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: "Per-tool-execution timeout in milliseconds (default 300000). A timed-out tool fails that step; the turn continues.",
|
|
67
|
+
},
|
|
52
68
|
disableTools: {
|
|
53
69
|
type: "boolean",
|
|
54
70
|
description: "Disable all tool usage for the AI.",
|
|
@@ -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;
|
package/dist/core/analytics.js
CHANGED
|
@@ -18,6 +18,15 @@ export function createAnalytics(provider, model, result, responseTime, context)
|
|
|
18
18
|
const tokens = extractTokenUsage(result);
|
|
19
19
|
// Estimate cost based on provider and tokens
|
|
20
20
|
const cost = estimateCost(provider, model, tokens);
|
|
21
|
+
// Turn-lifecycle telemetry from native agentic loops (Vertex
|
|
22
|
+
// Gemini/Claude): stopReason / rawFinishReason / stepsUsed ride the
|
|
23
|
+
// provider result; toolCallCount and elapsedMs derive from it.
|
|
24
|
+
const turnResult = result;
|
|
25
|
+
const toolCallCount = Array.isArray(turnResult.toolExecutions)
|
|
26
|
+
? turnResult.toolExecutions.length
|
|
27
|
+
: Array.isArray(turnResult.toolsUsed)
|
|
28
|
+
? turnResult.toolsUsed.length
|
|
29
|
+
: undefined;
|
|
21
30
|
const analytics = {
|
|
22
31
|
provider,
|
|
23
32
|
model,
|
|
@@ -26,6 +35,19 @@ export function createAnalytics(provider, model, result, responseTime, context)
|
|
|
26
35
|
requestDuration: responseTime,
|
|
27
36
|
context: context,
|
|
28
37
|
timestamp: new Date().toISOString(),
|
|
38
|
+
...(typeof turnResult.stepsUsed === "number" && {
|
|
39
|
+
stepsUsed: turnResult.stepsUsed,
|
|
40
|
+
}),
|
|
41
|
+
...(toolCallCount !== undefined && { toolCallCount }),
|
|
42
|
+
...(typeof turnResult.stopReason === "string" && {
|
|
43
|
+
stopReason: turnResult.stopReason,
|
|
44
|
+
}),
|
|
45
|
+
...(typeof turnResult.responseTime === "number" && {
|
|
46
|
+
elapsedMs: turnResult.responseTime,
|
|
47
|
+
}),
|
|
48
|
+
...(typeof turnResult.rawFinishReason === "string" && {
|
|
49
|
+
rawFinishReason: turnResult.rawFinishReason,
|
|
50
|
+
}),
|
|
29
51
|
};
|
|
30
52
|
logger.debug(`[${functionTag}] Analytics created`, {
|
|
31
53
|
provider,
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -23,6 +23,19 @@ export declare const DEFAULT_MAX_STEPS = 200;
|
|
|
23
23
|
export declare const DEFAULT_TOOL_MAX_RETRIES = 2;
|
|
24
24
|
/** Defensive wall-clock ceiling for a native Gemini-3 agentic turn (generate + stream). */
|
|
25
25
|
export declare const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300000;
|
|
26
|
+
/**
|
|
27
|
+
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
28
|
+
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
29
|
+
* continues instead of hanging on a wedged tool. Override per call with
|
|
30
|
+
* `toolTimeoutMs`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300000;
|
|
33
|
+
/**
|
|
34
|
+
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
35
|
+
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|
|
36
|
+
* a wrap-up nudge rides the next tool-result turn.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFAULT_WRAPUP_TIME_LEAD_MS = 120000;
|
|
26
39
|
export declare const TOOL_STORAGE_TIMEOUT_MS = 5000;
|
|
27
40
|
export declare const STEP_LIMITS: {
|
|
28
41
|
min: number;
|
package/dist/core/constants.js
CHANGED
|
@@ -109,6 +109,19 @@ export const DEFAULT_MAX_STEPS = 200;
|
|
|
109
109
|
export const DEFAULT_TOOL_MAX_RETRIES = 2; // Maximum retries per tool before permanently failing
|
|
110
110
|
/** Defensive wall-clock ceiling for a native Gemini-3 agentic turn (generate + stream). */
|
|
111
111
|
export const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300_000;
|
|
112
|
+
/**
|
|
113
|
+
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
114
|
+
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
115
|
+
* continues instead of hanging on a wedged tool. Override per call with
|
|
116
|
+
* `toolTimeoutMs`.
|
|
117
|
+
*/
|
|
118
|
+
export const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300_000;
|
|
119
|
+
/**
|
|
120
|
+
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
121
|
+
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|
|
122
|
+
* a wrap-up nudge rides the next tool-result turn.
|
|
123
|
+
*/
|
|
124
|
+
export const DEFAULT_WRAPUP_TIME_LEAD_MS = 120_000;
|
|
112
125
|
// Fire-and-forget tool storage writes (Redis). 5s is generous for a single
|
|
113
126
|
// Redis write; if breached, the .catch logs a warning.
|
|
114
127
|
export const TOOL_STORAGE_TIMEOUT_MS = 5000;
|
|
@@ -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;
|
|
@@ -18,6 +18,15 @@ export function createAnalytics(provider, model, result, responseTime, context)
|
|
|
18
18
|
const tokens = extractTokenUsage(result);
|
|
19
19
|
// Estimate cost based on provider and tokens
|
|
20
20
|
const cost = estimateCost(provider, model, tokens);
|
|
21
|
+
// Turn-lifecycle telemetry from native agentic loops (Vertex
|
|
22
|
+
// Gemini/Claude): stopReason / rawFinishReason / stepsUsed ride the
|
|
23
|
+
// provider result; toolCallCount and elapsedMs derive from it.
|
|
24
|
+
const turnResult = result;
|
|
25
|
+
const toolCallCount = Array.isArray(turnResult.toolExecutions)
|
|
26
|
+
? turnResult.toolExecutions.length
|
|
27
|
+
: Array.isArray(turnResult.toolsUsed)
|
|
28
|
+
? turnResult.toolsUsed.length
|
|
29
|
+
: undefined;
|
|
21
30
|
const analytics = {
|
|
22
31
|
provider,
|
|
23
32
|
model,
|
|
@@ -26,6 +35,19 @@ export function createAnalytics(provider, model, result, responseTime, context)
|
|
|
26
35
|
requestDuration: responseTime,
|
|
27
36
|
context: context,
|
|
28
37
|
timestamp: new Date().toISOString(),
|
|
38
|
+
...(typeof turnResult.stepsUsed === "number" && {
|
|
39
|
+
stepsUsed: turnResult.stepsUsed,
|
|
40
|
+
}),
|
|
41
|
+
...(toolCallCount !== undefined && { toolCallCount }),
|
|
42
|
+
...(typeof turnResult.stopReason === "string" && {
|
|
43
|
+
stopReason: turnResult.stopReason,
|
|
44
|
+
}),
|
|
45
|
+
...(typeof turnResult.responseTime === "number" && {
|
|
46
|
+
elapsedMs: turnResult.responseTime,
|
|
47
|
+
}),
|
|
48
|
+
...(typeof turnResult.rawFinishReason === "string" && {
|
|
49
|
+
rawFinishReason: turnResult.rawFinishReason,
|
|
50
|
+
}),
|
|
29
51
|
};
|
|
30
52
|
logger.debug(`[${functionTag}] Analytics created`, {
|
|
31
53
|
provider,
|
|
@@ -23,6 +23,19 @@ export declare const DEFAULT_MAX_STEPS = 200;
|
|
|
23
23
|
export declare const DEFAULT_TOOL_MAX_RETRIES = 2;
|
|
24
24
|
/** Defensive wall-clock ceiling for a native Gemini-3 agentic turn (generate + stream). */
|
|
25
25
|
export declare const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300000;
|
|
26
|
+
/**
|
|
27
|
+
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
28
|
+
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
29
|
+
* continues instead of hanging on a wedged tool. Override per call with
|
|
30
|
+
* `toolTimeoutMs`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300000;
|
|
33
|
+
/**
|
|
34
|
+
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
35
|
+
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|
|
36
|
+
* a wrap-up nudge rides the next tool-result turn.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFAULT_WRAPUP_TIME_LEAD_MS = 120000;
|
|
26
39
|
export declare const TOOL_STORAGE_TIMEOUT_MS = 5000;
|
|
27
40
|
export declare const STEP_LIMITS: {
|
|
28
41
|
min: number;
|
|
@@ -109,6 +109,19 @@ export const DEFAULT_MAX_STEPS = 200;
|
|
|
109
109
|
export const DEFAULT_TOOL_MAX_RETRIES = 2; // Maximum retries per tool before permanently failing
|
|
110
110
|
/** Defensive wall-clock ceiling for a native Gemini-3 agentic turn (generate + stream). */
|
|
111
111
|
export const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300_000;
|
|
112
|
+
/**
|
|
113
|
+
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
114
|
+
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
115
|
+
* continues instead of hanging on a wedged tool. Override per call with
|
|
116
|
+
* `toolTimeoutMs`.
|
|
117
|
+
*/
|
|
118
|
+
export const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300_000;
|
|
119
|
+
/**
|
|
120
|
+
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
121
|
+
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|
|
122
|
+
* a wrap-up nudge rides the next tool-result turn.
|
|
123
|
+
*/
|
|
124
|
+
export const DEFAULT_WRAPUP_TIME_LEAD_MS = 120_000;
|
|
112
125
|
// Fire-and-forget tool storage writes (Redis). 5s is generous for a single
|
|
113
126
|
// Redis write; if breached, the .catch logs a warning.
|
|
114
127
|
export const TOOL_STORAGE_TIMEOUT_MS = 5000;
|
|
@@ -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;
|