@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
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 || "";
|
|
@@ -3514,6 +3513,10 @@ Current user's request: ${currentInput}`;
|
|
|
3514
3513
|
stt: options.stt,
|
|
3515
3514
|
fileRegistry: this.fileRegistry,
|
|
3516
3515
|
timeout: options.timeout,
|
|
3516
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
3517
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
3518
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
3519
|
+
toolTimeoutMs: options.toolTimeoutMs,
|
|
3517
3520
|
abortSignal: options.abortSignal,
|
|
3518
3521
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
3519
3522
|
middleware: options.middleware,
|
|
@@ -3645,6 +3648,12 @@ Current user's request: ${currentInput}`;
|
|
|
3645
3648
|
content: textResult.content,
|
|
3646
3649
|
structuredData: textResult.structuredData,
|
|
3647
3650
|
finishReason: textResult.finishReason,
|
|
3651
|
+
// Turn-exit discriminator + raw provider stop reason + step count from
|
|
3652
|
+
// native agentic loops — this DTO is an explicit field list, so new
|
|
3653
|
+
// provider-result fields MUST be copied here or they silently vanish.
|
|
3654
|
+
stopReason: textResult.stopReason,
|
|
3655
|
+
rawFinishReason: textResult.rawFinishReason,
|
|
3656
|
+
stepsUsed: textResult.stepsUsed,
|
|
3648
3657
|
jsonRepaired: textResult.jsonRepaired,
|
|
3649
3658
|
jsonTruncated: textResult.jsonTruncated,
|
|
3650
3659
|
provider: textResult.provider,
|
|
@@ -3879,9 +3888,7 @@ Current user's request: ${currentInput}`;
|
|
|
3879
3888
|
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
3880
3889
|
.map((m) => ({
|
|
3881
3890
|
role: m.role,
|
|
3882
|
-
content:
|
|
3883
|
-
? m.content
|
|
3884
|
-
: JSON.stringify(m.content),
|
|
3891
|
+
content: stringifyContentSafe(m.content),
|
|
3885
3892
|
})) ??
|
|
3886
3893
|
options.conversationHistory,
|
|
3887
3894
|
timeout: options.timeout,
|
|
@@ -3978,9 +3985,7 @@ Current user's request: ${currentInput}`;
|
|
|
3978
3985
|
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
3979
3986
|
.map((m) => ({
|
|
3980
3987
|
role: m.role,
|
|
3981
|
-
content:
|
|
3982
|
-
? m.content
|
|
3983
|
-
: JSON.stringify(m.content),
|
|
3988
|
+
content: stringifyContentSafe(m.content),
|
|
3984
3989
|
})) ??
|
|
3985
3990
|
options.conversationHistory,
|
|
3986
3991
|
timeout: options.timeout,
|
|
@@ -4603,6 +4608,11 @@ Current user's request: ${currentInput}`;
|
|
|
4603
4608
|
*/
|
|
4604
4609
|
async performMCPGenerationRetries(options, generateInternalId, generateInternalStartTime, generateInternalHrTimeStart, functionTag) {
|
|
4605
4610
|
const maxMcpRetries = RETRY_ATTEMPTS.QUICK;
|
|
4611
|
+
// Captured BEFORE the first attempt: ensureMCPGenerationBudget (inside
|
|
4612
|
+
// tryMCPGeneration) auto-scales options.timeout for >100K-token contexts
|
|
4613
|
+
// when the caller left it unset, so checking options.timeout inside the
|
|
4614
|
+
// catch below could not tell caller-set apart from auto-scaled.
|
|
4615
|
+
const callerSetTimeout = options.timeout !== undefined;
|
|
4606
4616
|
// NL-007: Track retry metadata for observability
|
|
4607
4617
|
const retryErrors = [];
|
|
4608
4618
|
let retryCount = 0;
|
|
@@ -4645,6 +4655,20 @@ Current user's request: ${currentInput}`;
|
|
|
4645
4655
|
logger.debug(`[${functionTag}] AbortError detected on attempt ${attempt}, stopping retries`);
|
|
4646
4656
|
throw error;
|
|
4647
4657
|
}
|
|
4658
|
+
// A TimeoutError against an explicit caller-set options.timeout is
|
|
4659
|
+
// deterministic-by-construction: every remaining retry AND the
|
|
4660
|
+
// direct-generation fallback would re-run the same provider+model
|
|
4661
|
+
// under the same per-step budget. Observed in production as ~650s
|
|
4662
|
+
// of doomed follow-ups after a 90s step timeout. Surface it now.
|
|
4663
|
+
// Name check (not instanceof): two TimeoutError classes exist
|
|
4664
|
+
// (utils/timeout.ts and utils/async/withTimeout.ts) and both stamp
|
|
4665
|
+
// name = "TimeoutError".
|
|
4666
|
+
if (callerSetTimeout &&
|
|
4667
|
+
error instanceof Error &&
|
|
4668
|
+
error.name === "TimeoutError") {
|
|
4669
|
+
logger.warn(`[${functionTag}] Per-step timeout (${String(options.timeout)}) exhausted on attempt ${attempt} — surfacing instead of retrying with the same budget`);
|
|
4670
|
+
throw error;
|
|
4671
|
+
}
|
|
4648
4672
|
// NL-007: Record retry error for observability
|
|
4649
4673
|
retryCount++;
|
|
4650
4674
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -5049,6 +5073,9 @@ Current user's request: ${currentInput}`;
|
|
|
5049
5073
|
usage: result.usage,
|
|
5050
5074
|
responseTime,
|
|
5051
5075
|
finishReason: result.finishReason,
|
|
5076
|
+
stopReason: result.stopReason,
|
|
5077
|
+
rawFinishReason: result.rawFinishReason,
|
|
5078
|
+
stepsUsed: result.stepsUsed,
|
|
5052
5079
|
toolsUsed: result.toolsUsed || [],
|
|
5053
5080
|
toolExecutions: transformedToolExecutions,
|
|
5054
5081
|
enhancedWithTools: Boolean(hasToolExecutions),
|
|
@@ -5184,6 +5211,9 @@ Current user's request: ${currentInput}`;
|
|
|
5184
5211
|
usage: poolResult.usage,
|
|
5185
5212
|
responseTime,
|
|
5186
5213
|
finishReason: poolResult.finishReason,
|
|
5214
|
+
stopReason: poolResult.stopReason,
|
|
5215
|
+
rawFinishReason: poolResult.rawFinishReason,
|
|
5216
|
+
stepsUsed: poolResult.stepsUsed,
|
|
5187
5217
|
toolsUsed: poolResult.toolsUsed || [],
|
|
5188
5218
|
toolExecutions: poolResult.toolExecutions?.map((te) => {
|
|
5189
5219
|
const t = te;
|
|
@@ -5460,6 +5490,9 @@ Current user's request: ${currentInput}`;
|
|
|
5460
5490
|
usage: result.usage,
|
|
5461
5491
|
responseTime,
|
|
5462
5492
|
finishReason: result.finishReason,
|
|
5493
|
+
stopReason: result.stopReason,
|
|
5494
|
+
rawFinishReason: result.rawFinishReason,
|
|
5495
|
+
stepsUsed: result.stepsUsed,
|
|
5463
5496
|
toolsUsed: result.toolsUsed || [],
|
|
5464
5497
|
// Map toolExecutions from EnhancedGenerateResult shape ({name,input,output})
|
|
5465
5498
|
// to TextGenerationResult shape ({toolName,executionTime,success}).
|
|
@@ -6457,7 +6490,7 @@ Current user's request: ${currentInput}`;
|
|
|
6457
6490
|
factoryResult,
|
|
6458
6491
|
sessionId: enhancedOptions.context?.sessionId,
|
|
6459
6492
|
});
|
|
6460
|
-
const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, } = await this.createMCPStream(enhancedOptions);
|
|
6493
|
+
const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, metadata: providerStreamMetadata, } = await this.createMCPStream(enhancedOptions);
|
|
6461
6494
|
const streamState = {
|
|
6462
6495
|
finishReason: streamFinishReason ?? "stop",
|
|
6463
6496
|
toolCalls: streamToolCalls,
|
|
@@ -6804,6 +6837,7 @@ Current user's request: ${currentInput}`;
|
|
|
6804
6837
|
guardrailsBlocked: metadata.guardrailsBlocked,
|
|
6805
6838
|
error: metadata.error,
|
|
6806
6839
|
events: eventSequence,
|
|
6840
|
+
providerMetadata: providerStreamMetadata,
|
|
6807
6841
|
});
|
|
6808
6842
|
}
|
|
6809
6843
|
catch (error) {
|
|
@@ -7220,11 +7254,18 @@ Current user's request: ${currentInput}`;
|
|
|
7220
7254
|
*/
|
|
7221
7255
|
async storeStreamConversationMemory(params) {
|
|
7222
7256
|
const { enhancedOptions, providerName, originalPrompt, accumulatedContent, startTime, eventSequence, } = params;
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7257
|
+
// Logger Guard: the full options object (history + tool outputs) can be
|
|
7258
|
+
// enormous — an eager JSON.stringify here threw RangeError: Invalid
|
|
7259
|
+
// string length in production and killed the turn. Serialize lazily,
|
|
7260
|
+
// bounded, and only when debug is actually enabled; sanitizeRecord
|
|
7261
|
+
// redacts credentials/PII keys before anything reaches the sink.
|
|
7262
|
+
if (logger.shouldLog("debug")) {
|
|
7263
|
+
logger.debug("[NeuroLink.stream] Preparing to store conversation turn in memory", {
|
|
7264
|
+
options: safeDebugSerialize(sanitizeRecord(enhancedOptions)),
|
|
7265
|
+
sessionId: enhancedOptions.context
|
|
7266
|
+
?.sessionId,
|
|
7267
|
+
});
|
|
7268
|
+
}
|
|
7228
7269
|
// Guard: skip storing if no meaningful content was produced (no text AND no tool activity)
|
|
7229
7270
|
const hasToolEvents = eventSequence.some((e) => e.type === "tool:start" || e.type === "tool:end");
|
|
7230
7271
|
if (!accumulatedContent.trim() && !hasToolEvents) {
|
|
@@ -7234,12 +7275,16 @@ Current user's request: ${currentInput}`;
|
|
|
7234
7275
|
});
|
|
7235
7276
|
return;
|
|
7236
7277
|
}
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7278
|
+
// Logger Guard: see the matching block above — never eagerly stringify
|
|
7279
|
+
// the full options object, and always redact before serializing.
|
|
7280
|
+
if (logger.shouldLog("debug")) {
|
|
7281
|
+
logger.debug("[NeuroLink.stream] Storing conversation turn in memory", {
|
|
7282
|
+
options: safeDebugSerialize(sanitizeRecord(enhancedOptions)),
|
|
7283
|
+
sessionId: enhancedOptions.context
|
|
7284
|
+
?.sessionId,
|
|
7285
|
+
conversationMemoryExists: this.conversationMemory ? true : false,
|
|
7286
|
+
});
|
|
7287
|
+
}
|
|
7243
7288
|
// Store memory after stream consumption is complete
|
|
7244
7289
|
if (this.conversationMemory && enhancedOptions.context?.sessionId) {
|
|
7245
7290
|
const sessionId = enhancedOptions.context
|
|
@@ -7587,6 +7632,7 @@ Current user's request: ${currentInput}`;
|
|
|
7587
7632
|
toolCalls: poolStreamResult.toolCalls ?? [],
|
|
7588
7633
|
toolResults: poolStreamResult.toolResults ?? [],
|
|
7589
7634
|
analytics: poolStreamResult.analytics,
|
|
7635
|
+
metadata: poolStreamResult.metadata,
|
|
7590
7636
|
};
|
|
7591
7637
|
}
|
|
7592
7638
|
catch (poolStreamError) {
|
|
@@ -7640,6 +7686,12 @@ Current user's request: ${currentInput}`;
|
|
|
7640
7686
|
toolCalls: streamResult.toolCalls ?? [],
|
|
7641
7687
|
toolResults: streamResult.toolResults ?? [],
|
|
7642
7688
|
analytics: streamResult.analytics,
|
|
7689
|
+
// Pass the provider's metadata object THROUGH BY REFERENCE: native
|
|
7690
|
+
// background-loop streams (Vertex Gemini/Claude) resolve
|
|
7691
|
+
// finishReason/stopReason/rawFinishReason/stepsUsed onto it only when
|
|
7692
|
+
// the loop finishes — snapshotting fields here would freeze them as
|
|
7693
|
+
// undefined before the stream is drained.
|
|
7694
|
+
metadata: streamResult.metadata,
|
|
7643
7695
|
};
|
|
7644
7696
|
}
|
|
7645
7697
|
/**
|
|
@@ -7682,14 +7734,14 @@ Current user's request: ${currentInput}`;
|
|
|
7682
7734
|
analytics: streamResult.analytics,
|
|
7683
7735
|
evaluation: streamResult.evaluation,
|
|
7684
7736
|
events: config.events && config.events.length > 0 ? config.events : undefined,
|
|
7685
|
-
metadata: {
|
|
7737
|
+
metadata: Object.assign(config.providerMetadata ?? {}, {
|
|
7686
7738
|
streamId: config.streamId,
|
|
7687
7739
|
startTime: config.startTime,
|
|
7688
7740
|
responseTime: config.responseTime,
|
|
7689
7741
|
fallback: config.fallback || false,
|
|
7690
7742
|
guardrailsBlocked: config.guardrailsBlocked,
|
|
7691
7743
|
error: config.error,
|
|
7692
|
-
},
|
|
7744
|
+
}),
|
|
7693
7745
|
};
|
|
7694
7746
|
}
|
|
7695
7747
|
/**
|
|
@@ -8703,11 +8755,7 @@ Current user's request: ${currentInput}`;
|
|
|
8703
8755
|
: this.getCustomTools().has(toolName)
|
|
8704
8756
|
? "custom"
|
|
8705
8757
|
: "external";
|
|
8706
|
-
const inputStr =
|
|
8707
|
-
? params
|
|
8708
|
-
: params
|
|
8709
|
-
? JSON.stringify(params)
|
|
8710
|
-
: "";
|
|
8758
|
+
const inputStr = params ? stringifyContentSafe(params) : "";
|
|
8711
8759
|
const executionStartTime = Date.now();
|
|
8712
8760
|
// Per-invocation id so consumers can correlate a tool:start with its matching
|
|
8713
8761
|
// tool:end even when the same tool runs multiple times concurrently.
|
|
@@ -10271,8 +10319,15 @@ Current user's request: ${currentInput}`;
|
|
|
10271
10319
|
});
|
|
10272
10320
|
}
|
|
10273
10321
|
else {
|
|
10322
|
+
// Single ERROR record for a failed registration — the inner layers
|
|
10323
|
+
// (client factory, server manager) log at debug so one root cause no
|
|
10324
|
+
// longer fans out into 5 ERROR lines. Carry enough context here to
|
|
10325
|
+
// diagnose without the inner lines.
|
|
10274
10326
|
mcpLogger.error(`[NeuroLink] Failed to add external MCP server: ${serverId}`, {
|
|
10275
10327
|
error: result.error,
|
|
10328
|
+
transport: config.transport,
|
|
10329
|
+
command: config.command,
|
|
10330
|
+
url: config.url ? redactUrlCredentials(config.url) : undefined,
|
|
10276
10331
|
});
|
|
10277
10332
|
}
|
|
10278
10333
|
return result;
|
|
@@ -10304,6 +10359,11 @@ Current user's request: ${currentInput}`;
|
|
|
10304
10359
|
timestamp: Date.now(),
|
|
10305
10360
|
});
|
|
10306
10361
|
}
|
|
10362
|
+
else if (result.error?.includes("not found")) {
|
|
10363
|
+
// Expected no-op: consumers commonly call remove as cleanup after a
|
|
10364
|
+
// failed add, when the server never registered. Not an error.
|
|
10365
|
+
mcpLogger.debug(`[NeuroLink] Remove skipped — external MCP server not registered: ${serverId}`);
|
|
10366
|
+
}
|
|
10307
10367
|
else {
|
|
10308
10368
|
mcpLogger.error(`[NeuroLink] Failed to remove external MCP server: ${serverId}`, {
|
|
10309
10369
|
error: result.error,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This module extracts the functions that are duplicated between the two
|
|
9
9
|
* providers so they can share a single implementation.
|
|
10
10
|
*/
|
|
11
|
-
import type { ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js";
|
|
11
|
+
import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js";
|
|
12
12
|
import type { Tool } from "../types/index.js";
|
|
13
13
|
export declare function sanitizeForGoogleFunctionName(name: string): string;
|
|
14
14
|
/**
|
|
@@ -109,8 +109,14 @@ export declare function computeMaxSteps(rawMaxSteps?: number): number;
|
|
|
109
109
|
* Returns a plain string (not a `{ unified, raw }` object): the Vertex result
|
|
110
110
|
* builders and the consuming layer (neurolink.ts `finishReason || "unknown"`
|
|
111
111
|
* and `finishReason === "length"`) compare against plain strings.
|
|
112
|
+
*
|
|
113
|
+
* MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL map to "error", NOT
|
|
114
|
+
* "tool-calls": they are provider/model failures, while "tool-calls" is the
|
|
115
|
+
* exclusive contract for "step budget exhausted while the model still wanted
|
|
116
|
+
* tools" — consumers (e.g. curator's step-cap intercept) branch on it and
|
|
117
|
+
* were rendering fake step-limit messages for 2-4-step malformed-call turns.
|
|
112
118
|
*/
|
|
113
|
-
export declare function mapGeminiFinishReason(raw: string | null | undefined): "stop" | "length" | "tool-calls" | "content-filter";
|
|
119
|
+
export declare function mapGeminiFinishReason(raw: string | null | undefined): "stop" | "length" | "tool-calls" | "content-filter" | "error";
|
|
114
120
|
/**
|
|
115
121
|
* Append a step's text to a running cross-step accumulator, ignoring empty
|
|
116
122
|
* steps and inserting a single newline between non-empty contributions.
|
|
@@ -215,10 +221,92 @@ export declare function handleMaxStepsTermination(logLabel: string, step: number
|
|
|
215
221
|
export declare function isAbortError(error: unknown): boolean;
|
|
216
222
|
/**
|
|
217
223
|
* Build a graceful, user-facing message for when a single agentic turn hits
|
|
218
|
-
* the step cap
|
|
219
|
-
*
|
|
224
|
+
* the step cap without producing a final answer. Replaces the legacy
|
|
225
|
+
* bracketed "Tool execution limit reached" placeholder.
|
|
226
|
+
*
|
|
227
|
+
* Emit this ONLY when `step >= maxSteps` genuinely terminated the loop —
|
|
228
|
+
* aborted/timed-out/stalled turns must use {@link buildAbortedTurnMessage},
|
|
229
|
+
* {@link buildTurnTimeoutMessage}, or {@link buildTurnStalledMessage}, never
|
|
230
|
+
* this text (a killed 23-step turn once claimed "reached the 200-step limit").
|
|
220
231
|
*/
|
|
221
232
|
export declare function buildToolLoopCapMessage(maxSteps: number, toolCallCount: number): string;
|
|
233
|
+
/**
|
|
234
|
+
* Honest message for a turn ended by the `turnTimeoutMs` wall-clock deadline
|
|
235
|
+
* (stopReason "time-limit"). Sibling of {@link buildToolLoopCapMessage} —
|
|
236
|
+
* time exits must never claim a step limit was reached.
|
|
237
|
+
*/
|
|
238
|
+
export declare function buildTurnTimeoutMessage(elapsedMs: number, toolCallCount: number): string;
|
|
239
|
+
/**
|
|
240
|
+
* Honest message for a turn ended by the `stallTimeoutMs` no-progress
|
|
241
|
+
* watchdog (stopReason "stalled") — typically a wedged tool or a hung
|
|
242
|
+
* model call.
|
|
243
|
+
*/
|
|
244
|
+
export declare function buildTurnStalledMessage(stallTimeoutMs: number, toolCallCount: number): string;
|
|
245
|
+
/**
|
|
246
|
+
* Honest message for a caller-aborted turn (admin kill, coding-task
|
|
247
|
+
* short-circuit — stopReason "aborted").
|
|
248
|
+
*/
|
|
249
|
+
export declare function buildAbortedTurnMessage(toolCallCount: number): string;
|
|
250
|
+
/**
|
|
251
|
+
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
252
|
+
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
253
|
+
* nudge. Rides as a trailing text block on the tool-result user turn.
|
|
254
|
+
*/
|
|
255
|
+
export declare function buildWrapupNudgeText(useFinalResultTool: boolean): string;
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the turn's `stopReason` discriminator from the loop's exit
|
|
258
|
+
* bookkeeping. Precedence: time conditions beat the generic abort flag
|
|
259
|
+
* (the deadline/stall watchdogs abort through the same internal controller),
|
|
260
|
+
* abort beats step-cap, and a provider-error finishReason (e.g. persistent
|
|
261
|
+
* MALFORMED_FUNCTION_CALL) beats "completed".
|
|
262
|
+
*/
|
|
263
|
+
export declare function resolveTurnStopReason(params: {
|
|
264
|
+
timedOut: boolean;
|
|
265
|
+
stalled: boolean;
|
|
266
|
+
wasAborted: boolean;
|
|
267
|
+
/** Step budget ran out AND no clean/forced answer was produced. */
|
|
268
|
+
cappedWithoutAnswer: boolean;
|
|
269
|
+
/** The turn's resolved unified finishReason. */
|
|
270
|
+
finishReason?: string;
|
|
271
|
+
}): GenerateStopReason;
|
|
272
|
+
/**
|
|
273
|
+
* Wall-clock + progress watchdogs for a native agentic turn.
|
|
274
|
+
*
|
|
275
|
+
* - Deadline: `turnTimeoutMs` (caller knob) or `defaultTurnTimeoutMs` (the
|
|
276
|
+
* loop's pre-existing defensive bound) arms a whole-turn timer.
|
|
277
|
+
* - Stall: when `stallTimeoutMs` is set, a low-frequency interval checks the
|
|
278
|
+
* time since the last recorded progress (chunk received, tool started or
|
|
279
|
+
* finished, step started).
|
|
280
|
+
*
|
|
281
|
+
* Both fire `onDeadline(kind)` exactly once each and latch a flag the loop's
|
|
282
|
+
* terminal handling reads to pick the honest exit message and `stopReason`.
|
|
283
|
+
* Timers are unref'd so they never hold the process open; call `dispose()`
|
|
284
|
+
* in the loop's finally.
|
|
285
|
+
*/
|
|
286
|
+
export declare function createTurnClock(params: {
|
|
287
|
+
/** Explicit whole-turn deadline (ms); wins over defaultTurnTimeoutMs. */
|
|
288
|
+
turnTimeoutMs?: number;
|
|
289
|
+
/** Loop-specific defensive default when turnTimeoutMs is unset (undefined = no deadline). */
|
|
290
|
+
defaultTurnTimeoutMs?: number;
|
|
291
|
+
/** No-progress watchdog (ms); undefined = disabled. */
|
|
292
|
+
stallTimeoutMs?: number;
|
|
293
|
+
/** Wrap-up lead (ms); only honored when turnTimeoutMs is explicitly set. */
|
|
294
|
+
wrapupTimeLeadMs?: number;
|
|
295
|
+
onDeadline: (kind: "timeout" | "stall") => void;
|
|
296
|
+
}): {
|
|
297
|
+
readonly timedOut: boolean;
|
|
298
|
+
readonly stalled: boolean;
|
|
299
|
+
/** True when the turn ended on a time condition (deadline or stall). */
|
|
300
|
+
readonly expired: boolean;
|
|
301
|
+
/** The effective whole-turn deadline in ms (explicit or defensive default). */
|
|
302
|
+
readonly turnTimeoutMs: number | undefined;
|
|
303
|
+
elapsedMs(): number;
|
|
304
|
+
/** Record progress: chunk received, tool started/finished, step started. */
|
|
305
|
+
noteProgress(): void;
|
|
306
|
+
/** True when an explicit deadline exists and remaining time is inside the wrap-up lead. */
|
|
307
|
+
shouldNudgeWrapup(): boolean;
|
|
308
|
+
dispose(): void;
|
|
309
|
+
};
|
|
222
310
|
/**
|
|
223
311
|
* Push model response parts to conversation history, preserving thoughtSignature
|
|
224
312
|
* for Gemini 3 multi-turn tool calling.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { extname } from "node:path";
|
|
14
|
-
import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, } from "../core/constants.js";
|
|
14
|
+
import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../core/constants.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
16
|
import { convertZodToJsonSchema, ensureNestedSchemaTypes, inlineJsonSchema, isZodSchema, normalizeJsonSchemaObject, } from "../utils/schemaConversion.js";
|
|
17
17
|
import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
|
|
@@ -463,6 +463,12 @@ export function computeMaxSteps(rawMaxSteps) {
|
|
|
463
463
|
* Returns a plain string (not a `{ unified, raw }` object): the Vertex result
|
|
464
464
|
* builders and the consuming layer (neurolink.ts `finishReason || "unknown"`
|
|
465
465
|
* and `finishReason === "length"`) compare against plain strings.
|
|
466
|
+
*
|
|
467
|
+
* MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL map to "error", NOT
|
|
468
|
+
* "tool-calls": they are provider/model failures, while "tool-calls" is the
|
|
469
|
+
* exclusive contract for "step budget exhausted while the model still wanted
|
|
470
|
+
* tools" — consumers (e.g. curator's step-cap intercept) branch on it and
|
|
471
|
+
* were rendering fake step-limit messages for 2-4-step malformed-call turns.
|
|
466
472
|
*/
|
|
467
473
|
export function mapGeminiFinishReason(raw) {
|
|
468
474
|
switch (raw) {
|
|
@@ -470,7 +476,7 @@ export function mapGeminiFinishReason(raw) {
|
|
|
470
476
|
return "length";
|
|
471
477
|
case "MALFORMED_FUNCTION_CALL":
|
|
472
478
|
case "UNEXPECTED_TOOL_CALL":
|
|
473
|
-
return "
|
|
479
|
+
return "error";
|
|
474
480
|
case "SAFETY":
|
|
475
481
|
case "RECITATION":
|
|
476
482
|
case "BLOCKLIST":
|
|
@@ -832,8 +838,13 @@ export function isAbortError(error) {
|
|
|
832
838
|
}
|
|
833
839
|
/**
|
|
834
840
|
* Build a graceful, user-facing message for when a single agentic turn hits
|
|
835
|
-
* the step cap
|
|
836
|
-
*
|
|
841
|
+
* the step cap without producing a final answer. Replaces the legacy
|
|
842
|
+
* bracketed "Tool execution limit reached" placeholder.
|
|
843
|
+
*
|
|
844
|
+
* Emit this ONLY when `step >= maxSteps` genuinely terminated the loop —
|
|
845
|
+
* aborted/timed-out/stalled turns must use {@link buildAbortedTurnMessage},
|
|
846
|
+
* {@link buildTurnTimeoutMessage}, or {@link buildTurnStalledMessage}, never
|
|
847
|
+
* this text (a killed 23-step turn once claimed "reached the 200-step limit").
|
|
837
848
|
*/
|
|
838
849
|
export function buildToolLoopCapMessage(maxSteps, toolCallCount) {
|
|
839
850
|
const calls = toolCallCount > 0
|
|
@@ -842,6 +853,177 @@ export function buildToolLoopCapMessage(maxSteps, toolCallCount) {
|
|
|
842
853
|
return (`${calls}reached the ${maxSteps}-step limit for a single turn before I could finish. ` +
|
|
843
854
|
`Please narrow the request or break it into smaller asks and I'll continue.`);
|
|
844
855
|
}
|
|
856
|
+
/** Format an elapsed duration as "Xm Ys" (or "Ys" under a minute). */
|
|
857
|
+
function formatElapsed(elapsedMs) {
|
|
858
|
+
const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
|
|
859
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
860
|
+
const seconds = totalSeconds % 60;
|
|
861
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Honest message for a turn ended by the `turnTimeoutMs` wall-clock deadline
|
|
865
|
+
* (stopReason "time-limit"). Sibling of {@link buildToolLoopCapMessage} —
|
|
866
|
+
* time exits must never claim a step limit was reached.
|
|
867
|
+
*/
|
|
868
|
+
export function buildTurnTimeoutMessage(elapsedMs, toolCallCount) {
|
|
869
|
+
const calls = toolCallCount > 0
|
|
870
|
+
? ` I completed ${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"} before stopping;`
|
|
871
|
+
: "";
|
|
872
|
+
return (`I had to stop after ${formatElapsed(elapsedMs)} — this turn hit its ` +
|
|
873
|
+
`processing time limit.${calls} ask me to continue and I'll pick up from there.`);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Honest message for a turn ended by the `stallTimeoutMs` no-progress
|
|
877
|
+
* watchdog (stopReason "stalled") — typically a wedged tool or a hung
|
|
878
|
+
* model call.
|
|
879
|
+
*/
|
|
880
|
+
export function buildTurnStalledMessage(stallTimeoutMs, toolCallCount) {
|
|
881
|
+
const calls = toolCallCount > 0
|
|
882
|
+
? ` I completed ${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"} before stopping;`
|
|
883
|
+
: "";
|
|
884
|
+
return (`I had to stop because this turn made no progress for ` +
|
|
885
|
+
`${formatElapsed(stallTimeoutMs)} — a tool or model call appears to be stuck.` +
|
|
886
|
+
`${calls} ask me to continue and I'll pick up from there.`);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Honest message for a caller-aborted turn (admin kill, coding-task
|
|
890
|
+
* short-circuit — stopReason "aborted").
|
|
891
|
+
*/
|
|
892
|
+
export function buildAbortedTurnMessage(toolCallCount) {
|
|
893
|
+
const calls = toolCallCount > 0
|
|
894
|
+
? ` I completed ${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"} before stopping.`
|
|
895
|
+
: "";
|
|
896
|
+
return `This turn was stopped before I could finish.${calls}`;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
900
|
+
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
901
|
+
* nudge. Rides as a trailing text block on the tool-result user turn.
|
|
902
|
+
*/
|
|
903
|
+
export function buildWrapupNudgeText(useFinalResultTool) {
|
|
904
|
+
return ("NOTE: processing time for this turn is nearly up. Consolidate what you have and " +
|
|
905
|
+
(useFinalResultTool
|
|
906
|
+
? "call final_result with your best answer now."
|
|
907
|
+
: "provide your final answer now."));
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Resolve the turn's `stopReason` discriminator from the loop's exit
|
|
911
|
+
* bookkeeping. Precedence: time conditions beat the generic abort flag
|
|
912
|
+
* (the deadline/stall watchdogs abort through the same internal controller),
|
|
913
|
+
* abort beats step-cap, and a provider-error finishReason (e.g. persistent
|
|
914
|
+
* MALFORMED_FUNCTION_CALL) beats "completed".
|
|
915
|
+
*/
|
|
916
|
+
export function resolveTurnStopReason(params) {
|
|
917
|
+
if (params.timedOut) {
|
|
918
|
+
return "time-limit";
|
|
919
|
+
}
|
|
920
|
+
if (params.stalled) {
|
|
921
|
+
return "stalled";
|
|
922
|
+
}
|
|
923
|
+
if (params.wasAborted) {
|
|
924
|
+
return "aborted";
|
|
925
|
+
}
|
|
926
|
+
if (params.cappedWithoutAnswer) {
|
|
927
|
+
return "step-cap";
|
|
928
|
+
}
|
|
929
|
+
if (params.finishReason === "error") {
|
|
930
|
+
return "provider-error";
|
|
931
|
+
}
|
|
932
|
+
return "completed";
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Wall-clock + progress watchdogs for a native agentic turn.
|
|
936
|
+
*
|
|
937
|
+
* - Deadline: `turnTimeoutMs` (caller knob) or `defaultTurnTimeoutMs` (the
|
|
938
|
+
* loop's pre-existing defensive bound) arms a whole-turn timer.
|
|
939
|
+
* - Stall: when `stallTimeoutMs` is set, a low-frequency interval checks the
|
|
940
|
+
* time since the last recorded progress (chunk received, tool started or
|
|
941
|
+
* finished, step started).
|
|
942
|
+
*
|
|
943
|
+
* Both fire `onDeadline(kind)` exactly once each and latch a flag the loop's
|
|
944
|
+
* terminal handling reads to pick the honest exit message and `stopReason`.
|
|
945
|
+
* Timers are unref'd so they never hold the process open; call `dispose()`
|
|
946
|
+
* in the loop's finally.
|
|
947
|
+
*/
|
|
948
|
+
export function createTurnClock(params) {
|
|
949
|
+
const startedAt = Date.now();
|
|
950
|
+
const isValidMs = (value) => value !== undefined && Number.isFinite(value) && value > 0;
|
|
951
|
+
const effectiveTurnTimeoutMs = isValidMs(params.turnTimeoutMs)
|
|
952
|
+
? params.turnTimeoutMs
|
|
953
|
+
: isValidMs(params.defaultTurnTimeoutMs)
|
|
954
|
+
? params.defaultTurnTimeoutMs
|
|
955
|
+
: undefined;
|
|
956
|
+
// Nudging against the loop's defensive default would inject prompt text
|
|
957
|
+
// into turns whose caller never opted into a time budget — only nudge
|
|
958
|
+
// against an explicit turnTimeoutMs.
|
|
959
|
+
const wrapupLeadMs = isValidMs(params.turnTimeoutMs)
|
|
960
|
+
? (params.wrapupTimeLeadMs ?? DEFAULT_WRAPUP_TIME_LEAD_MS)
|
|
961
|
+
: undefined;
|
|
962
|
+
let timedOut = false;
|
|
963
|
+
let stalled = false;
|
|
964
|
+
let lastProgressAt = startedAt;
|
|
965
|
+
let deadlineTimer;
|
|
966
|
+
let stallTimer;
|
|
967
|
+
if (effectiveTurnTimeoutMs !== undefined) {
|
|
968
|
+
deadlineTimer = setTimeout(() => {
|
|
969
|
+
timedOut = true;
|
|
970
|
+
params.onDeadline("timeout");
|
|
971
|
+
}, effectiveTurnTimeoutMs);
|
|
972
|
+
deadlineTimer.unref?.();
|
|
973
|
+
}
|
|
974
|
+
if (isValidMs(params.stallTimeoutMs)) {
|
|
975
|
+
const stallTimeoutMs = params.stallTimeoutMs;
|
|
976
|
+
const checkEveryMs = Math.min(Math.max(1000, Math.floor(stallTimeoutMs / 4)), 15_000);
|
|
977
|
+
stallTimer = setInterval(() => {
|
|
978
|
+
if (!stalled &&
|
|
979
|
+
!timedOut &&
|
|
980
|
+
Date.now() - lastProgressAt >= stallTimeoutMs) {
|
|
981
|
+
stalled = true;
|
|
982
|
+
params.onDeadline("stall");
|
|
983
|
+
}
|
|
984
|
+
}, checkEveryMs);
|
|
985
|
+
stallTimer.unref?.();
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
get timedOut() {
|
|
989
|
+
return timedOut;
|
|
990
|
+
},
|
|
991
|
+
get stalled() {
|
|
992
|
+
return stalled;
|
|
993
|
+
},
|
|
994
|
+
/** True when the turn ended on a time condition (deadline or stall). */
|
|
995
|
+
get expired() {
|
|
996
|
+
return timedOut || stalled;
|
|
997
|
+
},
|
|
998
|
+
/** The effective whole-turn deadline in ms (explicit or defensive default). */
|
|
999
|
+
get turnTimeoutMs() {
|
|
1000
|
+
return effectiveTurnTimeoutMs;
|
|
1001
|
+
},
|
|
1002
|
+
elapsedMs() {
|
|
1003
|
+
return Date.now() - startedAt;
|
|
1004
|
+
},
|
|
1005
|
+
/** Record progress: chunk received, tool started/finished, step started. */
|
|
1006
|
+
noteProgress() {
|
|
1007
|
+
lastProgressAt = Date.now();
|
|
1008
|
+
},
|
|
1009
|
+
/** True when an explicit deadline exists and remaining time is inside the wrap-up lead. */
|
|
1010
|
+
shouldNudgeWrapup() {
|
|
1011
|
+
if (effectiveTurnTimeoutMs === undefined || wrapupLeadMs === undefined) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
const remaining = effectiveTurnTimeoutMs - (Date.now() - startedAt);
|
|
1015
|
+
return remaining > 0 && remaining <= wrapupLeadMs;
|
|
1016
|
+
},
|
|
1017
|
+
dispose() {
|
|
1018
|
+
if (deadlineTimer) {
|
|
1019
|
+
clearTimeout(deadlineTimer);
|
|
1020
|
+
}
|
|
1021
|
+
if (stallTimer) {
|
|
1022
|
+
clearInterval(stallTimer);
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
845
1027
|
/**
|
|
846
1028
|
* Push model response parts to conversation history, preserving thoughtSignature
|
|
847
1029
|
* for Gemini 3 multi-turn tool calling.
|
|
@@ -256,6 +256,21 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
256
256
|
* exporters. Mirrors the Bedrock + Ollama pattern.
|
|
257
257
|
*/
|
|
258
258
|
private emitGenerationEnd;
|
|
259
|
+
/**
|
|
260
|
+
* Emit a `turn:lifecycle` event on the SDK emitter (alongside
|
|
261
|
+
* tool:start/tool:end) so loop conditions that previously only reached
|
|
262
|
+
* process logs — step-cap, time-limit, stall, abort, tool timeouts,
|
|
263
|
+
* malformed-call retries — are observable by consumers' own pipelines.
|
|
264
|
+
*/
|
|
265
|
+
private emitTurnEvent;
|
|
266
|
+
/**
|
|
267
|
+
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
268
|
+
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
269
|
+
* only — time/stall/abort exits must never claim a step limit was reached
|
|
270
|
+
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
271
|
+
* abort wearing the cap message).
|
|
272
|
+
*/
|
|
273
|
+
private buildLoopExitMessage;
|
|
259
274
|
protected formatProviderError(error: unknown): Error;
|
|
260
275
|
/**
|
|
261
276
|
* Memory-safe cache management for model configurations
|