@juspay/neurolink 9.55.10 → 9.56.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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +301 -301
- package/dist/core/modules/StreamHandler.js +12 -0
- package/dist/core/modules/ToolsManager.js +4 -0
- package/dist/core/redisConversationMemoryManager.js +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -1
- package/dist/lib/core/modules/StreamHandler.js +12 -0
- package/dist/lib/core/modules/ToolsManager.js +4 -0
- package/dist/lib/core/redisConversationMemoryManager.js +25 -0
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +4 -1
- package/dist/lib/mcp/toolDiscoveryService.js +99 -3
- package/dist/lib/mcp/toolRegistry.js +3 -0
- package/dist/lib/neurolink.js +67 -23
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +26 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +98 -15
- package/dist/lib/utils/conversationMemory.js +14 -1
- package/dist/lib/utils/mcpErrorText.d.ts +10 -0
- package/dist/lib/utils/mcpErrorText.js +36 -0
- package/dist/lib/utils/timeout.js +6 -0
- package/dist/mcp/toolDiscoveryService.js +99 -3
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.js +67 -23
- package/dist/services/server/ai/observability/instrumentation.d.ts +26 -0
- package/dist/services/server/ai/observability/instrumentation.js +98 -15
- package/dist/utils/conversationMemory.js +14 -1
- package/dist/utils/mcpErrorText.d.ts +10 -0
- package/dist/utils/mcpErrorText.js +35 -0
- package/dist/utils/timeout.js +6 -0
- package/package.json +1 -1
|
@@ -113,6 +113,18 @@ export class StreamHandler {
|
|
|
113
113
|
// rather than crashing the process with an unhandled rejection.
|
|
114
114
|
if (NoOutputGeneratedError.isInstance(error)) {
|
|
115
115
|
logger.warn(`${providerName}: Stream produced no output (NoOutputGeneratedError), returning empty stream`);
|
|
116
|
+
// Curator P2-5: stamp the active OTel span so ContextEnricher.onEnd()
|
|
117
|
+
// surfaces a WARNING-level Langfuse observation instead of defaulting
|
|
118
|
+
// to DEFAULT with no status message.
|
|
119
|
+
try {
|
|
120
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
121
|
+
if (activeSpan) {
|
|
122
|
+
activeSpan.setAttribute("neurolink.no_output", true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Tracing not initialized — ignore.
|
|
127
|
+
}
|
|
116
128
|
// S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
|
|
117
129
|
// and set the span to WARNING status instead of OK
|
|
118
130
|
yield {
|
|
@@ -399,6 +399,10 @@ export class ToolsManager {
|
|
|
399
399
|
attributes: {
|
|
400
400
|
"tool.name": toolName,
|
|
401
401
|
"tool.type": "custom",
|
|
402
|
+
// Curator P1-3: pure wrapper — duplicates the AI SDK's
|
|
403
|
+
// ai.toolCall observation in Langfuse. Keep the OTel span
|
|
404
|
+
// for internal metrics; filter from Langfuse export.
|
|
405
|
+
"langfuse.internal": true,
|
|
402
406
|
},
|
|
403
407
|
});
|
|
404
408
|
const startTime = Date.now();
|
|
@@ -631,6 +631,11 @@ export class RedisConversationMemoryManager {
|
|
|
631
631
|
* Applies sendToolPreview toggle and hydrates result.result for backward compat
|
|
632
632
|
*/
|
|
633
633
|
async buildContextMessages(sessionId, userId, enableSummarization, requestId) {
|
|
634
|
+
logger.debug("[RedisConversationMemoryManager] Building context messages", {
|
|
635
|
+
sessionId,
|
|
636
|
+
userId,
|
|
637
|
+
enableSummarization,
|
|
638
|
+
});
|
|
634
639
|
await this.ensureInitialized();
|
|
635
640
|
if (!this.redisClient) {
|
|
636
641
|
logger.warn("[RedisConversationMemoryManager] Redis client not available in buildContextMessages");
|
|
@@ -654,6 +659,11 @@ export class RedisConversationMemoryManager {
|
|
|
654
659
|
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
|
655
660
|
const conversationData = await withTimeout(redisClient.get(redisKey), REDIS_TIMEOUT_MS);
|
|
656
661
|
const conversation = deserializeConversation(conversationData || null);
|
|
662
|
+
logger.debug("[RedisConversationMemoryManager] Retrieved conversation for context building", {
|
|
663
|
+
sessionId,
|
|
664
|
+
userId,
|
|
665
|
+
conversationFound: !!conversation,
|
|
666
|
+
});
|
|
657
667
|
if (!conversation) {
|
|
658
668
|
span.setAttribute("session.found", false);
|
|
659
669
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -672,6 +682,12 @@ export class RedisConversationMemoryManager {
|
|
|
672
682
|
lastActivity: new Date(conversation.updatedAt).getTime(),
|
|
673
683
|
};
|
|
674
684
|
const contextMessages = buildContextFromPointer(session, requestId);
|
|
685
|
+
logger.debug("[RedisConversationMemoryManager] Built context messages from pointer", {
|
|
686
|
+
sessionId,
|
|
687
|
+
userId,
|
|
688
|
+
contextMessageCount: contextMessages.length,
|
|
689
|
+
pointerMessageId: session.summarizedUpToMessageId || "none",
|
|
690
|
+
});
|
|
675
691
|
const sendToolPreview = this.config?.contextCompaction?.sendToolPreview === true;
|
|
676
692
|
// Map tool_result messages: apply preview toggle + hydrate result.result
|
|
677
693
|
const finalMessages = contextMessages.map((msg) => {
|
|
@@ -695,6 +711,15 @@ export class RedisConversationMemoryManager {
|
|
|
695
711
|
}
|
|
696
712
|
hydratedResult = { ...msg.result, result: parsedResult };
|
|
697
713
|
}
|
|
714
|
+
logger.debug("[RedisConversationMemoryManager] Processing tool_result message for context", {
|
|
715
|
+
sessionId,
|
|
716
|
+
userId,
|
|
717
|
+
messageId: msg.id,
|
|
718
|
+
sendToolPreview,
|
|
719
|
+
hasPreview: !!msg.metadata?.toolOutputPreview,
|
|
720
|
+
contentLength: content ? String(content).length : 0,
|
|
721
|
+
resultHydrated: hydratedResult !== msg.result,
|
|
722
|
+
});
|
|
698
723
|
return { ...msg, content, result: hydratedResult };
|
|
699
724
|
});
|
|
700
725
|
// Tool messages now have real content and participate in context properly.
|
package/dist/index.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export { TTSProcessor } from "./utils/ttsProcessor.js";
|
|
|
48
48
|
import { NeuroLink } from "./neurolink.js";
|
|
49
49
|
export { NeuroLink };
|
|
50
50
|
export { buildObservabilityConfigFromEnv } from "./utils/observabilityHelpers.js";
|
|
51
|
-
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
-
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
51
|
+
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
+
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, isLangfuseInternalSpan, langfuseShouldExportSpan, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
53
53
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
54
54
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
55
55
|
export { MiddlewareFactory } from "./middleware/factory.js";
|
package/dist/index.js
CHANGED
|
@@ -80,11 +80,14 @@ import { createContextEnricher, flushOpenTelemetry,
|
|
|
80
80
|
// Enhanced context and tracing
|
|
81
81
|
getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor,
|
|
82
82
|
// NEW EXPORTS - External TracerProvider Support
|
|
83
|
-
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
83
|
+
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
84
84
|
import { getTelemetryStatus as getStatus, initializeTelemetry as init, } from "./telemetry/index.js";
|
|
85
85
|
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized,
|
|
86
86
|
// NEW EXPORTS - External TracerProvider Support
|
|
87
87
|
getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider,
|
|
88
|
+
// Host-processor filter helpers — reuse NeuroLink's internal-span filtering
|
|
89
|
+
// when the host app registers its own LangfuseSpanProcessor.
|
|
90
|
+
isLangfuseInternalSpan, langfuseShouldExportSpan,
|
|
88
91
|
// Enhanced context and tracing
|
|
89
92
|
getLangfuseContext, getTracer,
|
|
90
93
|
// ALS context propagation helper
|
|
@@ -113,6 +113,18 @@ export class StreamHandler {
|
|
|
113
113
|
// rather than crashing the process with an unhandled rejection.
|
|
114
114
|
if (NoOutputGeneratedError.isInstance(error)) {
|
|
115
115
|
logger.warn(`${providerName}: Stream produced no output (NoOutputGeneratedError), returning empty stream`);
|
|
116
|
+
// Curator P2-5: stamp the active OTel span so ContextEnricher.onEnd()
|
|
117
|
+
// surfaces a WARNING-level Langfuse observation instead of defaulting
|
|
118
|
+
// to DEFAULT with no status message.
|
|
119
|
+
try {
|
|
120
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
121
|
+
if (activeSpan) {
|
|
122
|
+
activeSpan.setAttribute("neurolink.no_output", true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Tracing not initialized — ignore.
|
|
127
|
+
}
|
|
116
128
|
// S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
|
|
117
129
|
// and set the span to WARNING status instead of OK
|
|
118
130
|
yield {
|
|
@@ -399,6 +399,10 @@ export class ToolsManager {
|
|
|
399
399
|
attributes: {
|
|
400
400
|
"tool.name": toolName,
|
|
401
401
|
"tool.type": "custom",
|
|
402
|
+
// Curator P1-3: pure wrapper — duplicates the AI SDK's
|
|
403
|
+
// ai.toolCall observation in Langfuse. Keep the OTel span
|
|
404
|
+
// for internal metrics; filter from Langfuse export.
|
|
405
|
+
"langfuse.internal": true,
|
|
402
406
|
},
|
|
403
407
|
});
|
|
404
408
|
const startTime = Date.now();
|
|
@@ -631,6 +631,11 @@ export class RedisConversationMemoryManager {
|
|
|
631
631
|
* Applies sendToolPreview toggle and hydrates result.result for backward compat
|
|
632
632
|
*/
|
|
633
633
|
async buildContextMessages(sessionId, userId, enableSummarization, requestId) {
|
|
634
|
+
logger.debug("[RedisConversationMemoryManager] Building context messages", {
|
|
635
|
+
sessionId,
|
|
636
|
+
userId,
|
|
637
|
+
enableSummarization,
|
|
638
|
+
});
|
|
634
639
|
await this.ensureInitialized();
|
|
635
640
|
if (!this.redisClient) {
|
|
636
641
|
logger.warn("[RedisConversationMemoryManager] Redis client not available in buildContextMessages");
|
|
@@ -654,6 +659,11 @@ export class RedisConversationMemoryManager {
|
|
|
654
659
|
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
|
655
660
|
const conversationData = await withTimeout(redisClient.get(redisKey), REDIS_TIMEOUT_MS);
|
|
656
661
|
const conversation = deserializeConversation(conversationData || null);
|
|
662
|
+
logger.debug("[RedisConversationMemoryManager] Retrieved conversation for context building", {
|
|
663
|
+
sessionId,
|
|
664
|
+
userId,
|
|
665
|
+
conversationFound: !!conversation,
|
|
666
|
+
});
|
|
657
667
|
if (!conversation) {
|
|
658
668
|
span.setAttribute("session.found", false);
|
|
659
669
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -672,6 +682,12 @@ export class RedisConversationMemoryManager {
|
|
|
672
682
|
lastActivity: new Date(conversation.updatedAt).getTime(),
|
|
673
683
|
};
|
|
674
684
|
const contextMessages = buildContextFromPointer(session, requestId);
|
|
685
|
+
logger.debug("[RedisConversationMemoryManager] Built context messages from pointer", {
|
|
686
|
+
sessionId,
|
|
687
|
+
userId,
|
|
688
|
+
contextMessageCount: contextMessages.length,
|
|
689
|
+
pointerMessageId: session.summarizedUpToMessageId || "none",
|
|
690
|
+
});
|
|
675
691
|
const sendToolPreview = this.config?.contextCompaction?.sendToolPreview === true;
|
|
676
692
|
// Map tool_result messages: apply preview toggle + hydrate result.result
|
|
677
693
|
const finalMessages = contextMessages.map((msg) => {
|
|
@@ -695,6 +711,15 @@ export class RedisConversationMemoryManager {
|
|
|
695
711
|
}
|
|
696
712
|
hydratedResult = { ...msg.result, result: parsedResult };
|
|
697
713
|
}
|
|
714
|
+
logger.debug("[RedisConversationMemoryManager] Processing tool_result message for context", {
|
|
715
|
+
sessionId,
|
|
716
|
+
userId,
|
|
717
|
+
messageId: msg.id,
|
|
718
|
+
sendToolPreview,
|
|
719
|
+
hasPreview: !!msg.metadata?.toolOutputPreview,
|
|
720
|
+
contentLength: content ? String(content).length : 0,
|
|
721
|
+
resultHydrated: hydratedResult !== msg.result,
|
|
722
|
+
});
|
|
698
723
|
return { ...msg, content, result: hydratedResult };
|
|
699
724
|
});
|
|
700
725
|
// Tool messages now have real content and participate in context properly.
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export { TTSProcessor } from "./utils/ttsProcessor.js";
|
|
|
48
48
|
import { NeuroLink } from "./neurolink.js";
|
|
49
49
|
export { NeuroLink };
|
|
50
50
|
export { buildObservabilityConfigFromEnv } from "./utils/observabilityHelpers.js";
|
|
51
|
-
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
-
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
51
|
+
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
+
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, isLangfuseInternalSpan, langfuseShouldExportSpan, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
53
53
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
54
54
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
55
55
|
export { MiddlewareFactory } from "./middleware/factory.js";
|
package/dist/lib/index.js
CHANGED
|
@@ -80,11 +80,14 @@ import { createContextEnricher, flushOpenTelemetry,
|
|
|
80
80
|
// Enhanced context and tracing
|
|
81
81
|
getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor,
|
|
82
82
|
// NEW EXPORTS - External TracerProvider Support
|
|
83
|
-
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
83
|
+
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
84
84
|
import { getTelemetryStatus as getStatus, initializeTelemetry as init, } from "./telemetry/index.js";
|
|
85
85
|
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized,
|
|
86
86
|
// NEW EXPORTS - External TracerProvider Support
|
|
87
87
|
getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider,
|
|
88
|
+
// Host-processor filter helpers — reuse NeuroLink's internal-span filtering
|
|
89
|
+
// when the host app registers its own LangfuseSpanProcessor.
|
|
90
|
+
isLangfuseInternalSpan, langfuseShouldExportSpan,
|
|
88
91
|
// Enhanced context and tracing
|
|
89
92
|
getLangfuseContext, getTracer,
|
|
90
93
|
// ALS context propagation helper
|
|
@@ -9,10 +9,72 @@ import { globalCircuitBreakerManager, CircuitBreakerOpenError, } from "./mcpCirc
|
|
|
9
9
|
import { isObject, isNullish } from "../utils/typeUtils.js";
|
|
10
10
|
import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
|
|
11
11
|
import { withTimeout } from "../utils/errorHandling.js";
|
|
12
|
+
import { extractMcpErrorText } from "../utils/mcpErrorText.js";
|
|
12
13
|
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
13
14
|
import { tracers } from "../telemetry/tracers.js";
|
|
14
15
|
import { withSpan } from "../telemetry/withSpan.js";
|
|
15
16
|
const mcpTracer = tracers.mcp;
|
|
17
|
+
/**
|
|
18
|
+
* JSON-stringify a value for a Langfuse input/output preview attribute,
|
|
19
|
+
* truncated to a hard cap to stay under span attribute size limits. The
|
|
20
|
+
* returned string is guaranteed to be ≤ maxLen characters; when truncated,
|
|
21
|
+
* the last character is replaced with an ellipsis.
|
|
22
|
+
*/
|
|
23
|
+
function safeJsonStringify(value, maxLen) {
|
|
24
|
+
if (maxLen <= 0) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const str = JSON.stringify(value);
|
|
29
|
+
if (typeof str !== "string") {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
if (str.length <= maxLen) {
|
|
33
|
+
return str;
|
|
34
|
+
}
|
|
35
|
+
return str.slice(0, Math.max(0, maxLen - 1)) + "…";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Match property names that commonly hold secrets. Values under these keys
|
|
43
|
+
* are replaced with `[REDACTED]` before serialization. Case-insensitive.
|
|
44
|
+
* Conservative list — anything matching *here* is masked; the rest of the
|
|
45
|
+
* structure is preserved so Langfuse still gets a meaningful preview.
|
|
46
|
+
*/
|
|
47
|
+
const SENSITIVE_KEY_PATTERN = /^(password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|authorization|auth|bearer|credential|cookie|session[_-]?id|private[_-]?key|client[_-]?secret|refresh[_-]?token|x-api-key)$/i;
|
|
48
|
+
/**
|
|
49
|
+
* Walk a value, producing a structurally-equivalent copy with sensitive-key
|
|
50
|
+
* values masked. Unlike `transformParamsForLogging` (which collapses objects
|
|
51
|
+
* to a "N params" string), this preserves non-sensitive content so Langfuse
|
|
52
|
+
* input/output previews stay useful. Bounded depth guards against cycles.
|
|
53
|
+
*/
|
|
54
|
+
function redactForPreview(value, depth = 0) {
|
|
55
|
+
if (depth > 10) {
|
|
56
|
+
return "[...]";
|
|
57
|
+
}
|
|
58
|
+
if (value === null || value === undefined) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "object") {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.map((v) => redactForPreview(v, depth + 1));
|
|
66
|
+
}
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, v] of Object.entries(value)) {
|
|
69
|
+
if (SENSITIVE_KEY_PATTERN.test(k)) {
|
|
70
|
+
out[k] = "[REDACTED]";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out[k] = redactForPreview(v, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
16
78
|
/**
|
|
17
79
|
* Default timeout for MCP tool execution operations in milliseconds.
|
|
18
80
|
* Configurable via MCP_TOOL_TIMEOUT env var.
|
|
@@ -376,6 +438,18 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
376
438
|
"mcp.server_id": serverId,
|
|
377
439
|
"mcp.tool_name": toolName,
|
|
378
440
|
"mcp.timeout_ms": effectiveTimeout,
|
|
441
|
+
// Curator P1-4: Langfuse observations rely on ai.*/gen_ai.*
|
|
442
|
+
// attributes for tool name and I/O previews. Provide them so
|
|
443
|
+
// the SPAN observation in Langfuse is legible without
|
|
444
|
+
// timestamp-joining against the parent ai.toolCall. Redact
|
|
445
|
+
// parameters via the existing secret-stripping helper so
|
|
446
|
+
// tokens/credentials/paths don't leave the process.
|
|
447
|
+
"ai.tool.name": toolName,
|
|
448
|
+
"gen_ai.tool.name": toolName,
|
|
449
|
+
"gen_ai.request": safeJsonStringify({
|
|
450
|
+
name: toolName,
|
|
451
|
+
arguments: redactForPreview(parameters),
|
|
452
|
+
}, 2048),
|
|
379
453
|
},
|
|
380
454
|
}, async (callSpan) => {
|
|
381
455
|
try {
|
|
@@ -384,11 +458,26 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
384
458
|
name: toolName,
|
|
385
459
|
arguments: parameters,
|
|
386
460
|
}), timeout, new Error(`Tool execution timeout: ${toolName}`));
|
|
387
|
-
|
|
461
|
+
// Curator P0-1/P0-2: the MCP client does NOT throw on protocol
|
|
462
|
+
// errors — it returns { isError: true, content: [...] }. Detect
|
|
463
|
+
// that pattern so the span status reflects reality.
|
|
464
|
+
const resultObj = callResult;
|
|
465
|
+
if (resultObj && resultObj.isError === true) {
|
|
466
|
+
const errorText = extractMcpErrorText(resultObj);
|
|
467
|
+
callSpan.setStatus({
|
|
468
|
+
code: SpanStatusCode.ERROR,
|
|
469
|
+
message: errorText || `Tool ${toolName} returned isError`,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
callSpan.setStatus({ code: SpanStatusCode.OK });
|
|
474
|
+
}
|
|
388
475
|
// ── MCP output normalization ──────────────────────────────────
|
|
389
476
|
// Intercept here — after receive, before cache, before memory,
|
|
390
477
|
// before LLM context injection. Returns a compact surrogate when
|
|
391
478
|
// the payload exceeds mcp.outputLimits.maxBytes.
|
|
479
|
+
let resultForPreview = callResult;
|
|
480
|
+
let resultForReturn = callResult;
|
|
392
481
|
if (this.outputNormalizer) {
|
|
393
482
|
try {
|
|
394
483
|
const normalized = await this.outputNormalizer.normalize(callResult, { toolName, serverId });
|
|
@@ -396,7 +485,8 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
396
485
|
if (normalized.isExternalized) {
|
|
397
486
|
callSpan.setAttribute("mcp.output.original_bytes", normalized.originalBytes);
|
|
398
487
|
}
|
|
399
|
-
|
|
488
|
+
resultForPreview = normalized.result;
|
|
489
|
+
resultForReturn = normalized.result;
|
|
400
490
|
}
|
|
401
491
|
catch (normErr) {
|
|
402
492
|
mcpLogger.warn(`[ToolDiscoveryService] McpOutputNormalizer failed for ` +
|
|
@@ -405,7 +495,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
405
495
|
}
|
|
406
496
|
}
|
|
407
497
|
// ── end normalization ─────────────────────────────────────────
|
|
408
|
-
|
|
498
|
+
// Curator P1-4: build gen_ai.response AFTER normalization so
|
|
499
|
+
// large payloads use the compact surrogate instead of the raw
|
|
500
|
+
// result (avoids redundant stringify + memory hit on payloads
|
|
501
|
+
// that were specifically externalized to Redis). Redact via the
|
|
502
|
+
// same secret-stripping path used for request parameters.
|
|
503
|
+
callSpan.setAttribute("gen_ai.response", safeJsonStringify(redactForPreview(resultForPreview), 2048));
|
|
504
|
+
return resultForReturn;
|
|
409
505
|
}
|
|
410
506
|
catch (err) {
|
|
411
507
|
callSpan.setStatus({
|
|
@@ -257,6 +257,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
257
257
|
attributes: {
|
|
258
258
|
[ATTR.GEN_AI_TOOL_NAME]: toolName,
|
|
259
259
|
[ATTR.MCP_SERVER_ID]: preResolvedServerId || "builtin",
|
|
260
|
+
// Curator P1-3: registry-level wrapper — duplicates ai.toolCall in
|
|
261
|
+
// Langfuse. Retained for OTel/metrics; skipped for Langfuse export.
|
|
262
|
+
"langfuse.internal": true,
|
|
260
263
|
},
|
|
261
264
|
}, async (span) => {
|
|
262
265
|
try {
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -64,6 +64,7 @@ import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableErr
|
|
|
64
64
|
// Factory processing imports
|
|
65
65
|
import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
|
|
66
66
|
import { logger, mcpLogger } from "./utils/logger.js";
|
|
67
|
+
import { extractMcpErrorText } from "./utils/mcpErrorText.js";
|
|
67
68
|
import { createCustomToolServerInfo, detectCategory, } from "./utils/mcpDefaults.js";
|
|
68
69
|
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
69
70
|
// Import orchestration components
|
|
@@ -133,29 +134,6 @@ function mcpCategoryToErrorCategory(mcpCategory) {
|
|
|
133
134
|
return ErrorCategory.EXECUTION;
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Extract a human-readable error string from an MCP isError result object.
|
|
138
|
-
* Returns an empty string if nothing useful can be extracted.
|
|
139
|
-
*/
|
|
140
|
-
function extractMcpErrorText(raw) {
|
|
141
|
-
try {
|
|
142
|
-
const resultObj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
143
|
-
if (!resultObj || typeof resultObj !== "object") {
|
|
144
|
-
return "";
|
|
145
|
-
}
|
|
146
|
-
const content = resultObj.content;
|
|
147
|
-
if (!Array.isArray(content)) {
|
|
148
|
-
return "";
|
|
149
|
-
}
|
|
150
|
-
const texts = content
|
|
151
|
-
.filter((c) => c.type === "text" && c.text)
|
|
152
|
-
.map((c) => c.text);
|
|
153
|
-
return texts.join(" ").substring(0, 500);
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return "";
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
137
|
/**
|
|
160
138
|
* Check if an error is a non-retryable provider error that should immediately
|
|
161
139
|
* stop the retry/fallback chain. These errors represent permanent failures
|
|
@@ -4508,6 +4486,16 @@ Current user's request: ${currentInput}`;
|
|
|
4508
4486
|
* @throws {Error} When conversation memory operations fail (if enabled)
|
|
4509
4487
|
*/
|
|
4510
4488
|
async stream(options) {
|
|
4489
|
+
logger.debug("[NeuroLink] stream() called with options", {
|
|
4490
|
+
provider: options.provider,
|
|
4491
|
+
model: options.model,
|
|
4492
|
+
inputLength: options.input?.text?.length || 0,
|
|
4493
|
+
disableTools: options.disableTools,
|
|
4494
|
+
enableAnalytics: options.enableAnalytics,
|
|
4495
|
+
enableEvaluation: options.enableEvaluation,
|
|
4496
|
+
contextKeys: options.context ? Object.keys(options.context) : [],
|
|
4497
|
+
optionKeys: Object.keys(options),
|
|
4498
|
+
});
|
|
4511
4499
|
return metricsTraceContextStorage.run(this.createMetricsTraceContext(), () => this.executeStreamRequest({ ...options }));
|
|
4512
4500
|
}
|
|
4513
4501
|
async executeStreamRequest(options) {
|
|
@@ -4600,8 +4588,26 @@ Current user's request: ${currentInput}`;
|
|
|
4600
4588
|
}
|
|
4601
4589
|
async runStandardStreamRequest(params) {
|
|
4602
4590
|
const { options, streamSpan, spanStartTime, startTime, hrTimeStart, streamId, originalPrompt, } = params;
|
|
4591
|
+
logger.debug("[NeuroLink] Running standard stream request", {
|
|
4592
|
+
streamId,
|
|
4593
|
+
provider: options.provider,
|
|
4594
|
+
model: options.model,
|
|
4595
|
+
inputLength: options.input?.text?.length || 0,
|
|
4596
|
+
disableTools: options.disableTools,
|
|
4597
|
+
enableAnalytics: options.enableAnalytics,
|
|
4598
|
+
enableEvaluation: options.enableEvaluation,
|
|
4599
|
+
contextKeys: options.context ? Object.keys(options.context) : [],
|
|
4600
|
+
optionKeys: Object.keys(options),
|
|
4601
|
+
sessionId: options.context?.sessionId,
|
|
4602
|
+
});
|
|
4603
4603
|
try {
|
|
4604
4604
|
const { enhancedOptions, factoryResult } = await this.prepareStreamOptions(options, streamId, startTime, hrTimeStart);
|
|
4605
|
+
logger.debug("[NeuroLink] Stream options prepared", {
|
|
4606
|
+
streamId,
|
|
4607
|
+
options: enhancedOptions,
|
|
4608
|
+
factoryResult,
|
|
4609
|
+
sessionId: enhancedOptions.context?.sessionId,
|
|
4610
|
+
});
|
|
4605
4611
|
const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, } = await this.createMCPStream(enhancedOptions);
|
|
4606
4612
|
const streamState = {
|
|
4607
4613
|
finishReason: streamFinishReason ?? "stop",
|
|
@@ -4691,6 +4697,16 @@ Current user's request: ${currentInput}`;
|
|
|
4691
4697
|
});
|
|
4692
4698
|
}
|
|
4693
4699
|
catch (error) {
|
|
4700
|
+
logger.debug("[NeuroLink.stream] Stream error occurred", {
|
|
4701
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4702
|
+
name: error instanceof Error ? error.name : "UnknownError",
|
|
4703
|
+
provider: providerName,
|
|
4704
|
+
model: enhancedOptions.model,
|
|
4705
|
+
chunkCount,
|
|
4706
|
+
totalLength: accumulatedContent.length,
|
|
4707
|
+
durationMs: Date.now() - streamStartTime,
|
|
4708
|
+
sessionId,
|
|
4709
|
+
});
|
|
4694
4710
|
streamError = error;
|
|
4695
4711
|
self.emitter.emit("stream:error", {
|
|
4696
4712
|
type: "stream:error",
|
|
@@ -4709,6 +4725,16 @@ Current user's request: ${currentInput}`;
|
|
|
4709
4725
|
throw error;
|
|
4710
4726
|
}
|
|
4711
4727
|
finally {
|
|
4728
|
+
logger.debug("[NeuroLink.stream] Stream finished, performing cleanup", {
|
|
4729
|
+
provider: providerName,
|
|
4730
|
+
model: enhancedOptions.model,
|
|
4731
|
+
totalChunks: chunkCount,
|
|
4732
|
+
totalLength: accumulatedContent.length,
|
|
4733
|
+
durationMs: Date.now() - streamStartTime,
|
|
4734
|
+
fallbackAttempted: metadata.fallbackAttempted,
|
|
4735
|
+
guardrailsBlocked: metadata.guardrailsBlocked,
|
|
4736
|
+
error: metadata.error,
|
|
4737
|
+
});
|
|
4712
4738
|
self._disableToolCacheForCurrentRequest = false;
|
|
4713
4739
|
cleanupListeners();
|
|
4714
4740
|
streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
|
|
@@ -5106,6 +5132,11 @@ Current user's request: ${currentInput}`;
|
|
|
5106
5132
|
*/
|
|
5107
5133
|
async storeStreamConversationMemory(params) {
|
|
5108
5134
|
const { enhancedOptions, providerName, originalPrompt, accumulatedContent, startTime, eventSequence, } = params;
|
|
5135
|
+
logger.debug("[NeuroLink.stream] Preparing to store conversation turn in memory", {
|
|
5136
|
+
options: JSON.stringify(enhancedOptions),
|
|
5137
|
+
sessionId: enhancedOptions.context
|
|
5138
|
+
?.sessionId,
|
|
5139
|
+
});
|
|
5109
5140
|
// Guard: skip storing if no meaningful content was produced (no text AND no tool activity)
|
|
5110
5141
|
const hasToolEvents = eventSequence.some((e) => e.type === "tool:start" || e.type === "tool:end");
|
|
5111
5142
|
if (!accumulatedContent.trim() && !hasToolEvents) {
|
|
@@ -5115,6 +5146,12 @@ Current user's request: ${currentInput}`;
|
|
|
5115
5146
|
});
|
|
5116
5147
|
return;
|
|
5117
5148
|
}
|
|
5149
|
+
logger.debug("[NeuroLink.stream] Storing conversation turn in memory", {
|
|
5150
|
+
options: JSON.stringify(enhancedOptions),
|
|
5151
|
+
sessionId: enhancedOptions.context
|
|
5152
|
+
?.sessionId,
|
|
5153
|
+
conversationMemoryExists: this.conversationMemory ? true : false,
|
|
5154
|
+
});
|
|
5118
5155
|
// Store memory after stream consumption is complete
|
|
5119
5156
|
if (this.conversationMemory && enhancedOptions.context?.sessionId) {
|
|
5120
5157
|
const sessionId = enhancedOptions.context
|
|
@@ -6267,6 +6304,13 @@ Current user's request: ${currentInput}`;
|
|
|
6267
6304
|
"tool.type": executionContext.toolType,
|
|
6268
6305
|
"tool.input_size": executionContext.inputSize,
|
|
6269
6306
|
"tool.input_preview": executionContext.truncatedInput,
|
|
6307
|
+
// NOT marked langfuse.internal: this is the public entrypoint for
|
|
6308
|
+
// `NeuroLink.executeTool()`. Direct API callers (not going through
|
|
6309
|
+
// the AI SDK) would otherwise produce zero Langfuse observations —
|
|
6310
|
+
// the lower-level registry/discovery spans are internal wrappers.
|
|
6311
|
+
// AI-SDK-initiated custom tools will produce both ai.toolCall and
|
|
6312
|
+
// this span, which is the accepted tradeoff for keeping direct
|
|
6313
|
+
// invocations observable.
|
|
6270
6314
|
},
|
|
6271
6315
|
}, (toolSpan) => this.executeToolWithSpan(toolName, params, options, executionContext, toolSpan));
|
|
6272
6316
|
}
|
|
@@ -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
|
*
|