@juspay/neurolink 12.12.16 → 12.13.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 +3 -3
- package/dist/browser/neurolink.min.js +381 -381
- package/dist/core/baseProvider.d.ts +10 -0
- package/dist/core/baseProvider.js +21 -0
- package/dist/core/constants.d.ts +14 -1
- package/dist/core/constants.js +18 -1
- package/dist/core/loopEngine.js +150 -7
- package/dist/core/toolExecutionGuards.js +6 -1
- package/dist/providers/amazonBedrock/client.js +10 -0
- package/dist/providers/anthropic/client.d.ts +8 -0
- package/dist/providers/anthropic/client.js +132 -3
- package/dist/providers/anthropic/loopAdapter.js +352 -244
- package/dist/providers/googleAiStudio/client.js +12 -0
- package/dist/providers/googleVertex/client.js +19 -5
- package/dist/types/generate.d.ts +22 -9
- package/dist/types/loopEngine.d.ts +97 -2
- package/dist/types/stream.d.ts +141 -5
- package/dist/utils/parameterValidation.d.ts +30 -0
- package/dist/utils/parameterValidation.js +101 -0
- package/dist/utils/timeout.js +25 -2
- package/package.json +2 -1
|
@@ -8,7 +8,7 @@ import { BaseProvider } from "../../core/baseProvider.js";
|
|
|
8
8
|
import { unwrapImagePayload } from "../../adapters/imageFormatSupport.js";
|
|
9
9
|
import { appendNativeAudioParts } from "../googleNativeGemini3/utils.js";
|
|
10
10
|
import { getMimeTypeForExtension } from "../../processors/config/mimeConstants.js";
|
|
11
|
-
import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS,
|
|
11
|
+
import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, resolveToolTimeoutMs, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
|
|
12
12
|
import { resolveRequestKind } from "../../core/resolveRequestKind.js";
|
|
13
13
|
import { ModelConfigurationManager } from "../../core/modelConfiguration.js";
|
|
14
14
|
import { isSchemaComplexityError } from "../../core/modules/structuredOutputPolicy.js";
|
|
@@ -1560,7 +1560,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1560
1560
|
// (whole-turn deadline + optional stall detector) all trip it, and every
|
|
1561
1561
|
// request/tool-exec receives effectiveSignal.
|
|
1562
1562
|
const streamTimeoutMs = parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS;
|
|
1563
|
-
const toolExecTimeoutMs = options.toolTimeoutMs
|
|
1563
|
+
const toolExecTimeoutMs = resolveToolTimeoutMs(options.toolTimeoutMs);
|
|
1564
1564
|
const effectiveTurnDeadlineMs = options.turnTimeoutMs ?? streamTimeoutMs;
|
|
1565
1565
|
const internalAbort = new AbortController();
|
|
1566
1566
|
const onCallerAbort = () => internalAbort.abort();
|
|
@@ -1790,6 +1790,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1790
1790
|
onProgress: () => turnClock.noteProgress(),
|
|
1791
1791
|
}),
|
|
1792
1792
|
abortSignal: effectiveSignal,
|
|
1793
|
+
// The engine bounds every tool call itself. Passing the value this
|
|
1794
|
+
// loop already gave `buildDedupedEngineTools` keeps the engine's
|
|
1795
|
+
// backstop from being tighter than what the caller asked for.
|
|
1796
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
1793
1797
|
});
|
|
1794
1798
|
// Collected, NOT forwarded to the consumer here. This loop replays the
|
|
1795
1799
|
// gathered text after the turn rather than streaming it live, and a
|
|
@@ -2356,7 +2360,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2356
2360
|
// (whole-turn deadline + optional stall detector) all trip it, and every
|
|
2357
2361
|
// request/tool-exec receives effectiveSignal.
|
|
2358
2362
|
const streamTimeoutMs = parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS;
|
|
2359
|
-
const toolExecTimeoutMs = options.toolTimeoutMs
|
|
2363
|
+
const toolExecTimeoutMs = resolveToolTimeoutMs(options.toolTimeoutMs);
|
|
2360
2364
|
const effectiveTurnDeadlineMs = options.turnTimeoutMs ?? streamTimeoutMs;
|
|
2361
2365
|
const internalAbort = new AbortController();
|
|
2362
2366
|
const onCallerAbort = () => internalAbort.abort();
|
|
@@ -2585,6 +2589,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2585
2589
|
onProgress: () => turnClock.noteProgress(),
|
|
2586
2590
|
}),
|
|
2587
2591
|
abortSignal: effectiveSignal,
|
|
2592
|
+
// The engine bounds every tool call itself. Passing the value this
|
|
2593
|
+
// loop already gave `buildDedupedEngineTools` keeps the engine's
|
|
2594
|
+
// backstop from being tighter than what the caller asked for.
|
|
2595
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
2588
2596
|
});
|
|
2589
2597
|
// generate() returns one result rather than streaming, so the engine's
|
|
2590
2598
|
// chunks are drained and discarded — the answer comes off the turn's
|
|
@@ -3304,7 +3312,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3304
3312
|
if (options.abortSignal?.aborted) {
|
|
3305
3313
|
internalAbort.abort();
|
|
3306
3314
|
}
|
|
3307
|
-
const toolExecTimeoutMs = options.toolTimeoutMs
|
|
3315
|
+
const toolExecTimeoutMs = resolveToolTimeoutMs(options.toolTimeoutMs);
|
|
3308
3316
|
const effectiveTurnDeadlineMs = options.turnTimeoutMs ?? streamTimeoutMs;
|
|
3309
3317
|
// Whole-turn deadline + optional stall watchdog. When the caller sets no
|
|
3310
3318
|
// explicit turn budget, keep the pre-existing defensive bound
|
|
@@ -3668,6 +3676,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3668
3676
|
tools: engineTools,
|
|
3669
3677
|
abortSignal: internalAbort.signal,
|
|
3670
3678
|
...(activeSpan ? { span: activeSpan } : {}),
|
|
3679
|
+
// Same value `guardToolExecutor` already received above, so the
|
|
3680
|
+
// engine's own per-tool bound cannot undercut it.
|
|
3681
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
3671
3682
|
});
|
|
3672
3683
|
const pump = (async () => {
|
|
3673
3684
|
for await (const chunk of engineStream) {
|
|
@@ -4474,7 +4485,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4474
4485
|
if (options.abortSignal?.aborted) {
|
|
4475
4486
|
internalAbort.abort();
|
|
4476
4487
|
}
|
|
4477
|
-
const toolExecTimeoutMs = options.toolTimeoutMs
|
|
4488
|
+
const toolExecTimeoutMs = resolveToolTimeoutMs(options.toolTimeoutMs);
|
|
4478
4489
|
// Whole-turn deadline + optional stall watchdog. NOTE: unlike the stream
|
|
4479
4490
|
// twin, this path historically had NO whole-turn bound (only the per-call
|
|
4480
4491
|
// withTimeout) — so no defensive default is introduced here: without an
|
|
@@ -4668,6 +4679,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4668
4679
|
const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentMessages.slice(), {
|
|
4669
4680
|
tools: engineTools,
|
|
4670
4681
|
abortSignal: internalAbort.signal,
|
|
4682
|
+
// Same value `guardToolExecutor` already received above, so the
|
|
4683
|
+
// engine's own per-tool bound cannot undercut it.
|
|
4684
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
4671
4685
|
});
|
|
4672
4686
|
// Drained and discarded: generate() returns one result rather than
|
|
4673
4687
|
// streaming, and the per-step text is accumulated in
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -410,11 +410,24 @@ export type GenerateOptions = {
|
|
|
410
410
|
*/
|
|
411
411
|
wrapupTimeLeadMs?: number;
|
|
412
412
|
/**
|
|
413
|
-
* Per-tool-execution timeout in milliseconds (default 300_000)
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
|
|
417
|
-
|
|
413
|
+
* Per-tool-execution timeout in milliseconds (default 300_000), or `null`
|
|
414
|
+
* for no bound at all.
|
|
415
|
+
*
|
|
416
|
+
* A tool that exceeds it is told to stop — the AbortSignal it was handed is
|
|
417
|
+
* aborted — and then fails with an error tool_result costing one step, so
|
|
418
|
+
* the turn continues instead of hanging on a wedged tool. A tool that
|
|
419
|
+
* ignores its signal keeps running to completion in the background; nothing
|
|
420
|
+
* here can stop it, and its eventual result is discarded.
|
|
421
|
+
*
|
|
422
|
+
* `null` removes the bound and awaits `execute` unguarded, which is what the
|
|
423
|
+
* native loops did before they had a per-tool timer. It is the way to keep a
|
|
424
|
+
* legitimately long-running tool, since a finite number is always a ceiling
|
|
425
|
+
* and `Infinity` silently becomes `setTimeout`'s ~24.9-day cap. The one
|
|
426
|
+
* combination refused is `null` together with
|
|
427
|
+
* `executionControl.lifetimeTimeoutMs: null`, which would leave the turn
|
|
428
|
+
* with no bound anywhere.
|
|
429
|
+
*/
|
|
430
|
+
toolTimeoutMs?: number | null;
|
|
418
431
|
/** AbortSignal for external cancellation of the AI call */
|
|
419
432
|
abortSignal?: AbortSignal;
|
|
420
433
|
/**
|
|
@@ -1201,8 +1214,8 @@ export type TextGenerationOptions = {
|
|
|
1201
1214
|
stallTimeoutMs?: number;
|
|
1202
1215
|
/** Remaining-time threshold that triggers the wrap-up nudge (ms). See GenerateOptions.wrapupTimeLeadMs. */
|
|
1203
1216
|
wrapupTimeLeadMs?: number;
|
|
1204
|
-
/** Per-tool-execution timeout (ms, default 300_000). See GenerateOptions.toolTimeoutMs. */
|
|
1205
|
-
toolTimeoutMs?: number;
|
|
1217
|
+
/** Per-tool-execution timeout (ms, default 300_000; `null` for no bound). See GenerateOptions.toolTimeoutMs. */
|
|
1218
|
+
toolTimeoutMs?: number | null;
|
|
1206
1219
|
/** AbortSignal for external cancellation of the AI call */
|
|
1207
1220
|
abortSignal?: AbortSignal;
|
|
1208
1221
|
/** Bounds for tool execution capture. See GenerateOptions.toolExecutionCapture. */
|
|
@@ -1587,8 +1600,8 @@ export type NativeGenerateLoopArgs = {
|
|
|
1587
1600
|
maxOutputTokens?: number;
|
|
1588
1601
|
temperature?: number;
|
|
1589
1602
|
abortSignal?: AbortSignal;
|
|
1590
|
-
/** Per-tool-execution cap, forwarded into `guardToolExecutor`. */
|
|
1591
|
-
toolTimeoutMs?: number;
|
|
1603
|
+
/** Per-tool-execution cap, forwarded into `guardToolExecutor`. `null` for no bound. */
|
|
1604
|
+
toolTimeoutMs?: number | null;
|
|
1592
1605
|
/** Wraps one step: retry ladder plus provider error classification. */
|
|
1593
1606
|
runStep: (call: () => Promise<Record<string, unknown>>) => Promise<Record<string, unknown>>;
|
|
1594
1607
|
};
|
|
@@ -2,6 +2,7 @@ import type Anthropic from "@anthropic-ai/sdk";
|
|
|
2
2
|
import type { Span } from "@opentelemetry/api";
|
|
3
3
|
import type { Tool } from "./tools.js";
|
|
4
4
|
import type { CollectedChunkResult, NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
|
|
5
|
+
import type { ExecutionControlDecision, ExecutionControlStepContext } from "./stream.js";
|
|
5
6
|
/**
|
|
6
7
|
* One chunk on the engine's stream.
|
|
7
8
|
*
|
|
@@ -163,6 +164,17 @@ export type AgenticLoopAdapter<TConversation = unknown, TRaw = unknown> = {
|
|
|
163
164
|
readonly maxSteps: number;
|
|
164
165
|
/** Set only for adapter instances whose client has the TOOL_NOT_FOUND strike breaker today: both Gemini adapters (AI Studio, Vertex+Gemini) AND the Vertex+Claude call to createAnthropicLoopAdapter — NOT the native-Anthropic call to that same factory, and not Bedrock. See Verified Fact 4. */
|
|
165
166
|
readonly toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
167
|
+
/**
|
|
168
|
+
* Append a planning nudge to the conversation, in the provider's own message
|
|
169
|
+
* type. Supplied only by adapters whose caller can pass a step-boundary
|
|
170
|
+
* callback; the engine skips a nudge it has no way to write.
|
|
171
|
+
*
|
|
172
|
+
* Provider-supplied for the same reason `buildToolResultMessages` is: the
|
|
173
|
+
* engine does not know what a valid user turn looks like on this wire, and
|
|
174
|
+
* on Anthropic a nudge appended after a tool-result turn has to merge into
|
|
175
|
+
* that turn rather than open a second consecutive user message.
|
|
176
|
+
*/
|
|
177
|
+
readonly appendPlanningNudge?: (conversation: TConversation, text: string) => TConversation;
|
|
166
178
|
/**
|
|
167
179
|
* Second lookup path, consulted when a tool call names nothing executable
|
|
168
180
|
* in the caller's `options.tools` — used by adapters supporting mid-turn
|
|
@@ -265,6 +277,30 @@ export type AnthropicLoopAdapterConfig<TMessage = Anthropic.Messages.MessagePara
|
|
|
265
277
|
* tell them apart would be guesswork, so the adapter says which happened.
|
|
266
278
|
*/
|
|
267
279
|
onTerminalResult?: (text: string) => void;
|
|
280
|
+
/**
|
|
281
|
+
* Hard deadline for ONE `messages.create` request (ms). Composed with the
|
|
282
|
+
* engine's signal for the duration of that step only, so it bounds a stalled
|
|
283
|
+
* upstream even when the turn itself has no lifetime ceiling — and is armed
|
|
284
|
+
* fresh per step, which is why a step boundary can never extend a deadline
|
|
285
|
+
* already running.
|
|
286
|
+
*
|
|
287
|
+
* When the deadline fires, the step throws the timer's own TimeoutError
|
|
288
|
+
* rather than returning what it had: the Anthropic SDK's stream iterator
|
|
289
|
+
* exits WITHOUT throwing on an aborted read, so a truncated step would
|
|
290
|
+
* otherwise be reported as a model turn that simply said less.
|
|
291
|
+
*/
|
|
292
|
+
requestTimeoutMs?: number;
|
|
293
|
+
/**
|
|
294
|
+
* Require the upstream terminal event (`message_stop`) before a step counts
|
|
295
|
+
* as complete.
|
|
296
|
+
*
|
|
297
|
+
* A response that ends early carries syntactically complete content blocks —
|
|
298
|
+
* including tool_use blocks — so without this the loop dispatches tools the
|
|
299
|
+
* model never finished asking for and reports the turn as a normal stop.
|
|
300
|
+
* Off by default: turning it on unconditionally would change what every
|
|
301
|
+
* existing caller sees from a flaky connection.
|
|
302
|
+
*/
|
|
303
|
+
requireTerminalEvent?: boolean;
|
|
268
304
|
toolFailureBreaker?: AgenticLoopToolFailureBreaker;
|
|
269
305
|
/**
|
|
270
306
|
* In-turn context reclaim, run once per step before the request is built.
|
|
@@ -305,8 +341,15 @@ export type AnthropicLoopAdapterConfig<TMessage = Anthropic.Messages.MessagePara
|
|
|
305
341
|
* never had.
|
|
306
342
|
*/
|
|
307
343
|
export type ToolExecutionGuards = {
|
|
308
|
-
/**
|
|
309
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Upper bound on a single execute(). Omit, or pass `null`, for no bound.
|
|
346
|
+
*
|
|
347
|
+
* `null` is accepted as well as `undefined` because it is what a caller's
|
|
348
|
+
* own `toolTimeoutMs: null` resolves to, and the two must not diverge: a
|
|
349
|
+
* value that means "unbounded" at the public surface cannot arrive here
|
|
350
|
+
* meaning "0ms".
|
|
351
|
+
*/
|
|
352
|
+
toolTimeoutMs?: number | null;
|
|
310
353
|
/**
|
|
311
354
|
* Turn-level abort, raced against the call so a deadline or caller cancel is
|
|
312
355
|
* observed immediately instead of after the tool settles.
|
|
@@ -491,6 +534,44 @@ export type AgenticLoopOptions = {
|
|
|
491
534
|
* because only they threaded a span before moving onto the engine.
|
|
492
535
|
*/
|
|
493
536
|
span?: Span;
|
|
537
|
+
/**
|
|
538
|
+
* Upper bound on a single `tool.execute()` (ms), or `null` for no bound.
|
|
539
|
+
* Defaults to `DEFAULT_TOOL_EXECUTION_TIMEOUT_MS` (300_000) when omitted.
|
|
540
|
+
*
|
|
541
|
+
* The turn-level timers do not cover this: a per-request deadline bounds one
|
|
542
|
+
* model call and is disposed when the step settles, and the step cap only
|
|
543
|
+
* advances when a step completes. Between two steps, a tool that never
|
|
544
|
+
* returns has nothing watching it, and a turn that opted out of a lifetime
|
|
545
|
+
* ceiling then hangs forever. A tool that exceeds the bound is told to stop
|
|
546
|
+
* — the signal it was handed is aborted — and fails with an error tool
|
|
547
|
+
* result costing one step, exactly as it does on the native generate path.
|
|
548
|
+
*
|
|
549
|
+
* `null` restores the behaviour of a loop with no per-tool timer at all:
|
|
550
|
+
* `execute` is awaited unguarded, with the turn's own signal. Callers that
|
|
551
|
+
* relied on unbounded tool execution say so with it. The one combination
|
|
552
|
+
* refused is `null` together with `executionControl.lifetimeTimeoutMs: null`
|
|
553
|
+
* — that would leave the turn with no bound anywhere.
|
|
554
|
+
*
|
|
555
|
+
* Callers that already guard their executors (Vertex and the Gemini
|
|
556
|
+
* adapters, via `guardToolExecutor`) should pass the SAME value they gave
|
|
557
|
+
* those guards, so this backstop can never be tighter than what the caller
|
|
558
|
+
* asked for.
|
|
559
|
+
*/
|
|
560
|
+
toolTimeoutMs?: number | null;
|
|
561
|
+
/**
|
|
562
|
+
* Step-boundary callback. Runs after a step's tool results have settled and
|
|
563
|
+
* been written into the conversation, and BEFORE the loop re-checks the step
|
|
564
|
+
* cap — the one point in a turn where raising the cap changes what happens
|
|
565
|
+
* next without replaying anything that already happened.
|
|
566
|
+
*
|
|
567
|
+
* Already bounded and cancellable by the time it reaches the engine: the
|
|
568
|
+
* caller owns the callback's own budget, because the caller is what the
|
|
569
|
+
* public option was validated against. The engine only calls it, applies a
|
|
570
|
+
* strictly-larger finite cap if one comes back, and asks the adapter to
|
|
571
|
+
* write the nudge. It never restarts a step, retries a request, or replays a
|
|
572
|
+
* tool.
|
|
573
|
+
*/
|
|
574
|
+
beforeStep?: (context: ExecutionControlStepContext) => Promise<ExecutionControlDecision | undefined>;
|
|
494
575
|
};
|
|
495
576
|
export type AgenticLoopResult<TConversation> = {
|
|
496
577
|
text: string;
|
|
@@ -518,4 +599,18 @@ export type AgenticLoopResult<TConversation> = {
|
|
|
518
599
|
finishReason: string;
|
|
519
600
|
rawStopReason: string | undefined;
|
|
520
601
|
conversation: TConversation;
|
|
602
|
+
/**
|
|
603
|
+
* True when the turn ended because its abort signal fired rather than
|
|
604
|
+
* because the model finished.
|
|
605
|
+
*
|
|
606
|
+
* Not derivable from anything else on this result, which is why it is here.
|
|
607
|
+
* A turn cut short mid-stream never receives a terminal event, so
|
|
608
|
+
* `rawStopReason` is undefined and `mapFinishReason` lands on exactly the
|
|
609
|
+
* value a model that answered and stopped produces. Without this flag a
|
|
610
|
+
* caller reading the result cannot tell "the model finished" from "we
|
|
611
|
+
* stopped listening", and every consumer that branches on the outcome —
|
|
612
|
+
* fallback gates, retry policy, a UI that says why a turn ended — reads the
|
|
613
|
+
* interrupted turn as a success.
|
|
614
|
+
*/
|
|
615
|
+
aborted: boolean;
|
|
521
616
|
};
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -184,6 +184,114 @@ export type ProviderStreamChunk = {
|
|
|
184
184
|
base64: string;
|
|
185
185
|
};
|
|
186
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* What a `beforeStep` callback is told at a step boundary.
|
|
189
|
+
*
|
|
190
|
+
* The boundary is reached only after that step's tool results have settled and
|
|
191
|
+
* been written into the conversation, and before the step cap is re-checked —
|
|
192
|
+
* so a decision taken here applies to the NEXT step of the SAME turn, never to
|
|
193
|
+
* a step already in flight.
|
|
194
|
+
*/
|
|
195
|
+
export type ExecutionControlStepContext = {
|
|
196
|
+
/** Zero-based index of the step that just settled. */
|
|
197
|
+
stepIndex: number;
|
|
198
|
+
/** Steps that have completed in this turn, including the one that just settled. */
|
|
199
|
+
stepsCompleted: number;
|
|
200
|
+
/** The step cap currently in force — the number a renewal must exceed. */
|
|
201
|
+
maxSteps: number;
|
|
202
|
+
/** Milliseconds since the turn's first request was built. */
|
|
203
|
+
elapsedMs: number;
|
|
204
|
+
/** Names of the tools dispatched on the step that just settled, in order. */
|
|
205
|
+
toolNames: string[];
|
|
206
|
+
/**
|
|
207
|
+
* Fires when the turn is cancelled or the callback outlives its own budget.
|
|
208
|
+
* A callback that does real work (reading a live budget, asking a service)
|
|
209
|
+
* must honour it — the turn does not wait for a callback that ignores it.
|
|
210
|
+
*/
|
|
211
|
+
signal: AbortSignal;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* What a `beforeStep` callback may change about the rest of the turn.
|
|
215
|
+
*
|
|
216
|
+
* Returning nothing is a decision too: it declines the renewal, so the
|
|
217
|
+
* existing cap stands and a turn that has reached it ends as a step-limit
|
|
218
|
+
* outcome exactly as it would with no callback at all.
|
|
219
|
+
*/
|
|
220
|
+
export type ExecutionControlDecision = {
|
|
221
|
+
/**
|
|
222
|
+
* A new absolute step cap. Must be finite and greater than the cap in force;
|
|
223
|
+
* anything else is ignored, so a callback cannot shorten a turn by returning
|
|
224
|
+
* a smaller number or unbound one by returning Infinity.
|
|
225
|
+
*/
|
|
226
|
+
maxSteps?: number;
|
|
227
|
+
/**
|
|
228
|
+
* A planning nudge appended to the conversation before the next step, in the
|
|
229
|
+
* same loop and the same history — this is how a caller tells the model that
|
|
230
|
+
* its budget changed without restarting the turn.
|
|
231
|
+
*/
|
|
232
|
+
nudge?: string;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Opt-in execution policy for one streamed turn. Supported only on the native
|
|
236
|
+
* Anthropic stream path today; any other provider REJECTS it rather than
|
|
237
|
+
* ignoring it, because silently dropping a policy the caller set is how a turn
|
|
238
|
+
* ends at a limit its owner believed it had removed.
|
|
239
|
+
*
|
|
240
|
+
* Absent, none of the turn-level policy here applies: the turn is bounded by
|
|
241
|
+
* `timeout` / `turnTimeoutMs` / `maxSteps` as it always was.
|
|
242
|
+
*
|
|
243
|
+
* One thing is NOT conditional on this option, and it is worth stating because
|
|
244
|
+
* it changed: the agentic engine bounds every tool call, at
|
|
245
|
+
* `toolTimeoutMs` or the 300s default, whether or not `executionControl` is
|
|
246
|
+
* present. Loops that previously ran tools with no per-tool timer at all — the
|
|
247
|
+
* Bedrock and Google AI Studio paths — therefore acquired one. A caller that
|
|
248
|
+
* relied on unbounded tool execution says so with `toolTimeoutMs: null`, which
|
|
249
|
+
* restores exactly the old behaviour.
|
|
250
|
+
*/
|
|
251
|
+
export type ExecutionControlOptions = {
|
|
252
|
+
/**
|
|
253
|
+
* Hard deadline for a single HTTP request (ms). Required, finite, positive.
|
|
254
|
+
*
|
|
255
|
+
* This is the floor that makes the rest of the contract safe: whatever the
|
|
256
|
+
* turn-level policy is — including no lifetime ceiling at all — a stalled
|
|
257
|
+
* upstream is always caught here, with the timer's own identity on the
|
|
258
|
+
* error. A step boundary cannot reset a deadline already running.
|
|
259
|
+
*/
|
|
260
|
+
requestTimeoutMs: number;
|
|
261
|
+
/**
|
|
262
|
+
* The turn's wall-clock ceiling (ms).
|
|
263
|
+
*
|
|
264
|
+
* - `null` — no lifetime timer is armed at all. This is the case that cannot
|
|
265
|
+
* be expressed any other way: a very large number is still a ceiling, and
|
|
266
|
+
* it fires eventually, in the middle of work, dressed as a cancel. With no
|
|
267
|
+
* lifetime timer the per-tool deadline is the turn's last bound, so
|
|
268
|
+
* `toolTimeoutMs: null` is refused alongside it.
|
|
269
|
+
* - a finite positive number — an explicit cap, reported as `time-limit`.
|
|
270
|
+
* - `0`, negative, or non-finite — rejected.
|
|
271
|
+
* - absent — the legacy handling (`turnTimeoutMs`, else the provider
|
|
272
|
+
* timeout) is inherited untouched.
|
|
273
|
+
*
|
|
274
|
+
* Set to anything other than absent, this OWNS the turn's lifetime timer, so
|
|
275
|
+
* a `turnTimeoutMs` on the same request would be read by nothing. That
|
|
276
|
+
* combination is rejected rather than silently resolved — set one or the
|
|
277
|
+
* other. Note that "absent" means the property is not there AND that it is
|
|
278
|
+
* present holding `undefined`: both say "no opinion", and both inherit.
|
|
279
|
+
*/
|
|
280
|
+
lifetimeTimeoutMs?: number | null;
|
|
281
|
+
/**
|
|
282
|
+
* Runs at each step boundary. May renew the step cap and append a planning
|
|
283
|
+
* nudge. It is itself bounded by `beforeStepTimeoutMs` and cancelled with
|
|
284
|
+
* the turn; a callback that throws, or outlives its budget, is treated as
|
|
285
|
+
* declining to renew.
|
|
286
|
+
*/
|
|
287
|
+
beforeStep?: (context: ExecutionControlStepContext) => ExecutionControlDecision | undefined | Promise<ExecutionControlDecision | undefined>;
|
|
288
|
+
/**
|
|
289
|
+
* Bound on `beforeStep` itself (ms, finite and positive; default 30_000).
|
|
290
|
+
* A boundary callback sits between two model calls, so an unbounded one
|
|
291
|
+
* stalls the turn in a place no other timer is watching.
|
|
292
|
+
*/
|
|
293
|
+
beforeStepTimeoutMs?: number;
|
|
294
|
+
};
|
|
187
295
|
export type StreamOptions = {
|
|
188
296
|
/**
|
|
189
297
|
* Opt this stream call into the knowledge grounding configured on the
|
|
@@ -376,12 +484,39 @@ export type StreamOptions = {
|
|
|
376
484
|
timeout?: number | string;
|
|
377
485
|
/** Wall-clock cap for the whole agentic turn (ms). See GenerateOptions.turnTimeoutMs. */
|
|
378
486
|
turnTimeoutMs?: number;
|
|
487
|
+
/**
|
|
488
|
+
* Opt-in execution policy for this turn (native Anthropic streaming only).
|
|
489
|
+
* See ExecutionControlOptions. Absent means the legacy bounds apply
|
|
490
|
+
* unchanged; present on any other provider is an error, not a no-op.
|
|
491
|
+
*
|
|
492
|
+
* Two consequences of that "error, not a no-op" stance are worth knowing
|
|
493
|
+
* before you set it:
|
|
494
|
+
*
|
|
495
|
+
* - **It is incompatible with provider fallback.** Only the native Anthropic
|
|
496
|
+
* stream path implements the contract, so a turn that falls back to any
|
|
497
|
+
* other provider — internal fallback, or a configured fallback chain —
|
|
498
|
+
* fails there with a ValidationError instead of being served without the
|
|
499
|
+
* policy. That is deliberate: a fallback that silently dropped the policy
|
|
500
|
+
* would produce exactly the invisible ceiling this option exists to
|
|
501
|
+
* remove. Pair it with `disableInternalFallback: true` when you want the
|
|
502
|
+
* turn to stay on Anthropic, and handle the error if you do not.
|
|
503
|
+
* - **It cannot be combined with `turnTimeoutMs`** when it sets
|
|
504
|
+
* `lifetimeTimeoutMs`, because both name the turn's wall-clock ceiling and
|
|
505
|
+
* only one of them can win. Supplying both is rejected up front rather
|
|
506
|
+
* than resolved in silence.
|
|
507
|
+
* - **`lifetimeTimeoutMs: null` cannot be combined with
|
|
508
|
+
* `toolTimeoutMs: null`.** Removing the turn's ceiling leaves the per-tool
|
|
509
|
+
* deadline as the only thing that will ever end a tool which never
|
|
510
|
+
* returns; removing that as well leaves the turn with no bound anywhere.
|
|
511
|
+
* Also rejected up front.
|
|
512
|
+
*/
|
|
513
|
+
executionControl?: ExecutionControlOptions;
|
|
379
514
|
/** Max time with no progress before the turn ends as "stalled" (ms). Native Vertex loops only — see GenerateOptions.stallTimeoutMs. */
|
|
380
515
|
stallTimeoutMs?: number;
|
|
381
516
|
/** Remaining-time threshold that triggers the wrap-up nudge (ms). See GenerateOptions.wrapupTimeLeadMs. */
|
|
382
517
|
wrapupTimeLeadMs?: number;
|
|
383
|
-
/** Per-tool-execution timeout (ms, default 300_000). See GenerateOptions.toolTimeoutMs. */
|
|
384
|
-
toolTimeoutMs?: number;
|
|
518
|
+
/** Per-tool-execution timeout (ms, default 300_000; `null` for no bound). See GenerateOptions.toolTimeoutMs. */
|
|
519
|
+
toolTimeoutMs?: number | null;
|
|
385
520
|
/** AbortSignal for external cancellation of the AI call */
|
|
386
521
|
abortSignal?: AbortSignal;
|
|
387
522
|
/** Bounds for tool execution capture. See GenerateOptions.toolExecutionCapture. */
|
|
@@ -641,9 +776,10 @@ export type StreamResult = {
|
|
|
641
776
|
finishReason?: string;
|
|
642
777
|
/**
|
|
643
778
|
* Why the agentic turn ended (see GenerateStopReason). For background-loop
|
|
644
|
-
* streams (native Vertex paths
|
|
645
|
-
* the stream — this top-level
|
|
646
|
-
*
|
|
779
|
+
* streams (the native Vertex paths and the native Anthropic stream path)
|
|
780
|
+
* prefer `metadata.stopReason` after draining the stream — this top-level
|
|
781
|
+
* field may be a getter that resolves late, and wrapper spreads can
|
|
782
|
+
* snapshot it before the loop finishes.
|
|
647
783
|
*/
|
|
648
784
|
stopReason?: GenerateStopReason;
|
|
649
785
|
/** Verbatim provider finish/stop reason for the turn's terminal model call. */
|
|
@@ -198,3 +198,33 @@ export declare function createValidationSummary(result: EnhancedValidationResult
|
|
|
198
198
|
* Check if validation result has only warnings (no errors)
|
|
199
199
|
*/
|
|
200
200
|
export declare function hasOnlyWarnings(result: EnhancedValidationResult): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Default bound on a `beforeStep` callback (ms).
|
|
203
|
+
*
|
|
204
|
+
* A boundary callback sits between two model calls, in the one place no other
|
|
205
|
+
* timer in the turn is watching: the request deadline has been disposed and
|
|
206
|
+
* the next one is not armed yet. An unbounded callback therefore stalls the
|
|
207
|
+
* turn silently and indefinitely.
|
|
208
|
+
*/
|
|
209
|
+
export declare const DEFAULT_BEFORE_STEP_TIMEOUT_MS = 30000;
|
|
210
|
+
/**
|
|
211
|
+
* Validate an opt-in `executionControl` object and its provider support.
|
|
212
|
+
*
|
|
213
|
+
* Rejecting rather than ignoring is the whole point. A caller that asked for
|
|
214
|
+
* no lifetime ceiling and got one anyway does not find out at call time — it
|
|
215
|
+
* finds out much later, when a long turn dies at a limit its owner believed it
|
|
216
|
+
* had removed, reported as an ordinary cancel. So an unsupported provider is an
|
|
217
|
+
* error here, and so is every shape that could be read two ways.
|
|
218
|
+
*
|
|
219
|
+
* Throws ValidationError; returns void when the control is absent or valid.
|
|
220
|
+
*
|
|
221
|
+
* @param request the sibling options on the SAME call that this control has to
|
|
222
|
+
* be read together with. `turnTimeoutMs`, because a combination in which one
|
|
223
|
+
* of the two ceilings would be silently discarded is rejected instead; and
|
|
224
|
+
* `toolTimeoutMs`, because removing the turn's ceiling makes the per-tool
|
|
225
|
+
* deadline the last thing watching a tool that never returns.
|
|
226
|
+
*/
|
|
227
|
+
export declare function validateExecutionControl(control: unknown, providerName: string, supported: boolean, request?: {
|
|
228
|
+
turnTimeoutMs?: unknown;
|
|
229
|
+
toolTimeoutMs?: unknown;
|
|
230
|
+
}): void;
|
|
@@ -978,3 +978,104 @@ export function createValidationSummary(result) {
|
|
|
978
978
|
export function hasOnlyWarnings(result) {
|
|
979
979
|
return result.errors.length === 0 && result.warnings.length > 0;
|
|
980
980
|
}
|
|
981
|
+
// ============================================================================
|
|
982
|
+
// EXECUTION CONTROL
|
|
983
|
+
// ============================================================================
|
|
984
|
+
/**
|
|
985
|
+
* Default bound on a `beforeStep` callback (ms).
|
|
986
|
+
*
|
|
987
|
+
* A boundary callback sits between two model calls, in the one place no other
|
|
988
|
+
* timer in the turn is watching: the request deadline has been disposed and
|
|
989
|
+
* the next one is not armed yet. An unbounded callback therefore stalls the
|
|
990
|
+
* turn silently and indefinitely.
|
|
991
|
+
*/
|
|
992
|
+
export const DEFAULT_BEFORE_STEP_TIMEOUT_MS = 30_000;
|
|
993
|
+
function isFinitePositive(value) {
|
|
994
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Validate an opt-in `executionControl` object and its provider support.
|
|
998
|
+
*
|
|
999
|
+
* Rejecting rather than ignoring is the whole point. A caller that asked for
|
|
1000
|
+
* no lifetime ceiling and got one anyway does not find out at call time — it
|
|
1001
|
+
* finds out much later, when a long turn dies at a limit its owner believed it
|
|
1002
|
+
* had removed, reported as an ordinary cancel. So an unsupported provider is an
|
|
1003
|
+
* error here, and so is every shape that could be read two ways.
|
|
1004
|
+
*
|
|
1005
|
+
* Throws ValidationError; returns void when the control is absent or valid.
|
|
1006
|
+
*
|
|
1007
|
+
* @param request the sibling options on the SAME call that this control has to
|
|
1008
|
+
* be read together with. `turnTimeoutMs`, because a combination in which one
|
|
1009
|
+
* of the two ceilings would be silently discarded is rejected instead; and
|
|
1010
|
+
* `toolTimeoutMs`, because removing the turn's ceiling makes the per-tool
|
|
1011
|
+
* deadline the last thing watching a tool that never returns.
|
|
1012
|
+
*/
|
|
1013
|
+
export function validateExecutionControl(control, providerName, supported, request = {}) {
|
|
1014
|
+
const { turnTimeoutMs, toolTimeoutMs } = request;
|
|
1015
|
+
if (control === undefined || control === null) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (!isNonNullObject(control)) {
|
|
1019
|
+
throw new ValidationError("executionControl must be an object", "executionControl", "INVALID_TYPE");
|
|
1020
|
+
}
|
|
1021
|
+
if (!supported) {
|
|
1022
|
+
throw new ValidationError(`executionControl is not supported by provider "${providerName}" — it is implemented only on the native Anthropic stream path. Remove it, or route this turn to anthropic.`, "executionControl", "UNSUPPORTED_PROVIDER", [
|
|
1023
|
+
"Use provider: 'anthropic' for turns that need executionControl",
|
|
1024
|
+
"Use turnTimeoutMs / maxSteps for other providers",
|
|
1025
|
+
]);
|
|
1026
|
+
}
|
|
1027
|
+
const opts = control;
|
|
1028
|
+
if (!isFinitePositive(opts.requestTimeoutMs)) {
|
|
1029
|
+
throw new ValidationError(`executionControl.requestTimeoutMs is required and must be a finite positive number of milliseconds (received ${String(opts.requestTimeoutMs)}). It is the deadline that bounds a stalled upstream even when the turn has no lifetime ceiling, so there is no valid turn without it.`, "executionControl.requestTimeoutMs", "INVALID_VALUE");
|
|
1030
|
+
}
|
|
1031
|
+
// `null` is a value here, not an omission: it is the only way to say "no
|
|
1032
|
+
// lifetime timer at all", which no number can express — a very large one is
|
|
1033
|
+
// still a ceiling that fires mid-turn.
|
|
1034
|
+
if (opts.lifetimeTimeoutMs !== undefined &&
|
|
1035
|
+
opts.lifetimeTimeoutMs !== null &&
|
|
1036
|
+
!isFinitePositive(opts.lifetimeTimeoutMs)) {
|
|
1037
|
+
throw new ValidationError(`executionControl.lifetimeTimeoutMs must be null (no lifetime ceiling), a finite positive number of milliseconds, or absent (inherit the legacy timeout handling). Received ${String(opts.lifetimeTimeoutMs)}.`, "executionControl.lifetimeTimeoutMs", "INVALID_VALUE");
|
|
1038
|
+
}
|
|
1039
|
+
// Two whole-turn ceilings, one turn. An explicit `lifetimeTimeoutMs` — a
|
|
1040
|
+
// number or `null` — takes over the turn's lifetime timer completely, so a
|
|
1041
|
+
// `turnTimeoutMs` supplied alongside it was read by nothing at all. Dropping
|
|
1042
|
+
// it silently is the same defect this contract exists to remove, one layer
|
|
1043
|
+
// up: the caller's stated ceiling is discarded and it finds out when the
|
|
1044
|
+
// turn ends somewhere it did not expect.
|
|
1045
|
+
//
|
|
1046
|
+
// Scoped deliberately to the case where the drop happens. `lifetimeTimeoutMs`
|
|
1047
|
+
// ABSENT means "no opinion about the turn's lifetime", and that documented
|
|
1048
|
+
// case inherits `turnTimeoutMs` and honours it — there is nothing to reject.
|
|
1049
|
+
if (opts.lifetimeTimeoutMs !== undefined &&
|
|
1050
|
+
typeof turnTimeoutMs === "number" &&
|
|
1051
|
+
Number.isFinite(turnTimeoutMs) &&
|
|
1052
|
+
turnTimeoutMs > 0) {
|
|
1053
|
+
throw new ValidationError(`turnTimeoutMs (${turnTimeoutMs}) cannot be combined with executionControl.lifetimeTimeoutMs (${String(opts.lifetimeTimeoutMs)}): both set the turn's wall-clock ceiling, and executionControl wins, so the turnTimeoutMs would be ignored. Set exactly one of them.`, "executionControl.lifetimeTimeoutMs", "INVALID_VALUE", [
|
|
1054
|
+
"Drop turnTimeoutMs and express the ceiling as executionControl.lifetimeTimeoutMs",
|
|
1055
|
+
"Or drop executionControl.lifetimeTimeoutMs to inherit turnTimeoutMs unchanged",
|
|
1056
|
+
]);
|
|
1057
|
+
}
|
|
1058
|
+
// No ceiling, and no floor either. `lifetimeTimeoutMs: null` deliberately
|
|
1059
|
+
// arms no turn-level timer, which leaves the per-tool deadline as the only
|
|
1060
|
+
// thing that will ever end a tool that neither returns nor honours its
|
|
1061
|
+
// signal — the step cap does not advance while a tool is in flight, and the
|
|
1062
|
+
// request deadline was disposed when the step settled. `toolTimeoutMs: null`
|
|
1063
|
+
// removes that too, and the turn then has nothing watching it anywhere.
|
|
1064
|
+
//
|
|
1065
|
+
// Refused rather than resolved, because there is no safe way to pick which
|
|
1066
|
+
// of the two the caller meant to keep, and picking one silently is how a
|
|
1067
|
+
// turn ends up bounded by a limit its owner did not choose.
|
|
1068
|
+
if (opts.lifetimeTimeoutMs === null && toolTimeoutMs === null) {
|
|
1069
|
+
throw new ValidationError("executionControl.lifetimeTimeoutMs: null removes the turn's wall-clock ceiling, which leaves toolTimeoutMs as the only bound on a tool that never returns — and toolTimeoutMs: null removes that one too, so the turn would have no bound anywhere. Keep one of them.", "executionControl.lifetimeTimeoutMs", "INVALID_VALUE", [
|
|
1070
|
+
"Give toolTimeoutMs a finite bound, or omit it to take the 300000ms default",
|
|
1071
|
+
"Or give executionControl.lifetimeTimeoutMs a finite ceiling instead of null",
|
|
1072
|
+
]);
|
|
1073
|
+
}
|
|
1074
|
+
if (opts.beforeStep !== undefined && typeof opts.beforeStep !== "function") {
|
|
1075
|
+
throw new ValidationError("executionControl.beforeStep must be a function", "executionControl.beforeStep", "INVALID_TYPE");
|
|
1076
|
+
}
|
|
1077
|
+
if (opts.beforeStepTimeoutMs !== undefined &&
|
|
1078
|
+
!isFinitePositive(opts.beforeStepTimeoutMs)) {
|
|
1079
|
+
throw new ValidationError(`executionControl.beforeStepTimeoutMs must be a finite positive number of milliseconds when supplied. Received ${String(opts.beforeStepTimeoutMs)}.`, "executionControl.beforeStepTimeoutMs", "INVALID_VALUE");
|
|
1080
|
+
}
|
|
1081
|
+
}
|
package/dist/utils/timeout.js
CHANGED
|
@@ -259,12 +259,35 @@ export async function withTimeout(promise, timeout, provider, operation) {
|
|
|
259
259
|
if (!timeoutMs) {
|
|
260
260
|
return promise;
|
|
261
261
|
}
|
|
262
|
+
// The handle is captured, unref'd and cleared — the same shape
|
|
263
|
+
// `createTimeoutPromise` above already uses. `Promise.race` settles on the
|
|
264
|
+
// first outcome but cancels nothing, so an uncaptured timer stayed pending
|
|
265
|
+
// for its full duration after the wrapped promise had already resolved: one
|
|
266
|
+
// live timer per call, each holding the event loop open until it fired. The
|
|
267
|
+
// `finally` clears it the moment the race is decided, which is what makes
|
|
268
|
+
// this safe to wrap around something invoked once per tool call.
|
|
269
|
+
let timeoutHandle;
|
|
262
270
|
const timeoutPromise = new Promise((_, reject) => {
|
|
263
|
-
setTimeout(() => {
|
|
271
|
+
const timer = setTimeout(() => {
|
|
264
272
|
reject(new TimeoutError(`${provider} ${operation} operation timed out after ${timeoutMs}ms`, timeoutMs, provider, operation));
|
|
265
273
|
}, timeoutMs);
|
|
274
|
+
timeoutHandle = timer;
|
|
275
|
+
// Unref the timer so it doesn't keep the process alive (Node.js only)
|
|
276
|
+
if (typeof timer === "object" &&
|
|
277
|
+
timer &&
|
|
278
|
+
"unref" in timer &&
|
|
279
|
+
typeof timer.unref === "function") {
|
|
280
|
+
timer.unref();
|
|
281
|
+
}
|
|
266
282
|
});
|
|
267
|
-
|
|
283
|
+
try {
|
|
284
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
if (timeoutHandle !== undefined) {
|
|
288
|
+
clearTimeout(timeoutHandle);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
268
291
|
}
|
|
269
292
|
/**
|
|
270
293
|
* Wrap a streaming async generator with timeout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.13.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": {
|
|
@@ -236,6 +236,7 @@
|
|
|
236
236
|
"test:bedrock-loop-characterization": "tsx test/continuous-test-suite-bedrock-loop-characterization.ts",
|
|
237
237
|
"test:sagemaker-streaming": "tsx test/continuous-test-suite-sagemaker-streaming.ts",
|
|
238
238
|
"test:anthropic-loop-characterization": "tsx test/continuous-test-suite-anthropic-loop-characterization.ts",
|
|
239
|
+
"test:anthropic-execution-control": "tsx test/continuous-test-suite-anthropic-execution-control.ts",
|
|
239
240
|
"test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
|
|
240
241
|
"test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
|
|
241
242
|
"test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts"
|