@juspay/neurolink 12.26.0 → 12.26.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 +3 -3
- package/dist/browser/neurolink.min.js +119 -119
- package/dist/mcp/externalServerManager.js +16 -2
- package/dist/mcp/mcpCircuitBreaker.d.ts +24 -3
- package/dist/mcp/mcpCircuitBreaker.js +57 -18
- package/dist/mcp/toolDiscoveryService.js +20 -4
- package/dist/types/externalMcp.d.ts +7 -0
- package/package.json +3 -2
|
@@ -1702,11 +1702,25 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
1702
1702
|
instance.metrics.averageResponseTime =
|
|
1703
1703
|
totalTime / instance.metrics.totalToolCalls;
|
|
1704
1704
|
if (result.success) {
|
|
1705
|
-
|
|
1705
|
+
// A resolved `{ isError: true }` still comes back with success:true so
|
|
1706
|
+
// the caller receives the MCP error payload as-is, but it is a failed
|
|
1707
|
+
// call for telemetry: label mcp_tool_calls_total accordingly instead
|
|
1708
|
+
// of counting every resolved error as a success.
|
|
1709
|
+
const resolvedError = result.isErrorResult === true;
|
|
1710
|
+
if (resolvedError) {
|
|
1711
|
+
// Keep this instance's totalErrors (and the getStatistics() /
|
|
1712
|
+
// getExternalMCPStatistics() it feeds) consistent with the breaker,
|
|
1713
|
+
// TelemetryService and ToolDiscoveryService's failedCalls, all of
|
|
1714
|
+
// which already count a resolved isError result as a failure.
|
|
1715
|
+
instance.metrics.totalErrors++;
|
|
1716
|
+
}
|
|
1717
|
+
mcpLogger.debug(resolvedError
|
|
1718
|
+
? `[ExternalServerManager] Tool returned a resolved MCP error: ${toolName} on ${serverId}`
|
|
1719
|
+
: `[ExternalServerManager] Tool executed successfully: ${toolName} on ${serverId}`, {
|
|
1706
1720
|
duration,
|
|
1707
1721
|
});
|
|
1708
1722
|
try {
|
|
1709
|
-
TelemetryService.getInstance()?.recordMCPToolCall(toolName, duration,
|
|
1723
|
+
TelemetryService.getInstance()?.recordMCPToolCall(toolName, duration, !resolvedError);
|
|
1710
1724
|
}
|
|
1711
1725
|
catch {
|
|
1712
1726
|
/* telemetry should not break execution */
|
|
@@ -21,9 +21,30 @@ export declare class MCPCircuitBreaker extends EventEmitter {
|
|
|
21
21
|
private cleanupTimer?;
|
|
22
22
|
constructor(name: string, config?: Partial<CircuitBreakerConfig>);
|
|
23
23
|
/**
|
|
24
|
-
* Execute an operation with circuit breaker protection
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
* Execute an operation with circuit breaker protection.
|
|
25
|
+
*
|
|
26
|
+
* `operation` is handed a `recordResolvedFailure` callback so it can flag a
|
|
27
|
+
* *resolved* result as a logical failure without throwing. This matters for
|
|
28
|
+
* MCP tool calls: the client does not throw on a protocol error, it
|
|
29
|
+
* resolves with `{ isError: true, ... }`. Before this callback existed,
|
|
30
|
+
* such a call always fell through to the success path below — the breaker
|
|
31
|
+
* could never open for a tool that only ever "fails" by resolving an error
|
|
32
|
+
* result, and callers still need that resolved value returned as-is (not
|
|
33
|
+
* replaced by a thrown transport error). Call `recordResolvedFailure()`
|
|
34
|
+
* from inside `operation` when that resolved value represents a failure;
|
|
35
|
+
* the call is still counted as a normal, non-throwing success from the
|
|
36
|
+
* `Promise.race` below, so this callback is what tells `execute()`
|
|
37
|
+
* otherwise. Callers that never call it see no behavior change.
|
|
38
|
+
*/
|
|
39
|
+
execute<T>(operation: (recordResolvedFailure: (reason?: string) => void) => Promise<T>): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Shared bookkeeping for a failed outcome — a thrown error, or a resolved
|
|
42
|
+
* result the caller flagged via `recordResolvedFailure`. Records the call
|
|
43
|
+
* as a failure, emits `callFailure`, and runs the state transitions that
|
|
44
|
+
* used to live inline in the `catch` block: half-open failure immediately
|
|
45
|
+
* re-opens the circuit; closed checks the failure threshold.
|
|
46
|
+
*/
|
|
47
|
+
private recordFailureOutcome;
|
|
27
48
|
/**
|
|
28
49
|
* Record a call in the history
|
|
29
50
|
*/
|
|
@@ -46,7 +46,20 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
46
46
|
this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000);
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* Execute an operation with circuit breaker protection
|
|
49
|
+
* Execute an operation with circuit breaker protection.
|
|
50
|
+
*
|
|
51
|
+
* `operation` is handed a `recordResolvedFailure` callback so it can flag a
|
|
52
|
+
* *resolved* result as a logical failure without throwing. This matters for
|
|
53
|
+
* MCP tool calls: the client does not throw on a protocol error, it
|
|
54
|
+
* resolves with `{ isError: true, ... }`. Before this callback existed,
|
|
55
|
+
* such a call always fell through to the success path below — the breaker
|
|
56
|
+
* could never open for a tool that only ever "fails" by resolving an error
|
|
57
|
+
* result, and callers still need that resolved value returned as-is (not
|
|
58
|
+
* replaced by a thrown transport error). Call `recordResolvedFailure()`
|
|
59
|
+
* from inside `operation` when that resolved value represents a failure;
|
|
60
|
+
* the call is still counted as a normal, non-throwing success from the
|
|
61
|
+
* `Promise.race` below, so this callback is what tells `execute()`
|
|
62
|
+
* otherwise. Callers that never call it see no behavior change.
|
|
50
63
|
*/
|
|
51
64
|
async execute(operation) {
|
|
52
65
|
const startTime = Date.now();
|
|
@@ -91,11 +104,29 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
91
104
|
});
|
|
92
105
|
}
|
|
93
106
|
}
|
|
107
|
+
// resolvedFailureReason is set (via recordResolvedFailure, passed into
|
|
108
|
+
// operation() below) when operation() resolves normally but represents
|
|
109
|
+
// a logical failure — e.g. an MCP tool call that resolved
|
|
110
|
+
// { isError: true } instead of throwing. It is call-scoped (a local,
|
|
111
|
+
// not instance state), so concurrent execute() calls on the same
|
|
112
|
+
// breaker (the same tool invoked in parallel) can't cross-contaminate
|
|
113
|
+
// each other's outcome.
|
|
114
|
+
let resolvedFailureReason;
|
|
115
|
+
const recordResolvedFailure = (reason) => {
|
|
116
|
+
resolvedFailureReason = reason || "Operation resolved a failure result";
|
|
117
|
+
};
|
|
94
118
|
// Execute operation with timeout
|
|
95
119
|
const result = await Promise.race([
|
|
96
|
-
operation(),
|
|
120
|
+
operation(recordResolvedFailure),
|
|
97
121
|
this.timeoutPromise(this.config.operationTimeout),
|
|
98
122
|
]);
|
|
123
|
+
if (resolvedFailureReason !== undefined) {
|
|
124
|
+
// Same bookkeeping as a thrown error below, but the resolved value
|
|
125
|
+
// is still returned to the caller unchanged — no transport error is
|
|
126
|
+
// synthesized for a result the protocol itself resolved successfully.
|
|
127
|
+
this.recordFailureOutcome(resolvedFailureReason, Date.now() - startTime);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
99
130
|
// Record successful call
|
|
100
131
|
this.recordCall(true, Date.now() - startTime);
|
|
101
132
|
// Handle half-open success
|
|
@@ -111,26 +142,34 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
111
142
|
catch (error) {
|
|
112
143
|
// Record failed call
|
|
113
144
|
const duration = Date.now() - startTime;
|
|
114
|
-
this.recordCall(false, duration);
|
|
115
145
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
-
|
|
117
|
-
this.emit("callFailure", {
|
|
118
|
-
error: errorMessage,
|
|
119
|
-
duration,
|
|
120
|
-
timestamp: new Date(),
|
|
121
|
-
});
|
|
122
|
-
// Handle state transitions on failure
|
|
123
|
-
if (this.state === "half-open") {
|
|
124
|
-
// Failure in half-open immediately opens circuit
|
|
125
|
-
this.changeState("open", `Half-open test failed: ${errorMessage}`);
|
|
126
|
-
}
|
|
127
|
-
else if (this.state === "closed") {
|
|
128
|
-
// Check if we should open the circuit
|
|
129
|
-
this.checkFailureThreshold();
|
|
130
|
-
}
|
|
146
|
+
this.recordFailureOutcome(errorMessage, duration);
|
|
131
147
|
throw error;
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Shared bookkeeping for a failed outcome — a thrown error, or a resolved
|
|
152
|
+
* result the caller flagged via `recordResolvedFailure`. Records the call
|
|
153
|
+
* as a failure, emits `callFailure`, and runs the state transitions that
|
|
154
|
+
* used to live inline in the `catch` block: half-open failure immediately
|
|
155
|
+
* re-opens the circuit; closed checks the failure threshold.
|
|
156
|
+
*/
|
|
157
|
+
recordFailureOutcome(reason, duration) {
|
|
158
|
+
this.recordCall(false, duration);
|
|
159
|
+
this.emit("callFailure", {
|
|
160
|
+
error: reason,
|
|
161
|
+
duration,
|
|
162
|
+
timestamp: new Date(),
|
|
163
|
+
});
|
|
164
|
+
if (this.state === "half-open") {
|
|
165
|
+
// Failure in half-open immediately opens circuit
|
|
166
|
+
this.changeState("open", `Half-open test failed: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
else if (this.state === "closed") {
|
|
169
|
+
// Check if we should open the circuit
|
|
170
|
+
this.checkFailureThreshold();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
134
173
|
/**
|
|
135
174
|
* Record a call in the history
|
|
136
175
|
*/
|
|
@@ -435,8 +435,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
435
435
|
resetTimeout: 30000,
|
|
436
436
|
operationTimeout: effectiveTimeout,
|
|
437
437
|
});
|
|
438
|
-
// Execute tool with circuit breaker protection
|
|
439
|
-
|
|
438
|
+
// Execute tool with circuit breaker protection. `recordResolvedFailure`
|
|
439
|
+
// lets the callback below tell the breaker that a *resolved* MCP
|
|
440
|
+
// isError result is a failure — the MCP client does not throw on a
|
|
441
|
+
// protocol error, so without this the breaker would only ever see a
|
|
442
|
+
// clean resolve and could never open for a tool that fails this way.
|
|
443
|
+
let isErrorResultDetected = false;
|
|
444
|
+
const result = await circuitBreaker.execute(async (recordResolvedFailure) => {
|
|
440
445
|
return mcpTracer.startActiveSpan("neurolink.mcp.callTool", {
|
|
441
446
|
kind: SpanKind.CLIENT,
|
|
442
447
|
attributes: {
|
|
@@ -479,6 +484,11 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
479
484
|
code: SpanStatusCode.ERROR,
|
|
480
485
|
message: errorText || `Tool ${toolName} returned isError`,
|
|
481
486
|
});
|
|
487
|
+
// NLK: count a resolved isError result as a breaker failure
|
|
488
|
+
// and label completion telemetry as error below — without
|
|
489
|
+
// throwing, so the protocol result is still returned as-is.
|
|
490
|
+
isErrorResultDetected = true;
|
|
491
|
+
recordResolvedFailure(errorText || `Tool ${toolName} returned isError`);
|
|
482
492
|
}
|
|
483
493
|
else {
|
|
484
494
|
callSpan.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -528,8 +538,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
528
538
|
});
|
|
529
539
|
});
|
|
530
540
|
const duration = Date.now() - startTime;
|
|
531
|
-
// Update tool statistics
|
|
532
|
-
|
|
541
|
+
// Update tool statistics. A resolved isError result is a failed call
|
|
542
|
+
// for completion telemetry purposes even though the wrapper below
|
|
543
|
+
// still returns success:true / data:result unchanged — flipping
|
|
544
|
+
// `success` here would make externalServerManager.executeTool() throw
|
|
545
|
+
// instead of returning the resolved MCP error, which is exactly the
|
|
546
|
+
// "thrown transport error" this fix must not introduce.
|
|
547
|
+
this.updateToolStats(toolKey, !isErrorResultDetected, duration);
|
|
533
548
|
// Validate output if requested
|
|
534
549
|
if (options.validateOutput !== false) {
|
|
535
550
|
this.validateToolOutput(result);
|
|
@@ -541,6 +556,7 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
541
556
|
return {
|
|
542
557
|
success: true,
|
|
543
558
|
data: result,
|
|
559
|
+
isErrorResult: isErrorResultDetected,
|
|
544
560
|
duration,
|
|
545
561
|
metadata: {
|
|
546
562
|
toolName,
|
|
@@ -234,6 +234,13 @@ export type ExternalMCPToolResult = {
|
|
|
234
234
|
data?: unknown;
|
|
235
235
|
/** Error message if failed */
|
|
236
236
|
error?: string;
|
|
237
|
+
/**
|
|
238
|
+
* True when the call completed at the transport level but the MCP result
|
|
239
|
+
* itself is `{ isError: true }`. `success` stays true for such results so
|
|
240
|
+
* the resolved MCP error payload still reaches the caller unchanged; this
|
|
241
|
+
* flag is what lets stats and telemetry count the call as a failure.
|
|
242
|
+
*/
|
|
243
|
+
isErrorResult?: boolean;
|
|
237
244
|
/** Execution duration in milliseconds */
|
|
238
245
|
duration: number;
|
|
239
246
|
/** Tool execution metadata */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.26.
|
|
3
|
+
"version": "12.26.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"// CI tier — fast, no live AI calls, safe for every commit (test:unit; also see the separate provider-safety-net CI job, which runs build + test:providers-mocked + test:provider-structure + test:error-classifier-contract on every PR)": "",
|
|
182
182
|
"test:tool-routing": "pnpm exec tsx test/continuous-test-suite-tool-routing.ts",
|
|
183
183
|
"test:tool-routing-semantic": "pnpm exec tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
184
|
-
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:mcp-direct-name-repair && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
184
|
+
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:mcp-direct-name-repair && pnpm run test:mcp-breaker-resolved-errors && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
185
185
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit; test:matrix, a different suite covering the full provider capability matrix, runs nightly via .github/workflows/live-matrix.yml — test:providers itself is still only wired into test:live, not any GitHub Actions workflow)": "",
|
|
186
186
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
187
187
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run (not wired into any GitHub Actions workflow as of this comment; run manually or add to live-matrix.yml if nightly coverage is needed)": "",
|
|
@@ -243,6 +243,7 @@
|
|
|
243
243
|
"test:multimodal": "pnpm run test:file-formats && pnpm run test:multimodal:sdk",
|
|
244
244
|
"test:multimodal:sdk": "pnpm exec tsx test/continuous-test-suite-multimodal-sdk.ts",
|
|
245
245
|
"test:mcp-result-cache": "pnpm exec tsx test/continuous-test-suite-mcp-result-cache.ts",
|
|
246
|
+
"test:mcp-breaker-resolved-errors": "pnpm exec tsx test/continuous-test-suite-mcp-breaker-resolved-errors.ts",
|
|
246
247
|
"test:archive:security": "pnpm exec tsx test/continuous-test-suite-archive-security.ts",
|
|
247
248
|
"test:office:security": "pnpm exec tsx test/continuous-test-suite-office-security.ts",
|
|
248
249
|
"test:vision-image-format": "pnpm exec tsx test/continuous-test-suite-vision-image-format.ts",
|