@juspay/neurolink 11.9.0 → 11.11.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.
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
25
25
  import { stringifyAnthropicToolOutput } from "./toolOutput.js";
26
+ import { stringifyFinalResultInput } from "./structuredOutput.js";
26
27
  /** Map Anthropic's stop_reason onto the unified finish reason. */
27
28
  function mapAnthropicFinishReason(rawStopReason, hadToolCallsAtCap) {
28
29
  switch (rawStopReason) {
@@ -271,10 +272,19 @@ export function createAnthropicLoopAdapter(config) {
271
272
  // never dispatched, never counted against the breaker, and never shows
272
273
  // up as a tool execution.
273
274
  const terminal = config.finalResultToolName
274
- ? allCalls.find((call) => call.name === config.finalResultToolName)
275
+ ? [...toolByIndex.values()].find((pending) => pending.name === config.finalResultToolName)
275
276
  : undefined;
276
277
  const toolCalls = terminal ? [] : allCalls;
277
- const finalText = terminal ? JSON.stringify(terminal.args) : text;
278
+ // The RAW accumulated input_json, never the parsed-then-restringified
279
+ // args. `parseArgs` yields {} for a payload the token cap cut off
280
+ // mid-string, so re-stringifying would turn a truncated answer into
281
+ // "{}" and lose it outright. `stringifyFinalResultInput` canonicalizes
282
+ // when the JSON parses and returns it verbatim when it does not, which
283
+ // is what lets the caller's coercion layer repair a partial payload
284
+ // into a partial object instead of nothing.
285
+ const finalText = terminal
286
+ ? stringifyFinalResultInput(terminal.inputJson)
287
+ : text;
278
288
  return {
279
289
  text: finalText,
280
290
  ...(reasoning ? { reasoning } : {}),
@@ -657,10 +657,18 @@ export async function collectStreamChunksIncremental(stream, channel) {
657
657
  let outputTokens = 0;
658
658
  let cacheReadTokens = 0;
659
659
  let reasoningTokens = 0;
660
+ // Surfaced so a caller can map SAFETY / MALFORMED_FUNCTION_CALL rather
661
+ // than inferring the turn ended normally. Additive: existing callers that
662
+ // ignore it are unaffected.
663
+ let finishReason;
660
664
  for await (const chunk of stream) {
661
665
  const chunkRecord = chunk;
662
666
  const candidates = chunkRecord.candidates;
663
667
  const firstCandidate = candidates?.[0];
668
+ const candidateFinish = firstCandidate?.finishReason;
669
+ if (typeof candidateFinish === "string") {
670
+ finishReason = candidateFinish;
671
+ }
664
672
  const chunkContent = firstCandidate?.content;
665
673
  if (chunkContent && Array.isArray(chunkContent.parts)) {
666
674
  for (const part of chunkContent.parts) {
@@ -689,6 +697,7 @@ export async function collectStreamChunksIncremental(stream, channel) {
689
697
  return {
690
698
  rawResponseParts,
691
699
  stepFunctionCalls,
700
+ finishReason,
692
701
  inputTokens,
693
702
  outputTokens,
694
703
  cacheReadTokens,
@@ -1,5 +1,6 @@
1
1
  import type Anthropic from "@anthropic-ai/sdk";
2
2
  import type { Tool } from "./tools.js";
3
+ import type { NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
3
4
  /**
4
5
  * One chunk on the engine's stream.
5
6
  *
@@ -173,6 +174,94 @@ export type AnthropicLoopAdapterConfig = {
173
174
  noteObservedPromptTokens?: (promptTokens: number) => void;
174
175
  abortSignal?: AbortSignal;
175
176
  };
177
+ /** What one Gemini step produced, carried to `buildToolResultMessages`. */
178
+ export type GeminiStepRaw = {
179
+ rawResponseParts: unknown[];
180
+ stepFunctionCalls: NativeFunctionCall[];
181
+ };
182
+ /** One turn entry in a Gemini conversation: a role plus its content parts. */
183
+ export type GeminiTurnContent = {
184
+ role: string;
185
+ parts: unknown[];
186
+ };
187
+ /**
188
+ * Construction input for `createGeminiLoopAdapter`, shared by Google AI Studio
189
+ * and Vertex Gemini. Both issue `models.generateContentStream` and consume the
190
+ * same response shape, so one adapter serves four hand-rolled loops.
191
+ */
192
+ export type GeminiLoopAdapterCoreConfig = {
193
+ /** Used in log lines and generated tool-call ids. */
194
+ providerLabel: string;
195
+ maxSteps: number;
196
+ /** Build one step's request object (model, contents, config). */
197
+ buildRequest: (conversation: GeminiTurnContent[], step: number) => unknown;
198
+ /** Issue the request. Kept injectable so each provider keeps its own client. */
199
+ sendStep: (request: unknown, signal: AbortSignal) => Promise<AsyncIterable<{
200
+ functionCalls?: NativeFunctionCall[];
201
+ [key: string]: unknown;
202
+ }>>;
203
+ /**
204
+ * The turn's live tool record. Mid-turn `search_tools` discovery hydrates
205
+ * into this, which is what both the declaration refresh and
206
+ * `resolveToolOnMiss` read.
207
+ */
208
+ liveTools: Record<string, Tool>;
209
+ /**
210
+ * Declarations built for this turn. Carries `originalNameMap`, which keeps
211
+ * Google's function-name sanitization on the adapter side of the engine
212
+ * boundary.
213
+ */
214
+ declarations?: NativeToolDeclarationsResult;
215
+ toolFailureBreaker?: AgenticLoopToolFailureBreaker;
216
+ /**
217
+ * In-turn context reclaim, run once per step before the request is built.
218
+ * Returns the rebuilt conversation when it reclaimed, undefined when the
219
+ * request still fits.
220
+ *
221
+ * Provider-supplied rather than engine-owned because the two Gemini
222
+ * providers reclaim differently (reclaimAiStudioContext vs
223
+ * reclaimVertexLoopContext) while the engine only decides WHEN to ask. The
224
+ * loops append a model turn plus a tool turn every step with nothing else
225
+ * bounding growth, so a migration that drops this overflows the context
226
+ * window mid-turn and loses every completed step.
227
+ */
228
+ planReclaim?: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[] | undefined;
229
+ /**
230
+ * Usage feedback for the provider's own context guard, called after each
231
+ * step with that step's real token counts.
232
+ */
233
+ noteUsage?: (inputTokens: number, outputTokens: number) => void;
234
+ };
235
+ /**
236
+ * Opt in to the single MALFORMED_FUNCTION_CALL retry.
237
+ *
238
+ * Vertex Gemini only. AI Studio has no such retry today (confirmed: zero
239
+ * MALFORMED_FUNCTION_CALL handling in its client), and turning it on there
240
+ * would be a behaviour change disguised as a shared refactor. The engine owns
241
+ * the one-retry budget; this only says whether to ask.
242
+ *
243
+ * A union rather than two independent optional fields because the retry is
244
+ * only worth spending a step on if the re-issued request differs from the one
245
+ * that just failed. `runAgenticLoop` falls back to the unchanged conversation
246
+ * when no note builder is supplied (`buildMalformedRetryNote?.(…) ??
247
+ * conversation`), so enabling the retry without one re-sends a byte-identical
248
+ * request and most often reproduces the same malformed call — a step burned
249
+ * for nothing. Requiring the builder here makes that combination unsayable
250
+ * instead of merely discouraged.
251
+ */
252
+ export type GeminiMalformedRetryConfig = {
253
+ enableMalformedRetry: true;
254
+ /**
255
+ * Append the corrective turn that the retry re-issues with.
256
+ * Provider-supplied because the note is written in the provider's own
257
+ * content shape.
258
+ */
259
+ buildMalformedRetryNote: (conversation: GeminiTurnContent[]) => GeminiTurnContent[];
260
+ } | {
261
+ enableMalformedRetry?: false;
262
+ buildMalformedRetryNote?: never;
263
+ };
264
+ export type GeminiLoopAdapterConfig = GeminiLoopAdapterCoreConfig & GeminiMalformedRetryConfig;
176
265
  export type AgenticLoopOptions = {
177
266
  tools?: Record<string, {
178
267
  execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
@@ -182,10 +271,24 @@ export type AgenticLoopOptions = {
182
271
  export type AgenticLoopResult<TConversation> = {
183
272
  text: string;
184
273
  toolCalls: AgenticLoopToolCall[];
274
+ /**
275
+ * Every tool dispatch the loop performed, in order, including the ones that
276
+ * failed.
277
+ *
278
+ * `id` and `error` are carried because providers persist tool activity as
279
+ * paired call/result records keyed by the provider's own tool-call id, and
280
+ * a result that failed is stored differently from one that succeeded. A
281
+ * shape with only name/input/output cannot reconstruct either, so a
282
+ * provider migrating its hand-rolled loop onto this engine would have to
283
+ * silently drop both from its history — which is a behaviour change, not a
284
+ * refactor.
285
+ */
185
286
  toolExecutions: Array<{
287
+ id: string;
186
288
  name: string;
187
289
  input: Record<string, unknown>;
188
290
  output: unknown;
291
+ error?: string;
189
292
  }>;
190
293
  usage: AgenticLoopUsage;
191
294
  finishReason: string;
@@ -1738,6 +1738,8 @@ export type NativeFunctionResponse = {
1738
1738
  export type CollectedChunkResult = {
1739
1739
  rawResponseParts: unknown[];
1740
1740
  stepFunctionCalls: NativeFunctionCall[];
1741
+ /** Raw `Candidate.finishReason` from the last chunk that carried one. */
1742
+ finishReason?: string;
1741
1743
  inputTokens: number;
1742
1744
  outputTokens: number;
1743
1745
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.9.0",
3
+ "version": "11.11.0",
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": {