@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
1,096 lines • 509 kB
JavaScript
/**
* NeuroLink - Unified AI Interface with Real MCP Tool Integration
*
* REDESIGNED FALLBACK CHAIN - NO CIRCULAR DEPENDENCIES
* Enhanced AI provider system with natural MCP tool access.
* Uses real MCP infrastructure for tool discovery and execution.
*/
// Load environment variables from .env file (critical for SDK usage)
// Suppress dotenv v17 stdout banner — it pollutes CLI JSON output
try {
process.env.DOTENV_CONFIG_QUIET = process.env.DOTENV_CONFIG_QUIET ?? "true";
const { config: dotenvConfig } = await import("dotenv");
dotenvConfig({ quiet: true });
}
catch {
// Environment variables should be set externally in production
}
import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
import { AsyncLocalStorage } from "async_hooks";
import { EventEmitter } from "events";
import pLimit from "p-limit";
import { ErrorCategory, ErrorSeverity } from "./constants/enums.js";
import { CIRCUIT_BREAKER, CIRCUIT_BREAKER_RESET_MS, MEMORY_THRESHOLDS, NANOSECOND_TO_MS_DIVISOR, PERFORMANCE_THRESHOLDS, PROVIDER_TIMEOUTS, RETRY_ATTEMPTS, RETRY_DELAYS, TOOL_TIMEOUTS, } from "./constants/index.js";
import { checkContextBudget } from "./context/budgetChecker.js";
import { ContextCompactor } from "./context/contextCompactor.js";
import { emergencyContentTruncation } from "./context/emergencyTruncation.js";
import { getContextOverflowProvider, isContextOverflowError, parseProviderOverflowDetails, } from "./context/errorDetection.js";
import { ContextBudgetExceededError } from "./context/errors.js";
import { repairToolPairs } from "./context/toolPairRepair.js";
import { SYSTEM_LIMITS } from "./core/constants.js";
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
import { AIProviderFactory } from "./core/factory.js";
import { createToolEventPayload } from "./core/toolEvents.js";
import { ProviderRegistry } from "./factories/providerRegistry.js";
import { FileReferenceRegistry } from "./files/fileReferenceRegistry.js";
import { createFileTools } from "./files/fileTools.js";
import { HITLManager } from "./hitl/hitlManager.js";
import { ToolCallBatcher } from "./mcp/batching/index.js";
// MCP Enhancement modules - wired into core execution path
import { ToolResultCache } from "./mcp/caching/index.js";
import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
import { ExternalServerManager } from "./mcp/externalServerManager.js";
import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
import { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
import { ToolRouter } from "./mcp/routing/index.js";
// Import direct tools server for automatic registration
import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
import { inferAnnotations, isSafeToRetry } from "./mcp/toolAnnotations.js";
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
// Dynamic argument resolution imports
import { resolveDynamicArgument } from "./dynamic/dynamicResolver.js";
import { initializeHippocampus } from "./memory/hippocampusInitializer.js";
import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
import { getMetricsAggregator, MetricsAggregator, } from "./observability/metricsAggregator.js";
import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, AuthenticationError, AuthorizationError, InvalidModelError, ModelAccessDeniedError, } from "./types/index.js";
import { SpanSerializer } from "./observability/utils/spanSerializer.js";
import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
import { TaskManager } from "./tasks/taskManager.js";
import { createTaskTools } from "./tasks/tools/taskTools.js";
import { ATTR } from "./telemetry/attributes.js";
import { tracers } from "./telemetry/tracers.js";
import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
// Enhanced error handling imports
import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecycleCallbacks.js";
import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
// Factory processing imports
import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
import { logger, mcpLogger } from "./utils/logger.js";
import { extractMcpErrorText } from "./utils/mcpErrorText.js";
import { createCustomToolServerInfo, detectCategory, } from "./utils/mcpDefaults.js";
import { resolveModel } from "./utils/modelAliasResolver.js";
// Import orchestration components
import { ModelRouter } from "./utils/modelRouter.js";
import { getBestProvider } from "./utils/providerUtils.js";
import { NON_RETRYABLE_HTTP_STATUS_CODES } from "./utils/retryability.js";
import { isZodSchema } from "./utils/schemaConversion.js";
import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
// Tool detection and execution imports
// Transformation utilities
import { extractToolNames, optimizeToolForCollection, transformAvailableTools, transformParamsForLogging, transformToolExecutions, transformToolExecutionsForMCP, transformToolsForMCP, transformToolsToDescriptions, transformToolsToExpectedFormat, } from "./utils/transformationUtils.js";
import { isNonNullObject } from "./utils/typeUtils.js";
import { getWorkflow } from "./workflow/core/workflowRegistry.js";
import { runWorkflow } from "./workflow/core/workflowRunner.js";
/**
* NL-002: Classify MCP error messages into categories for AI disambiguation.
* Returns a human-readable error category based on error message content.
*/
function classifyMcpErrorMessage(text) {
const lower = text.toLowerCase();
if (lower.includes("not found") ||
lower.includes("404") ||
lower.includes("does not exist") ||
lower.includes("no such")) {
return "not_found";
}
if (lower.includes("permission") ||
lower.includes("forbidden") ||
lower.includes("403") ||
lower.includes("unauthorized") ||
lower.includes("401") ||
lower.includes("access denied")) {
return "permission_denied";
}
if (lower.includes("timeout") ||
lower.includes("timed out") ||
lower.includes("deadline exceeded")) {
return "timeout";
}
if (lower.includes("rate limit") ||
lower.includes("429") ||
lower.includes("too many requests") ||
lower.includes("throttl")) {
return "rate_limited";
}
if (lower.includes("invalid") ||
lower.includes("validation") ||
lower.includes("bad request") ||
lower.includes("400")) {
return "validation_error";
}
return "unknown";
}
function mcpCategoryToErrorCategory(mcpCategory) {
switch (mcpCategory) {
case "not_found":
return ErrorCategory.VALIDATION;
case "permission_denied":
return ErrorCategory.PERMISSION;
case "timeout":
return ErrorCategory.TIMEOUT;
case "rate_limited":
return ErrorCategory.RESOURCE;
case "validation_error":
return ErrorCategory.VALIDATION;
case "unknown":
return ErrorCategory.EXECUTION;
}
}
/**
* Check if an error is a non-retryable provider error that should immediately
* stop the retry/fallback chain. These errors represent permanent failures
* (e.g., model not found, authentication failed) where retrying with the
* same configuration will never succeed.
*
* This prevents wasting tokens and latency on guaranteed-to-fail retries.
* For example, a NOT_FOUND error for a model causes 6 retries of a 418KB
* message, wasting ~628,000 tokens and adding 10+ seconds of latency.
*/
/**
* Curator P2-3: detect model-access-denied without requiring the typed
* ModelAccessDeniedError class to be present (Issue #1 ships that class
* separately). Matches LiteLLM "team not allowed" / "team can only access
* models=[...]" plus typed-error markers when present.
*/
function looksLikeModelAccessDenied(error) {
if (!error) {
return false;
}
const e = error;
if (e.name === "ModelAccessDeniedError") {
return true;
}
if (e.code === "MODEL_ACCESS_DENIED") {
return true;
}
const msg = typeof e.message === "string"
? e.message
: error instanceof Error
? error.message
: String(error);
if (!msg) {
return false;
}
const lower = msg.toLowerCase();
return ((lower.includes("team") && lower.includes("not allowed")) ||
lower.includes("team can only access") ||
/not\s+allowed\s+to\s+access\s+(this\s+)?model/i.test(msg));
}
function isNonRetryableProviderError(error) {
// Check for typed error classes from providers
if (error instanceof InvalidModelError) {
return true;
}
if (error instanceof AuthenticationError) {
return true;
}
if (error instanceof AuthorizationError) {
return true;
}
// Curator P1-1: model-access-denied is permanent for the (provider, model)
// pair until the team whitelist changes. Retrying with the same config
// would just waste a second roundtrip. Caller / fallback-orchestrator
// should pick a different model.
if (error instanceof ModelAccessDeniedError) {
return true;
}
// Note: ContextBudgetExceededError is intentionally NOT non-retryable.
// Each provider has its own context window, so a budget rejection on
// one provider doesn't preclude another provider's window fitting the
// same payload. The directProviderGeneration loop should continue
// trying alternate providers; the after-loop rethrow preserves the
// typed error when all providers reject (see `directProviderGeneration`).
// Check for HTTP status codes on error objects (e.g., from Vercel AI SDK)
if (error && typeof error === "object") {
const err = error;
const status = typeof err.status === "number"
? err.status
: typeof err.statusCode === "number"
? err.statusCode
: undefined;
if (status && NON_RETRYABLE_HTTP_STATUS_CODES.includes(status)) {
return true;
}
}
// Check error message for NOT_FOUND patterns (catches wrapped errors)
if (error instanceof Error) {
const msg = error.message;
if (msg.includes("NOT_FOUND") ||
msg.includes("Model Not Found") ||
msg.includes("model not found") ||
msg.includes("PERMISSION_DENIED") ||
msg.includes("UNAUTHENTICATED")) {
return true;
}
}
return false;
}
/**
* NeuroLink - Universal AI Development Platform
*
* Main SDK class providing unified access to 14+ AI providers with enterprise features:
* - Multi-provider support (OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, etc.)
* - MCP (Model Context Protocol) tool integration with 58+ external servers
* - Human-in-the-Loop (HITL) security workflows for regulated industries
* - Redis-based conversation memory and persistence
* - Enterprise middleware system for monitoring and control
* - Automatic provider fallback and retry logic
* - Streaming with real-time token delivery
* - Multimodal support (text, images, PDFs, CSV)
*
* @category Core
*
* @example Basic usage
* ```typescript
* import { NeuroLink } from '@juspay/neurolink';
*
* const neurolink = new NeuroLink();
*
* const result = await neurolink.generate({
* input: { text: 'Explain quantum computing' },
* provider: 'vertex',
* model: 'gemini-3-flash-preview'
* });
*
* console.log(result.content);
* ```
*
* @example With HITL security
* ```typescript
* const neurolink = new NeuroLink({
* hitl: {
* enabled: true,
* requireApproval: ['writeFile', 'executeCode'],
* confidenceThreshold: 0.85
* }
* });
* ```
*
* @example With Redis memory
* ```typescript
* const neurolink = new NeuroLink({
* conversationMemory: {
* enabled: true,
* redis: {
* url: 'redis://localhost:6379'
* }
* }
* });
* ```
*
* @example With MCP tools
* ```typescript
* const neurolink = new NeuroLink();
*
* // Discover available tools
* const tools = await neurolink.getAvailableTools();
*
* // Use tools in generation
* const result = await neurolink.generate({
* input: { text: 'Read the README.md file' },
* tools: ['readFile']
* });
* ```
*
* @see {@link GenerateOptions} for generation options
* @see {@link StreamOptions} for streaming options
* @see {@link NeurolinkConstructorConfig} for configuration options
* @since 1.0.0
*/
/**
* Module-level AsyncLocalStorage for per-request metrics trace context.
* Eliminates the race condition where overlapping generate/stream calls on the
* same NeuroLink instance would clobber each other's trace context.
*/
const metricsTraceContextStorage = new AsyncLocalStorage();
/**
* Curator P2-4 dedup (concurrency-safe): native providers emit
* `generation:end` on the shared SDK emitter. We attach a fresh
* mutable `dedupContext` object directly to the per-call
* `StreamOptions` (under `_streamDedupContext`) so each stream gets
* its own instance — concurrent streams have different option objects
* and therefore different contexts, so they cannot interfere.
*
* Native provider emit sites read `options._streamDedupContext` and
* flip `.providerEmitted = true` before emitting; the orchestration's
* finally block reads the same closed-over reference and skips its
* own emit when the flag is set.
*
* This avoids the AsyncLocalStorage approach which doesn't reliably
* propagate through async-generator yield boundaries when iteration
* happens from outside the original `run()` scope (e.g. when the
* consumer drives `for await of result.stream` after `sdk.stream(...)`
* returns).
*/
export const STREAM_DEDUP_CONTEXT_KEY = "_streamDedupContext";
/**
* Native providers call this from their `generation:end` emit sites,
* passing the same `options` object they received. Safe no-op when
* the field isn't set.
*/
export function markStreamProviderEmittedGenerationEnd(options) {
const ctx = options?._streamDedupContext;
if (ctx) {
ctx.providerEmitted = true;
}
}
/**
* Symbol-based brand for cross-module identification without circular imports.
*
* Provider constructors receive `sdk?: unknown` (the factory layer's
* contract). Rather than duck-typing via `"getInMemoryServers" in sdk`,
* use `isNeuroLink(value)` from this module to do a brand check —
* survives minification AND doesn't rely on method-name stability.
*/
export const NEUROLINK_BRAND = Symbol.for("@juspay/neurolink/sdk-brand");
/**
* Type-guard for opaque values that should be a {@link NeuroLink} instance.
*
* Designed for the provider-factory boundary where TS can't carry the type
* through `UnknownRecord` without forcing every caller into a circular
* dependency. Cheap to call and unaffected by minification.
*/
export function isNeuroLink(value) {
return (typeof value === "object" &&
value !== null &&
value[NEUROLINK_BRAND] === true);
}
export class NeuroLink {
/** @internal Brand for cross-module identification — see {@link isNeuroLink}. */
[NEUROLINK_BRAND] = true;
mcpInitialized = false;
mcpSkipped = false;
mcpInitPromise = null;
emitter = new EventEmitter();
// TaskManager — lazy-initialized on first access via `this.tasks`
_taskManager;
_taskManagerConfig;
toolRegistry;
autoDiscoveredServerInfos = [];
// External MCP server management
externalServerManager;
// Cache for available tools to improve performance
toolCache = null;
toolCacheDuration;
// NL-004: Model alias/deprecation configuration
modelAliasConfig;
// Compaction watermark: prevents re-triggering compaction on already-compacted messages
// Per-session map to avoid cross-session pollution in server mode
lastCompactionMessageCount = new Map();
/** Extract sessionId from options context for compaction watermark keying */
getCompactionSessionId(options) {
return (options.context
?.sessionId || "__default__");
}
// MCP Enhancement modules - wired into core execution path
mcpToolResultCache;
mcpToolRouter;
mcpToolBatcher;
mcpEnhancedDiscovery;
mcpToolMiddlewares = [];
/** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
mcpArtifactStore;
_disableToolCacheForCurrentRequest = false;
mcpEnhancementsConfig;
// Enhanced error handling support
toolCircuitBreakers = new Map();
toolExecutionMetrics = new Map();
currentStreamToolExecutions = [];
toolExecutionHistory = [];
activeToolExecutions = new Map();
/**
* Helper method to emit tool end event in a consistent way
* Used by executeTool in both success and error paths
* @param toolName - Name of the tool
* @param startTime - Timestamp when tool execution started
* @param success - Whether the tool execution was successful
* @param result - The result of the tool execution (optional)
* @param error - The error if execution failed (optional)
*/
emitToolEndEvent(toolName, startTime, success, result, error) {
// Emit tool end event (NeuroLink format - enhanced with result/error)
// Serialize error to string for consumer compatibility (event listeners
// commonly check `typeof event.error === "string"`).
this.emitter.emit("tool:end", createToolEventPayload(toolName, {
responseTime: Date.now() - startTime,
success,
timestamp: Date.now(),
result,
error: error ? error.message : undefined,
}));
}
// Conversation memory support
conversationMemory;
conversationMemoryNeedsInit = false;
conversationMemoryConfig;
// Add orchestration property
enableOrchestration;
// Authentication provider for secure access control
authProvider;
pendingAuthConfig;
authInitPromise;
// Per-provider credential overrides (instance-level default)
credentials;
// Curator P2-3: instance-level fallback policy. Read by
// runWithFallbackOrchestration on model-access-denied.
fallbackConfig = {};
/**
* Merge instance-level credentials with per-call credentials.
*
* Semantics: **deep merge at the provider level.** For each provider key
* present in both `this.credentials` and `callCredentials`, the per-call
* fields are merged ON TOP of the instance-level fields, so fields not
* mentioned in the per-call slice are preserved.
*
* Example:
* ```
* instance: { openai: { apiKey: "key1", baseURL: "url1" } }
* per-call: { openai: { apiKey: "key2" } }
* merged: { openai: { apiKey: "key2", baseURL: "url1" } } // baseURL preserved
* ```
*
* Providers present only in one source are carried through unchanged.
* Unrelated providers (not overridden in callCredentials) are carried through
* from instance credentials unchanged.
*/
resolveCredentials(callCredentials) {
if (!this.credentials && !callCredentials) {
return undefined;
}
if (!this.credentials) {
return callCredentials;
}
if (!callCredentials) {
return this.credentials;
}
// Per-provider deep merge: for each provider key in the per-call
// override, merge its fields on top of the instance-level slice so
// individual fields (e.g. baseURL) are preserved when only apiKey
// is overridden per-call.
const merged = { ...this.credentials };
for (const key of Object.keys(callCredentials)) {
const instanceSlice = this.credentials[key];
const callSlice = callCredentials[key];
if (instanceSlice &&
callSlice &&
typeof instanceSlice === "object" &&
typeof callSlice === "object") {
merged[key] = {
...instanceSlice,
...callSlice,
};
}
else {
merged[key] = callSlice ?? instanceSlice;
}
}
return merged;
}
// HITL (Human-in-the-Loop) support
hitlManager;
// Accumulated cost in USD across all generate() calls on this instance
_sessionCostUsd = 0;
// File Reference Registry for lazy on-demand file processing
fileRegistry;
// Cached file tools to avoid redundant createFileTools() calls per generate/stream
cachedFileTools = null;
// Memory instance and config
memoryInstance;
memorySDKConfig;
/**
* Extract and set Langfuse context from options with proper async scoping
*/
async setLangfuseContextFromOptions(options, callback) {
if (options.context &&
typeof options.context === "object" &&
options.context !== null) {
let callbackExecuted = false;
try {
const ctx = options.context;
// Trigger context scoping if any meaningful Langfuse field is present
if (ctx.userId ||
ctx.sessionId ||
ctx.conversationId ||
ctx.requestId ||
ctx.traceName ||
ctx.metadata) {
// Build customAttributes from top-level metadata string/number/boolean fields
let customAttributes;
if (ctx.metadata && typeof ctx.metadata === "object") {
const metaObj = ctx.metadata;
const attrs = {};
for (const [k, v] of Object.entries(metaObj)) {
if (typeof v === "string" ||
typeof v === "number" ||
typeof v === "boolean") {
attrs[k] = v;
}
}
if (Object.keys(attrs).length > 0) {
customAttributes = attrs;
}
}
return await new Promise((resolve, reject) => {
setLangfuseContext({
userId: typeof ctx.userId === "string" ? ctx.userId : null,
sessionId: typeof ctx.sessionId === "string" ? ctx.sessionId : null,
conversationId: typeof ctx.conversationId === "string"
? ctx.conversationId
: null,
requestId: typeof ctx.requestId === "string" ? ctx.requestId : null,
traceName: typeof ctx.traceName === "string" ? ctx.traceName : null,
metadata: ctx.metadata && typeof ctx.metadata === "object"
? ctx.metadata
: null,
...(customAttributes !== undefined && { customAttributes }),
}, async () => {
try {
callbackExecuted = true;
const result = await callback();
resolve(result);
}
catch (error) {
reject(error);
}
});
});
}
}
catch (error) {
if (callbackExecuted) {
// Callback was executed inside Langfuse context but failed — do NOT retry
// Re-throw to avoid double API calls and preserve error context
throw error;
}
// Langfuse context setup itself failed — graceful degradation, run without context
logger.warn("Failed to set Langfuse context from options", {
error: error instanceof Error ? error.message : String(error),
});
}
}
return await callback();
}
createMetricsTraceContext() {
// Attempt to reuse the active OTel trace context so Pipeline B spans
// land in the same Langfuse trace as Pipeline A spans.
const activeSpan = trace.getSpan(context.active());
if (activeSpan) {
const spanCtx = activeSpan.spanContext();
// Only use the OTel context if it has a valid trace ID.
// parentSpanId stores the active span's ID as a parent reference;
// each Pipeline B span must generate its own unique spanId to comply
// with the OTel/W3C requirement that spanIds are unique per trace.
if (spanCtx.traceId &&
spanCtx.traceId !== "00000000000000000000000000000000") {
return {
traceId: spanCtx.traceId,
parentSpanId: spanCtx.spanId,
};
}
}
// Fallback: no active OTel context (e.g. standalone Pipeline B usage)
return {
traceId: crypto.randomUUID().replace(/-/g, ""),
parentSpanId: crypto.randomUUID().replace(/-/g, "").substring(0, 16),
};
}
enforceSessionBudget(maxBudgetUsd) {
if (maxBudgetUsd === undefined ||
maxBudgetUsd <= 0 ||
this._sessionCostUsd < maxBudgetUsd) {
return;
}
throw new NeuroLinkError({
code: "SESSION_BUDGET_EXCEEDED",
message: `Session budget exceeded: spent $${this._sessionCostUsd.toFixed(4)} of $${maxBudgetUsd.toFixed(4)} limit`,
category: ErrorCategory.VALIDATION,
severity: ErrorSeverity.HIGH,
retriable: false,
context: {
spent: this._sessionCostUsd,
limit: maxBudgetUsd,
},
});
}
assertInputText(text, message) {
if (!text || typeof text !== "string") {
throw new Error(message);
}
}
async applyAuthenticatedRequestContext(options) {
if (options.auth?.token) {
const { AuthError } = await import("./auth/errors.js");
await this.ensureAuthProvider();
if (!this.authProvider) {
throw AuthError.create("PROVIDER_ERROR", "No auth provider configured. Set auth in constructor or via setAuthProvider() before using auth: { token }.");
}
let authResult;
try {
authResult = await withTimeout(this.authProvider.authenticateToken(options.auth.token), 5000, AuthError.create("PROVIDER_ERROR", "Auth token validation timed out after 5000ms"));
}
catch (error) {
if (error instanceof Error &&
"feature" in error &&
error.feature === "Auth") {
throw error;
}
throw AuthError.create("PROVIDER_ERROR", `Auth token validation failed: ${error instanceof Error ? error.message : String(error)}`);
}
if (!authResult.valid) {
throw AuthError.create("INVALID_TOKEN", authResult.error || "Token validation failed");
}
if (!authResult.user) {
throw AuthError.create("INVALID_TOKEN", "Token validated but no user identity returned");
}
if (!authResult.user.id) {
throw AuthError.create("INVALID_TOKEN", "Token validated but user identity missing required 'id' field");
}
options.context = {
...(options.context || {}),
userId: authResult.user.id,
userEmail: authResult.user.email,
userRoles: authResult.user.roles,
};
}
if (!options.requestContext) {
return;
}
const tokenDerivedFields = options.auth?.token && this.authProvider
? {
userId: options.context?.userId,
userEmail: options.context?.userEmail,
userRoles: options.context?.userRoles,
}
: {};
options.context = {
...(options.context || {}),
...options.requestContext,
...tokenDerivedFields,
};
}
applyGenerateLifecycleMiddleware(options) {
if (!options.onFinish && !options.onError) {
return;
}
options.middleware = {
...options.middleware,
middlewareConfig: {
...options.middleware?.middlewareConfig,
lifecycle: {
...options.middleware?.middlewareConfig?.lifecycle,
enabled: true,
config: {
...options.middleware?.middlewareConfig?.lifecycle?.config,
...(options.onFinish !== undefined
? { onFinish: options.onFinish }
: {}),
...(options.onError !== undefined
? { onError: options.onError }
: {}),
},
},
},
};
}
applyStreamLifecycleMiddleware(options) {
if (!options.onFinish && !options.onError && !options.onChunk) {
return;
}
options.middleware = {
...options.middleware,
middlewareConfig: {
...options.middleware?.middlewareConfig,
lifecycle: {
...options.middleware?.middlewareConfig?.lifecycle,
enabled: true,
config: {
...options.middleware?.middlewareConfig?.lifecycle?.config,
...(options.onFinish !== undefined
? { onFinish: options.onFinish }
: {}),
...(options.onError !== undefined
? { onError: options.onError }
: {}),
...(options.onChunk !== undefined
? { onChunk: options.onChunk }
: {}),
},
},
},
};
}
initializeMemoryConfig() {
const memory = this.conversationMemoryConfig?.conversationMemory?.memory;
if (!memory?.enabled) {
return false;
}
this.memorySDKConfig = memory;
return true;
}
/**
* Lazy initialization for memory — called during generate/stream.
*/
ensureMemoryReady() {
if (this.memoryInstance !== undefined) {
return this.memoryInstance;
}
if (!this.initializeMemoryConfig()) {
this.memoryInstance = null;
return null;
}
if (!this.memorySDKConfig) {
this.memoryInstance = null;
return null;
}
this.memoryInstance = initializeHippocampus(this.memorySDKConfig);
return this.memoryInstance;
}
/**
* Context storage for tool execution
* This context will be merged with any runtime context passed by the AI model
*/
toolExecutionContext;
/**
* Creates a new NeuroLink instance for AI text generation with MCP tool integration.
*
* @param config - Optional configuration object
* @param config.conversationMemory - Configuration for conversation memory features
* @param config.conversationMemory.enabled - Whether to enable conversation memory (default: false)
* @param config.conversationMemory.maxSessions - Maximum number of concurrent sessions (default: 100)
* @param config.conversationMemory.maxTurnsPerSession - Maximum conversation turns per session (default: 50)
* @param config.enableOrchestration - Whether to enable smart model orchestration (default: false)
* @param config.hitl - Configuration for Human-in-the-Loop safety features
* @param config.hitl.enabled - Whether to enable HITL tool confirmation (default: false)
* @param config.hitl.dangerousActions - Keywords that trigger confirmation (default: ['delete', 'remove', 'drop'])
* @param config.hitl.timeout - Confirmation timeout in milliseconds (default: 30000)
* @param config.hitl.allowArgumentModification - Allow users to modify tool parameters (default: true)
* @param config.toolRegistry - Optional tool registry instance for advanced use cases (default: new MCPToolRegistry())
*
* @example
* ```typescript
* // Basic usage
* const neurolink = new NeuroLink();
*
* // With conversation memory
* const neurolink = new NeuroLink({
* conversationMemory: {
* enabled: true,
* maxSessions: 50,
* maxTurnsPerSession: 20
* }
* });
*
* // With orchestration enabled
* const neurolink = new NeuroLink({
* enableOrchestration: true
* });
*
* // With HITL safety features
* const neurolink = new NeuroLink({
* hitl: {
* enabled: true,
* dangerousActions: ['delete', 'remove', 'drop', 'truncate'],
* timeout: 30000,
* allowArgumentModification: true
* }
* });
* ```
*
* @throws {Error} When provider registry setup fails
* @throws {Error} When conversation memory initialization fails (if enabled)
* @throws {Error} When external server manager initialization fails
* @throws {Error} When HITL configuration is invalid (if enabled)
*/
observabilityConfig;
metricsAggregator = new MetricsAggregator();
/**
* Per-request metrics trace context backed by AsyncLocalStorage.
* Safe for concurrent requests on the same SDK instance.
* Context is set via metricsTraceContextStorage.run() in generate/stream.
*/
get _metricsTraceContext() {
return metricsTraceContextStorage.getStore() ?? null;
}
constructor(config) {
this.toolRegistry = config?.toolRegistry || new MCPToolRegistry();
this.fileRegistry = new FileReferenceRegistry();
this.observabilityConfig = config?.observability;
// Initialize orchestration setting
this.enableOrchestration = config?.enableOrchestration ?? false;
// NL-004: Initialize model alias configuration
if (config?.modelAliasConfig) {
this.modelAliasConfig = config.modelAliasConfig;
}
// Curator P2-3: capture fallback policy. Per-call options can still
// override, but these are the instance-level defaults.
if (config?.providerFallback) {
this.fallbackConfig.providerFallback = config.providerFallback;
}
if (config?.modelChain) {
this.fallbackConfig.modelChain = config.modelChain;
}
logger.setEventEmitter(this.emitter);
// Read tool cache duration from environment variables, with a default
const cacheDurationEnv = process.env.NEUROLINK_TOOL_CACHE_DURATION;
this.toolCacheDuration = cacheDurationEnv
? parseInt(cacheDurationEnv, 10)
: 20000;
const constructorStartTime = Date.now();
const constructorHrTimeStart = process.hrtime.bigint();
const constructorId = `neurolink-constructor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.initializeProviderRegistry(constructorId, constructorStartTime, constructorHrTimeStart);
this.initializeConversationMemory(config, constructorId, constructorStartTime, constructorHrTimeStart);
this.initializeExternalServerManager(constructorId, constructorStartTime, constructorHrTimeStart);
this.initializeHITL(config, constructorId, constructorStartTime, constructorHrTimeStart);
this.initializeMCPEnhancements(config);
this.registerFileTools();
this.registerMemoryRetrievalTools();
this.initializeLangfuse(constructorId, constructorStartTime, constructorHrTimeStart);
this.initializeMetricsListeners();
this.logConstructorComplete(constructorId, constructorStartTime, constructorHrTimeStart);
// Store auth config for lazy initialization
if (config?.auth) {
this.pendingAuthConfig = config.auth;
}
// Store per-provider credential overrides
if (config?.credentials) {
this.credentials = config.credentials;
}
// Store task config for lazy initialization
this._taskManagerConfig = config?.tasks;
// Eagerly create TaskManager and register tools if config is provided
if (this._taskManagerConfig) {
this._taskManager = new TaskManager(this, this._taskManagerConfig);
this._taskManager.setEmitter(this.emitter);
this.registerTaskTools(this._taskManager);
}
}
/**
* TaskManager — scheduled and self-running tasks.
* Lazy-initialized on first access. Configurable via constructor `tasks` option.
* The actual async initialization (Redis connect, backend start) happens
* lazily inside TaskManager on first operation.
*/
get tasks() {
if (!this._taskManager) {
this._taskManager = new TaskManager(this, this._taskManagerConfig);
this._taskManager.setEmitter(this.emitter);
this.registerTaskTools(this._taskManager);
}
return this._taskManager;
}
/**
* Initialize provider registry with security settings
*/
initializeProviderRegistry(constructorId, constructorStartTime, constructorHrTimeStart) {
const registrySetupStartTime = process.hrtime.bigint();
logger.debug(`[NeuroLink] 🏗️ LOG_POINT_C002_PROVIDER_REGISTRY_SETUP_START`, {
logPoint: "C002_PROVIDER_REGISTRY_SETUP_START",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
registrySetupStartTimeNs: registrySetupStartTime.toString(),
message: "Starting ProviderRegistry configuration for security",
});
ProviderRegistry.setOptions({ enableManualMCP: false });
}
/**
* Initialize conversation memory if enabled
*/
initializeConversationMemory(config, constructorId, constructorStartTime, constructorHrTimeStart) {
if (config?.conversationMemory?.enabled) {
const memoryInitStartTime = process.hrtime.bigint();
// Store config for later use and set flag for lazy initialization
this.conversationMemoryConfig = config;
this.conversationMemoryNeedsInit = true;
const memoryInitEndTime = process.hrtime.bigint();
const memoryInitDurationNs = memoryInitEndTime - memoryInitStartTime;
logger.debug(`[NeuroLink] ✅ LOG_POINT_C006_MEMORY_INIT_FLAG_SET_SUCCESS`, {
logPoint: "C006_MEMORY_INIT_FLAG_SET_SUCCESS",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
memoryInitDurationNs: memoryInitDurationNs.toString(),
memoryInitDurationMs: Number(memoryInitDurationNs) / NANOSECOND_TO_MS_DIVISOR,
message: "Conversation memory initialization flag set successfully for lazy loading",
});
}
else {
logger.debug(`[NeuroLink] 🚫 LOG_POINT_C008_MEMORY_DISABLED`, {
logPoint: "C008_MEMORY_DISABLED",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
hasConfig: !!config,
hasMemoryConfig: !!config?.conversationMemory,
memoryEnabled: config?.conversationMemory?.enabled || false,
reason: !config
? "NO_CONFIG"
: !config.conversationMemory
? "NO_MEMORY_CONFIG"
: !config.conversationMemory.enabled
? "MEMORY_DISABLED"
: "UNKNOWN",
message: "Conversation memory not enabled - skipping initialization",
});
}
}
/**
* Initialize HITL (Human-in-the-Loop) if enabled
*/
initializeHITL(config, constructorId, constructorStartTime, constructorHrTimeStart) {
if (config?.hitl?.enabled) {
const hitlInitStartTime = process.hrtime.bigint();
logger.debug(`[NeuroLink] 🛡️ LOG_POINT_C015_HITL_INIT_START`, {
logPoint: "C015_HITL_INIT_START",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
hitlInitStartTimeNs: hitlInitStartTime.toString(),
hitlConfig: {
enabled: config.hitl.enabled,
dangerousActions: config.hitl.dangerousActions || [],
timeout: config.hitl.timeout || 30000,
allowArgumentModification: config.hitl.allowArgumentModification ?? true,
auditLogging: config.hitl.auditLogging ?? false,
},
message: "Starting HITL (Human-in-the-Loop) initialization",
});
try {
// Initialize HITL manager
this.hitlManager = new HITLManager(config.hitl);
// Inject HITL manager into tool registry
this.toolRegistry.setHITLManager(this.hitlManager);
// Inject HITL manager into external server manager
this.externalServerManager.setHITLManager(this.hitlManager);
// Set up HITL event forwarding to main emitter
this.setupHITLEventForwarding();
const hitlInitEndTime = process.hrtime.bigint();
const hitlInitDurationNs = hitlInitEndTime - hitlInitStartTime;
logger.debug(`[NeuroLink] ✅ LOG_POINT_C016_HITL_INIT_SUCCESS`, {
logPoint: "C016_HITL_INIT_SUCCESS",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
hitlInitDurationNs: hitlInitDurationNs.toString(),
hitlInitDurationMs: Number(hitlInitDurationNs) / NANOSECOND_TO_MS_DIVISOR,
hasHitlManager: !!this.hitlManager,
message: "HITL (Human-in-the-Loop) initialized successfully",
});
logger.info(`[NeuroLink] HITL safety features enabled`, {
dangerousActions: config.hitl.dangerousActions?.length || 0,
timeout: config.hitl.timeout || 30000,
allowArgumentModification: config.hitl.allowArgumentModification ?? true,
auditLogging: config.hitl.auditLogging ?? false,
});
}
catch (error) {
const hitlInitErrorTime = process.hrtime.bigint();
const hitlInitDurationNs = hitlInitErrorTime - hitlInitStartTime;
logger.error(`[NeuroLink] ❌ LOG_POINT_C017_HITL_INIT_ERROR`, {
logPoint: "C017_HITL_INIT_ERROR",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
hitlInitDurationNs: hitlInitDurationNs.toString(),
hitlInitDurationMs: Number(hitlInitDurationNs) / NANOSECOND_TO_MS_DIVISOR,
error: error instanceof Error ? error.message : String(error),
errorName: error instanceof Error ? error.name : "UnknownError",
errorStack: error instanceof Error ? error.stack : undefined,
message: "HITL (Human-in-the-Loop) initialization failed",
});
throw error;
}
}
else {
logger.debug(`[NeuroLink] 🚫 LOG_POINT_C018_HITL_DISABLED`, {
logPoint: "C018_HITL_DISABLED",
constructorId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - constructorStartTime,
elapsedNs: (process.hrtime.bigint() - constructorHrTimeStart).toString(),
hasConfig: !!config,
hasHitlConfig: !!config?.hitl,
hitlEnabled: config?.hitl?.enabled || false,
reason: !config
? "NO_CONFIG"
: !config.hitl
? "NO_HITL_CONFIG"
: !config.hitl.enabled
? "HITL_DISABLED"
: "UNKNOWN",
message: "HITL (Human-in-the-Loop) not enabled - skipping initialization",
});
}
}
/**
* Initialize MCP enhancement modules (cache, router, batcher, discovery).
* Wires standalone MCP modules into the core SDK execution path.
*/
initializeMCPEnhancements(config) {
const mcpConfig = config?.mcp;
this.mcpEnhancementsConfig = mcpConfig;
// BZ-664: ToolCache — enabled by default to prevent duplicate tool calls.
// Callers can explicitly opt out via mcp.cache.enabled = false.
if (mcpConfig?.cache?.enabled !== false) {
this.mcpToolResultCache = new ToolResultCache({
ttl: mcpConfig?.cache?.ttl ?? 300_000,
maxSize: mcpConfig?.cache?.maxSize ?? 500,
strategy: mcpConfig?.cache?.strategy ?? "lru",
});
logger.debug("[NeuroLink] MCP tool result cache initialized", {
ttl: mcpConfig?.cache?.ttl ?? 300_000,
maxSize: mcpConfig?.cache?.maxSize ?? 500,
strategy: mcpConfig?.cache?.strategy ?? "lru",
});
}
// ToolCallBatcher — disabled by default, opt-in
if (mcpConfig?.batcher?.enabled) {
this.mcpToolBatcher = new ToolCallBatcher({
maxBatchSize: mcpConfig.batcher.maxBatchSize ?? 10,
maxWaitMs: mcpConfig.batcher.maxWaitMs ?? 100,
});
// Wire batcher to execute tools via the internal execution path (bypass batcher itself)
this.mcpToolBatcher.setToolExecutor(async (tool, args) => {
return this.executeToolInternal(tool, args, {
timeout: TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS,
maxRetries: RETRY_ATTEMPTS.DEFAULT,
retryDelayMs: RETRY_DELAYS.BASE_MS,
});
});
logger.debug("[NeuroLink] MCP tool call batcher initialized");
}
// EnhancedToolDiscovery — enabled by default
if (mcpConfig?.discovery?.enabled !== false) {
this.mcpEnhancedDiscovery = new EnhancedToolDiscovery();
logger.debug("[NeuroLink] Enhanced tool discovery initialized");
}
// Middleware — store from config (empty by default)
if (mcpConfig?.middleware?.length) {
this.mcpToolMiddlewares = [...mcpConfig.middleware];
logger.debug("[NeuroLink] MCP tool middlewares registered", {
count: this.mcpToolMiddlewares.length,
});
}
// ToolRouter — lazy-initialized when 2+ external servers exist (see addExternalMCPServer)
// McpOutputNormalizer — active when mcp.outputLimits is configured
if (mcpConfig?.outputLimits) {
const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
let artifactStore;
if (strategy === "externalize") {
artifactStore = new LocalTempArtifactStore();
this.mcpArtifactStore = artifactStore;
logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
}
const normalizer = new McpOutputNormalizer({ strategy, maxBytes, warnBytes }, artifactStore);
this.externalServerManager.setOutputNor