@juspay/neurolink 12.11.2 → 12.11.3
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 +3 -2
- package/dist/browser/neurolink.min.js +3 -3
- package/dist/mcp/toolRegistry.js +7 -0
- package/dist/neurolink.d.ts +7 -0
- package/dist/neurolink.js +61 -11
- package/dist/types/tools.d.ts +23 -0
- package/dist/utils/errorHandling.d.ts +20 -3
- package/dist/utils/errorHandling.js +22 -5
- package/dist/utils/mcpDefaults.d.ts +1 -1
- package/dist/utils/mcpDefaults.js +4 -1
- package/package.json +1 -1
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -167,6 +167,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
167
167
|
const toolId = isCustomTool ? tool.name : `${serverId}.${tool.name}`;
|
|
168
168
|
const toolTimeoutMs = serverInfo.metadata?.toolTimeoutMs;
|
|
169
169
|
const toolMaxRetries = serverInfo.metadata?.toolMaxRetries;
|
|
170
|
+
const toolTotalTimeoutMs = serverInfo.metadata?.toolTotalTimeoutMs;
|
|
170
171
|
const toolInfo = {
|
|
171
172
|
name: tool.name,
|
|
172
173
|
description: tool.description,
|
|
@@ -180,6 +181,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
180
181
|
permissions: [], // MCPServerInfo.tools doesn't have permissions
|
|
181
182
|
...(toolTimeoutMs !== undefined && { timeoutMs: toolTimeoutMs }),
|
|
182
183
|
...(toolMaxRetries !== undefined && { maxRetries: toolMaxRetries }),
|
|
184
|
+
...(toolTotalTimeoutMs !== undefined && {
|
|
185
|
+
totalTimeoutMs: toolTotalTimeoutMs,
|
|
186
|
+
}),
|
|
183
187
|
};
|
|
184
188
|
// Register only with fully-qualified toolId to avoid collisions
|
|
185
189
|
this.tools.set(toolId, toolInfo);
|
|
@@ -197,6 +201,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
197
201
|
}),
|
|
198
202
|
...(toolTimeoutMs !== undefined && { timeoutMs: toolTimeoutMs }),
|
|
199
203
|
...(toolMaxRetries !== undefined && { maxRetries: toolMaxRetries }),
|
|
204
|
+
...(toolTotalTimeoutMs !== undefined && {
|
|
205
|
+
totalTimeoutMs: toolTotalTimeoutMs,
|
|
206
|
+
}),
|
|
200
207
|
});
|
|
201
208
|
// Tool registered successfully
|
|
202
209
|
}
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -1587,8 +1587,15 @@ export declare class NeuroLink {
|
|
|
1587
1587
|
* @returns Tool execution result
|
|
1588
1588
|
*/
|
|
1589
1589
|
executeTool<T = unknown>(toolName: string, params?: unknown, options?: {
|
|
1590
|
+
/** Bound on ONE attempt. */
|
|
1590
1591
|
timeout?: number;
|
|
1591
1592
|
maxRetries?: number;
|
|
1593
|
+
/**
|
|
1594
|
+
* Bound on the WHOLE execution — every attempt plus the delays between
|
|
1595
|
+
* them. Defaults to `timeout * (maxRetries + 1)`, which is what the
|
|
1596
|
+
* retry loop already spent, so omitting it changes nothing.
|
|
1597
|
+
*/
|
|
1598
|
+
totalTimeoutMs?: number;
|
|
1592
1599
|
retryDelayMs?: number;
|
|
1593
1600
|
/** Disable tool result caching for this call */
|
|
1594
1601
|
disableToolCache?: boolean;
|
package/dist/neurolink.js
CHANGED
|
@@ -9566,7 +9566,7 @@ Current user's request: ${currentInput}`;
|
|
|
9566
9566
|
};
|
|
9567
9567
|
}
|
|
9568
9568
|
// SMART DEFAULTS: Use utility to eliminate boilerplate creation
|
|
9569
|
-
const mcpServerInfo = createCustomToolServerInfo(name, convertedTool, options?.timeout, options?.maxRetries);
|
|
9569
|
+
const mcpServerInfo = createCustomToolServerInfo(name, convertedTool, options?.timeout, options?.maxRetries, options?.totalTimeoutMs);
|
|
9570
9570
|
// Register with toolRegistry using MCPServerInfo directly
|
|
9571
9571
|
this.toolRegistry.registerServer(mcpServerInfo);
|
|
9572
9572
|
// Re-registration replaces options wholesale: omitting `cacheable`
|
|
@@ -10010,14 +10010,32 @@ Current user's request: ${currentInput}`;
|
|
|
10010
10010
|
executionId: executionContext.executionId,
|
|
10011
10011
|
}));
|
|
10012
10012
|
const toolInfo = this.toolRegistry.getToolInfo(toolName);
|
|
10013
|
+
const attemptTimeout = options?.timeout ??
|
|
10014
|
+
toolInfo?.tool?.timeoutMs ??
|
|
10015
|
+
TOOL_TIMEOUTS.EXECUTION_BATCH_MS;
|
|
10016
|
+
const maxRetries = options?.maxRetries ??
|
|
10017
|
+
toolInfo?.tool?.maxRetries ??
|
|
10018
|
+
RETRY_ATTEMPTS.DEFAULT;
|
|
10019
|
+
const retryDelayMs = options?.retryDelayMs || RETRY_DELAYS.BASE_MS;
|
|
10013
10020
|
const finalOptions = {
|
|
10014
|
-
timeout:
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
+
timeout: attemptTimeout,
|
|
10022
|
+
maxRetries,
|
|
10023
|
+
// Ceiling on the whole execution, not one attempt. The default is what
|
|
10024
|
+
// the retry loop already spent, and that is BOTH terms: every attempt at
|
|
10025
|
+
// full length PLUS the fixed wait between them. Omitting the delays
|
|
10026
|
+
// makes the default ceiling shorter than the envelope it is meant to
|
|
10027
|
+
// reproduce, so `attemptTimeout = min(timeout, remaining)` clamps a
|
|
10028
|
+
// later attempt below its configured timeout — the opposite of the
|
|
10029
|
+
// "unchanged unless you ask for less" contract this default exists to
|
|
10030
|
+
// keep. Before any of this the total was unbounded and merely implied:
|
|
10031
|
+
// a tool that reliably hung burned every attempt at full length, and
|
|
10032
|
+
// the surfaced error reported the per-attempt bound beside the
|
|
10033
|
+
// whole-execution elapsed time, which reads as a timeout that was never
|
|
10034
|
+
// enforced.
|
|
10035
|
+
totalTimeout: options?.totalTimeoutMs ??
|
|
10036
|
+
toolInfo?.tool?.totalTimeoutMs ??
|
|
10037
|
+
attemptTimeout * (maxRetries + 1) + retryDelayMs * maxRetries,
|
|
10038
|
+
retryDelayMs,
|
|
10021
10039
|
authContext: options?.authContext,
|
|
10022
10040
|
disableToolCache: options?.disableToolCache,
|
|
10023
10041
|
};
|
|
@@ -10062,11 +10080,43 @@ Current user's request: ${currentInput}`;
|
|
|
10062
10080
|
options: prepared.finalOptions,
|
|
10063
10081
|
circuitBreakerState: prepared.circuitBreaker.getState(),
|
|
10064
10082
|
});
|
|
10083
|
+
const maxAttempts = prepared.finalOptions.maxRetries + 1;
|
|
10084
|
+
const totalTimeout = prepared.finalOptions.totalTimeout;
|
|
10085
|
+
const budgetStart = Date.now();
|
|
10086
|
+
const deadline = budgetStart + totalTimeout;
|
|
10087
|
+
let attemptNumber = 0;
|
|
10065
10088
|
const result = await prepared.circuitBreaker.execute(async () => {
|
|
10066
|
-
return withRetry(async () =>
|
|
10067
|
-
|
|
10089
|
+
return withRetry(async () => {
|
|
10090
|
+
attemptNumber++;
|
|
10091
|
+
const remaining = deadline - Date.now();
|
|
10092
|
+
if (remaining <= 0) {
|
|
10093
|
+
throw ErrorFactory.toolTimeout(toolName, totalTimeout, undefined, {
|
|
10094
|
+
attempt: attemptNumber,
|
|
10095
|
+
maxAttempts,
|
|
10096
|
+
attemptTimeoutMs: prepared.finalOptions.timeout,
|
|
10097
|
+
totalTimeoutMs: totalTimeout,
|
|
10098
|
+
elapsedMs: Date.now() - budgetStart,
|
|
10099
|
+
exhausted: true,
|
|
10100
|
+
});
|
|
10101
|
+
}
|
|
10102
|
+
// Clamping to what is left is what makes the ceiling hard. Gating
|
|
10103
|
+
// retries alone would not: the last attempt could start just under
|
|
10104
|
+
// the deadline and still run a full attempt timeout past it.
|
|
10105
|
+
const attemptTimeout = Math.min(prepared.finalOptions.timeout, remaining);
|
|
10106
|
+
return withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions, executionContext.hitlState), attemptTimeout, ErrorFactory.toolTimeout(toolName, attemptTimeout, undefined, {
|
|
10107
|
+
attempt: attemptNumber,
|
|
10108
|
+
maxAttempts,
|
|
10109
|
+
attemptTimeoutMs: prepared.finalOptions.timeout,
|
|
10110
|
+
totalTimeoutMs: totalTimeout,
|
|
10111
|
+
elapsedMs: Date.now() - budgetStart,
|
|
10112
|
+
}));
|
|
10113
|
+
}, {
|
|
10114
|
+
maxAttempts,
|
|
10068
10115
|
delayMs: prepared.finalOptions.retryDelayMs,
|
|
10069
|
-
|
|
10116
|
+
// Stop when there is not enough budget left for the retry delay
|
|
10117
|
+
// plus any real work after it.
|
|
10118
|
+
isRetriable: (error) => Date.now() + prepared.finalOptions.retryDelayMs < deadline &&
|
|
10119
|
+
isRetriableError(error),
|
|
10070
10120
|
onRetry: (attempt, error) => {
|
|
10071
10121
|
toolRetryCount = attempt;
|
|
10072
10122
|
mcpLogger.warn(`[${executionContext.functionTag}] Retrying tool execution (attempt ${attempt})`, {
|
package/dist/types/tools.d.ts
CHANGED
|
@@ -87,6 +87,12 @@ export type ToolInfo = {
|
|
|
87
87
|
/** Per-tool timeout in milliseconds, set at registration time */
|
|
88
88
|
timeoutMs?: number;
|
|
89
89
|
maxRetries?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Ceiling on the WHOLE execution — every attempt plus the delays between
|
|
92
|
+
* them. Declared explicitly rather than left to the index signature below,
|
|
93
|
+
* which would type it `unknown` and silently defeat the default.
|
|
94
|
+
*/
|
|
95
|
+
totalTimeoutMs?: number;
|
|
90
96
|
[key: string]: unknown;
|
|
91
97
|
};
|
|
92
98
|
/**
|
|
@@ -103,6 +109,13 @@ export type ToolImplementation = {
|
|
|
103
109
|
/** Per-tool timeout in milliseconds, set at registration time */
|
|
104
110
|
timeoutMs?: number;
|
|
105
111
|
maxRetries?: number;
|
|
112
|
+
/**
|
|
113
|
+
* Ceiling on the WHOLE execution — every attempt plus the delays between
|
|
114
|
+
* them — in milliseconds. `timeoutMs` bounds one attempt; without this, a
|
|
115
|
+
* tool that reliably hangs burns `timeoutMs * (maxRetries + 1)`.
|
|
116
|
+
* Defaults to exactly that product, so behaviour is unchanged unless set.
|
|
117
|
+
*/
|
|
118
|
+
totalTimeoutMs?: number;
|
|
106
119
|
};
|
|
107
120
|
/**
|
|
108
121
|
* Tool execution options for enhanced control
|
|
@@ -130,6 +143,13 @@ export type ToolExecutionOptions = {
|
|
|
130
143
|
*/
|
|
131
144
|
timeoutMs?: number;
|
|
132
145
|
maxRetries?: number;
|
|
146
|
+
/**
|
|
147
|
+
* Ceiling on the WHOLE execution — every attempt plus the delays between
|
|
148
|
+
* them. `timeout` bounds one attempt. Defaults to
|
|
149
|
+
* `timeout * (maxRetries + 1)`, which is what the retry loop already spent,
|
|
150
|
+
* so supplying nothing changes nothing.
|
|
151
|
+
*/
|
|
152
|
+
totalTimeoutMs?: number;
|
|
133
153
|
};
|
|
134
154
|
/**
|
|
135
155
|
* Options for tool registration via registerTool()
|
|
@@ -153,6 +173,9 @@ export type ToolRegistrationOptions = {
|
|
|
153
173
|
* When omitted, the SDK's global default (2 retries) is used.
|
|
154
174
|
* Set to 0 to disable retries for this tool. */
|
|
155
175
|
maxRetries?: number;
|
|
176
|
+
/** Ceiling on the whole execution across every attempt and the delays
|
|
177
|
+
* between them. When omitted, `timeout * (maxRetries + 1)` is used. */
|
|
178
|
+
totalTimeoutMs?: number;
|
|
156
179
|
/**
|
|
157
180
|
* Whether this tool's result may be served from the tool-result cache
|
|
158
181
|
* (default true).
|
|
@@ -108,9 +108,26 @@ export declare class ErrorFactory {
|
|
|
108
108
|
*/
|
|
109
109
|
static toolExecutionFailed(toolName: string, originalError: Error, serverId?: string): NeuroLinkError;
|
|
110
110
|
/**
|
|
111
|
-
* Create a tool timeout error
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
* Create a tool timeout error.
|
|
112
|
+
*
|
|
113
|
+
* `timeoutMs` is the bound that was actually exceeded. Pass `budget` when
|
|
114
|
+
* the timeout happened inside a retry loop, so the error can say which
|
|
115
|
+
* number it is reporting: a reader who sees `timeoutMs: 120000` next to an
|
|
116
|
+
* `executionTime` of 483005 will otherwise conclude the timeout was never
|
|
117
|
+
* enforced, when in fact four attempts of 120s each were.
|
|
118
|
+
*
|
|
119
|
+
* `budget.exhausted` marks the case where the whole-execution ceiling ran
|
|
120
|
+
* out rather than a single attempt overrunning; that error is NOT retriable,
|
|
121
|
+
* because there is no budget left to retry into.
|
|
122
|
+
*/
|
|
123
|
+
static toolTimeout(toolName: string, timeoutMs: number, serverId?: string, budget?: {
|
|
124
|
+
attempt?: number;
|
|
125
|
+
maxAttempts?: number;
|
|
126
|
+
attemptTimeoutMs?: number;
|
|
127
|
+
totalTimeoutMs?: number;
|
|
128
|
+
elapsedMs?: number;
|
|
129
|
+
exhausted?: boolean;
|
|
130
|
+
}): NeuroLinkError;
|
|
114
131
|
/**
|
|
115
132
|
* Create a parameter validation error
|
|
116
133
|
*/
|
|
@@ -164,16 +164,33 @@ export class ErrorFactory {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
/**
|
|
167
|
-
* Create a tool timeout error
|
|
167
|
+
* Create a tool timeout error.
|
|
168
|
+
*
|
|
169
|
+
* `timeoutMs` is the bound that was actually exceeded. Pass `budget` when
|
|
170
|
+
* the timeout happened inside a retry loop, so the error can say which
|
|
171
|
+
* number it is reporting: a reader who sees `timeoutMs: 120000` next to an
|
|
172
|
+
* `executionTime` of 483005 will otherwise conclude the timeout was never
|
|
173
|
+
* enforced, when in fact four attempts of 120s each were.
|
|
174
|
+
*
|
|
175
|
+
* `budget.exhausted` marks the case where the whole-execution ceiling ran
|
|
176
|
+
* out rather than a single attempt overrunning; that error is NOT retriable,
|
|
177
|
+
* because there is no budget left to retry into.
|
|
168
178
|
*/
|
|
169
|
-
static toolTimeout(toolName, timeoutMs, serverId) {
|
|
179
|
+
static toolTimeout(toolName, timeoutMs, serverId, budget) {
|
|
180
|
+
const scope = budget?.exhausted
|
|
181
|
+
? `exhausted its ${budget.totalTimeoutMs}ms total budget`
|
|
182
|
+
: budget?.maxAttempts && budget.maxAttempts > 1
|
|
183
|
+
? `timed out after ${timeoutMs}ms on attempt ${budget.attempt ?? 1} of ${budget.maxAttempts}`
|
|
184
|
+
: `timed out after ${timeoutMs}ms`;
|
|
170
185
|
return new NeuroLinkError({
|
|
171
186
|
code: ERROR_CODES.TOOL_TIMEOUT,
|
|
172
|
-
message: `Tool '${toolName}'
|
|
187
|
+
message: `Tool '${toolName}' ${scope}`,
|
|
173
188
|
category: ErrorCategory.TIMEOUT,
|
|
174
189
|
severity: ErrorSeverity.HIGH,
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
// A single attempt timing out is worth another try; an exhausted total
|
|
191
|
+
// budget is not — retrying it can only overshoot the caller's ceiling.
|
|
192
|
+
retriable: budget?.exhausted !== true,
|
|
193
|
+
context: { timeoutMs, ...(budget ?? {}) },
|
|
177
194
|
toolName,
|
|
178
195
|
serverId,
|
|
179
196
|
});
|
|
@@ -37,7 +37,7 @@ export declare function createMCPServerInfo(options: {
|
|
|
37
37
|
* Create MCPServerInfo for custom tool registration
|
|
38
38
|
* Specialized version with smart defaults for registerTool usage
|
|
39
39
|
*/
|
|
40
|
-
export declare function createCustomToolServerInfo(toolName: string, tool: MCPExecutableTool, timeoutMs?: number, maxRetries?: number): MCPServerInfo;
|
|
40
|
+
export declare function createCustomToolServerInfo(toolName: string, tool: MCPExecutableTool, timeoutMs?: number, maxRetries?: number, totalTimeoutMs?: number): MCPServerInfo;
|
|
41
41
|
/**
|
|
42
42
|
* Create MCPServerInfo for external servers
|
|
43
43
|
* Specialized version with smart defaults for external server usage
|
|
@@ -93,7 +93,7 @@ export function createMCPServerInfo(options) {
|
|
|
93
93
|
* Create MCPServerInfo for custom tool registration
|
|
94
94
|
* Specialized version with smart defaults for registerTool usage
|
|
95
95
|
*/
|
|
96
|
-
export function createCustomToolServerInfo(toolName, tool, timeoutMs, maxRetries) {
|
|
96
|
+
export function createCustomToolServerInfo(toolName, tool, timeoutMs, maxRetries, totalTimeoutMs) {
|
|
97
97
|
const serverInfo = createMCPServerInfo({
|
|
98
98
|
id: `custom-tool-${toolName}`,
|
|
99
99
|
name: toolName,
|
|
@@ -112,6 +112,9 @@ export function createCustomToolServerInfo(toolName, tool, timeoutMs, maxRetries
|
|
|
112
112
|
if (maxRetries !== undefined) {
|
|
113
113
|
serverInfo.metadata.toolMaxRetries = maxRetries;
|
|
114
114
|
}
|
|
115
|
+
if (totalTimeoutMs !== undefined) {
|
|
116
|
+
serverInfo.metadata.toolTotalTimeoutMs = totalTimeoutMs;
|
|
117
|
+
}
|
|
115
118
|
}
|
|
116
119
|
return serverInfo;
|
|
117
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.11.
|
|
3
|
+
"version": "12.11.3",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|