@juspay/neurolink 12.21.1 → 12.22.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 +6 -4
- package/dist/browser/neurolink.min.js +398 -398
- package/dist/core/baseProvider.d.ts +18 -0
- package/dist/core/baseProvider.js +22 -0
- package/dist/core/modules/MessageBuilder.js +16 -0
- package/dist/factories/providerFactory.d.ts +20 -0
- package/dist/factories/providerFactory.js +100 -19
- package/dist/models/modelRegistry.d.ts +25 -0
- package/dist/models/modelRegistry.js +39 -0
- package/dist/providers/googleAiStudio/client.js +382 -50
- package/dist/providers/googleNativeGemini3/utils.d.ts +22 -0
- package/dist/providers/googleNativeGemini3/utils.js +24 -0
- package/dist/providers/googleVertex/client.d.ts +3 -12
- package/dist/providers/googleVertex/client.js +15 -34
- package/dist/providers/openaiChatCompletionsClient.js +8 -1
- package/dist/utils/stringDistance.d.ts +54 -0
- package/dist/utils/stringDistance.js +133 -0
- package/dist/utils/toolCallRepair.js +54 -55
- package/package.json +1 -1
|
@@ -250,6 +250,28 @@ export declare function buildTurnStalledMessage(stallTimeoutMs: number, toolCall
|
|
|
250
250
|
* short-circuit — stopReason "aborted").
|
|
251
251
|
*/
|
|
252
252
|
export declare function buildAbortedTurnMessage(toolCallCount: number): string;
|
|
253
|
+
/**
|
|
254
|
+
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
255
|
+
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
256
|
+
* only — time/stall/abort exits must never claim a step limit was reached
|
|
257
|
+
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
258
|
+
* abort wearing the cap message).
|
|
259
|
+
*
|
|
260
|
+
* Takes plain booleans rather than a turn clock so a loop whose wall-clock
|
|
261
|
+
* deadline is owned by something other than `createTurnClock` — AI Studio's
|
|
262
|
+
* pre-existing per-request timeout controller — can report through the same
|
|
263
|
+
* builders instead of growing a second set.
|
|
264
|
+
*/
|
|
265
|
+
export declare function buildLoopExitMessage(params: {
|
|
266
|
+
timedOut: boolean;
|
|
267
|
+
stalled: boolean;
|
|
268
|
+
/** Turn duration so far, for the time-limit message. */
|
|
269
|
+
elapsedMs: number;
|
|
270
|
+
wasAborted: boolean;
|
|
271
|
+
stallTimeoutMs?: number;
|
|
272
|
+
maxSteps: number;
|
|
273
|
+
toolCallCount: number;
|
|
274
|
+
}): string;
|
|
253
275
|
/**
|
|
254
276
|
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
255
277
|
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
@@ -922,6 +922,30 @@ export function buildAbortedTurnMessage(toolCallCount) {
|
|
|
922
922
|
: "";
|
|
923
923
|
return `This turn was stopped before I could finish.${calls}`;
|
|
924
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
927
|
+
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
928
|
+
* only — time/stall/abort exits must never claim a step limit was reached
|
|
929
|
+
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
930
|
+
* abort wearing the cap message).
|
|
931
|
+
*
|
|
932
|
+
* Takes plain booleans rather than a turn clock so a loop whose wall-clock
|
|
933
|
+
* deadline is owned by something other than `createTurnClock` — AI Studio's
|
|
934
|
+
* pre-existing per-request timeout controller — can report through the same
|
|
935
|
+
* builders instead of growing a second set.
|
|
936
|
+
*/
|
|
937
|
+
export function buildLoopExitMessage(params) {
|
|
938
|
+
if (params.timedOut) {
|
|
939
|
+
return buildTurnTimeoutMessage(params.elapsedMs, params.toolCallCount);
|
|
940
|
+
}
|
|
941
|
+
if (params.stalled) {
|
|
942
|
+
return buildTurnStalledMessage(params.stallTimeoutMs ?? 0, params.toolCallCount);
|
|
943
|
+
}
|
|
944
|
+
if (params.wasAborted) {
|
|
945
|
+
return buildAbortedTurnMessage(params.toolCallCount);
|
|
946
|
+
}
|
|
947
|
+
return buildToolLoopCapMessage(params.maxSteps, params.toolCallCount);
|
|
948
|
+
}
|
|
925
949
|
/**
|
|
926
950
|
* Wrap-up nudge injected when the remaining turn time drops inside the
|
|
927
951
|
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
|
|
@@ -300,18 +300,9 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
300
300
|
*/
|
|
301
301
|
private emitGenerationEnd;
|
|
302
302
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* malformed-call retries — are observable by consumers' own pipelines.
|
|
307
|
-
*/
|
|
308
|
-
private emitTurnEvent;
|
|
309
|
-
/**
|
|
310
|
-
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
311
|
-
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
312
|
-
* only — time/stall/abort exits must never claim a step limit was reached
|
|
313
|
-
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
314
|
-
* abort wearing the cap message).
|
|
303
|
+
* Turn-clock-shaped adapter over the shared {@link buildLoopExitMessage}.
|
|
304
|
+
* Every call site in this file holds a clock, so the mapping lives here
|
|
305
|
+
* rather than at ten call sites; the decision itself is the shared one.
|
|
315
306
|
*/
|
|
316
307
|
private buildLoopExitMessage;
|
|
317
308
|
protected formatProviderError(error: unknown): Error;
|
|
@@ -32,7 +32,7 @@ import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } fr
|
|
|
32
32
|
import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
|
|
33
33
|
import { TimeoutError, withTimeout } from "../../utils/async/index.js";
|
|
34
34
|
import { parseTimeout } from "../../utils/timeout.js";
|
|
35
|
-
import { appendStepText,
|
|
35
|
+
import { appendStepText, buildContextCapMessage, buildDedupedEngineTools, buildLoopExitMessage, buildToolLoopCapMessage, buildWrapupNudgeText, createContextGuard, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
|
|
36
36
|
import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
|
|
37
37
|
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
38
38
|
import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
|
|
@@ -5697,41 +5697,22 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5697
5697
|
}
|
|
5698
5698
|
}
|
|
5699
5699
|
/**
|
|
5700
|
-
*
|
|
5701
|
-
*
|
|
5702
|
-
*
|
|
5703
|
-
* malformed-call retries — are observable by consumers' own pipelines.
|
|
5704
|
-
*/
|
|
5705
|
-
emitTurnEvent(payload) {
|
|
5706
|
-
try {
|
|
5707
|
-
this.neurolink?.getEventEmitter()?.emit("turn:lifecycle", {
|
|
5708
|
-
provider: this.providerName,
|
|
5709
|
-
timestamp: Date.now(),
|
|
5710
|
-
...payload,
|
|
5711
|
-
});
|
|
5712
|
-
}
|
|
5713
|
-
catch {
|
|
5714
|
-
/* listener errors are non-fatal */
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
/**
|
|
5718
|
-
* Pick the honest terminal message for a native-loop exit by its ACTUAL
|
|
5719
|
-
* cause. The step-cap text is the fallback for genuine budget exhaustion
|
|
5720
|
-
* only — time/stall/abort exits must never claim a step limit was reached
|
|
5721
|
-
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
|
|
5722
|
-
* abort wearing the cap message).
|
|
5700
|
+
* Turn-clock-shaped adapter over the shared {@link buildLoopExitMessage}.
|
|
5701
|
+
* Every call site in this file holds a clock, so the mapping lives here
|
|
5702
|
+
* rather than at ten call sites; the decision itself is the shared one.
|
|
5723
5703
|
*/
|
|
5724
5704
|
buildLoopExitMessage(params) {
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5705
|
+
return buildLoopExitMessage({
|
|
5706
|
+
timedOut: params.turnClock.timedOut,
|
|
5707
|
+
stalled: params.turnClock.stalled,
|
|
5708
|
+
elapsedMs: params.turnClock.elapsedMs(),
|
|
5709
|
+
wasAborted: params.wasAborted,
|
|
5710
|
+
...(params.stallTimeoutMs !== undefined
|
|
5711
|
+
? { stallTimeoutMs: params.stallTimeoutMs }
|
|
5712
|
+
: {}),
|
|
5713
|
+
maxSteps: params.maxSteps,
|
|
5714
|
+
toolCallCount: params.toolCallCount,
|
|
5715
|
+
});
|
|
5735
5716
|
}
|
|
5736
5717
|
formatProviderError(error) {
|
|
5737
5718
|
const errorRecord = error;
|
|
@@ -193,7 +193,14 @@ export const convertContentForOpenAI = (content) => {
|
|
|
193
193
|
const data = part.image ??
|
|
194
194
|
part.data ??
|
|
195
195
|
part.url;
|
|
196
|
-
|
|
196
|
+
// ImagePart carries the resolved format as `mediaType` (MessageBuilder
|
|
197
|
+
// canonicalizes to it); `mimeType` is accepted for parts that bypass
|
|
198
|
+
// that normalization. Without this, a raw base64 payload (as opposed to
|
|
199
|
+
// an already-labelled data: URI) always fell back to imageDataToURL's
|
|
200
|
+
// hardcoded "image/png" default, mislabeling real JPEG/WebP/etc bytes.
|
|
201
|
+
const mediaType = part.mediaType ??
|
|
202
|
+
part.mimeType;
|
|
203
|
+
const url = imageDataToURL(data, mediaType);
|
|
197
204
|
if (url) {
|
|
198
205
|
out.push({ type: "image_url", image_url: { url } });
|
|
199
206
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared string-distance helpers for "did you mean?" suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from toolCallRepair.ts, which had the only implementation, so
|
|
5
|
+
* provider-name validation can reuse it rather than carry a second copy.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Longest string the distance primitive will compare.
|
|
9
|
+
*
|
|
10
|
+
* Exported so every call site can apply the same limit *before* calling
|
|
11
|
+
* `levenshtein` — skipping an over-limit operand entirely, rather than
|
|
12
|
+
* comparing a truncated prefix of it. Truncating silently merged any two
|
|
13
|
+
* strings that happened to share their first `MAX_COMPARABLE_LENGTH`
|
|
14
|
+
* characters into distance 0, which let a direct MCP tool-name lookup
|
|
15
|
+
* (`resolveToolName`) resolve to the wrong tool for a long, colliding name.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_COMPARABLE_LENGTH = 128;
|
|
18
|
+
/**
|
|
19
|
+
* Compute Levenshtein edit distance between two strings.
|
|
20
|
+
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
21
|
+
*
|
|
22
|
+
* Throws rather than truncating when either input exceeds
|
|
23
|
+
* `MAX_COMPARABLE_LENGTH`: silently comparing truncated prefixes produces a
|
|
24
|
+
* *wrong* distance (two distinct strings sharing a long prefix collapse to
|
|
25
|
+
* distance 0) instead of no distance, which is worse than refusing outright.
|
|
26
|
+
* Callers that want fuzzy matching over long input must apply the length
|
|
27
|
+
* check themselves and skip the comparison — see `suggestClosest` below and
|
|
28
|
+
* `resolveToolName`/`rankToolNameCandidates` in toolCallRepair.ts.
|
|
29
|
+
*/
|
|
30
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Candidates within `maxDistance` edits of `input`, closest first.
|
|
33
|
+
*
|
|
34
|
+
* Ties break on the candidate's own order in `candidates`, so a caller that
|
|
35
|
+
* passes canonical names before aliases gets the canonical one suggested.
|
|
36
|
+
* Comparison is case-insensitive; the returned strings keep their original
|
|
37
|
+
* casing so they can be pasted back verbatim.
|
|
38
|
+
*
|
|
39
|
+
* `input` longer than `MAX_COMPARABLE_LENGTH` is rejected outright (no
|
|
40
|
+
* suggestions), and any individual candidate over the same limit is skipped
|
|
41
|
+
* rather than compared — never truncated. Truncating either side risks
|
|
42
|
+
* exactly the false distance-0 match `levenshtein`'s guard rejects: two
|
|
43
|
+
* distinct strings sharing a long-enough prefix would otherwise look
|
|
44
|
+
* identical and "suggest" the wrong one.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The string the user actually typed
|
|
47
|
+
* @param candidates - Valid values, most-preferred first
|
|
48
|
+
* @param options.maxDistance - Edit budget (default 3)
|
|
49
|
+
* @param options.limit - Most suggestions to return (default 3)
|
|
50
|
+
*/
|
|
51
|
+
export declare function suggestClosest(input: string, candidates: readonly string[], options?: {
|
|
52
|
+
maxDistance?: number;
|
|
53
|
+
limit?: number;
|
|
54
|
+
}): string[];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared string-distance helpers for "did you mean?" suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from toolCallRepair.ts, which had the only implementation, so
|
|
5
|
+
* provider-name validation can reuse it rather than carry a second copy.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Longest string the distance primitive will compare.
|
|
9
|
+
*
|
|
10
|
+
* Exported so every call site can apply the same limit *before* calling
|
|
11
|
+
* `levenshtein` — skipping an over-limit operand entirely, rather than
|
|
12
|
+
* comparing a truncated prefix of it. Truncating silently merged any two
|
|
13
|
+
* strings that happened to share their first `MAX_COMPARABLE_LENGTH`
|
|
14
|
+
* characters into distance 0, which let a direct MCP tool-name lookup
|
|
15
|
+
* (`resolveToolName`) resolve to the wrong tool for a long, colliding name.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_COMPARABLE_LENGTH = 128;
|
|
18
|
+
const OVER_LIMIT_MESSAGE = `levenshtein: input exceeds MAX_COMPARABLE_LENGTH (${MAX_COMPARABLE_LENGTH}); callers must skip over-limit operands rather than truncate them`;
|
|
19
|
+
/**
|
|
20
|
+
* Compute Levenshtein edit distance between two strings.
|
|
21
|
+
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
22
|
+
*
|
|
23
|
+
* Throws rather than truncating when either input exceeds
|
|
24
|
+
* `MAX_COMPARABLE_LENGTH`: silently comparing truncated prefixes produces a
|
|
25
|
+
* *wrong* distance (two distinct strings sharing a long prefix collapse to
|
|
26
|
+
* distance 0) instead of no distance, which is worse than refusing outright.
|
|
27
|
+
* Callers that want fuzzy matching over long input must apply the length
|
|
28
|
+
* check themselves and skip the comparison — see `suggestClosest` below and
|
|
29
|
+
* `resolveToolName`/`rankToolNameCandidates` in toolCallRepair.ts.
|
|
30
|
+
*/
|
|
31
|
+
export function levenshtein(a, b) {
|
|
32
|
+
// Names reach this from request bodies and model output, where the static
|
|
33
|
+
// type is no runtime guarantee: a parsed-JSON object with a forged `length`
|
|
34
|
+
// must never size the matrix below. This `typeof` test is what CodeQL's
|
|
35
|
+
// js/loop-bound-injection recognises as ruling such an object out; a length
|
|
36
|
+
// comparison alone is not.
|
|
37
|
+
if (typeof a !== "string") {
|
|
38
|
+
throw new TypeError("levenshtein: operands must be strings");
|
|
39
|
+
}
|
|
40
|
+
if (typeof b !== "string") {
|
|
41
|
+
throw new TypeError("levenshtein: operands must be strings");
|
|
42
|
+
}
|
|
43
|
+
// Ahead of every return, so the limit holds for all inputs, including equal
|
|
44
|
+
// strings and an empty operand.
|
|
45
|
+
if (a.length > MAX_COMPARABLE_LENGTH) {
|
|
46
|
+
throw new RangeError(OVER_LIMIT_MESSAGE);
|
|
47
|
+
}
|
|
48
|
+
if (b.length > MAX_COMPARABLE_LENGTH) {
|
|
49
|
+
throw new RangeError(OVER_LIMIT_MESSAGE);
|
|
50
|
+
}
|
|
51
|
+
if (a === b) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (a.length === 0) {
|
|
55
|
+
return b.length;
|
|
56
|
+
}
|
|
57
|
+
if (b.length === 0) {
|
|
58
|
+
return a.length;
|
|
59
|
+
}
|
|
60
|
+
// Use shorter string as column to minimize space
|
|
61
|
+
if (a.length > b.length) {
|
|
62
|
+
[a, b] = [b, a];
|
|
63
|
+
}
|
|
64
|
+
const aLen = a.length;
|
|
65
|
+
const bLen = b.length;
|
|
66
|
+
let prev = new Array(aLen + 1);
|
|
67
|
+
let curr = new Array(aLen + 1);
|
|
68
|
+
for (let i = 0; i <= aLen; i++) {
|
|
69
|
+
prev[i] = i;
|
|
70
|
+
}
|
|
71
|
+
for (let j = 1; j <= bLen; j++) {
|
|
72
|
+
curr[0] = j;
|
|
73
|
+
for (let i = 1; i <= aLen; i++) {
|
|
74
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
75
|
+
curr[i] = Math.min(prev[i] + 1, // deletion
|
|
76
|
+
curr[i - 1] + 1, // insertion
|
|
77
|
+
prev[i - 1] + cost);
|
|
78
|
+
}
|
|
79
|
+
[prev, curr] = [curr, prev];
|
|
80
|
+
}
|
|
81
|
+
return prev[aLen];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Candidates within `maxDistance` edits of `input`, closest first.
|
|
85
|
+
*
|
|
86
|
+
* Ties break on the candidate's own order in `candidates`, so a caller that
|
|
87
|
+
* passes canonical names before aliases gets the canonical one suggested.
|
|
88
|
+
* Comparison is case-insensitive; the returned strings keep their original
|
|
89
|
+
* casing so they can be pasted back verbatim.
|
|
90
|
+
*
|
|
91
|
+
* `input` longer than `MAX_COMPARABLE_LENGTH` is rejected outright (no
|
|
92
|
+
* suggestions), and any individual candidate over the same limit is skipped
|
|
93
|
+
* rather than compared — never truncated. Truncating either side risks
|
|
94
|
+
* exactly the false distance-0 match `levenshtein`'s guard rejects: two
|
|
95
|
+
* distinct strings sharing a long-enough prefix would otherwise look
|
|
96
|
+
* identical and "suggest" the wrong one.
|
|
97
|
+
*
|
|
98
|
+
* @param input - The string the user actually typed
|
|
99
|
+
* @param candidates - Valid values, most-preferred first
|
|
100
|
+
* @param options.maxDistance - Edit budget (default 3)
|
|
101
|
+
* @param options.limit - Most suggestions to return (default 3)
|
|
102
|
+
*/
|
|
103
|
+
export function suggestClosest(input, candidates, options) {
|
|
104
|
+
const maxDistance = options?.maxDistance ?? 3;
|
|
105
|
+
const limit = options?.limit ?? 3;
|
|
106
|
+
// Limits apply to the lowercased strings, since those are what get compared
|
|
107
|
+
// and lowercasing can lengthen a string ("İ" becomes two code units).
|
|
108
|
+
const needle = input.toLowerCase();
|
|
109
|
+
if (needle.length > MAX_COMPARABLE_LENGTH) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const scored = [];
|
|
113
|
+
candidates.forEach((candidate, order) => {
|
|
114
|
+
const lowered = candidate.toLowerCase();
|
|
115
|
+
// Two strings whose lengths differ by more than the edit budget cannot be
|
|
116
|
+
// within it, so this rules most candidates out without building a matrix
|
|
117
|
+
// for them.
|
|
118
|
+
if (Math.abs(lowered.length - needle.length) > maxDistance) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (lowered.length > MAX_COMPARABLE_LENGTH) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const distance = levenshtein(needle, lowered);
|
|
125
|
+
if (distance <= maxDistance) {
|
|
126
|
+
scored.push({ candidate, order, distance });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return scored
|
|
130
|
+
.sort((a, b) => a.distance - b.distance || a.order - b.order)
|
|
131
|
+
.slice(0, limit)
|
|
132
|
+
.map((entry) => entry.candidate);
|
|
133
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "./logger.js";
|
|
2
|
+
import { levenshtein, MAX_COMPARABLE_LENGTH } from "./stringDistance.js";
|
|
2
3
|
/**
|
|
3
4
|
* Create an `experimental_repairToolCall` handler for streamText/generateText.
|
|
4
5
|
* Fully dynamic — reads the tool schema at repair time, no configuration needed.
|
|
@@ -58,15 +59,27 @@ export function resolveToolName(calledName, availableTools) {
|
|
|
58
59
|
}
|
|
59
60
|
// 3. Levenshtein distance — accept if normalized distance < 0.3
|
|
60
61
|
// Compare by normalized score (not raw edits) so length differences don't skew selection.
|
|
62
|
+
// `levenshtein` throws rather than truncates past MAX_COMPARABLE_LENGTH (see
|
|
63
|
+
// stringDistance.ts), so an over-limit name is skipped here instead of
|
|
64
|
+
// compared on a truncated prefix — a called name and a tool name that
|
|
65
|
+
// merely share a long-enough prefix must never tie at distance 0 and
|
|
66
|
+
// resolve to the wrong tool. The guard checks the lowercased operands, since
|
|
67
|
+
// those are what get compared and lowercasing can lengthen a string ("İ").
|
|
61
68
|
let bestMatch = null;
|
|
62
69
|
let bestNormalized = Infinity;
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
if (calledLower.length <= MAX_COMPARABLE_LENGTH) {
|
|
71
|
+
for (const t of availableTools) {
|
|
72
|
+
const tLower = t.toLowerCase();
|
|
73
|
+
if (tLower.length > MAX_COMPARABLE_LENGTH) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const dist = levenshtein(calledLower, tLower);
|
|
77
|
+
const maxLen = Math.max(calledName.length, t.length);
|
|
78
|
+
const normalized = maxLen === 0 ? 0 : dist / maxLen;
|
|
79
|
+
if (normalized < 0.3 && normalized < bestNormalized) {
|
|
80
|
+
bestNormalized = normalized;
|
|
81
|
+
bestMatch = t;
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
}
|
|
72
85
|
if (bestMatch) {
|
|
@@ -85,10 +98,27 @@ export function resolveToolName(calledName, availableTools) {
|
|
|
85
98
|
*/
|
|
86
99
|
export function rankToolNameCandidates(calledName, availableTools, limit = 5) {
|
|
87
100
|
const calledLower = calledName.toLowerCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
const calledWithinLimit = calledLower.length <= MAX_COMPARABLE_LENGTH;
|
|
102
|
+
// `levenshtein` throws past MAX_COMPARABLE_LENGTH instead of truncating, so
|
|
103
|
+
// an over-limit operand is scored as "furthest away" rather than compared
|
|
104
|
+
// on a truncated (and potentially falsely-tied) prefix. This still only
|
|
105
|
+
// orders the list — no candidate is dropped — matching this function's
|
|
106
|
+
// "no accept/reject judgment" contract.
|
|
107
|
+
const distanceTo = (name) => {
|
|
108
|
+
const nameLower = name.toLowerCase();
|
|
109
|
+
if (!calledWithinLimit || nameLower.length > MAX_COMPARABLE_LENGTH) {
|
|
110
|
+
return Number.POSITIVE_INFINITY;
|
|
111
|
+
}
|
|
112
|
+
return levenshtein(calledLower, nameLower);
|
|
113
|
+
};
|
|
114
|
+
// Precompute once per tool (not per comparison), and compare with an
|
|
115
|
+
// explicit equality check — two Infinity distances (both over-limit) would
|
|
116
|
+
// otherwise subtract to NaN, an invalid sort-comparator result.
|
|
117
|
+
return availableTools
|
|
118
|
+
.map((name) => ({ name, distance: distanceTo(name) }))
|
|
119
|
+
.sort((a, b) => (a.distance === b.distance ? 0 : a.distance - b.distance))
|
|
120
|
+
.slice(0, limit)
|
|
121
|
+
.map((entry) => entry.name);
|
|
92
122
|
}
|
|
93
123
|
/**
|
|
94
124
|
* Thrown at a direct MCP execution boundary (`NeuroLink.executeExternalMCPTool`)
|
|
@@ -220,14 +250,21 @@ function findMatchingKey(inputKey, schemaKeys) {
|
|
|
220
250
|
if (ciMatch) {
|
|
221
251
|
return ciMatch;
|
|
222
252
|
}
|
|
223
|
-
// Levenshtein — threshold ≤2 edits
|
|
253
|
+
// Levenshtein — threshold ≤2 edits. Same over-limit guard as
|
|
254
|
+
// resolveToolName above: skip rather than let levenshtein() truncate.
|
|
224
255
|
let best = null;
|
|
225
256
|
let bestDist = Infinity;
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
257
|
+
if (inputLower.length <= MAX_COMPARABLE_LENGTH) {
|
|
258
|
+
for (const k of schemaKeys) {
|
|
259
|
+
const kLower = k.toLowerCase();
|
|
260
|
+
if (kLower.length > MAX_COMPARABLE_LENGTH) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const dist = levenshtein(inputLower, kLower);
|
|
264
|
+
if (dist <= 2 && dist < bestDist) {
|
|
265
|
+
bestDist = dist;
|
|
266
|
+
best = k;
|
|
267
|
+
}
|
|
231
268
|
}
|
|
232
269
|
}
|
|
233
270
|
return best;
|
|
@@ -308,41 +345,3 @@ export function coerceType(value, propSchema) {
|
|
|
308
345
|
}
|
|
309
346
|
return value;
|
|
310
347
|
}
|
|
311
|
-
// ─── Levenshtein Distance ──────────────────────────────────────────
|
|
312
|
-
/**
|
|
313
|
-
* Compute Levenshtein edit distance between two strings.
|
|
314
|
-
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
315
|
-
*/
|
|
316
|
-
function levenshtein(a, b) {
|
|
317
|
-
if (a === b) {
|
|
318
|
-
return 0;
|
|
319
|
-
}
|
|
320
|
-
if (a.length === 0) {
|
|
321
|
-
return b.length;
|
|
322
|
-
}
|
|
323
|
-
if (b.length === 0) {
|
|
324
|
-
return a.length;
|
|
325
|
-
}
|
|
326
|
-
// Use shorter string as column to minimize space
|
|
327
|
-
if (a.length > b.length) {
|
|
328
|
-
[a, b] = [b, a];
|
|
329
|
-
}
|
|
330
|
-
const aLen = a.length;
|
|
331
|
-
const bLen = b.length;
|
|
332
|
-
let prev = new Array(aLen + 1);
|
|
333
|
-
let curr = new Array(aLen + 1);
|
|
334
|
-
for (let i = 0; i <= aLen; i++) {
|
|
335
|
-
prev[i] = i;
|
|
336
|
-
}
|
|
337
|
-
for (let j = 1; j <= bLen; j++) {
|
|
338
|
-
curr[0] = j;
|
|
339
|
-
for (let i = 1; i <= aLen; i++) {
|
|
340
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
341
|
-
curr[i] = Math.min(prev[i] + 1, // deletion
|
|
342
|
-
curr[i - 1] + 1, // insertion
|
|
343
|
-
prev[i - 1] + cost);
|
|
344
|
-
}
|
|
345
|
-
[prev, curr] = [curr, prev];
|
|
346
|
-
}
|
|
347
|
-
return prev[aLen];
|
|
348
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.22.0",
|
|
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": {
|