@juspay/neurolink 9.80.4 → 9.81.1
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 +351 -351
- package/dist/cli/loop/optionsSchema.js +16 -0
- package/dist/core/analytics.js +22 -0
- package/dist/core/constants.d.ts +13 -0
- package/dist/core/constants.js +13 -0
- 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/neurolink.js +30 -3
- 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 +603 -120
- package/dist/lib/types/analytics.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +75 -0
- package/dist/lib/types/stream.d.ts +21 -1
- package/dist/neurolink.js +30 -3
- 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 +603 -120
- package/dist/types/analytics.d.ts +10 -0
- package/dist/types/generate.d.ts +75 -0
- package/dist/types/stream.d.ts +21 -1
- 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
|
|
@@ -310,6 +310,38 @@ export type GenerateOptions = {
|
|
|
310
310
|
* provider+model with the same doomed budget.
|
|
311
311
|
*/
|
|
312
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;
|
|
313
345
|
/** AbortSignal for external cancellation of the AI call */
|
|
314
346
|
abortSignal?: AbortSignal;
|
|
315
347
|
/**
|
|
@@ -562,6 +594,21 @@ export type AdditionalMemoryUser = {
|
|
|
562
594
|
/** Max words for this user's condensed memory. Overrides the default maxWords. */
|
|
563
595
|
maxWords?: number;
|
|
564
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";
|
|
565
612
|
/**
|
|
566
613
|
* Generate function result type - Primary output format
|
|
567
614
|
* Future-ready for multi-modal outputs while maintaining text focus
|
|
@@ -662,6 +709,20 @@ export type GenerateResult = {
|
|
|
662
709
|
provider?: string;
|
|
663
710
|
model?: string;
|
|
664
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;
|
|
665
726
|
/**
|
|
666
727
|
* True when the schema JSON in `content`/`structuredData` was repaired from
|
|
667
728
|
* malformed model text (jsonrepair ran). The result is still valid JSON.
|
|
@@ -882,6 +943,14 @@ export type TextGenerationOptions = {
|
|
|
882
943
|
*/
|
|
883
944
|
enabledToolNames?: string[];
|
|
884
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;
|
|
885
954
|
/** AbortSignal for external cancellation of the AI call */
|
|
886
955
|
abortSignal?: AbortSignal;
|
|
887
956
|
disableTools?: boolean;
|
|
@@ -1128,6 +1197,12 @@ export type TextGenerationResult = {
|
|
|
1128
1197
|
/** Parsed structured object when a `schema` was requested (see GenerateResult.structuredData). */
|
|
1129
1198
|
structuredData?: unknown;
|
|
1130
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;
|
|
1131
1206
|
/** True when the schema JSON was repaired from malformed model text. */
|
|
1132
1207
|
jsonRepaired?: boolean;
|
|
1133
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;
|
package/dist/neurolink.js
CHANGED
|
@@ -3513,6 +3513,10 @@ Current user's request: ${currentInput}`;
|
|
|
3513
3513
|
stt: options.stt,
|
|
3514
3514
|
fileRegistry: this.fileRegistry,
|
|
3515
3515
|
timeout: options.timeout,
|
|
3516
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
3517
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
3518
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
3519
|
+
toolTimeoutMs: options.toolTimeoutMs,
|
|
3516
3520
|
abortSignal: options.abortSignal,
|
|
3517
3521
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
3518
3522
|
middleware: options.middleware,
|
|
@@ -3644,6 +3648,12 @@ Current user's request: ${currentInput}`;
|
|
|
3644
3648
|
content: textResult.content,
|
|
3645
3649
|
structuredData: textResult.structuredData,
|
|
3646
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,
|
|
3647
3657
|
jsonRepaired: textResult.jsonRepaired,
|
|
3648
3658
|
jsonTruncated: textResult.jsonTruncated,
|
|
3649
3659
|
provider: textResult.provider,
|
|
@@ -5063,6 +5073,9 @@ Current user's request: ${currentInput}`;
|
|
|
5063
5073
|
usage: result.usage,
|
|
5064
5074
|
responseTime,
|
|
5065
5075
|
finishReason: result.finishReason,
|
|
5076
|
+
stopReason: result.stopReason,
|
|
5077
|
+
rawFinishReason: result.rawFinishReason,
|
|
5078
|
+
stepsUsed: result.stepsUsed,
|
|
5066
5079
|
toolsUsed: result.toolsUsed || [],
|
|
5067
5080
|
toolExecutions: transformedToolExecutions,
|
|
5068
5081
|
enhancedWithTools: Boolean(hasToolExecutions),
|
|
@@ -5198,6 +5211,9 @@ Current user's request: ${currentInput}`;
|
|
|
5198
5211
|
usage: poolResult.usage,
|
|
5199
5212
|
responseTime,
|
|
5200
5213
|
finishReason: poolResult.finishReason,
|
|
5214
|
+
stopReason: poolResult.stopReason,
|
|
5215
|
+
rawFinishReason: poolResult.rawFinishReason,
|
|
5216
|
+
stepsUsed: poolResult.stepsUsed,
|
|
5201
5217
|
toolsUsed: poolResult.toolsUsed || [],
|
|
5202
5218
|
toolExecutions: poolResult.toolExecutions?.map((te) => {
|
|
5203
5219
|
const t = te;
|
|
@@ -5474,6 +5490,9 @@ Current user's request: ${currentInput}`;
|
|
|
5474
5490
|
usage: result.usage,
|
|
5475
5491
|
responseTime,
|
|
5476
5492
|
finishReason: result.finishReason,
|
|
5493
|
+
stopReason: result.stopReason,
|
|
5494
|
+
rawFinishReason: result.rawFinishReason,
|
|
5495
|
+
stepsUsed: result.stepsUsed,
|
|
5477
5496
|
toolsUsed: result.toolsUsed || [],
|
|
5478
5497
|
// Map toolExecutions from EnhancedGenerateResult shape ({name,input,output})
|
|
5479
5498
|
// to TextGenerationResult shape ({toolName,executionTime,success}).
|
|
@@ -6471,7 +6490,7 @@ Current user's request: ${currentInput}`;
|
|
|
6471
6490
|
factoryResult,
|
|
6472
6491
|
sessionId: enhancedOptions.context?.sessionId,
|
|
6473
6492
|
});
|
|
6474
|
-
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);
|
|
6475
6494
|
const streamState = {
|
|
6476
6495
|
finishReason: streamFinishReason ?? "stop",
|
|
6477
6496
|
toolCalls: streamToolCalls,
|
|
@@ -6818,6 +6837,7 @@ Current user's request: ${currentInput}`;
|
|
|
6818
6837
|
guardrailsBlocked: metadata.guardrailsBlocked,
|
|
6819
6838
|
error: metadata.error,
|
|
6820
6839
|
events: eventSequence,
|
|
6840
|
+
providerMetadata: providerStreamMetadata,
|
|
6821
6841
|
});
|
|
6822
6842
|
}
|
|
6823
6843
|
catch (error) {
|
|
@@ -7612,6 +7632,7 @@ Current user's request: ${currentInput}`;
|
|
|
7612
7632
|
toolCalls: poolStreamResult.toolCalls ?? [],
|
|
7613
7633
|
toolResults: poolStreamResult.toolResults ?? [],
|
|
7614
7634
|
analytics: poolStreamResult.analytics,
|
|
7635
|
+
metadata: poolStreamResult.metadata,
|
|
7615
7636
|
};
|
|
7616
7637
|
}
|
|
7617
7638
|
catch (poolStreamError) {
|
|
@@ -7665,6 +7686,12 @@ Current user's request: ${currentInput}`;
|
|
|
7665
7686
|
toolCalls: streamResult.toolCalls ?? [],
|
|
7666
7687
|
toolResults: streamResult.toolResults ?? [],
|
|
7667
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,
|
|
7668
7695
|
};
|
|
7669
7696
|
}
|
|
7670
7697
|
/**
|
|
@@ -7707,14 +7734,14 @@ Current user's request: ${currentInput}`;
|
|
|
7707
7734
|
analytics: streamResult.analytics,
|
|
7708
7735
|
evaluation: streamResult.evaluation,
|
|
7709
7736
|
events: config.events && config.events.length > 0 ? config.events : undefined,
|
|
7710
|
-
metadata: {
|
|
7737
|
+
metadata: Object.assign(config.providerMetadata ?? {}, {
|
|
7711
7738
|
streamId: config.streamId,
|
|
7712
7739
|
startTime: config.startTime,
|
|
7713
7740
|
responseTime: config.responseTime,
|
|
7714
7741
|
fallback: config.fallback || false,
|
|
7715
7742
|
guardrailsBlocked: config.guardrailsBlocked,
|
|
7716
7743
|
error: config.error,
|
|
7717
|
-
},
|
|
7744
|
+
}),
|
|
7718
7745
|
};
|
|
7719
7746
|
}
|
|
7720
7747
|
/**
|
|
@@ -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
|