@juspay/neurolink 9.55.9 → 9.55.11

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/browser/neurolink.min.js +507 -378
  3. package/dist/core/modules/StreamHandler.js +12 -0
  4. package/dist/core/modules/ToolsManager.js +4 -0
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +4 -1
  7. package/dist/lib/core/modules/StreamHandler.js +12 -0
  8. package/dist/lib/core/modules/ToolsManager.js +4 -0
  9. package/dist/lib/index.d.ts +2 -2
  10. package/dist/lib/index.js +4 -1
  11. package/dist/lib/mcp/toolDiscoveryService.js +99 -3
  12. package/dist/lib/mcp/toolRegistry.js +3 -0
  13. package/dist/lib/neurolink.js +8 -23
  14. package/dist/lib/processors/media/AudioProcessor.js +22 -3
  15. package/dist/lib/processors/media/VideoProcessor.js +48 -11
  16. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +26 -0
  17. package/dist/lib/services/server/ai/observability/instrumentation.js +98 -15
  18. package/dist/lib/types/processor.d.ts +27 -0
  19. package/dist/lib/utils/mcpErrorText.d.ts +10 -0
  20. package/dist/lib/utils/mcpErrorText.js +36 -0
  21. package/dist/lib/utils/timeout.js +6 -0
  22. package/dist/mcp/toolDiscoveryService.js +99 -3
  23. package/dist/mcp/toolRegistry.js +3 -0
  24. package/dist/neurolink.js +8 -23
  25. package/dist/processors/media/AudioProcessor.js +22 -3
  26. package/dist/processors/media/VideoProcessor.js +48 -11
  27. package/dist/services/server/ai/observability/instrumentation.d.ts +26 -0
  28. package/dist/services/server/ai/observability/instrumentation.js +98 -15
  29. package/dist/types/processor.d.ts +27 -0
  30. package/dist/utils/mcpErrorText.d.ts +10 -0
  31. package/dist/utils/mcpErrorText.js +35 -0
  32. package/dist/utils/timeout.js +6 -0
  33. package/package.json +4 -4
@@ -11,6 +11,32 @@ import { LoggerProvider } from "@opentelemetry/sdk-logs";
11
11
  import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
12
12
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
13
  import type { LangfuseConfig, LangfuseContext } from "../../../../types/index.js";
14
+ /**
15
+ * True when a span is an internal NeuroLink wrapper that should NOT be sent to
16
+ * Langfuse. Internal wrappers carry the `langfuse.internal: true` attribute.
17
+ *
18
+ * Exposed so host apps that bring their own `LangfuseSpanProcessor` (e.g.
19
+ * `skipLangfuseSpanProcessor: true`, or manual registration on an existing
20
+ * TracerProvider) can apply the same filter and avoid duplicate observations.
21
+ */
22
+ export declare function isLangfuseInternalSpan(span: {
23
+ attributes?: Record<string, unknown>;
24
+ }): boolean;
25
+ /**
26
+ * Drop-in `shouldExportSpan` predicate for a `LangfuseSpanProcessor` that
27
+ * filters out NeuroLink internal wrapper spans.
28
+ *
29
+ * Usage in host apps:
30
+ * ```ts
31
+ * import { langfuseShouldExportSpan } from "@juspay/neurolink";
32
+ * new LangfuseSpanProcessor({ ..., shouldExportSpan: langfuseShouldExportSpan });
33
+ * ```
34
+ */
35
+ export declare function langfuseShouldExportSpan({ otelSpan, }: {
36
+ otelSpan: {
37
+ attributes?: Record<string, unknown>;
38
+ };
39
+ }): boolean;
14
40
  /**
15
41
  * Initialize OpenTelemetry with Langfuse span processor
16
42
  *
@@ -18,6 +18,7 @@ import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
18
18
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
19
19
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
20
20
  import { AsyncLocalStorage } from "async_hooks";
21
+ import { extractMcpErrorText } from "../../../../utils/mcpErrorText.js";
21
22
  import { logger } from "../../../../utils/logger.js";
22
23
  const LOG_PREFIX = "[OpenTelemetry]";
23
24
  function createOtelResource(config, serviceName) {
@@ -131,6 +132,64 @@ function _hasExternalTracerProvider() {
131
132
  return false;
132
133
  }
133
134
  }
135
+ /**
136
+ * Parse `ai.toolCall.result` on a Vercel AI SDK tool span and surface any
137
+ * embedded MCP `{ isError: true }` as a Langfuse ERROR + status message.
138
+ */
139
+ function applyToolCallIsErrorStatus(attrs) {
140
+ const resultAttr = attrs["ai.toolCall.result"];
141
+ if (typeof resultAttr !== "string" || resultAttr.length === 0) {
142
+ return;
143
+ }
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(resultAttr);
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ if (!parsed ||
152
+ typeof parsed !== "object" ||
153
+ parsed.isError !== true) {
154
+ return;
155
+ }
156
+ attrs["langfuse.level"] = "ERROR";
157
+ // Always set a status_message, even when the MCP payload has non-text or
158
+ // empty content. Without a fallback the Curator P0-1 gap reappears for
159
+ // those failures (level=ERROR but statusMessage=null).
160
+ const errorText = extractMcpErrorText(parsed);
161
+ const toolName = typeof attrs["ai.toolCall.name"] === "string"
162
+ ? attrs["ai.toolCall.name"]
163
+ : "tool";
164
+ attrs["langfuse.status_message"] =
165
+ errorText || `MCP ${toolName} returned isError=true`;
166
+ }
167
+ /**
168
+ * Map non-ERROR span conditions (content-filter, length, client abort, SDK
169
+ * timeout, empty output) onto Langfuse WARNING/ERROR levels. Mutates `attrs`.
170
+ */
171
+ function applyNonErrorLangfuseLevel(attrs) {
172
+ const finishReason = attrs["ai.finishReason"] ?? attrs["gen_ai.response.finish_reasons"];
173
+ const reasonStr = Array.isArray(finishReason)
174
+ ? finishReason.join(",")
175
+ : String(finishReason ?? "");
176
+ if (reasonStr.includes("content-filter") || reasonStr === "length") {
177
+ attrs["langfuse.level"] = "WARNING";
178
+ attrs["langfuse.status_message"] =
179
+ `Generation stopped: finishReason=${reasonStr}`;
180
+ return;
181
+ }
182
+ if (attrs["neurolink.no_output"] === true) {
183
+ attrs["langfuse.level"] = "WARNING";
184
+ attrs["langfuse.status_message"] =
185
+ "Stream produced no output (NoOutputGeneratedError)";
186
+ return;
187
+ }
188
+ if (reasonStr === "aborted") {
189
+ attrs["langfuse.level"] = "WARNING";
190
+ attrs["langfuse.status_message"] = "Generation aborted by client";
191
+ }
192
+ }
134
193
  /**
135
194
  * Span processor that enriches spans with user and session context from AsyncLocalStorage
136
195
  * Also extracts GenAI semantic convention attributes for Langfuse integration
@@ -459,26 +518,23 @@ class ContextEnricher {
459
518
  const readableStatus = span.status;
460
519
  try {
461
520
  const mutableAttrs = span.attributes;
521
+ // Curator P0-1/P0-2: detect MCP isError pattern on AI SDK tool call spans.
522
+ // The AI SDK's `ai.toolCall` span stays status=UNSET when the tool
523
+ // *returns* { isError:true } (no exception thrown), so Langfuse sees
524
+ // level=DEFAULT and no status message. Parse the stringified result
525
+ // and surface the embedded error text.
526
+ if (readableSpan.name === "ai.toolCall" &&
527
+ readableStatus?.code !== SpanStatusCode.ERROR) {
528
+ applyToolCallIsErrorStatus(mutableAttrs);
529
+ }
462
530
  if (readableStatus?.code === SpanStatusCode.ERROR) {
463
531
  mutableAttrs["langfuse.level"] = "ERROR";
464
532
  if (readableStatus.message) {
465
533
  mutableAttrs["langfuse.status_message"] = readableStatus.message;
466
534
  }
467
535
  }
468
- else {
469
- // P8 extended: Detect WARNING-level conditions on non-ERROR spans.
470
- // The AI SDK sets ai.finishReason on its spans; content-filter and
471
- // length finish reasons indicate partial failures that deserve WARNING.
472
- const finishReason = mutableAttrs["ai.finishReason"] ??
473
- mutableAttrs["gen_ai.response.finish_reasons"];
474
- const reasonStr = Array.isArray(finishReason)
475
- ? finishReason.join(",")
476
- : String(finishReason ?? "");
477
- if (reasonStr.includes("content-filter") || reasonStr === "length") {
478
- mutableAttrs["langfuse.level"] = "WARNING";
479
- mutableAttrs["langfuse.status_message"] =
480
- `Generation stopped: finishReason=${reasonStr}`;
481
- }
536
+ else if (mutableAttrs["langfuse.level"] === undefined) {
537
+ applyNonErrorLangfuseLevel(mutableAttrs);
482
538
  }
483
539
  }
484
540
  catch {
@@ -520,9 +576,36 @@ async function createLangfuseProcessor(config) {
520
576
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
521
577
  environment: config.environment || "dev",
522
578
  release: config.release || "v1.0.0",
523
- shouldExportSpan: () => true,
579
+ // Curator P1-3: skip internal wrapper spans that duplicate ai.toolCall /
580
+ // ai.generateText observations in Langfuse. Wrappers still emit OTel spans
581
+ // for internal metrics; they just aren't forwarded to Langfuse.
582
+ shouldExportSpan: langfuseShouldExportSpan,
524
583
  });
525
584
  }
585
+ /**
586
+ * True when a span is an internal NeuroLink wrapper that should NOT be sent to
587
+ * Langfuse. Internal wrappers carry the `langfuse.internal: true` attribute.
588
+ *
589
+ * Exposed so host apps that bring their own `LangfuseSpanProcessor` (e.g.
590
+ * `skipLangfuseSpanProcessor: true`, or manual registration on an existing
591
+ * TracerProvider) can apply the same filter and avoid duplicate observations.
592
+ */
593
+ export function isLangfuseInternalSpan(span) {
594
+ return span.attributes?.["langfuse.internal"] === true;
595
+ }
596
+ /**
597
+ * Drop-in `shouldExportSpan` predicate for a `LangfuseSpanProcessor` that
598
+ * filters out NeuroLink internal wrapper spans.
599
+ *
600
+ * Usage in host apps:
601
+ * ```ts
602
+ * import { langfuseShouldExportSpan } from "@juspay/neurolink";
603
+ * new LangfuseSpanProcessor({ ..., shouldExportSpan: langfuseShouldExportSpan });
604
+ * ```
605
+ */
606
+ export function langfuseShouldExportSpan({ otelSpan, }) {
607
+ return !isLangfuseInternalSpan(otelSpan);
608
+ }
526
609
  async function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds) {
527
610
  if (langfuseRequested && !hasLangfuseCreds) {
528
611
  if (!otlpEndpoint) {
@@ -543,6 +543,33 @@ export type ProcessedYaml = ProcessedFileBase & {
543
543
  /** YAML content converted to JSON string for AI consumption */
544
544
  asJson: string | null;
545
545
  };
546
+ /**
547
+ * Structural types for fluent-ffmpeg probe data.
548
+ * Defined here so the optional fluent-ffmpeg package is not required at typecheck time.
549
+ */
550
+ export type FfprobeStream = {
551
+ codec_type?: string;
552
+ codec_name?: string;
553
+ width?: number;
554
+ height?: number;
555
+ r_frame_rate?: string;
556
+ avg_frame_rate?: string;
557
+ bit_rate?: number | string;
558
+ channels?: number;
559
+ sample_rate?: number | string;
560
+ tags?: Record<string, string | number>;
561
+ [key: string]: unknown;
562
+ };
563
+ export type FfprobeData = {
564
+ streams: FfprobeStream[];
565
+ format: {
566
+ duration?: number;
567
+ size?: number | string;
568
+ bit_rate?: number | string;
569
+ tags?: Record<string, string | number>;
570
+ [key: string]: unknown;
571
+ };
572
+ };
546
573
  /**
547
574
  * Structural types for exceljs objects.
548
575
  * Defined here so the optional exceljs package is not required at typecheck time.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Extract a human-readable error string from an MCP isError result object.
3
+ *
4
+ * Shared utility — no side effects, no dependencies on other SDK modules —
5
+ * so it can be imported from the neurolink.ts event loop, the telemetry
6
+ * instrumentation (which loads earlier), and the MCP discovery layer without
7
+ * creating circular imports. Any change to truncation or content-type parsing
8
+ * must happen here and propagate to all three surfaces.
9
+ */
10
+ export declare function extractMcpErrorText(raw: unknown): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Extract a human-readable error string from an MCP isError result object.
3
+ *
4
+ * Shared utility — no side effects, no dependencies on other SDK modules —
5
+ * so it can be imported from the neurolink.ts event loop, the telemetry
6
+ * instrumentation (which loads earlier), and the MCP discovery layer without
7
+ * creating circular imports. Any change to truncation or content-type parsing
8
+ * must happen here and propagate to all three surfaces.
9
+ */
10
+ export function extractMcpErrorText(raw) {
11
+ let resultObj;
12
+ try {
13
+ resultObj = typeof raw === "string" ? JSON.parse(raw) : raw;
14
+ }
15
+ catch {
16
+ return "";
17
+ }
18
+ if (!resultObj || typeof resultObj !== "object") {
19
+ return "";
20
+ }
21
+ const content = resultObj.content;
22
+ if (!Array.isArray(content)) {
23
+ return "";
24
+ }
25
+ // Fail closed on malformed entries (e.g. `content: [null]`) rather than
26
+ // throwing — the caller expects an empty string for unparseable input.
27
+ const texts = content
28
+ .filter((c) => c !== null &&
29
+ typeof c === "object" &&
30
+ c.type === "text" &&
31
+ typeof c.text === "string" &&
32
+ c.text.length > 0)
33
+ .map((c) => c.text);
34
+ return texts.join(" ").substring(0, 500);
35
+ }
@@ -313,6 +313,12 @@ export function createTimeoutController(timeout, provider, operation) {
313
313
  }
314
314
  const controller = new AbortController();
315
315
  const timer = setTimeout(() => {
316
+ // NOTE: we cannot stamp the AI SDK's ai.streamText/ai.generateText span
317
+ // from here — the setTimeout callback runs in the async context captured
318
+ // at schedule time, which is BEFORE the AI SDK span exists. Instead we
319
+ // rely on the AI SDK propagating the TimeoutError through its recordSpan
320
+ // wrapper, which sets span.status = ERROR + message. ContextEnricher's
321
+ // SpanStatusCode.ERROR branch then surfaces level=ERROR + status_message.
316
322
  controller.abort(new TimeoutError(`${provider} ${operation} operation timed out after ${timeout}`, timeoutMs, provider, operation));
317
323
  }, timeoutMs);
318
324
  const cleanup = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.55.9",
3
+ "version": "9.55.11",
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": {
@@ -230,14 +230,11 @@
230
230
  "croner": "^9.1.0",
231
231
  "csv-parser": "^3.2.0",
232
232
  "dotenv": "^17.3.1",
233
- "fluent-ffmpeg": "^2.1.3",
234
233
  "google-auth-library": "^10.6.1",
235
234
  "hono": "^4.12.3",
236
235
  "inquirer": "^13.3.0",
237
236
  "jose": "^6.1.3",
238
237
  "json-schema-to-zod": "^2.7.0",
239
- "mediabunny": "^1.40.1",
240
- "music-metadata": "^11.11.2",
241
238
  "nanoid": "^5.1.5",
242
239
  "ollama-ai-provider": "^1.2.0",
243
240
  "open": "^11.0.0",
@@ -271,6 +268,9 @@
271
268
  "@picovoice/cobra-node": "^3.0.2",
272
269
  "bullmq": "^5.52.2",
273
270
  "exceljs": "^4.4.0",
271
+ "fluent-ffmpeg": "^2.1.3",
272
+ "mediabunny": "^1.40.1",
273
+ "music-metadata": "^11.11.2",
274
274
  "mammoth": "^1.11.0",
275
275
  "pptxgenjs": "^4.0.1",
276
276
  "pdf-parse": "^2.4.5",