@juspay/neurolink 12.49.18 → 12.49.20

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.
@@ -28,6 +28,11 @@ import type { NativeGenerateLoopArgs, NativeGenerateLoopResult, StepResult, Step
28
28
  * how a caller forces a tool on step 3 only. A hook that returns no
29
29
  * `toolChoice` defers to the rule. Nothing else on the hook's result is read.
30
30
  *
31
+ * A forced single-tool choice, from the hook or from the turn, that names a
32
+ * tool outside `declaredTools` is warned about and not sent. A hook's choice
33
+ * is then treated as "no override" and the rule decides; the turn's own
34
+ * choice becomes `"auto"`. Without `declaredTools` nothing is checked.
35
+ *
31
36
  * A hook that THROWS is logged and treated as "no override": the steps
32
37
  * already billed are kept and the rule decides. An abort of the turn while
33
38
  * the hook is pending is not a hook failure and is re-thrown. The two are
@@ -32,6 +32,60 @@ const isForcedToolChoice = (choice) => {
32
32
  }
33
33
  return false;
34
34
  };
35
+ /**
36
+ * The tool a choice forces by name, when it forces one. Both wire mappers
37
+ * honour exactly `{ type: "tool", toolName }` with a non-empty name, so
38
+ * that is the only shape that names a function on the wire.
39
+ */
40
+ const forcedToolName = (choice) => {
41
+ if (typeof choice !== "object" || choice === null) {
42
+ return undefined;
43
+ }
44
+ const { type, toolName } = choice;
45
+ return type === "tool" && typeof toolName === "string" && toolName !== ""
46
+ ? toolName
47
+ : undefined;
48
+ };
49
+ /**
50
+ * The name a choice forces when `declaredTools` is known and lacks it, else
51
+ * undefined. Own keys only: the live record may have a null prototype or be
52
+ * keyed by a name such as "constructor".
53
+ */
54
+ const undeclaredForcedToolName = (choice, declaredTools) => {
55
+ if (!declaredTools) {
56
+ return undefined;
57
+ }
58
+ const name = forcedToolName(choice);
59
+ return name !== undefined && !Object.hasOwn(declaredTools, name)
60
+ ? name
61
+ : undefined;
62
+ };
63
+ /**
64
+ * A forced choice for a tool the request does not declare is dropped for the
65
+ * step rather than sent, so a stale hook or a platform-gated tool degrades
66
+ * the turn to the model's own choice instead of failing it with a 400.
67
+ */
68
+ const warnUndeclaredForcedTool = (toolName, step, source) => {
69
+ logger.warn(source === "prepareStep"
70
+ ? `prepareStep forced tool "${toolName}" at step ${step}, but the request does not declare it; ignoring the override for this step`
71
+ : `toolChoice forces tool "${toolName}" at step ${step}, but the request does not declare it; using "auto" for this step`, { toolName, step, source });
72
+ };
73
+ /**
74
+ * The names this request actually carries, in the record shape
75
+ * `declaredTools` takes. The loop sends `args.tools`, built once by the
76
+ * caller; `args.toolsRecord` can hold more (discovery hydrates into it, and
77
+ * this loop never re-declares), so only `tools` answers "is it offered".
78
+ * Null prototype: a tool may be named `__proto__` or `constructor`.
79
+ */
80
+ const declaredToolRecord = (tools) => {
81
+ const declared = Object.create(null);
82
+ for (const tool of tools ?? []) {
83
+ if (typeof tool.name === "string") {
84
+ declared[tool.name] = true;
85
+ }
86
+ }
87
+ return declared;
88
+ };
35
89
  /**
36
90
  * `toolChoiceSteps` is a count of leading steps, so anything but a
37
91
  * non-negative integer is meaningless. Fall back to the default with a WARN
@@ -127,6 +181,11 @@ const abortReason = (signal) => {
127
181
  * how a caller forces a tool on step 3 only. A hook that returns no
128
182
  * `toolChoice` defers to the rule. Nothing else on the hook's result is read.
129
183
  *
184
+ * A forced single-tool choice, from the hook or from the turn, that names a
185
+ * tool outside `declaredTools` is warned about and not sent. A hook's choice
186
+ * is then treated as "no override" and the rule decides; the turn's own
187
+ * choice becomes `"auto"`. Without `declaredTools` nothing is checked.
188
+ *
130
189
  * A hook that THROWS is logged and treated as "no override": the steps
131
190
  * already billed are kept and the rule decides. An abort of the turn while
132
191
  * the hook is pending is not a hook failure and is re-thrown. The two are
@@ -156,16 +215,27 @@ export async function resolveStepToolChoice(input) {
156
215
  }
157
216
  warnIgnoredPrepareStepFields(prepared);
158
217
  if (prepared?.toolChoice !== undefined) {
159
- return prepared.toolChoice;
218
+ const undeclaredPrepared = undeclaredForcedToolName(prepared.toolChoice, input.declaredTools);
219
+ if (undeclaredPrepared === undefined) {
220
+ return prepared.toolChoice;
221
+ }
222
+ warnUndeclaredForcedTool(undeclaredPrepared, input.step, "prepareStep");
160
223
  }
161
224
  }
162
225
  if (input.base === undefined) {
163
226
  return undefined;
164
227
  }
165
- if (!isForcedToolChoice(input.base) ||
166
- input.step < normalizeToolChoiceSteps(input.toolChoiceSteps)) {
228
+ if (!isForcedToolChoice(input.base)) {
229
+ return input.base;
230
+ }
231
+ if (input.step >= normalizeToolChoiceSteps(input.toolChoiceSteps)) {
232
+ return "auto";
233
+ }
234
+ const undeclaredBase = undeclaredForcedToolName(input.base, input.declaredTools);
235
+ if (undeclaredBase === undefined) {
167
236
  return input.base;
168
237
  }
238
+ warnUndeclaredForcedTool(undeclaredBase, input.step, "base");
169
239
  return "auto";
170
240
  }
171
241
  /**
@@ -356,6 +426,7 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
356
426
  steps: stepRecords,
357
427
  maxSteps: args.maxSteps,
358
428
  model: args.modelId,
429
+ declaredTools: declaredToolRecord(args.tools),
359
430
  ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
360
431
  })
361
432
  : undefined;
@@ -66,6 +66,8 @@ export declare class NeuroLink {
66
66
  private mcpSkipped;
67
67
  private mcpInitPromise;
68
68
  private emitter;
69
+ /** Request ownership for conversation capture; unscoped emitter events are ignored. */
70
+ private readonly conversationEventContext;
69
71
  /**
70
72
  * Process-unique id used to attribute this instance's log events. The SDK
71
73
  * entry points run their bodies inside `logger.runInInstanceScope(this.logInstanceId, …)`,
@@ -1047,6 +1049,7 @@ export declare class NeuroLink {
1047
1049
  */
1048
1050
  private generateTextInternal;
1049
1051
  private executeGenerateTextInternalWithSpan;
1052
+ private executeGenerateTextInternalInEventScope;
1050
1053
  private initializeGenerateTextInternalContext;
1051
1054
  private runGenerateTextInternalFlow;
1052
1055
  private captureOriginalConversationMessagesForRecovery;
@@ -1196,6 +1199,8 @@ export declare class NeuroLink {
1196
1199
  * @throws {Error} When conversation memory operations fail (if enabled)
1197
1200
  */
1198
1201
  stream(options: StreamOptions | DynamicOptions): Promise<StreamResult>;
1202
+ /** Re-enter request ownership when the caller drives a deferred async iterator. */
1203
+ private bindConversationEventStream;
1199
1204
  /**
1200
1205
  * Body of {@link stream}, always entered through the instance log scope.
1201
1206
  *
package/dist/neurolink.js CHANGED
@@ -370,6 +370,8 @@ export class NeuroLink {
370
370
  mcpSkipped = false;
371
371
  mcpInitPromise = null;
372
372
  emitter = createTypedEmitter();
373
+ /** Request ownership for conversation capture; unscoped emitter events are ignored. */
374
+ conversationEventContext = new AsyncLocalStorage();
373
375
  /**
374
376
  * Process-unique id used to attribute this instance's log events. The SDK
375
377
  * entry points run their bodies inside `logger.runInInstanceScope(this.logInstanceId, …)`,
@@ -4724,6 +4726,7 @@ Current user's request: ${currentInput}`;
4724
4726
  stallTimeoutMs: options.stallTimeoutMs,
4725
4727
  wrapupTimeLeadMs: options.wrapupTimeLeadMs,
4726
4728
  toolTimeoutMs: options.toolTimeoutMs,
4729
+ toolExecutionCapture: options.toolExecutionCapture,
4727
4730
  abortSignal: options.abortSignal,
4728
4731
  skipToolPromptInjection: options.skipToolPromptInjection,
4729
4732
  middleware: options.middleware,
@@ -4946,7 +4949,7 @@ Current user's request: ${currentInput}`;
4946
4949
  responseTime: textResult.responseTime,
4947
4950
  toolsUsed: textResult.toolsUsed,
4948
4951
  toolCalls: textResult.toolCalls ?? [],
4949
- toolExecutions: toToolExecutionRecords(textResult.toolExecutions),
4952
+ toolExecutions: toToolExecutionRecords(textResult.toolExecutions, options.toolExecutionCapture),
4950
4953
  enhancedWithTools: textResult.enhancedWithTools,
4951
4954
  availableTools: transformAvailableTools(textResult.availableTools),
4952
4955
  analytics: textResult.analytics,
@@ -5483,8 +5486,15 @@ Current user's request: ${currentInput}`;
5483
5486
  return tracers.sdk.startActiveSpan("neurolink.generateTextInternal", { kind: SpanKind.INTERNAL }, (internalSpan) => this.executeGenerateTextInternalWithSpan(options, internalSpan));
5484
5487
  }
5485
5488
  async executeGenerateTextInternalWithSpan(options, internalSpan) {
5489
+ return this.conversationEventContext.run(Symbol("generate-conversation"), () => this.executeGenerateTextInternalInEventScope(options, internalSpan));
5490
+ }
5491
+ async executeGenerateTextInternalInEventScope(options, internalSpan) {
5492
+ const { eventSequence, cleanup } = this.setupStreamEventListeners();
5486
5493
  try {
5487
- const context = this.initializeGenerateTextInternalContext(options);
5494
+ const context = {
5495
+ ...this.initializeGenerateTextInternalContext(options),
5496
+ eventSequence,
5497
+ };
5488
5498
  internalSpan.setAttribute("neurolink.request_id", context.requestId);
5489
5499
  internalSpan.setAttribute("neurolink.has_conversation_memory", !!this.conversationMemory);
5490
5500
  internalSpan.setAttribute("neurolink.provider", options.provider || "auto");
@@ -5514,6 +5524,7 @@ Current user's request: ${currentInput}`;
5514
5524
  throw error;
5515
5525
  }
5516
5526
  finally {
5527
+ cleanup();
5517
5528
  internalSpan.end();
5518
5529
  }
5519
5530
  }
@@ -5544,6 +5555,7 @@ Current user's request: ${currentInput}`;
5544
5555
  internalSpan,
5545
5556
  requestId: context.requestId,
5546
5557
  startTime: context.generateInternalStartTime,
5558
+ events: context.eventSequence,
5547
5559
  });
5548
5560
  }
5549
5561
  if (options.abortSignal?.aborted) {
@@ -5559,6 +5571,7 @@ Current user's request: ${currentInput}`;
5559
5571
  internalSpan,
5560
5572
  requestId: context.requestId,
5561
5573
  startTime: context.generateInternalStartTime,
5574
+ events: context.eventSequence,
5562
5575
  });
5563
5576
  }
5564
5577
  catch (error) {
@@ -5586,7 +5599,11 @@ Current user's request: ${currentInput}`;
5586
5599
  : undefined;
5587
5600
  }
5588
5601
  async finalizeGenerateTextInternalResult(params) {
5589
- const { path, result, options, internalSpan, requestId, startTime } = params;
5602
+ const { path, result, options, internalSpan, requestId, startTime, events, } = params;
5603
+ const historyEvents = events?.filter((event) => event.type !== "response:chunk");
5604
+ if (historyEvents?.length) {
5605
+ result.events = historyEvents;
5606
+ }
5590
5607
  logger.info(`[NeuroLink.generateTextInternal] generate() - COMPLETE SUCCESS${path === "mcp" ? " (MCP path)" : ""}`, {
5591
5608
  provider: result.provider,
5592
5609
  model: result.model,
@@ -7347,7 +7364,36 @@ Current user's request: ${currentInput}`;
7347
7364
  * @throws {Error} When conversation memory operations fail (if enabled)
7348
7365
  */
7349
7366
  async stream(options) {
7350
- return logger.runInInstanceScope(this.logInstanceId, () => this.streamInInstanceScope(options));
7367
+ const eventScope = Symbol("stream-conversation");
7368
+ return this.conversationEventContext.run(eventScope, () => logger.runInInstanceScope(this.logInstanceId, async () => {
7369
+ const result = await this.streamInInstanceScope(options);
7370
+ return preserveLiveStreamAccessors(result, {
7371
+ ...result,
7372
+ stream: this.bindConversationEventStream(result.stream, eventScope),
7373
+ });
7374
+ }));
7375
+ }
7376
+ /** Re-enter request ownership when the caller drives a deferred async iterator. */
7377
+ bindConversationEventStream(stream, eventScope) {
7378
+ const scope = this.conversationEventContext;
7379
+ return (async function* () {
7380
+ const iterator = stream[Symbol.asyncIterator]();
7381
+ try {
7382
+ while (true) {
7383
+ const next = await scope.run(eventScope, () => iterator.next());
7384
+ if (next.done) {
7385
+ return;
7386
+ }
7387
+ yield next.value;
7388
+ }
7389
+ }
7390
+ finally {
7391
+ const close = iterator.return?.bind(iterator);
7392
+ if (close) {
7393
+ await scope.run(eventScope, () => close());
7394
+ }
7395
+ }
7396
+ })();
7351
7397
  }
7352
7398
  /**
7353
7399
  * Body of {@link stream}, always entered through the instance log scope.
@@ -7909,7 +7955,11 @@ Current user's request: ${currentInput}`;
7909
7955
  // (Finding 1 fix). It is cleared by setToolRoutingServers() when the
7910
7956
  // catalog changes so stale vectors are never used after an update.
7911
7957
  let routingEmbedFn;
7912
- const embeddingCfg = routingConfig.embedding;
7958
+ // A host-owned routing callback owns every inference account. The
7959
+ // instance's embedding fast-path would otherwise borrow turn credentials.
7960
+ const embeddingCfg = routingConfig.generateFn
7961
+ ? undefined
7962
+ : routingConfig.embedding;
7913
7963
  if (embeddingCfg?.enabled === true) {
7914
7964
  try {
7915
7965
  // Resolve the embedding provider: use the explicitly configured one
@@ -7959,7 +8009,7 @@ Current user's request: ${currentInput}`;
7959
8009
  timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
7960
8010
  // Forward the abort signal so a cancelled turn aborts the router
7961
8011
  // call promptly instead of waiting out the routing timeout.
7962
- generateFn: (generateOptions) => this.preservingTurnState(() => this.generate({
8012
+ generateFn: (generateOptions) => this.preservingTurnState(() => (routingConfig.generateFn ?? ((input) => this.generate(input)))({
7963
8013
  ...generateOptions,
7964
8014
  abortSignal: options.abortSignal,
7965
8015
  })),
@@ -7967,7 +8017,11 @@ Current user's request: ${currentInput}`;
7967
8017
  // configured. siteDecide returns null without one, so the resolver
7968
8018
  // falls straight through to the generative router as before. The
7969
8019
  // outer request's credentials, abort signal and ids ride along.
7970
- decideFn: (decisionOptions) => this.siteDecide(decisionOptions, this.decisionSiteContext(options, sessionId || undefined)),
8020
+ // A host-owned routing callback owns the inference account too.
8021
+ // Bypass the instance's decision fast-path in that explicit mode.
8022
+ decideFn: routingConfig.generateFn
8023
+ ? undefined
8024
+ : (decisionOptions) => this.siteDecide(decisionOptions, this.decisionSiteContext(options, sessionId || undefined)),
7971
8025
  decisionMinDropConfidence: routingConfig.minDropConfidence,
7972
8026
  emitDecision: captureDecision,
7973
8027
  // L2 / ITEM D — only populated when embedding is configured.
@@ -8948,7 +9002,30 @@ Current user's request: ${currentInput}`;
8948
9002
  setupStreamEventListeners() {
8949
9003
  const eventSequence = [];
8950
9004
  let eventSeqCounter = 0;
8951
- const captureEvent = (type, data) => {
9005
+ const eventScope = this.conversationEventContext.getStore();
9006
+ const ownedConfirmations = new Set();
9007
+ const confirmationId = (event) => {
9008
+ if (!event ||
9009
+ typeof event !== "object" ||
9010
+ !("payload" in event) ||
9011
+ !event.payload ||
9012
+ typeof event.payload !== "object" ||
9013
+ !("confirmationId" in event.payload) ||
9014
+ typeof event.payload.confirmationId !== "string") {
9015
+ return undefined;
9016
+ }
9017
+ return event.payload.confirmationId;
9018
+ };
9019
+ const captureEvent = (type, data, ownedConfirmation) => {
9020
+ // Never infer ownership from whichever listeners happen to be active.
9021
+ // This also fails closed where async context propagation is unavailable.
9022
+ const belongsToTurn = eventScope !== undefined &&
9023
+ this.conversationEventContext.getStore() === eventScope;
9024
+ const belongsToConfirmation = ownedConfirmation !== undefined &&
9025
+ ownedConfirmations.has(ownedConfirmation);
9026
+ if (!belongsToTurn && !belongsToConfirmation) {
9027
+ return;
9028
+ }
8952
9029
  eventSequence.push({
8953
9030
  type,
8954
9031
  seq: eventSeqCounter++,
@@ -8990,19 +9067,46 @@ Current user's request: ${currentInput}`;
8990
9067
  });
8991
9068
  }
8992
9069
  };
9070
+ const onHostConversationEvent = (...args) => {
9071
+ const event = args[0];
9072
+ if (event &&
9073
+ typeof event === "object" &&
9074
+ "type" in event &&
9075
+ typeof event.type === "string" &&
9076
+ "data" in event &&
9077
+ event.data &&
9078
+ typeof event.data === "object" &&
9079
+ !Array.isArray(event.data)) {
9080
+ captureEvent(event.type, { data: event.data });
9081
+ }
9082
+ };
8993
9083
  const onUIComponent = (...args) => {
8994
9084
  captureEvent("ui-component", args[0]);
8995
9085
  };
8996
9086
  const onHITLRequest = (...args) => {
8997
- captureEvent("hitl:confirmation-request", args[0]);
9087
+ if (eventScope !== undefined &&
9088
+ this.conversationEventContext.getStore() === eventScope) {
9089
+ const id = confirmationId(args[0]);
9090
+ if (id !== undefined) {
9091
+ ownedConfirmations.add(id);
9092
+ }
9093
+ captureEvent("hitl:confirmation-request", args[0]);
9094
+ }
8998
9095
  };
8999
9096
  const onHITLResponse = (...args) => {
9000
- captureEvent("hitl:confirmation-response", args[0]);
9097
+ const id = confirmationId(args[0]);
9098
+ // A host answers from its own HTTP/async context. The issued confirmation
9099
+ // id carries ownership without admitting arbitrary global emitter data.
9100
+ if (id !== undefined && ownedConfirmations.has(id)) {
9101
+ captureEvent("hitl:confirmation-response", args[0], id);
9102
+ ownedConfirmations.delete(id);
9103
+ }
9001
9104
  };
9002
9105
  this.emitter.on("response:chunk", onResponseChunk);
9003
9106
  this.emitter.on("tool:start", onToolStart);
9004
9107
  this.emitter.on("tool:end", onToolEnd);
9005
9108
  this.emitter.on("ui-component", onUIComponent);
9109
+ this.emitter.on("host:conversation-event", onHostConversationEvent);
9006
9110
  this.emitter.on("hitl:confirmation-request", onHITLRequest);
9007
9111
  this.emitter.on("hitl:confirmation-response", onHITLResponse);
9008
9112
  const cleanup = () => {
@@ -9010,6 +9114,7 @@ Current user's request: ${currentInput}`;
9010
9114
  this.emitter.off("tool:start", onToolStart);
9011
9115
  this.emitter.off("tool:end", onToolEnd);
9012
9116
  this.emitter.off("ui-component", onUIComponent);
9117
+ this.emitter.off("host:conversation-event", onHostConversationEvent);
9013
9118
  this.emitter.off("hitl:confirmation-request", onHITLRequest);
9014
9119
  this.emitter.off("hitl:confirmation-response", onHITLResponse);
9015
9120
  };
@@ -2008,6 +2008,7 @@ export class AnthropicProvider extends BaseProvider {
2008
2008
  steps: stepRecords,
2009
2009
  maxSteps,
2010
2010
  model: modelId,
2011
+ declaredTools: toolsRecord,
2011
2012
  ...(abortSignal ? { abortSignal } : {}),
2012
2013
  })))
2013
2014
  : undefined;
@@ -1741,6 +1741,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1741
1741
  steps,
1742
1742
  maxSteps,
1743
1743
  model: modelId,
1744
+ declaredTools: toolsRecord,
1744
1745
  ...(abortSignal ? { abortSignal } : {}),
1745
1746
  }), wireNameMaps?.toWire)
1746
1747
  : undefined,
@@ -7,7 +7,7 @@
7
7
  import type { z } from "zod";
8
8
  import type { AIProviderName } from "../constants/enums.js";
9
9
  import type { TokenUsage } from "./analytics.js";
10
- import type { GenerateStopReason, ToolExecutionRecord } from "./generate.js";
10
+ import type { GenerateStopReason, ToolExecutionRecord, ToolExecutionCaptureOptions } from "./generate.js";
11
11
  /**
12
12
  * Agent definition for creating agents in the network
13
13
  */
@@ -86,6 +86,20 @@ export type AgentExecutionOptions = {
86
86
  enableAnalytics?: boolean;
87
87
  /** Cumulative budget for the NeuroLink instance, not a per-run cap. */
88
88
  maxBudgetUsd?: number;
89
+ /** Per-turn tool opt-out; cannot expand the agent tool scope. */
90
+ disableTools?: boolean;
91
+ /** Provider region override for this turn. */
92
+ region?: string;
93
+ /** Output bound for this turn. */
94
+ maxTokens?: number;
95
+ /** Opt out of session history and user-memory reads/writes for this turn, retaining stored state. */
96
+ useMemory?: boolean;
97
+ /** Conversation memory identity; never included in the model prompt. */
98
+ sessionId?: string;
99
+ /** Refuse SDK-internal provider/model fallback for this execution. */
100
+ disableInternalFallback?: boolean;
101
+ /** Bounds and callback for the underlying SDK's tool execution capture. */
102
+ toolExecutionCapture?: ToolExecutionCaptureOptions;
89
103
  /** Additional context for the agent */
90
104
  context?: Record<string, unknown>;
91
105
  /** Override max steps for this execution */
@@ -493,6 +507,12 @@ export type AgentStreamChunk = {
493
507
  usage?: TokenUsage;
494
508
  /** Duration in ms (for complete chunks) */
495
509
  duration?: number;
510
+ /** Complete-turn status; errors remain failures even after partial text. */
511
+ status?: AgentResult["status"];
512
+ /** Final, bounded per-call records in the same shape as AgentResult. */
513
+ toolExecutions?: ToolExecutionRecord[];
514
+ /** Terminal reason from the underlying stream. */
515
+ stopReason?: GenerateStopReason;
496
516
  /** Error message (for error chunks) */
497
517
  error?: string;
498
518
  /** Tool name (for tool chunks) */
@@ -93,6 +93,11 @@ export type InternalStreamEvent = {
93
93
  * Flexible type to support both typed and legacy event patterns
94
94
  */
95
95
  export type NeuroLinkEvents = {
96
+ /** Application-owned events captured in ordered conversation history. */
97
+ "host:conversation-event": {
98
+ type: string;
99
+ data: Record<string, unknown>;
100
+ };
96
101
  "tool:start": unknown;
97
102
  "tool:end": unknown;
98
103
  "stream:start": unknown;
@@ -7,7 +7,7 @@ import type { ClaudeLimitSnapshot } from "./subscription.js";
7
7
  import type { JsonValue } from "./common.js";
8
8
  import type { TerminalAgentModeOption, TerminalAgentModeVersion } from "./agentMode.js";
9
9
  import type { Content, ImageWithAltText } from "./content.js";
10
- import type { ChatMessage, ConversationMemoryConfig, ToolReplayMode } from "./conversation.js";
10
+ import type { ChatMessage, ConversationMemoryConfig, StreamEventSequence, ToolReplayMode } from "./conversation.js";
11
11
  import type { MemoryCallOptions } from "./memory.js";
12
12
  import type { EvaluationData } from "./evaluation.js";
13
13
  import type { MiddlewareFactoryOptions, OnFinishCallback, OnErrorCallback } from "./middleware.js";
@@ -584,6 +584,10 @@ export type GenerateOptions = {
584
584
  * compel a tool call on every step, and the loop would only end when
585
585
  * `maxSteps` ran out. `"auto"` and `"none"` are applied unchanged on every
586
586
  * step. Vertex, Google AI Studio and Bedrock do not honour `toolChoice`.
587
+ *
588
+ * A named tool that the request does not declare is not sent, because the
589
+ * provider would reject the request: a warning is logged and the step uses
590
+ * `"auto"`.
587
591
  */
588
592
  toolChoice?: ToolChoice<Record<string, Tool>>;
589
593
  /**
@@ -606,7 +610,9 @@ export type GenerateOptions = {
606
610
  * Optional callback that runs before each step in a multi-step generation.
607
611
  *
608
612
  * Honoured field of the result: `toolChoice`, applied to that step only
609
- * and taking precedence over `toolChoice` / `toolChoiceSteps`.
613
+ * and taking precedence over `toolChoice` / `toolChoiceSteps`. A named tool
614
+ * the request does not declare is ignored with a warning, as if the hook had
615
+ * returned no `toolChoice`.
610
616
  *
611
617
  * Not honoured — accepted for source compatibility with the former Vercel
612
618
  * AI SDK `experimental_prepareStep` shape, but ignored by every native
@@ -1663,6 +1669,8 @@ export type TextGenerationOptions = {
1663
1669
  * Text generation result (consolidated from core types)
1664
1670
  */
1665
1671
  export type TextGenerationResult = {
1672
+ /** Ordered tool and host events persisted with the conversation turn. */
1673
+ events?: StreamEventSequence[];
1666
1674
  content: string;
1667
1675
  /** Parsed structured object when a `schema` was requested (see GenerateResult.structuredData). */
1668
1676
  structuredData?: unknown;
@@ -1797,6 +1805,17 @@ export type StepToolChoiceInput = {
1797
1805
  model: string;
1798
1806
  /** The turn's abort signal; a pending `prepareStep` is released when it fires. */
1799
1807
  abortSignal?: AbortSignal;
1808
+ /**
1809
+ * The tools this request declares, keyed by SDK-side tool name. When given,
1810
+ * a forced single-tool choice naming a tool outside it is not sent: a
1811
+ * provider answers a request that forces a function its own tools list
1812
+ * lacks with a 400. A loop that re-declares hydrated tools in the same
1813
+ * step (the streaming loops) passes the live record, because discovery
1814
+ * hydrates tools into it between steps; a loop that sends a fixed tools
1815
+ * array (the shared generate loop) passes the names of that array.
1816
+ * Absent means the choice is not checked.
1817
+ */
1818
+ declaredTools?: Readonly<Record<string, unknown>>;
1800
1819
  };
1801
1820
  /**
1802
1821
  * Inputs to the shared native generate loop (`core/nativeGenerateLoop.ts`).
@@ -106,6 +106,12 @@ export type ToolRoutingEmbeddingConfig = {
106
106
  };
107
107
  /** Constructor-level configuration for pre-call tool routing. */
108
108
  export type ToolRoutingConfig = {
109
+ /**
110
+ * Host-owned generative routing, independent of main-turn credentials.
111
+ * When provided, instance decision and embedding fast-paths are bypassed;
112
+ * routing selection is performed by this callback.
113
+ */
114
+ generateFn?: (options: GenerateOptions) => Promise<GenerateResult>;
109
115
  /** Master switch. Routing runs only when true AND the server catalog is non-empty. */
110
116
  enabled: boolean;
111
117
  /**
@@ -384,7 +384,7 @@ export async function storeConversationTurn(conversationMemory, originalOptions,
384
384
  providerDetails,
385
385
  enableSummarization: originalOptions.enableSummarization,
386
386
  requestId,
387
- events: toolActivityEvents,
387
+ events: result.events?.length ? result.events : toolActivityEvents,
388
388
  ...(skillMessages && skillMessages.length > 0
389
389
  ? { skillMessages }
390
390
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.49.18",
3
+ "version": "12.49.20",
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": {
@@ -75,6 +75,8 @@
75
75
  "test": "pnpm exec tsx test/continuous-test-suite.ts",
76
76
  "test:agent-delegation": "pnpm exec tsx test/continuous-test-suite-agent-delegation.ts",
77
77
  "test:agent-tasks": "pnpm exec tsx test/continuous-test-suite-agent-tasks.ts",
78
+ "test:agent:session-stream": "pnpm exec tsx test/continuous-test-suite-agent-session-stream.ts",
79
+ "test:agent:platform-router": "pnpm exec tsx test/continuous-test-suite-agent-platform-router.ts",
78
80
  "test:artifact-banking": "pnpm exec tsx test/continuous-test-suite-artifact-banking.ts",
79
81
  "test:background-commands": "pnpm exec tsx test/continuous-test-suite-background-commands.ts",
80
82
  "test:client": "pnpm exec tsx test/continuous-test-suite-client.ts",
@@ -110,6 +112,7 @@
110
112
  "test:agent:cost": "pnpm exec tsx test/continuous-test-suite-agent-cost.ts",
111
113
  "test:adjust-body-after-400": "pnpm exec tsx test/continuous-test-suite-adjust-body-after-400.ts",
112
114
  "test:zod3-schema-native-path": "pnpm exec tsx test/continuous-test-suite-zod3-schema-native-path.ts",
115
+ "test:forced-tool-declared": "pnpm exec tsx test/continuous-test-suite-forced-tool-declared.ts",
113
116
  "test:error-classification-e2e": "pnpm exec tsx test/continuous-test-suite-error-classification-e2e.ts",
114
117
  "test:error-classifier-contract": "pnpm exec tsx test/continuous-test-suite-error-classifier-contract.ts",
115
118
  "test:bedrock-inference-profile": "pnpm exec tsx test/continuous-test-suite-bedrock-inference-profile.ts",