UNPKG

@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
/** * 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