@juspay/neurolink 12.49.18 → 12.49.19

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.
@@ -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
  };
@@ -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";
@@ -1663,6 +1663,8 @@ export type TextGenerationOptions = {
1663
1663
  * Text generation result (consolidated from core types)
1664
1664
  */
1665
1665
  export type TextGenerationResult = {
1666
+ /** Ordered tool and host events persisted with the conversation turn. */
1667
+ events?: StreamEventSequence[];
1666
1668
  content: string;
1667
1669
  /** Parsed structured object when a `schema` was requested (see GenerateResult.structuredData). */
1668
1670
  structuredData?: unknown;
@@ -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.19",
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",