@juspay/neurolink 11.15.7 → 11.15.9

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.
@@ -46,12 +46,64 @@ export type AgenticLoopToolCallResult = AgenticLoopToolCall & {
46
46
  };
47
47
  export type AgenticLoopStepRequest = {
48
48
  raw: unknown;
49
+ /**
50
+ * Tools that became callable while this step's request was being built —
51
+ * mid-turn discovery hydrating a name the model had already tried.
52
+ *
53
+ * The engine clears each one's failure strikes before dispatching, because
54
+ * TOOL_NOT_FOUND strikes accrued while a tool was deferred are snapshot
55
+ * artifacts rather than real failures. Clearing them at the miss-resolution
56
+ * path instead would be too late: the breaker is consulted BEFORE the
57
+ * lookup, so a tool already at the strike limit is refused without the
58
+ * resolution path ever running.
59
+ */
60
+ hydratedToolNames?: string[];
49
61
  };
50
62
  export type AgenticLoopReclaimResult<TConversation> = {
51
- conversation: TConversation;
63
+ conversation?: TConversation;
64
+ /**
65
+ * End the turn now, BEFORE this step's request is issued.
66
+ *
67
+ * A context guard does two things, and only one of them is "reclaim". When
68
+ * dropping old exchanges buys enough room the turn continues; when it does
69
+ * not, the guard has to stop rather than step into a provider rejection
70
+ * that would lose every completed step. Nothing else can express that: the
71
+ * hook returns a conversation, and an adapter cannot break the engine's
72
+ * loop.
73
+ *
74
+ * Aborting the caller's own signal from inside this hook does NOT work as a
75
+ * substitute — the engine checks for an abort at the TOP of the step, above
76
+ * this call, so the request would still be issued and the stop would not
77
+ * take effect until the following step.
78
+ */
79
+ stop?: boolean;
52
80
  };
53
81
  export type AgenticLoopToolFailureBreaker = {
54
82
  maxRetries: number;
83
+ /**
84
+ * Count CONSECUTIVE failures rather than lifetime ones: a clean result
85
+ * clears the strike count for that tool.
86
+ *
87
+ * Off by default because the two behaviours diverge for a tool that fails
88
+ * intermittently, and the providers already on this engine accumulate. What
89
+ * it protects is the argument-dependent soft error — a file-not-found on one
90
+ * path, fine on the next — which under lifetime counting disables a working
91
+ * tool for the rest of the turn.
92
+ */
93
+ consecutive?: boolean;
94
+ /**
95
+ * Decide whether a RESOLVED tool result is really a failure.
96
+ *
97
+ * Some tools report failure without throwing: MCP `isError` payloads, a
98
+ * proxy-blocked call returning `{ error }`. Counting only thrown errors lets
99
+ * the model grind on one of those for the entire step budget. Returning a
100
+ * non-empty string strikes the breaker exactly as a throw does; returning
101
+ * undefined leaves the result a success.
102
+ *
103
+ * Off by default — a provider whose loop never inspected results this way
104
+ * must not start doing so as a side effect of migrating.
105
+ */
106
+ classifyResultFailure?: (output: unknown) => string | undefined;
55
107
  };
56
108
  /**
57
109
  * DESIGN DECISION — mid-turn tool-discovery hydration (Plan 08 blocker 2,
@@ -114,13 +166,21 @@ export type AgenticLoopAdapter<TConversation = unknown, TRaw = unknown> = {
114
166
  executeStep(request: AgenticLoopStepRequest, channel: {
115
167
  push(chunk: AgenticLoopChunk): void;
116
168
  }, signal: AbortSignal): Promise<AgenticLoopStepResult<TRaw>>;
117
- buildToolResultMessages(conversation: TConversation, stepResult: AgenticLoopStepResult<TRaw>, toolResults: AgenticLoopToolCallResult[]): TConversation;
169
+ /**
170
+ * `step` is the engine's own zero-based step index, not a count of times
171
+ * this hook ran. Adapters persist tool activity keyed by it, and the two
172
+ * numbers diverge: a malformed-call retry `continue`s before this hook is
173
+ * reached and still consumes a step, so an adapter counting its own
174
+ * invocations drifts by exactly the number of retries and mislabels every
175
+ * row after the first one.
176
+ */
177
+ buildToolResultMessages(conversation: TConversation, stepResult: AgenticLoopStepResult<TRaw>, toolResults: AgenticLoopToolCallResult[], step: number): TConversation;
118
178
  mapFinishReason(rawStopReason: string | undefined, hadToolCalls: boolean): string;
119
179
  /** Optional: in-turn context-budget reclaim, called once per step before buildStepRequest. */
120
180
  planReclaim?(conversation: TConversation, step: number): AgenticLoopReclaimResult<TConversation> | undefined;
121
181
  /** Optional: Vertex+Gemini-only single-retry-on-malformed-call. */
122
182
  isMalformedStep?(stepResult: AgenticLoopStepResult<TRaw>): boolean;
123
- buildMalformedRetryNote?(conversation: TConversation): TConversation;
183
+ buildMalformedRetryNote?(conversation: TConversation, step: number): TConversation;
124
184
  };
125
185
  /**
126
186
  * Construction input for `createAnthropicLoopAdapter`, shared by direct
@@ -184,6 +244,31 @@ export type AnthropicLoopAdapterConfig = {
184
244
  noteObservedPromptTokens?: (promptTokens: number) => void;
185
245
  abortSignal?: AbortSignal;
186
246
  };
247
+ /**
248
+ * The three things a native Gemini loop wraps around every tool call that the
249
+ * shared engine does not do itself.
250
+ *
251
+ * All optional, and the whole object is optional, because the two Gemini
252
+ * providers differ here: Vertex bounds tool execution and runs a stall
253
+ * watchdog, AI Studio does neither. Passing nothing leaves an executor exactly
254
+ * as the caller supplied it, so this cannot quietly give AI Studio behaviour
255
+ * its hand-rolled loops never had.
256
+ */
257
+ export type GeminiToolExecutionGuards = {
258
+ /** Upper bound on a single execute(); omit for no bound. */
259
+ toolTimeoutMs?: number;
260
+ /**
261
+ * Turn-level abort, raced against the call so a deadline or caller cancel is
262
+ * observed immediately instead of after the tool settles.
263
+ */
264
+ abortSignal?: AbortSignal;
265
+ /**
266
+ * Stall-watchdog ping, called either side of the await. The watchdog is a
267
+ * whole-turn interval measuring wall-clock since the last mark, so a
268
+ * legitimately slow tool reads as a stalled turn without this.
269
+ */
270
+ onProgress?: () => void;
271
+ };
187
272
  /** What one Gemini step produced, carried to `buildToolResultMessages`. */
188
273
  export type GeminiStepRaw = {
189
274
  rawResponseParts: unknown[];
@@ -235,12 +320,30 @@ export type GeminiLoopAdapterCoreConfig = {
235
320
  * bounding growth, so a migration that drops this overflows the context
236
321
  * window mid-turn and loses every completed step.
237
322
  */
238
- planReclaim?: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[] | undefined;
323
+ planReclaim?: (conversation: GeminiTurnContent[], step: number) => AgenticLoopReclaimResult<GeminiTurnContent[]> | undefined;
239
324
  /**
240
325
  * Usage feedback for the provider's own context guard, called after each
241
326
  * step with that step's real token counts.
242
327
  */
243
328
  noteUsage?: (inputTokens: number, outputTokens: number) => void;
329
+ /**
330
+ * Name of the terminal structured-output tool when one is in play. A call
331
+ * to it ends the turn: its arguments ARE the answer, so it is reported as
332
+ * text and omitted from `toolCalls`, which routes it through the engine's
333
+ * ordinary zero-tool-calls exit — never dispatched, never counted against
334
+ * the breaker, never recorded as a tool execution.
335
+ */
336
+ finalResultToolName?: string;
337
+ /**
338
+ * Called with the terminal tool's payload when one was actually detected.
339
+ *
340
+ * The caller cannot infer this from the turn's result: a structured turn
341
+ * ends with the payload in `text` when the model called the terminal tool,
342
+ * and with ordinary prose in `text` when it answered directly instead, and
343
+ * those two are indistinguishable downstream while being handled
344
+ * differently. Comparing strings to tell them apart would be guesswork.
345
+ */
346
+ onTerminalResult?: (text: string) => void;
244
347
  /**
245
348
  * Fold one step's raw stream into the shape the adapter reports.
246
349
  *
@@ -282,7 +385,7 @@ export type GeminiMalformedRetryConfig = {
282
385
  * Provider-supplied because the note is written in the provider's own
283
386
  * content shape.
284
387
  */
285
- buildMalformedRetryNote: (conversation: GeminiTurnContent[]) => GeminiTurnContent[];
388
+ buildMalformedRetryNote: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[];
286
389
  } | {
287
390
  enableMalformedRetry?: false;
288
391
  buildMalformedRetryNote?: never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.15.7",
3
+ "version": "11.15.9",
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": {