@juspay/neurolink 9.81.2 → 9.82.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 +18 -0
- package/README.md +4 -0
- package/dist/browser/neurolink.min.js +355 -355
- package/dist/cli/commands/proxy.js +5 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -1
- package/dist/cli/factories/commandFactory.js +36 -16
- package/dist/cli/utils/audioPlayer.d.ts +37 -0
- package/dist/cli/utils/audioPlayer.js +138 -0
- package/dist/constants/contextWindows.js +10 -0
- package/dist/core/constants.d.ts +16 -0
- package/dist/core/constants.js +16 -0
- package/dist/core/modules/GenerationHandler.js +7 -1
- package/dist/core/modules/ToolsManager.d.ts +5 -0
- package/dist/core/modules/ToolsManager.js +62 -6
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -6
- package/dist/lib/constants/contextWindows.js +10 -0
- package/dist/lib/core/constants.d.ts +16 -0
- package/dist/lib/core/constants.js +16 -0
- package/dist/lib/core/modules/GenerationHandler.js +7 -1
- package/dist/lib/core/modules/ToolsManager.d.ts +5 -0
- package/dist/lib/core/modules/ToolsManager.js +62 -6
- package/dist/lib/index.d.ts +6 -1
- package/dist/lib/index.js +14 -6
- package/dist/lib/neurolink.js +21 -3
- package/dist/lib/providers/googleNativeGemini3.d.ts +43 -0
- package/dist/lib/providers/googleNativeGemini3.js +73 -1
- package/dist/lib/providers/googleVertex.js +495 -80
- package/dist/lib/proxy/proxyDispatcher.d.ts +5 -0
- package/dist/lib/proxy/proxyDispatcher.js +61 -0
- package/dist/lib/types/generate.d.ts +5 -1
- package/dist/lib/utils/async/index.d.ts +1 -1
- package/dist/lib/utils/async/index.js +1 -1
- package/dist/lib/utils/async/withTimeout.d.ts +12 -0
- package/dist/lib/utils/async/withTimeout.js +45 -0
- package/dist/lib/utils/mcpErrorText.d.ts +11 -0
- package/dist/lib/utils/mcpErrorText.js +23 -0
- package/dist/lib/utils/systemMessages.d.ts +29 -0
- package/dist/lib/utils/systemMessages.js +45 -0
- package/dist/neurolink.js +21 -3
- package/dist/providers/googleNativeGemini3.d.ts +43 -0
- package/dist/providers/googleNativeGemini3.js +73 -1
- package/dist/providers/googleVertex.js +495 -80
- package/dist/proxy/proxyDispatcher.d.ts +5 -0
- package/dist/proxy/proxyDispatcher.js +60 -0
- package/dist/types/generate.d.ts +5 -1
- package/dist/utils/async/index.d.ts +1 -1
- package/dist/utils/async/index.js +1 -1
- package/dist/utils/async/withTimeout.d.ts +12 -0
- package/dist/utils/async/withTimeout.js +45 -0
- package/dist/utils/mcpErrorText.d.ts +11 -0
- package/dist/utils/mcpErrorText.js +23 -0
- package/dist/utils/systemMessages.d.ts +29 -0
- package/dist/utils/systemMessages.js +44 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +1258 -0
- package/package.json +3 -2
|
@@ -122,6 +122,22 @@ export const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300_000;
|
|
|
122
122
|
* a wrap-up nudge rides the next tool-result turn.
|
|
123
123
|
*/
|
|
124
124
|
export const DEFAULT_WRAPUP_TIME_LEAD_MS = 120_000;
|
|
125
|
+
/**
|
|
126
|
+
* In-loop context guard threshold for native agentic loops: when the last
|
|
127
|
+
* model call's actual prompt size (provider-reported usage) plus the
|
|
128
|
+
* estimated growth from this step's tool results crosses this fraction of
|
|
129
|
+
* the model's context window, the loop stops calling tools and synthesizes a
|
|
130
|
+
* final answer instead of stepping into a provider 400 ("prompt is too
|
|
131
|
+
* long") that would destroy the whole turn's work.
|
|
132
|
+
*/
|
|
133
|
+
export const DEFAULT_CONTEXT_GUARD_RATIO = 0.85;
|
|
134
|
+
/**
|
|
135
|
+
* Floor for the turn budget handed to the post-overflow recovery retry.
|
|
136
|
+
* The retry inherits the REMAINING `turnTimeoutMs` (whole-turn semantics —
|
|
137
|
+
* one generate() must not stack two full budgets), but never less than this,
|
|
138
|
+
* so a compacted retry still gets a workable window.
|
|
139
|
+
*/
|
|
140
|
+
export const MIN_RECOVERY_TURN_BUDGET_MS = 30_000;
|
|
125
141
|
// Fire-and-forget tool storage writes (Redis). 5s is generous for a single
|
|
126
142
|
// Redis write; if breached, the .catch logs a warning.
|
|
127
143
|
export const TOOL_STORAGE_TIMEOUT_MS = 5000;
|
|
@@ -26,6 +26,7 @@ import { coerceJsonToSchema } from "../../utils/json/coerce.js";
|
|
|
26
26
|
import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
|
|
27
27
|
import { Output, stepCountIs } from "../../utils/tool.js";
|
|
28
28
|
import { generateText } from "../../utils/generation.js";
|
|
29
|
+
import { extractSystemMessages } from "../../utils/systemMessages.js";
|
|
29
30
|
const genTracer = tracers.generation;
|
|
30
31
|
/**
|
|
31
32
|
* Safely preview-serialize a value for debug logging.
|
|
@@ -105,9 +106,14 @@ export class GenerationHandler {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
const prepareStep = options.prepareStep;
|
|
109
|
+
// Hoist system-role messages into generateText's top-level `system` option
|
|
110
|
+
// rather than passing them inside `messages` (deprecated by the AI SDK,
|
|
111
|
+
// rejected in v7). See extractSystemMessages for the rationale. (#1024)
|
|
112
|
+
const { system, messages: nonSystemMessages } = extractSystemMessages(messages);
|
|
108
113
|
return await generateText({
|
|
109
114
|
model,
|
|
110
|
-
|
|
115
|
+
...(system && { system }),
|
|
116
|
+
messages: nonSystemMessages,
|
|
111
117
|
...(shouldUseTools &&
|
|
112
118
|
Object.keys(toolsWithCache).length > 0 && { tools: toolsWithCache }),
|
|
113
119
|
stopWhen: stepCountIs(options.maxSteps ?? DEFAULT_MAX_STEPS),
|
|
@@ -18,6 +18,11 @@ export declare class ToolsManager {
|
|
|
18
18
|
/**
|
|
19
19
|
* BZ-666: Wrap tool execute with output truncation to prevent
|
|
20
20
|
* context overflow when large results flow into the AI SDK accumulator.
|
|
21
|
+
*
|
|
22
|
+
* Passes the AI-SDK second argument (execution options: abortSignal,
|
|
23
|
+
* toolCallId, messages) through to the inner execute — the native loops
|
|
24
|
+
* provide an abortSignal there, and dropping it at this wrapper made
|
|
25
|
+
* every tool uncancellable (deadline overshoot / ghost executions).
|
|
21
26
|
*/
|
|
22
27
|
private wrapExecuteWithTruncation;
|
|
23
28
|
/**
|
|
@@ -7,6 +7,48 @@ import { getKeyCount } from "../../utils/transformationUtils.js";
|
|
|
7
7
|
import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
|
|
8
8
|
import { generateToolOutputPreview } from "../../context/toolOutputLimits.js";
|
|
9
9
|
import { tool as createAISDKTool, jsonSchema } from "../../utils/tool.js";
|
|
10
|
+
/** Abort-shaped error so provider loops route it to their cancellation path. */
|
|
11
|
+
function makeToolAbortError() {
|
|
12
|
+
const e = new Error("Tool execution aborted");
|
|
13
|
+
e.name = "AbortError";
|
|
14
|
+
return e;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Race a tool-execution promise against an AbortSignal so the calling loop
|
|
18
|
+
* observes a deadline/caller abort IMMEDIATELY instead of waiting for the
|
|
19
|
+
* tool to finish or its execution timeout to expire. The underlying call is
|
|
20
|
+
* not cancelled (bounded ghost execution — the same tradeoff as the tool
|
|
21
|
+
* timeout); real transport-level cancellation (executeExternalMCPTool → MCP
|
|
22
|
+
* client RequestOptions.signal) is tracked as a follow-up.
|
|
23
|
+
*/
|
|
24
|
+
function raceWithAbortSignal(promise, signal) {
|
|
25
|
+
if (signal.aborted) {
|
|
26
|
+
// Swallow the abandoned settlement so it can't become an unhandled
|
|
27
|
+
// rejection later.
|
|
28
|
+
promise.catch(() => {
|
|
29
|
+
// Swallow the abandoned settlement — it must never surface as an
|
|
30
|
+
// unhandled rejection after the race has already been decided.
|
|
31
|
+
});
|
|
32
|
+
return Promise.reject(makeToolAbortError());
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const onAbort = () => {
|
|
36
|
+
promise.catch(() => {
|
|
37
|
+
// Swallow the abandoned settlement — it must never surface as an
|
|
38
|
+
// unhandled rejection after the race has already been decided.
|
|
39
|
+
});
|
|
40
|
+
reject(makeToolAbortError());
|
|
41
|
+
};
|
|
42
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
promise.then((value) => {
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve(value);
|
|
46
|
+
}, (error) => {
|
|
47
|
+
signal.removeEventListener("abort", onAbort);
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
10
52
|
/**
|
|
11
53
|
* ToolsManager class - Handles all tool management operations
|
|
12
54
|
*/
|
|
@@ -32,10 +74,24 @@ export class ToolsManager {
|
|
|
32
74
|
/**
|
|
33
75
|
* BZ-666: Wrap tool execute with output truncation to prevent
|
|
34
76
|
* context overflow when large results flow into the AI SDK accumulator.
|
|
77
|
+
*
|
|
78
|
+
* Passes the AI-SDK second argument (execution options: abortSignal,
|
|
79
|
+
* toolCallId, messages) through to the inner execute — the native loops
|
|
80
|
+
* provide an abortSignal there, and dropping it at this wrapper made
|
|
81
|
+
* every tool uncancellable (deadline overshoot / ghost executions).
|
|
35
82
|
*/
|
|
36
83
|
wrapExecuteWithTruncation(toolName, originalExecute) {
|
|
37
|
-
return async (params) => {
|
|
38
|
-
const
|
|
84
|
+
return async (params, execOptions) => {
|
|
85
|
+
const signal = execOptions
|
|
86
|
+
?.abortSignal;
|
|
87
|
+
const inner = originalExecute(params, execOptions);
|
|
88
|
+
// Inner executes that ignore the signal (external MCP / custom tools —
|
|
89
|
+
// the signal isn't plumbed to their transports yet) still return
|
|
90
|
+
// promptly on abort via the race; the loop's isAbortError handling
|
|
91
|
+
// treats the rejection as a cancellation, not a tool failure.
|
|
92
|
+
const result = signal && typeof signal.addEventListener === "function"
|
|
93
|
+
? await raceWithAbortSignal(inner, signal)
|
|
94
|
+
: await inner;
|
|
39
95
|
return this.truncateToolResult(toolName, result);
|
|
40
96
|
};
|
|
41
97
|
}
|
|
@@ -241,11 +297,11 @@ export class ToolsManager {
|
|
|
241
297
|
const guardedExecute = this.wrapExecuteWithTruncation(toolName, originalExecute);
|
|
242
298
|
tools[toolName] = {
|
|
243
299
|
...directTool,
|
|
244
|
-
execute: async (params) => {
|
|
300
|
+
execute: async (params, execOptions) => {
|
|
245
301
|
const startTime = Date.now();
|
|
246
302
|
this.emitToolEvent("tool:start", toolName, { input: params });
|
|
247
303
|
try {
|
|
248
|
-
const result = await guardedExecute(params);
|
|
304
|
+
const result = await guardedExecute(params, execOptions);
|
|
249
305
|
this.emitToolEvent("tool:end", toolName, {
|
|
250
306
|
result,
|
|
251
307
|
success: true,
|
|
@@ -541,11 +597,11 @@ export class ToolsManager {
|
|
|
541
597
|
return createAISDKTool({
|
|
542
598
|
description: tool.description || `External MCP tool ${tool.name}`,
|
|
543
599
|
inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
|
|
544
|
-
execute: async (params) => {
|
|
600
|
+
execute: async (params, execOptions) => {
|
|
545
601
|
const startTime = Date.now();
|
|
546
602
|
this.emitToolEvent("tool:start", tool.name, { input: params });
|
|
547
603
|
try {
|
|
548
|
-
const result = await guardedExecute(params);
|
|
604
|
+
const result = await guardedExecute(params, execOptions);
|
|
549
605
|
this.emitToolEvent("tool:end", tool.name, {
|
|
550
606
|
result,
|
|
551
607
|
success: true,
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -35,7 +35,12 @@ import { AIProviderFactory } from "./core/factory.js";
|
|
|
35
35
|
export { AIProviderFactory };
|
|
36
36
|
export { NeuroLinkConfigManager as ConfigManager } from "./config/configManager.js";
|
|
37
37
|
export { BaseFactory, BaseRegistry, NeuroLinkFeatureError, createErrorFactory, withRetry, TypedEventEmitter, } from "./core/infrastructure/index.js";
|
|
38
|
-
export { NeuroLinkClient, createClient, NeuroLinkApiError,
|
|
38
|
+
export { NeuroLinkClient, createClient, NeuroLinkApiError, } from "./client/httpClient.js";
|
|
39
|
+
export { NeuroLinkLanguageModel, NeuroLinkProvider as NeuroLinkAIProvider, createNeuroLinkProvider, createNeuroLinkModel, createStreamingResponse, neurolink as neuroLinkAIInstance, } from "./client/aiSdkAdapter.js";
|
|
40
|
+
export { createApiKeyAuthInterceptor, createBearerAuthInterceptor, createDynamicAuthInterceptor, createLoggingInterceptor, createRetryInterceptor, createRateLimitInterceptor, createRequestTransformInterceptor, createResponseTransformInterceptor, createCacheInterceptor, createTimeoutInterceptor, createErrorHandlerInterceptor, composeMiddleware, conditionalMiddleware, } from "./client/interceptors.js";
|
|
41
|
+
export { SSEClient, WebSocketStreamingClient, createStreamingClient, createAsyncStream, collectStream, } from "./client/streamingClient.js";
|
|
42
|
+
export { OAuth2TokenManager, JWTTokenManager, createApiKeyMiddleware, createBearerTokenMiddleware, createTokenManagerMiddleware, createAuthWithRetryMiddleware, createMultiAuthMiddleware, OAuth2Error, OAuth2AuthenticationError as OAuth2AuthError, TokenRefreshError, decodeJWTPayload, isJWTExpired, getJWTExpiry, getApiKeyFromEnv, } from "./client/auth.js";
|
|
43
|
+
export { ErrorCode as ClientErrorCode, NeuroLinkError as ClientNeuroLinkError, HttpError, ClientRateLimitError, ClientValidationError, ClientAuthenticationError, ClientAuthorizationError, NotFoundError, ClientNetworkError, ClientTimeoutError, ClientConnectionError, AbortError, ClientConfigurationError, StreamError, ClientProviderError, ContextLengthError, ContentFilterError, createErrorFromResponse, createErrorFromNative, mapStatusToErrorCode, isRetryableStatus, isRetryableError, isNeuroLinkError, isApiError, } from "./client/errors.js";
|
|
39
44
|
export { AIProviderName, BedrockModels, OpenAIModels, VertexModels, } from "./constants/enums.js";
|
|
40
45
|
export { dynamicModelProvider } from "./core/dynamicModels.js";
|
|
41
46
|
export { validateTool } from "./sdk/toolRegistration.js";
|
package/dist/lib/index.js
CHANGED
|
@@ -41,20 +41,28 @@ export { BaseFactory, BaseRegistry, NeuroLinkFeatureError, createErrorFactory, w
|
|
|
41
41
|
// ============================================================================
|
|
42
42
|
// CLIENT SDK EXPORTS - Type-safe API access for browser and Node.js
|
|
43
43
|
// Note: React hooks are NOT re-exported here. Import from '@juspay/neurolink/client'.
|
|
44
|
+
// These re-exports intentionally bypass ./client/index.js: that barrel statically
|
|
45
|
+
// re-exports ./reactHooks.js, so routing through it makes the ROOT import require
|
|
46
|
+
// `react` (an optional peer dep) and crash react-less installs at import time.
|
|
44
47
|
// ============================================================================
|
|
45
48
|
export {
|
|
46
49
|
// HTTP Client
|
|
47
|
-
NeuroLinkClient, createClient, NeuroLinkApiError,
|
|
50
|
+
NeuroLinkClient, createClient, NeuroLinkApiError, } from "./client/httpClient.js";
|
|
51
|
+
export {
|
|
48
52
|
// AI SDK Adapter
|
|
49
|
-
NeuroLinkLanguageModel, NeuroLinkAIProvider, createNeuroLinkProvider, createNeuroLinkModel, createStreamingResponse, neurolink as neuroLinkAIInstance,
|
|
53
|
+
NeuroLinkLanguageModel, NeuroLinkProvider as NeuroLinkAIProvider, createNeuroLinkProvider, createNeuroLinkModel, createStreamingResponse, neurolink as neuroLinkAIInstance, } from "./client/aiSdkAdapter.js";
|
|
54
|
+
export {
|
|
50
55
|
// Interceptors
|
|
51
|
-
createApiKeyAuthInterceptor, createBearerAuthInterceptor, createDynamicAuthInterceptor, createLoggingInterceptor, createRetryInterceptor, createRateLimitInterceptor, createRequestTransformInterceptor, createResponseTransformInterceptor, createCacheInterceptor, createTimeoutInterceptor, createErrorHandlerInterceptor, composeMiddleware, conditionalMiddleware,
|
|
56
|
+
createApiKeyAuthInterceptor, createBearerAuthInterceptor, createDynamicAuthInterceptor, createLoggingInterceptor, createRetryInterceptor, createRateLimitInterceptor, createRequestTransformInterceptor, createResponseTransformInterceptor, createCacheInterceptor, createTimeoutInterceptor, createErrorHandlerInterceptor, composeMiddleware, conditionalMiddleware, } from "./client/interceptors.js";
|
|
57
|
+
export {
|
|
52
58
|
// Streaming Client
|
|
53
|
-
SSEClient, WebSocketStreamingClient, createStreamingClient, createAsyncStream, collectStream,
|
|
59
|
+
SSEClient, WebSocketStreamingClient, createStreamingClient, createAsyncStream, collectStream, } from "./client/streamingClient.js";
|
|
60
|
+
export {
|
|
54
61
|
// Authentication
|
|
55
|
-
OAuth2TokenManager, JWTTokenManager, createApiKeyMiddleware, createBearerTokenMiddleware, createTokenManagerMiddleware, createAuthWithRetryMiddleware, createMultiAuthMiddleware, OAuth2Error, OAuth2AuthError, TokenRefreshError, decodeJWTPayload, isJWTExpired, getJWTExpiry, getApiKeyFromEnv,
|
|
62
|
+
OAuth2TokenManager, JWTTokenManager, createApiKeyMiddleware, createBearerTokenMiddleware, createTokenManagerMiddleware, createAuthWithRetryMiddleware, createMultiAuthMiddleware, OAuth2Error, OAuth2AuthenticationError as OAuth2AuthError, TokenRefreshError, decodeJWTPayload, isJWTExpired, getJWTExpiry, getApiKeyFromEnv, } from "./client/auth.js";
|
|
63
|
+
export {
|
|
56
64
|
// Errors
|
|
57
|
-
ErrorCode as ClientErrorCode, NeuroLinkError as ClientNeuroLinkError, HttpError, ClientRateLimitError, ClientValidationError, ClientAuthenticationError, ClientAuthorizationError, NotFoundError, ClientNetworkError, ClientTimeoutError, ClientConnectionError, AbortError, ClientConfigurationError, StreamError, ClientProviderError, ContextLengthError, ContentFilterError, createErrorFromResponse, createErrorFromNative, mapStatusToErrorCode, isRetryableStatus, isRetryableError, isNeuroLinkError, isApiError, } from "./client/
|
|
65
|
+
ErrorCode as ClientErrorCode, NeuroLinkError as ClientNeuroLinkError, HttpError, ClientRateLimitError, ClientValidationError, ClientAuthenticationError, ClientAuthorizationError, NotFoundError, ClientNetworkError, ClientTimeoutError, ClientConnectionError, AbortError, ClientConfigurationError, StreamError, ClientProviderError, ContextLengthError, ContentFilterError, createErrorFromResponse, createErrorFromNative, mapStatusToErrorCode, isRetryableStatus, isRetryableError, isNeuroLinkError, isApiError, } from "./client/errors.js";
|
|
58
66
|
export { AIProviderName, BedrockModels, OpenAIModels, VertexModels, } from "./constants/enums.js";
|
|
59
67
|
// Dynamic Models exports
|
|
60
68
|
export { dynamicModelProvider } from "./core/dynamicModels.js";
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -28,7 +28,7 @@ import { emergencyContentTruncation } from "./context/emergencyTruncation.js";
|
|
|
28
28
|
import { getContextOverflowProvider, isContextOverflowError, parseProviderOverflowDetails, } from "./context/errorDetection.js";
|
|
29
29
|
import { ContextBudgetExceededError } from "./context/errors.js";
|
|
30
30
|
import { repairToolPairs } from "./context/toolPairRepair.js";
|
|
31
|
-
import { SYSTEM_LIMITS, DEFAULT_TOOL_ROUTING_TIMEOUT_MS, } from "./core/constants.js";
|
|
31
|
+
import { SYSTEM_LIMITS, DEFAULT_TOOL_ROUTING_TIMEOUT_MS, MIN_RECOVERY_TURN_BUDGET_MS, } from "./core/constants.js";
|
|
32
32
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
33
33
|
import { buildToolRoutingCatalog, buildRoutingQueryFromHistory, resolveToolRoutingExclusions, } from "./core/toolRouting.js";
|
|
34
34
|
import { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
@@ -4277,7 +4277,7 @@ Current user's request: ${currentInput}`;
|
|
|
4277
4277
|
return result;
|
|
4278
4278
|
}
|
|
4279
4279
|
async handleGenerateTextInternalFailure(options, context, error) {
|
|
4280
|
-
const recoveredResult = await this.tryRecoverGenerateTextOverflow(options, context.functionTag, error);
|
|
4280
|
+
const recoveredResult = await this.tryRecoverGenerateTextOverflow(options, context.functionTag, error, context.generateInternalStartTime);
|
|
4281
4281
|
if (recoveredResult) {
|
|
4282
4282
|
return recoveredResult;
|
|
4283
4283
|
}
|
|
@@ -4317,7 +4317,7 @@ Current user's request: ${currentInput}`;
|
|
|
4317
4317
|
}
|
|
4318
4318
|
return null;
|
|
4319
4319
|
}
|
|
4320
|
-
async tryRecoverGenerateTextOverflow(options, functionTag, error) {
|
|
4320
|
+
async tryRecoverGenerateTextOverflow(options, functionTag, error, attemptStartTimeMs) {
|
|
4321
4321
|
// Reviewer Finding #3: drop the `!this.conversationMemory` gate so
|
|
4322
4322
|
// inline-conversationMessages callers also benefit from post-provider
|
|
4323
4323
|
// recovery when their pre-dispatch estimate happens to undershoot
|
|
@@ -4467,16 +4467,34 @@ Current user's request: ${currentInput}`;
|
|
|
4467
4467
|
breakdown: verifiedBudget?.breakdown ?? {},
|
|
4468
4468
|
});
|
|
4469
4469
|
}
|
|
4470
|
+
// Whole-turn budget semantics: the retry inherits the REMAINING
|
|
4471
|
+
// turnTimeoutMs, not a fresh clock — attempt 1 already spent part of
|
|
4472
|
+
// the caller's budget, and a fresh clock let one generate() run ~2×
|
|
4473
|
+
// the configured deadline (the 66-minute-turn incident shape).
|
|
4474
|
+
// Floored so a compacted retry still gets a workable window — but the
|
|
4475
|
+
// floor never exceeds the caller's own turnTimeoutMs (a 10s caller
|
|
4476
|
+
// budget must not receive a 30s retry).
|
|
4477
|
+
let retryTurnTimeoutMs = options.turnTimeoutMs;
|
|
4478
|
+
if (typeof options.turnTimeoutMs === "number" &&
|
|
4479
|
+
Number.isFinite(options.turnTimeoutMs) &&
|
|
4480
|
+
attemptStartTimeMs !== undefined) {
|
|
4481
|
+
const elapsedMs = Date.now() - attemptStartTimeMs;
|
|
4482
|
+
retryTurnTimeoutMs = Math.max(options.turnTimeoutMs - elapsedMs, Math.min(MIN_RECOVERY_TURN_BUDGET_MS, options.turnTimeoutMs));
|
|
4483
|
+
}
|
|
4470
4484
|
logger.info(`[${functionTag}] Smart recovery verified, retrying generation`, {
|
|
4471
4485
|
tokensSaved: lastCompactionResult.tokensSaved,
|
|
4472
4486
|
compactionTarget,
|
|
4473
4487
|
verifiedTokens: verifiedBudget.estimatedInputTokens,
|
|
4474
4488
|
verifiedBudget: verifiedBudget.availableInputTokens,
|
|
4475
4489
|
recoveredFraction,
|
|
4490
|
+
retryTurnTimeoutMs,
|
|
4476
4491
|
});
|
|
4477
4492
|
return this.directProviderGeneration({
|
|
4478
4493
|
...options,
|
|
4479
4494
|
conversationMessages: compactedMessages,
|
|
4495
|
+
...(retryTurnTimeoutMs !== undefined && {
|
|
4496
|
+
turnTimeoutMs: retryTurnTimeoutMs,
|
|
4497
|
+
}),
|
|
4480
4498
|
});
|
|
4481
4499
|
}
|
|
4482
4500
|
catch (retryError) {
|
|
@@ -230,6 +230,13 @@ export declare function isAbortError(error: unknown): boolean;
|
|
|
230
230
|
* this text (a killed 23-step turn once claimed "reached the 200-step limit").
|
|
231
231
|
*/
|
|
232
232
|
export declare function buildToolLoopCapMessage(maxSteps: number, toolCallCount: number): string;
|
|
233
|
+
/**
|
|
234
|
+
* Honest message for a turn stopped by the in-loop context guard
|
|
235
|
+
* (stopReason "context-cap") without a synthesized answer. Sibling of
|
|
236
|
+
* {@link buildToolLoopCapMessage} — context exits must never claim a step
|
|
237
|
+
* limit was reached.
|
|
238
|
+
*/
|
|
239
|
+
export declare function buildContextCapMessage(toolCallCount: number): string;
|
|
233
240
|
/**
|
|
234
241
|
* Honest message for a turn ended by the `turnTimeoutMs` wall-clock deadline
|
|
235
242
|
* (stopReason "time-limit"). Sibling of {@link buildToolLoopCapMessage} —
|
|
@@ -266,6 +273,8 @@ export declare function resolveTurnStopReason(params: {
|
|
|
266
273
|
wasAborted: boolean;
|
|
267
274
|
/** Step budget ran out AND no clean/forced answer was produced. */
|
|
268
275
|
cappedWithoutAnswer: boolean;
|
|
276
|
+
/** Context guard stopped the loop AND no clean/forced answer was produced. */
|
|
277
|
+
contextCappedWithoutAnswer?: boolean;
|
|
269
278
|
/** The turn's resolved unified finishReason. */
|
|
270
279
|
finishReason?: string;
|
|
271
280
|
}): GenerateStopReason;
|
|
@@ -307,6 +316,40 @@ export declare function createTurnClock(params: {
|
|
|
307
316
|
shouldNudgeWrapup(): boolean;
|
|
308
317
|
dispose(): void;
|
|
309
318
|
};
|
|
319
|
+
/**
|
|
320
|
+
* In-loop context guard for native agentic turns.
|
|
321
|
+
*
|
|
322
|
+
* The provider reports the ACTUAL prompt size of every model call
|
|
323
|
+
* (usage.input_tokens + cache reads/writes on Anthropic;
|
|
324
|
+
* usageMetadata.promptTokenCount on Gemini). The guard tracks that number
|
|
325
|
+
* plus an estimate of what the current step appends (assistant output, tool
|
|
326
|
+
* results), and tells the loop to stop calling tools once the projected next
|
|
327
|
+
* prompt crosses `thresholdRatio` of the model's context window — the turn
|
|
328
|
+
* then synthesizes a final answer from what it has instead of stepping into
|
|
329
|
+
* a provider 400 ("prompt is too long") that destroys all completed work.
|
|
330
|
+
*
|
|
331
|
+
* Fail-open: until the first usage report arrives, `shouldStop()` is false.
|
|
332
|
+
*/
|
|
333
|
+
export declare function createContextGuard(contextWindowTokens: number, thresholdRatio?: number): {
|
|
334
|
+
/** Tokens at which the guard trips (ratio × window). */
|
|
335
|
+
readonly thresholdTokens: number;
|
|
336
|
+
/** Last observed prompt size plus estimated growth since. */
|
|
337
|
+
readonly projectedNextPromptTokens: number;
|
|
338
|
+
/**
|
|
339
|
+
* Record a model call's reported usage. `promptTokens` must be the FULL
|
|
340
|
+
* prompt size (uncached input + cache read + cache creation for
|
|
341
|
+
* Anthropic). The response's own output is counted as growth — it is
|
|
342
|
+
* appended to the conversation for the next call.
|
|
343
|
+
*/
|
|
344
|
+
noteUsage(promptTokens: number, outputTokens: number): void;
|
|
345
|
+
/**
|
|
346
|
+
* Add growth for content appended since the last model call (tool
|
|
347
|
+
* results, nudge text) using the ~4 chars/token heuristic.
|
|
348
|
+
*/
|
|
349
|
+
noteAppendedChars(chars: number): void;
|
|
350
|
+
/** True when issuing another model call risks crossing the threshold. */
|
|
351
|
+
shouldStop(): boolean;
|
|
352
|
+
};
|
|
310
353
|
/**
|
|
311
354
|
* Push model response parts to conversation history, preserving thoughtSignature
|
|
312
355
|
* 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, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../core/constants.js";
|
|
14
|
+
import { DEFAULT_CONTEXT_GUARD_RATIO, 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";
|
|
@@ -853,6 +853,20 @@ export function buildToolLoopCapMessage(maxSteps, toolCallCount) {
|
|
|
853
853
|
return (`${calls}reached the ${maxSteps}-step limit for a single turn before I could finish. ` +
|
|
854
854
|
`Please narrow the request or break it into smaller asks and I'll continue.`);
|
|
855
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Honest message for a turn stopped by the in-loop context guard
|
|
858
|
+
* (stopReason "context-cap") without a synthesized answer. Sibling of
|
|
859
|
+
* {@link buildToolLoopCapMessage} — context exits must never claim a step
|
|
860
|
+
* limit was reached.
|
|
861
|
+
*/
|
|
862
|
+
export function buildContextCapMessage(toolCallCount) {
|
|
863
|
+
const calls = toolCallCount > 0
|
|
864
|
+
? `I gathered information across ${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"} but `
|
|
865
|
+
: "I ";
|
|
866
|
+
return (`${calls}had to stop because the gathered material filled this turn's ` +
|
|
867
|
+
`context window before I could finish. Please narrow the request or ` +
|
|
868
|
+
`break it into smaller asks and I'll continue.`);
|
|
869
|
+
}
|
|
856
870
|
/** Format an elapsed duration as "Xm Ys" (or "Ys" under a minute). */
|
|
857
871
|
function formatElapsed(elapsedMs) {
|
|
858
872
|
const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
|
|
@@ -923,6 +937,9 @@ export function resolveTurnStopReason(params) {
|
|
|
923
937
|
if (params.wasAborted) {
|
|
924
938
|
return "aborted";
|
|
925
939
|
}
|
|
940
|
+
if (params.contextCappedWithoutAnswer) {
|
|
941
|
+
return "context-cap";
|
|
942
|
+
}
|
|
926
943
|
if (params.cappedWithoutAnswer) {
|
|
927
944
|
return "step-cap";
|
|
928
945
|
}
|
|
@@ -1024,6 +1041,61 @@ export function createTurnClock(params) {
|
|
|
1024
1041
|
},
|
|
1025
1042
|
};
|
|
1026
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* In-loop context guard for native agentic turns.
|
|
1046
|
+
*
|
|
1047
|
+
* The provider reports the ACTUAL prompt size of every model call
|
|
1048
|
+
* (usage.input_tokens + cache reads/writes on Anthropic;
|
|
1049
|
+
* usageMetadata.promptTokenCount on Gemini). The guard tracks that number
|
|
1050
|
+
* plus an estimate of what the current step appends (assistant output, tool
|
|
1051
|
+
* results), and tells the loop to stop calling tools once the projected next
|
|
1052
|
+
* prompt crosses `thresholdRatio` of the model's context window — the turn
|
|
1053
|
+
* then synthesizes a final answer from what it has instead of stepping into
|
|
1054
|
+
* a provider 400 ("prompt is too long") that destroys all completed work.
|
|
1055
|
+
*
|
|
1056
|
+
* Fail-open: until the first usage report arrives, `shouldStop()` is false.
|
|
1057
|
+
*/
|
|
1058
|
+
export function createContextGuard(contextWindowTokens, thresholdRatio = DEFAULT_CONTEXT_GUARD_RATIO) {
|
|
1059
|
+
const thresholdTokens = Math.floor(contextWindowTokens * thresholdRatio);
|
|
1060
|
+
let observedPromptTokens = 0;
|
|
1061
|
+
let projectedGrowthTokens = 0;
|
|
1062
|
+
return {
|
|
1063
|
+
/** Tokens at which the guard trips (ratio × window). */
|
|
1064
|
+
get thresholdTokens() {
|
|
1065
|
+
return thresholdTokens;
|
|
1066
|
+
},
|
|
1067
|
+
/** Last observed prompt size plus estimated growth since. */
|
|
1068
|
+
get projectedNextPromptTokens() {
|
|
1069
|
+
return observedPromptTokens + projectedGrowthTokens;
|
|
1070
|
+
},
|
|
1071
|
+
/**
|
|
1072
|
+
* Record a model call's reported usage. `promptTokens` must be the FULL
|
|
1073
|
+
* prompt size (uncached input + cache read + cache creation for
|
|
1074
|
+
* Anthropic). The response's own output is counted as growth — it is
|
|
1075
|
+
* appended to the conversation for the next call.
|
|
1076
|
+
*/
|
|
1077
|
+
noteUsage(promptTokens, outputTokens) {
|
|
1078
|
+
if (promptTokens > 0) {
|
|
1079
|
+
observedPromptTokens = promptTokens;
|
|
1080
|
+
projectedGrowthTokens = Math.max(0, outputTokens);
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
/**
|
|
1084
|
+
* Add growth for content appended since the last model call (tool
|
|
1085
|
+
* results, nudge text) using the ~4 chars/token heuristic.
|
|
1086
|
+
*/
|
|
1087
|
+
noteAppendedChars(chars) {
|
|
1088
|
+
if (chars > 0) {
|
|
1089
|
+
projectedGrowthTokens += Math.ceil(chars / 4);
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
/** True when issuing another model call risks crossing the threshold. */
|
|
1093
|
+
shouldStop() {
|
|
1094
|
+
return (observedPromptTokens > 0 &&
|
|
1095
|
+
observedPromptTokens + projectedGrowthTokens >= thresholdTokens);
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1027
1099
|
/**
|
|
1028
1100
|
* Push model response parts to conversation history, preserving thoughtSignature
|
|
1029
1101
|
* for Gemini 3 multi-turn tool calling.
|