@juspay/neurolink 13.5.0 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
  3. package/dist/adapters/video/videoAnalyzer.js +4 -0
  4. package/dist/autoresearch/index.d.ts +1 -0
  5. package/dist/autoresearch/index.js +4 -0
  6. package/dist/autoresearch/worker.js +4 -0
  7. package/dist/browser/neurolink.min.js +476 -476
  8. package/dist/cli/commands/observability.js +11 -4
  9. package/dist/cli/commands/telemetry.js +36 -6
  10. package/dist/cli/utils/formatters.d.ts +17 -0
  11. package/dist/cli/utils/formatters.js +39 -0
  12. package/dist/constants/langfuse.d.ts +17 -1
  13. package/dist/constants/langfuse.js +23 -1
  14. package/dist/constants/telemetry.d.ts +6 -0
  15. package/dist/constants/telemetry.js +6 -0
  16. package/dist/core/baseProvider.d.ts +3 -1
  17. package/dist/core/baseProvider.js +30 -8
  18. package/dist/core/geminiLoopAdapter.js +19 -0
  19. package/dist/core/loopEngine.js +10 -7
  20. package/dist/core/modules/StreamHandler.js +5 -4
  21. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  22. package/dist/core/modules/TelemetryHandler.js +16 -0
  23. package/dist/core/modules/ToolsManager.js +14 -3
  24. package/dist/core/nativeGenerateLoop.d.ts +2 -0
  25. package/dist/core/nativeGenerateLoop.js +35 -9
  26. package/dist/evaluation/index.d.ts +1 -0
  27. package/dist/evaluation/index.js +4 -0
  28. package/dist/evaluation/ragasEvaluator.js +1 -0
  29. package/dist/mcp/externalServerManager.js +6 -2
  30. package/dist/mcp/toolRegistry.js +3 -0
  31. package/dist/neurolink.d.ts +16 -8
  32. package/dist/neurolink.js +115 -61
  33. package/dist/observability/FEATURE-STATUS.md +0 -4
  34. package/dist/observability/exporterRegistry.d.ts +6 -1
  35. package/dist/observability/exporterRegistry.js +18 -0
  36. package/dist/observability/exporterSpanProcessor.d.ts +20 -6
  37. package/dist/observability/exporterSpanProcessor.js +166 -34
  38. package/dist/observability/exporters/baseExporter.d.ts +22 -3
  39. package/dist/observability/exporters/baseExporter.js +87 -5
  40. package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
  41. package/dist/observability/exporters/langfuseExporter.js +251 -41
  42. package/dist/observability/langfuseMasking.d.ts +1 -1
  43. package/dist/observability/langfuseMasking.js +1 -1
  44. package/dist/observability/otlpEncoding.d.ts +18 -5
  45. package/dist/observability/otlpEncoding.js +69 -12
  46. package/dist/observability/utils/spanSerializer.d.ts +3 -2
  47. package/dist/observability/utils/spanSerializer.js +5 -3
  48. package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
  49. package/dist/processors/base/BaseFileProcessor.js +4 -0
  50. package/dist/processors/index.d.ts +1 -0
  51. package/dist/processors/index.js +4 -0
  52. package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
  53. package/dist/processors/registry/ProcessorRegistry.js +5 -0
  54. package/dist/providers/amazonBedrock/client.js +7 -5
  55. package/dist/providers/amazonSagemaker.js +32 -12
  56. package/dist/providers/anthropic/client.js +1 -0
  57. package/dist/providers/anthropic/loopAdapter.js +7 -3
  58. package/dist/providers/googleAiStudio/client.js +2 -0
  59. package/dist/providers/googleVertex/client.d.ts +6 -5
  60. package/dist/providers/googleVertex/client.js +46 -19
  61. package/dist/providers/openaiChatCompletionsBase.js +29 -18
  62. package/dist/proxy/codexUsage.js +59 -2
  63. package/dist/proxy/proxyTracer.d.ts +25 -1
  64. package/dist/proxy/proxyTracer.js +146 -22
  65. package/dist/proxy/proxyTranslationEngine.js +4 -2
  66. package/dist/proxy/sseInterceptor.js +6 -4
  67. package/dist/rag/chunkers/BaseChunker.js +1 -0
  68. package/dist/rag/index.d.ts +1 -0
  69. package/dist/rag/index.js +4 -0
  70. package/dist/rag/ragIntegration.js +1 -0
  71. package/dist/rag/reranker/reranker.js +2 -0
  72. package/dist/rag/retrieval/vectorQueryTool.js +1 -0
  73. package/dist/server/routes/codexProxyRoutes.js +3 -1
  74. package/dist/server/utils/validation.d.ts +3 -3
  75. package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
  76. package/dist/services/server/ai/observability/instrumentation.js +217 -47
  77. package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
  78. package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
  79. package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
  80. package/dist/services/server/ai/observability/otlpSetup.js +55 -4
  81. package/dist/telemetry/aiMetrics.d.ts +56 -0
  82. package/dist/telemetry/aiMetrics.js +229 -0
  83. package/dist/telemetry/attributes.d.ts +24 -1
  84. package/dist/telemetry/attributes.js +94 -9
  85. package/dist/telemetry/chatMessages.d.ts +6 -0
  86. package/dist/telemetry/chatMessages.js +37 -0
  87. package/dist/telemetry/index.d.ts +1 -1
  88. package/dist/telemetry/index.js +1 -1
  89. package/dist/telemetry/observation.d.ts +5 -5
  90. package/dist/telemetry/observation.js +128 -39
  91. package/dist/telemetry/spanSanitizer.d.ts +2 -2
  92. package/dist/telemetry/spanSanitizer.js +76 -8
  93. package/dist/telemetry/telemetryStarter.d.ts +16 -0
  94. package/dist/telemetry/telemetryStarter.js +28 -0
  95. package/dist/telemetry/traceContext.d.ts +3 -3
  96. package/dist/telemetry/traceContext.js +3 -3
  97. package/dist/telemetry/tracers.d.ts +13 -1
  98. package/dist/telemetry/tracers.js +49 -10
  99. package/dist/telemetry/withSpan.d.ts +19 -3
  100. package/dist/telemetry/withSpan.js +149 -20
  101. package/dist/tools/toolDiscovery.js +2 -1
  102. package/dist/tools/toolPolicy.js +22 -2
  103. package/dist/types/config.d.ts +19 -0
  104. package/dist/types/exporter.d.ts +98 -3
  105. package/dist/types/generate.d.ts +7 -1
  106. package/dist/types/livekit.d.ts +16 -1
  107. package/dist/types/loopEngine.d.ts +16 -1
  108. package/dist/types/observability.d.ts +38 -5
  109. package/dist/types/proxy.d.ts +2 -0
  110. package/dist/types/span.d.ts +26 -0
  111. package/dist/types/toolResolution.d.ts +5 -1
  112. package/dist/utils/timeout.js +4 -2
  113. package/dist/voice/livekit/realtimeMcpTools.js +11 -2
  114. package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
  115. package/dist/voice/livekit/realtimeMemory.js +139 -46
  116. package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
  117. package/dist/voice/livekit/turnRecorder.js +22 -9
  118. package/dist/workflow/core/ensembleExecutor.js +2 -0
  119. package/dist/workflow/core/judgeScorer.js +1 -0
  120. package/dist/workflow/core/responseConditioner.js +1 -0
  121. package/dist/workflow/core/workflowRunner.js +6 -1
  122. package/dist/workflow/index.d.ts +1 -0
  123. package/dist/workflow/index.js +4 -0
  124. package/docs-site/static/search-index.json +32 -27
  125. package/package.json +21 -2
@@ -11,7 +11,8 @@ import chalk from "chalk";
11
11
  import ora from "ora";
12
12
  import { logger } from "../../utils/logger.js";
13
13
  import { NeuroLink } from "../../neurolink.js";
14
- import { formatRow, formatCost } from "../utils/formatters.js";
14
+ import { ensureTelemetry } from "../../services/server/ai/observability/instrumentation.js";
15
+ import { formatRow, formatCost, formatExporterLines, formatOtelStatus, } from "../utils/formatters.js";
15
16
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
16
17
  /**
17
18
  * Observability Command Factory
@@ -66,6 +67,9 @@ export class ObservabilityCommandFactory {
66
67
  ? null
67
68
  : ora("Checking observability status...").start();
68
69
  try {
70
+ // A one-shot process only starts telemetry with its first span, so
71
+ // start it here to report the exporters this process would run.
72
+ ensureTelemetry();
69
73
  const neurolink = new NeuroLink();
70
74
  const status = neurolink.getTelemetryStatus();
71
75
  if (spinner) {
@@ -105,9 +109,7 @@ export class ObservabilityCommandFactory {
105
109
  if (status.openTelemetry) {
106
110
  logger.always("");
107
111
  logger.always(chalk.bold("OpenTelemetry:"));
108
- const otelStatus = status.openTelemetry.enabled
109
- ? chalk.green("Enabled")
110
- : chalk.gray("Disabled");
112
+ const otelStatus = formatOtelStatus(status.openTelemetry, "Enabled", "Disabled");
111
113
  logger.always(formatRow(" Status:", otelStatus));
112
114
  if (status.openTelemetry.endpoint) {
113
115
  logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
@@ -117,6 +119,11 @@ export class ObservabilityCommandFactory {
117
119
  }
118
120
  }
119
121
  logger.always("");
122
+ logger.always(chalk.bold("Exporters:"));
123
+ for (const line of formatExporterLines(status.exporters)) {
124
+ logger.always(line);
125
+ }
126
+ logger.always("");
120
127
  }
121
128
  }
122
129
  catch (error) {
@@ -11,10 +11,20 @@ import chalk from "chalk";
11
11
  import ora from "ora";
12
12
  import { logger } from "../../utils/logger.js";
13
13
  import { NeuroLink } from "../../neurolink.js";
14
- import { flushOpenTelemetry } from "../../services/server/ai/observability/instrumentation.js";
15
- import { formatRow, formatCost } from "../utils/formatters.js";
14
+ import { ensureTelemetry, flushOpenTelemetry, } from "../../services/server/ai/observability/instrumentation.js";
15
+ import { getExporterRegistry } from "../../observability/exporterRegistry.js";
16
+ import { formatRow, formatCost, formatExporterLines, formatOtelStatus, } from "../utils/formatters.js";
16
17
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
17
18
  import { prepareNativeTelemetry, validateNativeTelemetry, } from "../utils/nativeTelemetry.js";
19
+ function exporterTotals() {
20
+ return getExporterRegistry()
21
+ .getStatuses()
22
+ .reduce((sum, exporter) => ({
23
+ pending: sum.pending + exporter.pendingSpans,
24
+ delivered: sum.delivered + exporter.deliveredSpans,
25
+ failed: sum.failed + exporter.failedSpans,
26
+ }), { pending: 0, delivered: 0, failed: 0 });
27
+ }
18
28
  /**
19
29
  * Telemetry Command Factory
20
30
  */
@@ -180,6 +190,9 @@ export class TelemetryCommandFactory {
180
190
  ? null
181
191
  : ora("Checking telemetry status...").start();
182
192
  try {
193
+ // A one-shot process only starts telemetry with its first span, so
194
+ // start it here to report the exporters this process would run.
195
+ ensureTelemetry();
183
196
  const neurolink = new NeuroLink();
184
197
  const status = neurolink.getTelemetryStatus();
185
198
  if (spinner) {
@@ -204,9 +217,7 @@ export class TelemetryCommandFactory {
204
217
  if (status.openTelemetry) {
205
218
  logger.always("");
206
219
  logger.always(chalk.bold("OpenTelemetry:"));
207
- const otelStatus = status.openTelemetry.enabled
208
- ? chalk.green("Active")
209
- : chalk.gray("Inactive");
220
+ const otelStatus = formatOtelStatus(status.openTelemetry, "Active", "Inactive");
210
221
  logger.always(formatRow(" Status:", otelStatus));
211
222
  if (status.openTelemetry.endpoint) {
212
223
  logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
@@ -231,6 +242,11 @@ export class TelemetryCommandFactory {
231
242
  }
232
243
  }
233
244
  logger.always("");
245
+ logger.always(chalk.bold("Exporters:"));
246
+ for (const line of formatExporterLines(status.exporters)) {
247
+ logger.always(line);
248
+ }
249
+ logger.always("");
234
250
  }
235
251
  }
236
252
  catch (error) {
@@ -277,6 +293,7 @@ export class TelemetryCommandFactory {
277
293
  ? null
278
294
  : ora("Flushing pending spans...").start();
279
295
  try {
296
+ const before = exporterTotals();
280
297
  // Create a timeout promise
281
298
  let flushTimer;
282
299
  const timeoutPromise = new Promise((_, reject) => {
@@ -293,13 +310,26 @@ export class TelemetryCommandFactory {
293
310
  if (spinner) {
294
311
  spinner.succeed("Flush completed");
295
312
  }
313
+ const after = exporterTotals();
314
+ const flushed = after.delivered - before.delivered;
315
+ const dropped = after.failed - before.failed;
296
316
  if (args.format === "json") {
297
- logger.always(JSON.stringify({ success: true }, null, 2));
317
+ logger.always(JSON.stringify({
318
+ success: true,
319
+ pendingBefore: before.pending,
320
+ pendingAfter: after.pending,
321
+ flushed,
322
+ dropped,
323
+ }, null, 2));
298
324
  }
299
325
  else {
300
326
  logger.always("");
301
327
  logger.always(chalk.bold.cyan("=== Flush Complete ==="));
302
328
  logger.always("");
329
+ logger.always(formatRow("Spans before:", before.pending.toString()));
330
+ logger.always(formatRow("Spans after:", after.pending.toString()));
331
+ logger.always(formatRow("Flushed:", chalk.green(flushed.toString())));
332
+ logger.always(formatRow("Dropped:", dropped > 0 ? chalk.red(dropped.toString()) : "0"));
303
333
  logger.always("");
304
334
  }
305
335
  }
@@ -3,10 +3,27 @@
3
3
  *
4
4
  * Common formatting functions used across CLI command modules.
5
5
  */
6
+ import type { ExporterStatus } from "../../types/index.js";
6
7
  /**
7
8
  * Format a table row with padding
8
9
  */
9
10
  export declare function formatRow(label: string, value: string, width?: number): string;
11
+ /**
12
+ * Format the status rows of the registered exporters
13
+ */
14
+ export declare function formatExporterLines(exporters: ExporterStatus[]): string[];
15
+ /**
16
+ * @description Labels NeuroLink's OpenTelemetry export for the status commands. Beside a host TracerProvider NeuroLink exports nothing to the endpoint, so it is shown as ignored rather than active.
17
+ * @param otel {{ enabled: boolean; endpoint?: string; usingExternalProvider?: boolean }} - `getTelemetryStatus().openTelemetry`.
18
+ * @param on {string} - Label for an export NeuroLink runs.
19
+ * @param off {string} - Label for no export.
20
+ * @returns {string} The coloured label.
21
+ */
22
+ export declare function formatOtelStatus(otel: {
23
+ enabled: boolean;
24
+ endpoint?: string;
25
+ usingExternalProvider?: boolean;
26
+ }, on: string, off: string): string;
10
27
  /**
11
28
  * Format currency value
12
29
  */
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Common formatting functions used across CLI command modules.
5
5
  */
6
+ import chalk from "chalk";
6
7
  /**
7
8
  * Format a table row with padding
8
9
  */
@@ -10,6 +11,44 @@ export function formatRow(label, value, width = 30) {
10
11
  const paddedLabel = label.padEnd(width);
11
12
  return `${paddedLabel} ${value}`;
12
13
  }
14
+ /**
15
+ * Format the status rows of the registered exporters
16
+ */
17
+ export function formatExporterLines(exporters) {
18
+ if (exporters.length === 0) {
19
+ return [" None registered"];
20
+ }
21
+ return exporters.flatMap((exporter) => {
22
+ const state = !exporter.enabled
23
+ ? "Stopped"
24
+ : exporter.healthy
25
+ ? "Active"
26
+ : "Failing";
27
+ const counts = `pending ${exporter.pendingSpans}, delivered ${exporter.deliveredSpans}, failed ${exporter.failedSpans}`;
28
+ return [
29
+ formatRow(` ${exporter.name}:`, `${state} (${counts})`),
30
+ ...(exporter.lastExportTime
31
+ ? [formatRow(" Last export:", exporter.lastExportTime)]
32
+ : []),
33
+ ...(exporter.errors ?? []).map((error) => formatRow(" Error:", error)),
34
+ ];
35
+ });
36
+ }
37
+ /**
38
+ * @description Labels NeuroLink's OpenTelemetry export for the status commands. Beside a host TracerProvider NeuroLink exports nothing to the endpoint, so it is shown as ignored rather than active.
39
+ * @param otel {{ enabled: boolean; endpoint?: string; usingExternalProvider?: boolean }} - `getTelemetryStatus().openTelemetry`.
40
+ * @param on {string} - Label for an export NeuroLink runs.
41
+ * @param off {string} - Label for no export.
42
+ * @returns {string} The coloured label.
43
+ */
44
+ export function formatOtelStatus(otel, on, off) {
45
+ if (otel.usingExternalProvider) {
46
+ return chalk.yellow(otel.endpoint
47
+ ? "Ignored (a host TracerProvider is registered)"
48
+ : "Host TracerProvider");
49
+ }
50
+ return otel.enabled ? chalk.green(on) : chalk.gray(off);
51
+ }
13
52
  /**
14
53
  * Format currency value
15
54
  */
@@ -11,15 +11,31 @@ export declare const DEFAULT_FLUSH_AT = 15;
11
11
  export declare const DEFAULT_FLUSH_INTERVAL_MS = 5000;
12
12
  /** @description Milliseconds before an export request is aborted. */
13
13
  export declare const DEFAULT_TIMEOUT_MS = 30000;
14
+ /** @description Milliseconds a flush or shutdown waits for delivery before it abandons what is still in flight. */
15
+ export declare const DEFAULT_FLUSH_BUDGET_MS = 3000;
14
16
  /** @description Maximum spans sent in one export request. */
15
17
  export declare const MAX_SPANS_PER_REQUEST = 500;
18
+ /** @description Export requests to Langfuse in flight at once. Spans that end while every one is busy wait in the buffer. */
19
+ export declare const MAX_CONCURRENT_DELIVERIES = 4;
20
+ /** @description Spans that may wait for a delivery slot. Beyond it the oldest waiting spans are dropped and counted, so a Langfuse that stops answering cannot hold an unbounded backlog in memory. */
21
+ export declare const MAX_QUEUED_SPANS = 10000;
22
+ /** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
23
+ export declare const QUEUE_DROP_WARN_INTERVAL_MS = 60000;
24
+ /** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
25
+ export declare const MAX_EXPORT_RETRY_AFTER_MS = 60000;
16
26
  /** @description Maximum serialized bytes sent in one export request. */
17
27
  export declare const MAX_BYTES_PER_REQUEST = 3500000;
18
28
  /** @description Placeholder that replaces a value fully removed by the user's mask. */
19
29
  export declare const FULLY_MASKED = "<fully masked>";
20
- /** @description Span attributes holding trace and observation input or output, which the mask applies to. */
30
+ /** @description Span attributes holding trace and observation input or output, which the Langfuse exporter's mask and `redactIO` apply to. */
21
31
  export declare const CONTENT_KEYS: Set<string>;
22
32
  /** @description Attribute prefixes whose values are metadata entries. */
23
33
  export declare const METADATA_PREFIXES: string[];
34
+ /** @description Observation types that hold no model, usage or cost in Langfuse. GenAI model and usage keys on them are left out of the Langfuse export, so a request's generation stays its only priced record. */
35
+ export declare const UNPRICED_OBSERVATION_TYPES: ReadonlySet<string>;
36
+ /** @description GenAI attributes Langfuse reads a model name from. */
37
+ export declare const MODEL_ATTRIBUTE_KEYS: ReadonlySet<string>;
38
+ /** @description Prefix of the GenAI attributes Langfuse reads usage and cost from. */
39
+ export declare const USAGE_ATTRIBUTE_PREFIX = "gen_ai.usage.";
24
40
  /** @description OTLP numeric status code for each span status. */
25
41
  export declare const OTLP_STATUS_CODES: Record<SpanStatus, number>;
@@ -11,13 +11,23 @@ export const DEFAULT_FLUSH_AT = 15;
11
11
  export const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
12
12
  /** @description Milliseconds before an export request is aborted. */
13
13
  export const DEFAULT_TIMEOUT_MS = 30_000;
14
+ /** @description Milliseconds a flush or shutdown waits for delivery before it abandons what is still in flight. */
15
+ export const DEFAULT_FLUSH_BUDGET_MS = 3_000;
14
16
  /** @description Maximum spans sent in one export request. */
15
17
  export const MAX_SPANS_PER_REQUEST = 500;
18
+ /** @description Export requests to Langfuse in flight at once. Spans that end while every one is busy wait in the buffer. */
19
+ export const MAX_CONCURRENT_DELIVERIES = 4;
20
+ /** @description Spans that may wait for a delivery slot. Beyond it the oldest waiting spans are dropped and counted, so a Langfuse that stops answering cannot hold an unbounded backlog in memory. */
21
+ export const MAX_QUEUED_SPANS = 10_000;
22
+ /** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
23
+ export const QUEUE_DROP_WARN_INTERVAL_MS = 60_000;
24
+ /** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
25
+ export const MAX_EXPORT_RETRY_AFTER_MS = 60_000;
16
26
  /** @description Maximum serialized bytes sent in one export request. */
17
27
  export const MAX_BYTES_PER_REQUEST = 3_500_000;
18
28
  /** @description Placeholder that replaces a value fully removed by the user's mask. */
19
29
  export const FULLY_MASKED = "<fully masked>";
20
- /** @description Span attributes holding trace and observation input or output, which the mask applies to. */
30
+ /** @description Span attributes holding trace and observation input or output, which the Langfuse exporter's mask and `redactIO` apply to. */
21
31
  export const CONTENT_KEYS = new Set([
22
32
  "langfuse.observation.input",
23
33
  "langfuse.observation.output",
@@ -29,6 +39,18 @@ export const METADATA_PREFIXES = [
29
39
  "langfuse.observation.metadata.",
30
40
  "langfuse.trace.metadata.",
31
41
  ];
42
+ /** @description Observation types that hold no model, usage or cost in Langfuse. GenAI model and usage keys on them are left out of the Langfuse export, so a request's generation stays its only priced record. */
43
+ export const UNPRICED_OBSERVATION_TYPES = new Set([
44
+ "span",
45
+ "event",
46
+ ]);
47
+ /** @description GenAI attributes Langfuse reads a model name from. */
48
+ export const MODEL_ATTRIBUTE_KEYS = new Set([
49
+ "gen_ai.request.model",
50
+ "gen_ai.response.model",
51
+ ]);
52
+ /** @description Prefix of the GenAI attributes Langfuse reads usage and cost from. */
53
+ export const USAGE_ATTRIBUTE_PREFIX = "gen_ai.usage.";
32
54
  /** @description OTLP numeric status code for each span status. */
33
55
  export const OTLP_STATUS_CODES = {
34
56
  [SpanStatus.UNSET]: 0,
@@ -18,3 +18,9 @@ export declare const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
18
18
  export declare const OTEL_LOG_PREFIX = "[OpenTelemetry]";
19
19
  /** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
20
20
  export declare const MAX_REMEMBERED_SPANS = 5000;
21
+ /** @description Most redacted span attribute strings remembered so `observe` does not redact them a second time. */
22
+ export declare const MAX_REDACTED_ATTRIBUTES = 64;
23
+ /** @description Span attribute that keeps a wrapper span out of every exporter. */
24
+ export declare const INTERNAL_SPAN_ATTRIBUTE = "langfuse.internal";
25
+ /** @description Instrumentation scope of the proxy's tracer. Of the proxy's spans only the ones it marks as observations (the request root and its generation) reach the exporters, although its spans also carry `gen_ai.*` keys for collectors. */
26
+ export declare const PROXY_TRACER_SCOPE = "neurolink.proxy";
@@ -32,3 +32,9 @@ export const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
32
32
  export const OTEL_LOG_PREFIX = "[OpenTelemetry]";
33
33
  /** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
34
34
  export const MAX_REMEMBERED_SPANS = 5_000;
35
+ /** @description Most redacted span attribute strings remembered so `observe` does not redact them a second time. */
36
+ export const MAX_REDACTED_ATTRIBUTES = 64;
37
+ /** @description Span attribute that keeps a wrapper span out of every exporter. */
38
+ export const INTERNAL_SPAN_ATTRIBUTE = "langfuse.internal";
39
+ /** @description Instrumentation scope of the proxy's tracer. Of the proxy's spans only the ones it marks as observations (the request root and its generation) reach the exporters, although its spans also carry `gen_ai.*` keys for collectors. */
40
+ export const PROXY_TRACER_SCOPE = "neurolink.proxy";
@@ -152,7 +152,9 @@ export declare abstract class BaseProvider implements AIProvider {
152
152
  private sortToolRecord;
153
153
  /**
154
154
  * On-demand discovery (`tools.discovery: true`): defer external MCP tool
155
- * schemas behind one `search_tools` meta-tool. Built-in tools, per-call
155
+ * schemas — or, with `tools.discoveryScope: "all"`, every non-built-in
156
+ * tool — behind one `search_tools` meta-tool, except names matching
157
+ * `tools.discoveryAlwaysVisible`. Built-in tools, per-call
156
158
  * tools, explicitly whitelisted tools, and session-pinned (previously
157
159
  * discovered) tools always stay hot. No-op when discovery is off — with a
158
160
  * one-time WARN when the catalog is large enough that selection accuracy
@@ -1246,7 +1246,9 @@ export class BaseProvider {
1246
1246
  }
1247
1247
  /**
1248
1248
  * On-demand discovery (`tools.discovery: true`): defer external MCP tool
1249
- * schemas behind one `search_tools` meta-tool. Built-in tools, per-call
1249
+ * schemas — or, with `tools.discoveryScope: "all"`, every non-built-in
1250
+ * tool — behind one `search_tools` meta-tool, except names matching
1251
+ * `tools.discoveryAlwaysVisible`. Built-in tools, per-call
1250
1252
  * tools, explicitly whitelisted tools, and session-pinned (previously
1251
1253
  * discovered) tools always stay hot. No-op when discovery is off — with a
1252
1254
  * one-time WARN when the catalog is large enough that selection accuracy
@@ -1273,8 +1275,13 @@ export class BaseProvider {
1273
1275
  }
1274
1276
  return tools;
1275
1277
  }
1276
- const externalTools = this.neurolink?.getExternalMCPTools() ?? [];
1277
- if (externalTools.length === 0) {
1278
+ // "external" defers only attached MCP servers' tools; "all" defers every
1279
+ // tool in the record except the built-ins.
1280
+ const builtinNames = new Set(Object.keys(this.directTools ?? {}));
1281
+ const candidateNames = policy.discoveryScope === "all"
1282
+ ? Object.keys(tools).filter((name) => !builtinNames.has(name))
1283
+ : (this.neurolink?.getExternalMCPTools() ?? []).map((t) => t.name);
1284
+ if (candidateNames.length === 0) {
1278
1285
  return tools;
1279
1286
  }
1280
1287
  // Session pinning requires a caller-provided sessionId. Without one there
@@ -1305,12 +1312,15 @@ export class BaseProvider {
1305
1312
  explicitPatterns.push(toolChoice.toolName);
1306
1313
  }
1307
1314
  const explicitMatcher = explicitPatterns.length > 0 ? toolNameMatcher(explicitPatterns) : null;
1308
- const deferrableNames = externalTools
1309
- .map((t) => t.name)
1310
- .filter((name) => name in tools &&
1315
+ const alwaysVisiblePatterns = policy.discoveryAlwaysVisible ?? [];
1316
+ const alwaysVisibleMatcher = alwaysVisiblePatterns.length > 0
1317
+ ? toolNameMatcher(alwaysVisiblePatterns)
1318
+ : null;
1319
+ const deferrableNames = candidateNames.filter((name) => name in tools &&
1311
1320
  !perCallNames.has(name) &&
1312
- !(explicitMatcher ? explicitMatcher(name) : false));
1313
- return partitionToolsForDiscovery(tools, {
1321
+ !(explicitMatcher ? explicitMatcher(name) : false) &&
1322
+ !(alwaysVisibleMatcher ? alwaysVisibleMatcher(name) : false));
1323
+ const partitioned = partitionToolsForDiscovery(tools, {
1314
1324
  deferrableNames,
1315
1325
  pinnedNames,
1316
1326
  onHydrate: (names) => {
@@ -1319,6 +1329,16 @@ export class BaseProvider {
1319
1329
  }
1320
1330
  },
1321
1331
  });
1332
+ // The meta-tool is built after the event wrappers ran, so without this
1333
+ // a search_tools call never reaches tool:start / tool:end listeners.
1334
+ const metaTool = partitioned["search_tools"];
1335
+ if (isDiscoveryMetaTool(metaTool)) {
1336
+ partitioned["search_tools"] =
1337
+ this.toolsManager.wrapExternalToolsWithEvents({
1338
+ search_tools: metaTool,
1339
+ })["search_tools"];
1340
+ }
1341
+ return partitioned;
1322
1342
  }
1323
1343
  static warnedLargeCatalog = false;
1324
1344
  warnLargeCatalogOnce(toolCount) {
@@ -1760,6 +1780,7 @@ export class BaseProvider {
1760
1780
  error,
1761
1781
  latency,
1762
1782
  };
1783
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1763
1784
  logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
1764
1785
  }
1765
1786
  return this.enhanceResult(baseResult, options, startTime);
@@ -2117,6 +2138,7 @@ export class BaseProvider {
2117
2138
  catch (ttsError) {
2118
2139
  const latency = Date.now() - ttsStartTime;
2119
2140
  const error = this.getTTSErrorDetails(ttsError);
2141
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
2120
2142
  logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
2121
2143
  return {
2122
2144
  ...enhancedResult,
@@ -242,6 +242,25 @@ export function createGeminiLoopAdapter(config) {
242
242
  });
243
243
  return next;
244
244
  },
245
+ // Gemini's prompt count already contains the cached tokens, and its
246
+ // candidate count leaves out the thinking tokens. The step's usage keeps
247
+ // that raw shape because the provider clients reconcile the turn totals
248
+ // from it once, and callers get those counts as the API reported them;
249
+ // only the generation observation is rebased, so a cached token is not
250
+ // billed at the input rate as well as the cache rate, a cached count above
251
+ // the prompt is not billed as extra tokens, and a thinking token is billed
252
+ // at the output rate.
253
+ observedUsage(usage) {
254
+ const cached = usage.cacheReadTokens === undefined
255
+ ? undefined
256
+ : Math.min(usage.cacheReadTokens, usage.inputTokens);
257
+ return {
258
+ ...usage,
259
+ inputTokens: Math.max(0, usage.inputTokens - (cached ?? 0)),
260
+ outputTokens: usage.outputTokens + (usage.reasoningTokens ?? 0),
261
+ ...(cached !== undefined && { cacheReadTokens: cached }),
262
+ };
263
+ },
245
264
  mapFinishReason(rawStopReason, hadToolCallsAtCap) {
246
265
  const mapped = mapGeminiFinishReason(rawStopReason);
247
266
  // "tool-calls" from this hook means the step cap cut the turn off with
@@ -306,6 +306,7 @@ async function executeObservedStep(adapter, request, channel, signal, span) {
306
306
  catch (err) {
307
307
  throw err instanceof PostEmissionStepError ? err.cause : err;
308
308
  }
309
+ const observed = adapter.observedUsage?.(result.usage) ?? result.usage;
309
310
  observeGeneration(generationSpan, {
310
311
  provider: request.generation?.provider,
311
312
  model: request.generation?.model,
@@ -316,13 +317,15 @@ async function executeObservedStep(adapter, request, channel, signal, span) {
316
317
  ...(result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}),
317
318
  },
318
319
  usage: {
319
- inputTokens: result.usage.inputTokens,
320
- outputTokens: result.usage.outputTokens,
321
- cacheReadTokens: result.usage.cacheReadTokens,
322
- cacheCreationTokens: result.usage.cacheWriteTokens,
323
- reasoningTokens: result.usage.reasoningTokens,
320
+ inputTokens: observed.inputTokens,
321
+ outputTokens: observed.outputTokens,
322
+ cacheReadTokens: observed.cacheReadTokens,
323
+ cacheCreationTokens: observed.cacheWriteTokens,
324
+ reasoningTokens: observed.reasoningTokens,
324
325
  },
325
- finishReason: result.rawStopReason,
326
+ // A step cut short before the provider's terminal event has no stop
327
+ // reason; record "other", as the turn's own finishReason does.
328
+ finishReason: result.rawStopReason ?? (signal.aborted ? "other" : undefined),
326
329
  aborted: signal.aborted,
327
330
  completionStartTime: firstChunkAt,
328
331
  });
@@ -370,7 +373,7 @@ export function runAgenticLoop(adapter, initialConversation, options) {
370
373
  // ever writes to it, so the loop is the same loop it was.
371
374
  let stepCap = adapter.maxSteps;
372
375
  const turnStartedAt = Date.now();
373
- const stepSpans = new StepSpans();
376
+ const stepSpans = new StepSpans(options.operation);
374
377
  try {
375
378
  for (let step = 0; step < stepCap; step++) {
376
379
  if (internalAbort.signal.aborted) {
@@ -129,10 +129,11 @@ export class StreamHandler {
129
129
  // captured upstream error (if any) so providerError /
130
130
  // modelResponseRaw carry the real cause.
131
131
  const sentinel = await buildNoOutputSentinel(error, result, getUnderlyingError?.());
132
- // Curator P2-5 + P3-6: stamp the active OTel span so
133
- // ContextEnricher.onEnd() surfaces a WARNING-level Langfuse
134
- // observation with finishReason + token usage. Centralized in
135
- // stampNoOutputSpan so every wired site stamps consistently.
132
+ // Curator P2-5 + P3-6: stamp the active OTel span with the
133
+ // WARNING observation level and a status message carrying
134
+ // finishReason + token usage, which ExporterSpanProcessor exports
135
+ // with the span. Centralized in stampNoOutputSpan so every wired
136
+ // site stamps consistently.
136
137
  stampNoOutputSpan(sentinel);
137
138
  // S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
138
139
  // and set the span to WARNING status instead of OK
@@ -13,7 +13,7 @@
13
13
  * @module core/modules/TelemetryHandler
14
14
  */
15
15
  import type { NeuroLink } from "../../neurolink.js";
16
- import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
16
+ import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage, TTSMetadata } from "../../types/index.js";
17
17
  /**
18
18
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
19
19
  */
@@ -34,6 +34,10 @@ export declare class TelemetryHandler {
34
34
  * Record performance metrics for a generation
35
35
  */
36
36
  recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
37
+ /**
38
+ * Record a TTS synthesis failure without affecting the generation result.
39
+ */
40
+ recordTTSFailure(ttsProvider: string, error: NonNullable<TTSMetadata["error"]>, latency: number): void;
37
41
  /**
38
42
  * Estimate cost only from known per-model rates. Unknown pricing remains
39
43
  * absent from cost metrics instead of using another model's default rate.
@@ -16,6 +16,7 @@ import { nanoid } from "nanoid";
16
16
  import { extractTokenUsage } from "../../utils/tokenUtils.js";
17
17
  import { logger } from "../../utils/logger.js";
18
18
  import { recordProviderPerformanceFromMetrics } from "../evaluationProviders.js";
19
+ import { recordAIRequest, recordCustomHistogram, recordCustomMetric, } from "../../telemetry/aiMetrics.js";
19
20
  import { calculateCost, hasPricing } from "../../utils/pricing.js";
20
21
  /**
21
22
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
@@ -84,6 +85,7 @@ export class TelemetryHandler {
84
85
  cost: actualCost,
85
86
  success: true,
86
87
  });
88
+ recordAIRequest(this.providerName, this.modelName, totalTokens, responseTime, actualCost !== undefined && actualCost > 0 ? actualCost : undefined);
87
89
  logger.debug(`Performance recorded for ${this.providerName}`, {
88
90
  responseTime: `${responseTime}ms`,
89
91
  tokens: totalTokens,
@@ -94,6 +96,20 @@ export class TelemetryHandler {
94
96
  logger.warn("⚠️ Performance recording failed:", perfError);
95
97
  }
96
98
  }
99
+ /**
100
+ * Record a TTS synthesis failure without affecting the generation result.
101
+ */
102
+ recordTTSFailure(ttsProvider, error, latency) {
103
+ const labels = {
104
+ provider: ttsProvider,
105
+ error_code: error.code,
106
+ ...(error.retriable !== undefined
107
+ ? { retriable: error.retriable.toString() }
108
+ : {}),
109
+ };
110
+ recordCustomMetric("tts_failures", 1, labels);
111
+ recordCustomHistogram("tts_failure_latency_ms", latency, labels);
112
+ }
97
113
  /**
98
114
  * Estimate cost only from known per-model rates. Unknown pricing remains
99
115
  * absent from cost metrics instead of using another model's default rate.
@@ -512,6 +512,14 @@ export class ToolsManager {
512
512
  const idField = typeof toolCallId === "string" && toolCallId.length > 0
513
513
  ? { toolCallId }
514
514
  : {};
515
+ // Same per-invocation id executeTool gives registered tools, so a
516
+ // listener can pair this start with its end.
517
+ const eventIds = {
518
+ ...idField,
519
+ executionId: `${toolName}-${Date.now()}-${Math.random()
520
+ .toString(36)
521
+ .slice(2, 11)}`,
522
+ };
515
523
  return withSpan({
516
524
  name: toolSpanName(toolName),
517
525
  tracer: tracers.mcp,
@@ -521,7 +529,7 @@ export class ToolsManager {
521
529
  const startTime = Date.now();
522
530
  this.emitToolEvent("tool:start", toolName, {
523
531
  input: params,
524
- ...idField,
532
+ ...eventIds,
525
533
  });
526
534
  let result;
527
535
  try {
@@ -533,7 +541,7 @@ export class ToolsManager {
533
541
  error: errorMsg,
534
542
  success: false,
535
543
  responseTime: Date.now() - startTime,
536
- ...idField,
544
+ ...eventIds,
537
545
  });
538
546
  throw error;
539
547
  }
@@ -549,7 +557,7 @@ export class ToolsManager {
549
557
  success: isErrorText === undefined,
550
558
  ...(isErrorText !== undefined ? { error: isErrorText } : {}),
551
559
  responseTime: Date.now() - startTime,
552
- ...idField,
560
+ ...eventIds,
553
561
  });
554
562
  return result;
555
563
  });
@@ -860,6 +868,9 @@ export class ToolsManager {
860
868
  attributes: {
861
869
  "tool.name": toolName,
862
870
  "tool.type": "custom",
871
+ // Pure wrapper around the `tool` observation of the same
872
+ // call: it stays an OpenTelemetry span but is not exported.
873
+ [ATTR.NL_INTERNAL]: true,
863
874
  },
864
875
  });
865
876
  const startTime = Date.now();
@@ -84,6 +84,8 @@ export declare const toPrepareStepRecord: (step: {
84
84
  export declare const hasNativeDoGenerate: (value: unknown) => value is {
85
85
  doGenerate: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
86
86
  };
87
+ /** A tool's inputSchema with its lazy (function) form, if any, resolved. */
88
+ export declare const resolveToolInputSchema: (schema: Tool["inputSchema"]) => Exclude<Tool["inputSchema"], (...args: never[]) => unknown>;
87
89
  /**
88
90
  * Spell a JSON Schema into the conversation's system turn.
89
91
  *