@juspay/neurolink 12.21.2 → 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.
@@ -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
- * Emit a `turn:lifecycle` event on the SDK emitter (alongside
304
- * tool:start/tool:end) so loop conditions that previously only reached
305
- * process logs — step-cap, time-limit, stall, abort, tool timeouts,
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, buildAbortedTurnMessage, buildContextCapMessage, buildDedupedEngineTools, buildToolLoopCapMessage, buildTurnStalledMessage, buildTurnTimeoutMessage, buildWrapupNudgeText, createContextGuard, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
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
- * Emit a `turn:lifecycle` event on the SDK emitter (alongside
5701
- * tool:start/tool:end) so loop conditions that previously only reached
5702
- * process logs — step-cap, time-limit, stall, abort, tool timeouts,
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
- if (params.turnClock.timedOut) {
5726
- return buildTurnTimeoutMessage(params.turnClock.elapsedMs(), params.toolCallCount);
5727
- }
5728
- if (params.turnClock.stalled) {
5729
- return buildTurnStalledMessage(params.stallTimeoutMs ?? 0, params.toolCallCount);
5730
- }
5731
- if (params.wasAborted) {
5732
- return buildAbortedTurnMessage(params.toolCallCount);
5733
- }
5734
- return buildToolLoopCapMessage(params.maxSteps, params.toolCallCount);
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;
@@ -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
- for (const t of availableTools) {
64
- const dist = levenshtein(calledLower, t.toLowerCase());
65
- const maxLen = Math.max(calledName.length, t.length);
66
- const normalized = maxLen === 0 ? 0 : dist / maxLen;
67
- if (normalized < 0.3 && normalized < bestNormalized) {
68
- bestNormalized = normalized;
69
- bestMatch = t;
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
- return [...availableTools]
89
- .sort((a, b) => levenshtein(calledLower, a.toLowerCase()) -
90
- levenshtein(calledLower, b.toLowerCase()))
91
- .slice(0, limit);
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
- for (const k of schemaKeys) {
227
- const dist = levenshtein(inputLower, k.toLowerCase());
228
- if (dist <= 2 && dist < bestDist) {
229
- bestDist = dist;
230
- best = k;
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.21.2",
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": {