@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.
@@ -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
  }
@@ -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: options?.timeout ??
10015
- toolInfo?.tool?.timeoutMs ??
10016
- TOOL_TIMEOUTS.EXECUTION_BATCH_MS,
10017
- maxRetries: options?.maxRetries ??
10018
- toolInfo?.tool?.maxRetries ??
10019
- RETRY_ATTEMPTS.DEFAULT,
10020
- retryDelayMs: options?.retryDelayMs || RETRY_DELAYS.BASE_MS,
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 () => withTimeout(this.executeToolInternal(toolName, params, prepared.finalOptions, executionContext.hitlState), prepared.finalOptions.timeout, ErrorFactory.toolTimeout(toolName, prepared.finalOptions.timeout)), {
10067
- maxAttempts: prepared.finalOptions.maxRetries + 1,
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
- isRetriable: isRetriableError,
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})`, {
@@ -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
- static toolTimeout(toolName: string, timeoutMs: number, serverId?: string): NeuroLinkError;
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}' timed out after ${timeoutMs}ms`,
187
+ message: `Tool '${toolName}' ${scope}`,
173
188
  category: ErrorCategory.TIMEOUT,
174
189
  severity: ErrorSeverity.HIGH,
175
- retriable: true,
176
- context: { timeoutMs },
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.2",
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": {