@juspay/neurolink 9.59.0 → 9.59.1

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.
@@ -5,13 +5,41 @@
5
5
  * Enhanced AI provider system with natural MCP tool access.
6
6
  * Uses real MCP infrastructure for tool discovery and execution.
7
7
  */
8
- import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult } from "./types/index.js";
8
+ import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext } from "./types/index.js";
9
9
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
10
10
  import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
11
11
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
12
12
  import { MCPToolRegistry } from "./mcp/toolRegistry.js";
13
13
  import type { DynamicOptions } from "./types/index.js";
14
14
  import { TaskManager } from "./tasks/taskManager.js";
15
+ /**
16
+ * Curator P2-4 dedup (concurrency-safe): native providers emit
17
+ * `generation:end` on the shared SDK emitter. We attach a fresh
18
+ * mutable `dedupContext` object directly to the per-call
19
+ * `StreamOptions` (under `_streamDedupContext`) so each stream gets
20
+ * its own instance — concurrent streams have different option objects
21
+ * and therefore different contexts, so they cannot interfere.
22
+ *
23
+ * Native provider emit sites read `options._streamDedupContext` and
24
+ * flip `.providerEmitted = true` before emitting; the orchestration's
25
+ * finally block reads the same closed-over reference and skips its
26
+ * own emit when the flag is set.
27
+ *
28
+ * This avoids the AsyncLocalStorage approach which doesn't reliably
29
+ * propagate through async-generator yield boundaries when iteration
30
+ * happens from outside the original `run()` scope (e.g. when the
31
+ * consumer drives `for await of result.stream` after `sdk.stream(...)`
32
+ * returns).
33
+ */
34
+ export declare const STREAM_DEDUP_CONTEXT_KEY: "_streamDedupContext";
35
+ /**
36
+ * Native providers call this from their `generation:end` emit sites,
37
+ * passing the same `options` object they received. Safe no-op when
38
+ * the field isn't set.
39
+ */
40
+ export declare function markStreamProviderEmittedGenerationEnd(options: {
41
+ _streamDedupContext?: StreamGenerationEndContext;
42
+ } | undefined): void;
15
43
  export declare class NeuroLink {
16
44
  private mcpInitialized;
17
45
  private mcpSkipped;
@@ -297,6 +297,37 @@ function isNonRetryableProviderError(error) {
297
297
  * same NeuroLink instance would clobber each other's trace context.
298
298
  */
299
299
  const metricsTraceContextStorage = new AsyncLocalStorage();
300
+ /**
301
+ * Curator P2-4 dedup (concurrency-safe): native providers emit
302
+ * `generation:end` on the shared SDK emitter. We attach a fresh
303
+ * mutable `dedupContext` object directly to the per-call
304
+ * `StreamOptions` (under `_streamDedupContext`) so each stream gets
305
+ * its own instance — concurrent streams have different option objects
306
+ * and therefore different contexts, so they cannot interfere.
307
+ *
308
+ * Native provider emit sites read `options._streamDedupContext` and
309
+ * flip `.providerEmitted = true` before emitting; the orchestration's
310
+ * finally block reads the same closed-over reference and skips its
311
+ * own emit when the flag is set.
312
+ *
313
+ * This avoids the AsyncLocalStorage approach which doesn't reliably
314
+ * propagate through async-generator yield boundaries when iteration
315
+ * happens from outside the original `run()` scope (e.g. when the
316
+ * consumer drives `for await of result.stream` after `sdk.stream(...)`
317
+ * returns).
318
+ */
319
+ export const STREAM_DEDUP_CONTEXT_KEY = "_streamDedupContext";
320
+ /**
321
+ * Native providers call this from their `generation:end` emit sites,
322
+ * passing the same `options` object they received. Safe no-op when
323
+ * the field isn't set.
324
+ */
325
+ export function markStreamProviderEmittedGenerationEnd(options) {
326
+ const ctx = options?._streamDedupContext;
327
+ if (ctx) {
328
+ ctx.providerEmitted = true;
329
+ }
330
+ }
300
331
  export class NeuroLink {
301
332
  mcpInitialized = false;
302
333
  mcpSkipped = false;
@@ -4984,8 +5015,23 @@ Current user's request: ${currentInput}`;
4984
5015
  const streamStartTime = Date.now();
4985
5016
  const sessionId = enhancedOptions.context
4986
5017
  ?.sessionId;
5018
+ // Curator P2-4 dedup (concurrency-safe): native provider stream paths
5019
+ // (Gemini 3 on Vertex / Google AI Studio) emit `generation:end`
5020
+ // themselves. We attach a per-stream mutable flag directly to
5021
+ // `enhancedOptions._streamDedupContext` — native providers receive
5022
+ // these options and flip the flag before their emit; this finally
5023
+ // block reads the same closed-over reference. Concurrent streams
5024
+ // have different option objects so the contexts don't interfere.
5025
+ const dedupContext = {
5026
+ providerEmitted: false,
5027
+ };
5028
+ enhancedOptions._streamDedupContext = dedupContext;
4987
5029
  const processedStream = (async function* () {
4988
5030
  let streamError;
5031
+ // Curator P2-4: hoist `resolvedUsage` so the finally block can emit a
5032
+ // single `generation:end` event with cost data. Cost listeners
5033
+ // subscribe here; previously the stream path never fired it.
5034
+ let resolvedUsage;
4989
5035
  try {
4990
5036
  for await (const chunk of mcpStream) {
4991
5037
  chunkCount++;
@@ -5015,7 +5061,7 @@ Current user's request: ${currentInput}`;
5015
5061
  accumulatedContent += content;
5016
5062
  });
5017
5063
  }
5018
- let resolvedUsage = streamUsage;
5064
+ resolvedUsage = streamUsage;
5019
5065
  if (!resolvedUsage && streamAnalytics) {
5020
5066
  try {
5021
5067
  const resolved = await Promise.resolve(streamAnalytics);
@@ -5090,6 +5136,61 @@ Current user's request: ${currentInput}`;
5090
5136
  guardrailsBlocked: metadata.guardrailsBlocked,
5091
5137
  error: metadata.error,
5092
5138
  });
5139
+ // Curator P2-4: emit `generation:end` exactly once per stream so
5140
+ // cost listeners receive the same contract as for `generate()`.
5141
+ // The previous implementation only fired `stream:complete`, leaving
5142
+ // any subscriber to `generation:end` with zero events.
5143
+ //
5144
+ // Dedup: native provider stream paths (Gemini 3 on Vertex / Google
5145
+ // AI Studio) already emit `generation:end` themselves so Pipeline B
5146
+ // (Langfuse) records a GENERATION observation. Skip our emit when
5147
+ // they already fired — preserves their Pipeline B observation
5148
+ // source and keeps the "exactly once" contract. Per-stream flag
5149
+ // is concurrency-safe because it's scoped via AsyncLocalStorage.
5150
+ if (!dedupContext.providerEmitted) {
5151
+ try {
5152
+ const finalProvider = metadata.fallbackProvider ?? providerName ?? "unknown";
5153
+ const finalModel = metadata.fallbackModel ??
5154
+ streamModel ??
5155
+ enhancedOptions.model ??
5156
+ "unknown";
5157
+ const finalFinishReason = streamError
5158
+ ? "error"
5159
+ : (streamState.finishReason ?? "stop");
5160
+ self.emitter.emit("generation:end", {
5161
+ provider: finalProvider,
5162
+ model: finalModel,
5163
+ responseTime: Date.now() - streamStartTime,
5164
+ toolsUsed: streamState.toolCalls?.map((t) => t.toolName),
5165
+ timestamp: Date.now(),
5166
+ result: {
5167
+ content: accumulatedContent,
5168
+ usage: resolvedUsage,
5169
+ model: finalModel,
5170
+ provider: finalProvider,
5171
+ finishReason: finalFinishReason,
5172
+ },
5173
+ prompt: enhancedOptions.input?.text ||
5174
+ enhancedOptions.prompt,
5175
+ temperature: enhancedOptions.temperature,
5176
+ maxTokens: enhancedOptions.maxTokens,
5177
+ success: !streamError,
5178
+ error: streamError
5179
+ ? streamError instanceof Error
5180
+ ? streamError.message
5181
+ : String(streamError)
5182
+ : undefined,
5183
+ pipelineAHandled: true,
5184
+ });
5185
+ }
5186
+ catch (emitError) {
5187
+ logger.debug("[NeuroLink.stream] generation:end listener threw — ignored", {
5188
+ error: emitError instanceof Error
5189
+ ? emitError.message
5190
+ : String(emitError),
5191
+ });
5192
+ }
5193
+ }
5093
5194
  self._disableToolCacheForCurrentRequest = false;
5094
5195
  cleanupListeners();
5095
5196
  streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
@@ -4,6 +4,7 @@ import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enum
4
4
  import { BaseProvider } from "../core/baseProvider.js";
5
5
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
6
6
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
7
+ import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
7
8
  import { SpanStatusCode } from "@opentelemetry/api";
8
9
  import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
9
10
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
@@ -735,6 +736,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
735
736
  // AI SDK so experimental_telemetry is never injected; we emit manually.
736
737
  const nativeStreamEmitter = this.neurolink?.getEventEmitter();
737
738
  if (nativeStreamEmitter) {
739
+ // Curator P2-4 dedup: flag the per-stream context attached
740
+ // to options so the orchestration skips its own emit.
741
+ markStreamProviderEmittedGenerationEnd(options);
738
742
  nativeStreamEmitter.emit("generation:end", {
739
743
  provider: this.providerName,
740
744
  responseTime,
@@ -767,6 +771,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
767
771
  // Emit failure generation:end so Pipeline B records the failed stream
768
772
  const errorEmitter = this.neurolink?.getEventEmitter();
769
773
  if (errorEmitter) {
774
+ // Curator P2-4 dedup: flag the per-stream context attached
775
+ // to options so the orchestration skips its own emit.
776
+ markStreamProviderEmittedGenerationEnd(options);
770
777
  errorEmitter.emit("generation:end", {
771
778
  provider: this.providerName,
772
779
  responseTime: Date.now() - startTime,
@@ -10,6 +10,7 @@ import { ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
10
10
  import { BaseProvider } from "../core/baseProvider.js";
11
11
  import { DEFAULT_MAX_STEPS, GLOBAL_LOCATION_MODELS, } from "../core/constants.js";
12
12
  import { ModelConfigurationManager } from "../core/modelConfiguration.js";
13
+ import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
13
14
  import { createProxyFetch } from "../proxy/proxyFetch.js";
14
15
  import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
15
16
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
@@ -1630,8 +1631,12 @@ export class GoogleVertexProvider extends BaseProvider {
1630
1631
  // Emit generation:end so Pipeline B (Langfuse) creates a GENERATION
1631
1632
  // observation. The native @google/genai stream path on Vertex bypasses the
1632
1633
  // Vercel AI SDK so experimental_telemetry is never injected; we emit manually.
1634
+ // Curator P2-4 dedup: flag the per-stream context attached to options
1635
+ // so the orchestration in `runStandardStreamRequest` knows we already
1636
+ // emitted and skips its own emit (preserving exactly-once).
1633
1637
  const vertexStreamEmitter = this.neurolink?.getEventEmitter();
1634
1638
  if (vertexStreamEmitter) {
1639
+ markStreamProviderEmittedGenerationEnd(params.options);
1635
1640
  vertexStreamEmitter.emit("generation:end", {
1636
1641
  provider: this.providerName,
1637
1642
  responseTime,
@@ -57,3 +57,4 @@ export * from "./span.js";
57
57
  export * from "./imageGen.js";
58
58
  export * from "./elicitation.js";
59
59
  export * from "./dynamic.js";
60
+ export * from "./streamDedup.js";
@@ -60,4 +60,6 @@ export * from "./imageGen.js";
60
60
  export * from "./elicitation.js";
61
61
  // Dynamic Arguments types
62
62
  export * from "./dynamic.js";
63
+ // Curator P2-4 dedup: per-stream AsyncLocalStorage context
64
+ export * from "./streamDedup.js";
63
65
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Curator P2-4 dedup (concurrency-safe): per-stream context that lets
3
+ * the orchestration's `runStandardStreamRequest` finally block know
4
+ * whether a *native provider* path within THIS stream's async chain
5
+ * already emitted `generation:end`. Native providers (Vertex / Google
6
+ * AI Studio for Gemini 3, etc.) emit on the shared SDK emitter; without
7
+ * scoping, a concurrent unrelated stream's emit on the same NeuroLink
8
+ * instance would suppress the wrong stream's orchestration emit.
9
+ *
10
+ * AsyncLocalStorage scopes each stream's flag to its own async chain.
11
+ */
12
+ export type StreamGenerationEndContext = {
13
+ providerEmitted: boolean;
14
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=streamDedup.js.map
@@ -5,13 +5,41 @@
5
5
  * Enhanced AI provider system with natural MCP tool access.
6
6
  * Uses real MCP infrastructure for tool discovery and execution.
7
7
  */
8
- import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult } from "./types/index.js";
8
+ import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext } from "./types/index.js";
9
9
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
10
10
  import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
11
11
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
12
12
  import { MCPToolRegistry } from "./mcp/toolRegistry.js";
13
13
  import type { DynamicOptions } from "./types/index.js";
14
14
  import { TaskManager } from "./tasks/taskManager.js";
15
+ /**
16
+ * Curator P2-4 dedup (concurrency-safe): native providers emit
17
+ * `generation:end` on the shared SDK emitter. We attach a fresh
18
+ * mutable `dedupContext` object directly to the per-call
19
+ * `StreamOptions` (under `_streamDedupContext`) so each stream gets
20
+ * its own instance — concurrent streams have different option objects
21
+ * and therefore different contexts, so they cannot interfere.
22
+ *
23
+ * Native provider emit sites read `options._streamDedupContext` and
24
+ * flip `.providerEmitted = true` before emitting; the orchestration's
25
+ * finally block reads the same closed-over reference and skips its
26
+ * own emit when the flag is set.
27
+ *
28
+ * This avoids the AsyncLocalStorage approach which doesn't reliably
29
+ * propagate through async-generator yield boundaries when iteration
30
+ * happens from outside the original `run()` scope (e.g. when the
31
+ * consumer drives `for await of result.stream` after `sdk.stream(...)`
32
+ * returns).
33
+ */
34
+ export declare const STREAM_DEDUP_CONTEXT_KEY: "_streamDedupContext";
35
+ /**
36
+ * Native providers call this from their `generation:end` emit sites,
37
+ * passing the same `options` object they received. Safe no-op when
38
+ * the field isn't set.
39
+ */
40
+ export declare function markStreamProviderEmittedGenerationEnd(options: {
41
+ _streamDedupContext?: StreamGenerationEndContext;
42
+ } | undefined): void;
15
43
  export declare class NeuroLink {
16
44
  private mcpInitialized;
17
45
  private mcpSkipped;
package/dist/neurolink.js CHANGED
@@ -297,6 +297,37 @@ function isNonRetryableProviderError(error) {
297
297
  * same NeuroLink instance would clobber each other's trace context.
298
298
  */
299
299
  const metricsTraceContextStorage = new AsyncLocalStorage();
300
+ /**
301
+ * Curator P2-4 dedup (concurrency-safe): native providers emit
302
+ * `generation:end` on the shared SDK emitter. We attach a fresh
303
+ * mutable `dedupContext` object directly to the per-call
304
+ * `StreamOptions` (under `_streamDedupContext`) so each stream gets
305
+ * its own instance — concurrent streams have different option objects
306
+ * and therefore different contexts, so they cannot interfere.
307
+ *
308
+ * Native provider emit sites read `options._streamDedupContext` and
309
+ * flip `.providerEmitted = true` before emitting; the orchestration's
310
+ * finally block reads the same closed-over reference and skips its
311
+ * own emit when the flag is set.
312
+ *
313
+ * This avoids the AsyncLocalStorage approach which doesn't reliably
314
+ * propagate through async-generator yield boundaries when iteration
315
+ * happens from outside the original `run()` scope (e.g. when the
316
+ * consumer drives `for await of result.stream` after `sdk.stream(...)`
317
+ * returns).
318
+ */
319
+ export const STREAM_DEDUP_CONTEXT_KEY = "_streamDedupContext";
320
+ /**
321
+ * Native providers call this from their `generation:end` emit sites,
322
+ * passing the same `options` object they received. Safe no-op when
323
+ * the field isn't set.
324
+ */
325
+ export function markStreamProviderEmittedGenerationEnd(options) {
326
+ const ctx = options?._streamDedupContext;
327
+ if (ctx) {
328
+ ctx.providerEmitted = true;
329
+ }
330
+ }
300
331
  export class NeuroLink {
301
332
  mcpInitialized = false;
302
333
  mcpSkipped = false;
@@ -4984,8 +5015,23 @@ Current user's request: ${currentInput}`;
4984
5015
  const streamStartTime = Date.now();
4985
5016
  const sessionId = enhancedOptions.context
4986
5017
  ?.sessionId;
5018
+ // Curator P2-4 dedup (concurrency-safe): native provider stream paths
5019
+ // (Gemini 3 on Vertex / Google AI Studio) emit `generation:end`
5020
+ // themselves. We attach a per-stream mutable flag directly to
5021
+ // `enhancedOptions._streamDedupContext` — native providers receive
5022
+ // these options and flip the flag before their emit; this finally
5023
+ // block reads the same closed-over reference. Concurrent streams
5024
+ // have different option objects so the contexts don't interfere.
5025
+ const dedupContext = {
5026
+ providerEmitted: false,
5027
+ };
5028
+ enhancedOptions._streamDedupContext = dedupContext;
4987
5029
  const processedStream = (async function* () {
4988
5030
  let streamError;
5031
+ // Curator P2-4: hoist `resolvedUsage` so the finally block can emit a
5032
+ // single `generation:end` event with cost data. Cost listeners
5033
+ // subscribe here; previously the stream path never fired it.
5034
+ let resolvedUsage;
4989
5035
  try {
4990
5036
  for await (const chunk of mcpStream) {
4991
5037
  chunkCount++;
@@ -5015,7 +5061,7 @@ Current user's request: ${currentInput}`;
5015
5061
  accumulatedContent += content;
5016
5062
  });
5017
5063
  }
5018
- let resolvedUsage = streamUsage;
5064
+ resolvedUsage = streamUsage;
5019
5065
  if (!resolvedUsage && streamAnalytics) {
5020
5066
  try {
5021
5067
  const resolved = await Promise.resolve(streamAnalytics);
@@ -5090,6 +5136,61 @@ Current user's request: ${currentInput}`;
5090
5136
  guardrailsBlocked: metadata.guardrailsBlocked,
5091
5137
  error: metadata.error,
5092
5138
  });
5139
+ // Curator P2-4: emit `generation:end` exactly once per stream so
5140
+ // cost listeners receive the same contract as for `generate()`.
5141
+ // The previous implementation only fired `stream:complete`, leaving
5142
+ // any subscriber to `generation:end` with zero events.
5143
+ //
5144
+ // Dedup: native provider stream paths (Gemini 3 on Vertex / Google
5145
+ // AI Studio) already emit `generation:end` themselves so Pipeline B
5146
+ // (Langfuse) records a GENERATION observation. Skip our emit when
5147
+ // they already fired — preserves their Pipeline B observation
5148
+ // source and keeps the "exactly once" contract. Per-stream flag
5149
+ // is concurrency-safe because it's scoped via AsyncLocalStorage.
5150
+ if (!dedupContext.providerEmitted) {
5151
+ try {
5152
+ const finalProvider = metadata.fallbackProvider ?? providerName ?? "unknown";
5153
+ const finalModel = metadata.fallbackModel ??
5154
+ streamModel ??
5155
+ enhancedOptions.model ??
5156
+ "unknown";
5157
+ const finalFinishReason = streamError
5158
+ ? "error"
5159
+ : (streamState.finishReason ?? "stop");
5160
+ self.emitter.emit("generation:end", {
5161
+ provider: finalProvider,
5162
+ model: finalModel,
5163
+ responseTime: Date.now() - streamStartTime,
5164
+ toolsUsed: streamState.toolCalls?.map((t) => t.toolName),
5165
+ timestamp: Date.now(),
5166
+ result: {
5167
+ content: accumulatedContent,
5168
+ usage: resolvedUsage,
5169
+ model: finalModel,
5170
+ provider: finalProvider,
5171
+ finishReason: finalFinishReason,
5172
+ },
5173
+ prompt: enhancedOptions.input?.text ||
5174
+ enhancedOptions.prompt,
5175
+ temperature: enhancedOptions.temperature,
5176
+ maxTokens: enhancedOptions.maxTokens,
5177
+ success: !streamError,
5178
+ error: streamError
5179
+ ? streamError instanceof Error
5180
+ ? streamError.message
5181
+ : String(streamError)
5182
+ : undefined,
5183
+ pipelineAHandled: true,
5184
+ });
5185
+ }
5186
+ catch (emitError) {
5187
+ logger.debug("[NeuroLink.stream] generation:end listener threw — ignored", {
5188
+ error: emitError instanceof Error
5189
+ ? emitError.message
5190
+ : String(emitError),
5191
+ });
5192
+ }
5193
+ }
5093
5194
  self._disableToolCacheForCurrentRequest = false;
5094
5195
  cleanupListeners();
5095
5196
  streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
@@ -4,6 +4,7 @@ import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enum
4
4
  import { BaseProvider } from "../core/baseProvider.js";
5
5
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
6
6
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
7
+ import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
7
8
  import { SpanStatusCode } from "@opentelemetry/api";
8
9
  import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
9
10
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
@@ -735,6 +736,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
735
736
  // AI SDK so experimental_telemetry is never injected; we emit manually.
736
737
  const nativeStreamEmitter = this.neurolink?.getEventEmitter();
737
738
  if (nativeStreamEmitter) {
739
+ // Curator P2-4 dedup: flag the per-stream context attached
740
+ // to options so the orchestration skips its own emit.
741
+ markStreamProviderEmittedGenerationEnd(options);
738
742
  nativeStreamEmitter.emit("generation:end", {
739
743
  provider: this.providerName,
740
744
  responseTime,
@@ -767,6 +771,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
767
771
  // Emit failure generation:end so Pipeline B records the failed stream
768
772
  const errorEmitter = this.neurolink?.getEventEmitter();
769
773
  if (errorEmitter) {
774
+ // Curator P2-4 dedup: flag the per-stream context attached
775
+ // to options so the orchestration skips its own emit.
776
+ markStreamProviderEmittedGenerationEnd(options);
770
777
  errorEmitter.emit("generation:end", {
771
778
  provider: this.providerName,
772
779
  responseTime: Date.now() - startTime,
@@ -10,6 +10,7 @@ import { ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
10
10
  import { BaseProvider } from "../core/baseProvider.js";
11
11
  import { DEFAULT_MAX_STEPS, GLOBAL_LOCATION_MODELS, } from "../core/constants.js";
12
12
  import { ModelConfigurationManager } from "../core/modelConfiguration.js";
13
+ import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
13
14
  import { createProxyFetch } from "../proxy/proxyFetch.js";
14
15
  import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
15
16
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
@@ -1630,8 +1631,12 @@ export class GoogleVertexProvider extends BaseProvider {
1630
1631
  // Emit generation:end so Pipeline B (Langfuse) creates a GENERATION
1631
1632
  // observation. The native @google/genai stream path on Vertex bypasses the
1632
1633
  // Vercel AI SDK so experimental_telemetry is never injected; we emit manually.
1634
+ // Curator P2-4 dedup: flag the per-stream context attached to options
1635
+ // so the orchestration in `runStandardStreamRequest` knows we already
1636
+ // emitted and skips its own emit (preserving exactly-once).
1633
1637
  const vertexStreamEmitter = this.neurolink?.getEventEmitter();
1634
1638
  if (vertexStreamEmitter) {
1639
+ markStreamProviderEmittedGenerationEnd(params.options);
1635
1640
  vertexStreamEmitter.emit("generation:end", {
1636
1641
  provider: this.providerName,
1637
1642
  responseTime,
@@ -57,3 +57,4 @@ export * from "./span.js";
57
57
  export * from "./imageGen.js";
58
58
  export * from "./elicitation.js";
59
59
  export * from "./dynamic.js";
60
+ export * from "./streamDedup.js";
@@ -60,3 +60,5 @@ export * from "./imageGen.js";
60
60
  export * from "./elicitation.js";
61
61
  // Dynamic Arguments types
62
62
  export * from "./dynamic.js";
63
+ // Curator P2-4 dedup: per-stream AsyncLocalStorage context
64
+ export * from "./streamDedup.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Curator P2-4 dedup (concurrency-safe): per-stream context that lets
3
+ * the orchestration's `runStandardStreamRequest` finally block know
4
+ * whether a *native provider* path within THIS stream's async chain
5
+ * already emitted `generation:end`. Native providers (Vertex / Google
6
+ * AI Studio for Gemini 3, etc.) emit on the shared SDK emitter; without
7
+ * scoping, a concurrent unrelated stream's emit on the same NeuroLink
8
+ * instance would suppress the wrong stream's orchestration emit.
9
+ *
10
+ * AsyncLocalStorage scopes each stream's flag to its own async chain.
11
+ */
12
+ export type StreamGenerationEndContext = {
13
+ providerEmitted: boolean;
14
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.59.0",
3
+ "version": "9.59.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
6
6
  "author": {