UNPKG

n8n

Version:

n8n Workflow Automation Tool

1,204 lines 140 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiService = void 0; const api_types_1 = require("@n8n/api-types"); const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const n8n_core_1 = require("n8n-core"); const ssrf_protection_service_1 = require("../../services/ssrf/ssrf-protection.service"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const permissions_1 = require("@n8n/permissions"); const url_service_1 = require("../../services/url.service"); const instance_ai_1 = require("@n8n/instance-ai"); const workflow_sdk_1 = require("@n8n/workflow-sdk"); const nanoid_1 = require("nanoid"); const n8n_workflow_1 = require("n8n-workflow"); const uuid_1 = require("uuid"); const constants_1 = require("../../constants"); const event_service_1 = require("../../events/event.service"); const source_control_preferences_service_ee_1 = require("../../modules/source-control.ee/source-control-preferences.service.ee"); const ai_service_1 = require("../../services/ai.service"); const push_1 = require("../../push"); const telemetry_1 = require("../../telemetry"); const in_process_event_bus_1 = require("./event-bus/in-process-event-bus"); const filesystem_1 = require("./filesystem"); const instance_ai_settings_service_1 = require("./instance-ai-settings.service"); const instance_ai_adapter_service_1 = require("./instance-ai.adapter.service"); const internal_messages_1 = require("./internal-messages"); const typeorm_composite_store_1 = require("./storage/typeorm-composite-store"); const db_snapshot_storage_1 = require("./storage/db-snapshot-storage"); const db_iteration_log_storage_1 = require("./storage/db-iteration-log-storage"); const compaction_service_1 = require("./compaction.service"); const proxy_token_manager_1 = require("../../services/proxy-token-manager"); const instance_ai_thread_repository_1 = require("./repositories/instance-ai-thread.repository"); const trace_replay_state_1 = require("./trace-replay-state"); const liveness_1 = require("./liveness"); function getErrorMessage(error) { return error instanceof Error ? error.message : String(error); } const ORCHESTRATOR_AGENT_ID = 'agent-001'; function getUserFacingErrorMessage(error) { if (error instanceof n8n_workflow_1.UserError) { return error.message; } if (error instanceof n8n_workflow_1.OperationalError) { return 'I hit an operational error before I could finish that response. Please try again.'; } if (error instanceof n8n_workflow_1.UnexpectedError) { return 'Something went wrong before I could finish that response. Please try again.'; } return 'Something went wrong before I could finish that response. Please try again.'; } function getBackgroundOutcomeResponseId(outcome) { return `background-outcome:${outcome.id}`; } function createTerminalOutcomeAgentTree(outcome, responseId) { return { agentId: ORCHESTRATOR_AGENT_ID, role: 'orchestrator', status: outcome.status === 'cancelled' ? 'cancelled' : outcome.status === 'failed' ? 'error' : 'completed', textContent: outcome.userFacingMessage, reasoning: '', toolCalls: [], children: [], timeline: [{ type: 'text', content: outcome.userFacingMessage, responseId }], }; } function appendTerminalOutcomeToAgentTree(tree, outcome, responseId) { const text = outcome.userFacingMessage.trim(); if (!text) return { tree, appended: false }; const alreadyInTimeline = tree.timeline.some((entry) => entry.type === 'text' && entry.responseId === responseId); if (alreadyInTimeline) { return { tree, appended: false }; } return { appended: true, tree: { ...tree, textContent: tree.textContent ? `${tree.textContent}\n\n${outcome.userFacingMessage}` : text, timeline: [ ...tree.timeline, { type: 'text', content: outcome.userFacingMessage, responseId }, ], }, }; } function createInertAbortSignal() { return new AbortController().signal; } function getAbortReason(signal) { const reason = signal.reason; if (typeof reason === 'object' && reason !== null && 'name' in reason && reason.name === 'AbortError') { return 'user_cancelled'; } if (reason instanceof Error) return reason.message; return typeof reason === 'string' ? reason : 'user_cancelled'; } const INSTANCE_AI_FEEDBACK_NAMESPACE = 'c5be4c87-5b6e-49ed-afe1-9c5c1f99a5c0'; const MAX_CONCURRENT_BACKGROUND_TASKS_PER_THREAD = 5; function estimateTokens(text) { return Math.ceil(text.length / 4); } function stringifyForContextValue(value) { if (typeof value === 'string') return value; try { return JSON.stringify(value); } catch { return String(value); } } const PLANNED_TASK_CONTEXT_VALUE_LIMIT = 1_500; function truncateContextValue(value) { if (value.length <= PLANNED_TASK_CONTEXT_VALUE_LIMIT) return value; return `${value.slice(0, PLANNED_TASK_CONTEXT_VALUE_LIMIT)}...`; } function buildPlannedTaskConversationContext(task, graph) { if (!graph) return undefined; const parts = [ `Approved plan task: ${task.title}`, `Task id: ${task.id}`, `Task kind: ${task.kind}`, `Plan run id: ${graph.planRunId}`, ]; if (task.workflowId) { parts.push(`Target workflow id: ${task.workflowId}`); } const dependencies = graph.tasks.filter((candidate) => task.deps.includes(candidate.id)); if (dependencies.length > 0) { parts.push('Completed dependency context:'); for (const dependency of dependencies) { const dependencyParts = [ `- ${dependency.id} (${dependency.kind}, ${dependency.status}): ${dependency.title}`, ]; if (dependency.result) { dependencyParts.push(`result=${truncateContextValue(dependency.result)}`); } if (dependency.error) { dependencyParts.push(`error=${truncateContextValue(dependency.error)}`); } if (dependency.outcome) { dependencyParts.push(`outcome=${truncateContextValue(stringifyForContextValue(dependency.outcome))}`); } parts.push(dependencyParts.join(' ')); } } return parts.join('\n'); } function getProxyFetch() { const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; if (!proxyUrl) return undefined; const { ProxyAgent } = require('undici'); const dispatcher = new ProxyAgent(proxyUrl); return (async (url, init) => await globalThis.fetch(url, { ...init, dispatcher, })); } function toConfirmationData(request) { switch (request.kind) { case 'approval': return { approved: request.approved, userInput: request.userInput }; case 'domainAccessApprove': return { approved: true, domainAccessAction: request.domainAccessAction }; case 'domainAccessDeny': return { approved: false }; case 'questions': return { approved: true, answers: request.answers }; case 'credentialSelection': return { approved: true, credentials: request.credentials }; case 'resourceDecision': return { approved: true, resourceDecision: request.resourceDecision }; case 'setupWorkflowApply': return { approved: true, action: 'apply', nodeCredentials: request.nodeCredentials, nodeParameters: request.nodeParameters, }; case 'setupWorkflowTestTrigger': return { approved: true, action: 'test-trigger', testTriggerNode: request.testTriggerNode, nodeCredentials: request.nodeCredentials, nodeParameters: request.nodeParameters, }; } } let InstanceAiService = class InstanceAiService { constructor(logger, globalConfig, adapterService, eventBus, settingsService, compositeStore, compactionService, aiService, push, threadRepo, urlService, dbSnapshotStorage, dbIterationLogStorage, sourceControlPreferencesService, telemetry, userRepository, aiBuilderTemporaryWorkflowRepository, errorReporter, ssrfProtectionConfig, ssrfProtectionService, eventService) { this.adapterService = adapterService; this.eventBus = eventBus; this.settingsService = settingsService; this.compositeStore = compositeStore; this.compactionService = compactionService; this.aiService = aiService; this.push = push; this.threadRepo = threadRepo; this.urlService = urlService; this.dbSnapshotStorage = dbSnapshotStorage; this.dbIterationLogStorage = dbIterationLogStorage; this.sourceControlPreferencesService = sourceControlPreferencesService; this.telemetry = telemetry; this.userRepository = userRepository; this.aiBuilderTemporaryWorkflowRepository = aiBuilderTemporaryWorkflowRepository; this.errorReporter = errorReporter; this.eventService = eventService; this.runState = new instance_ai_1.RunStateRegistry(); this.backgroundTasks = new instance_ai_1.BackgroundTaskManager(MAX_CONCURRENT_BACKGROUND_TASKS_PER_THREAD); this.traceContextsByRunId = new Map(); this.sandboxes = new Map(); this.gatewayRegistry = new filesystem_1.LocalGatewayRegistry(); this.domainAccessTrackersByThread = new Map(); this.threadPushRef = new Map(); this.schedulerLocks = new Map(); this.pendingCheckpointReentries = new Map(); this.pendingTerminalOutcomes = new Map(); this.creditedThreads = new Set(); this.traceReplay = new trace_replay_state_1.TraceReplayState(); this.logger = logger.scoped('instance-ai'); this.instanceAiConfig = globalConfig.instanceAi; const livenessPolicyConfig = (0, instance_ai_1.createInstanceAiLivenessPolicyConfig)({ confirmationTimeoutMs: this.instanceAiConfig.confirmationTimeout, }); this.liveness = new liveness_1.InstanceAiLivenessService({ policy: new instance_ai_1.InstanceAiLivenessPolicy(livenessPolicyConfig), backgroundTaskIdleTimeoutMs: livenessPolicyConfig.backgroundTaskIdleTimeoutMs, runState: this.runState, backgroundTasks: this.backgroundTasks, eventBus: this.eventBus, logger: this.logger, finalizeCancelledSuspendedRun: (suspended, reason) => { void this.finalizeCancelledSuspendedRun(suspended, reason); }, }); this.builderSandboxSessions = new instance_ai_1.BuilderSandboxSessionRegistry(this.instanceAiConfig.builderSandboxTtlMs); this.defaultTimeZone = globalConfig.generic.timezone; const restEndpoint = globalConfig.endpoints.rest; this.oauth2CallbackUrl = `${this.urlService.getInstanceBaseUrl()}/${restEndpoint}/oauth2-credential/callback`; this.webhookBaseUrl = `${this.urlService.getWebhookBaseUrl()}${globalConfig.endpoints.webhook}`; this.formBaseUrl = `${this.urlService.getWebhookBaseUrl()}${globalConfig.endpoints.form}`; this.mcpClientManager = new instance_ai_1.McpClientManager(ssrfProtectionConfig.enabled ? ssrfProtectionService : undefined); this.eventService.on('instance-ai-settings-updated', ({ mcpSettingsChanged }) => { if (!mcpSettingsChanged) return; this.mcpClientManager.disconnect().catch((error) => { this.logger.warn('Failed to disconnect MCP clients after settings change', { error: getErrorMessage(error), }); }); }); this.liveness.start(); } getSandboxConfigFromEnv() { const { sandboxEnabled, sandboxProvider, daytonaApiUrl, daytonaApiKey, n8nSandboxServiceUrl, n8nSandboxServiceApiKey, sandboxImage, sandboxTimeout, } = this.instanceAiConfig; if (!sandboxEnabled) { return { enabled: false, provider: sandboxProvider === 'n8n-sandbox' ? 'n8n-sandbox' : sandboxProvider === 'daytona' ? 'daytona' : 'local', timeout: sandboxTimeout, }; } if (sandboxProvider === 'daytona') { return { enabled: true, provider: 'daytona', daytonaApiUrl: daytonaApiUrl || undefined, daytonaApiKey: daytonaApiKey || undefined, image: sandboxImage || undefined, n8nVersion: constants_1.N8N_VERSION || undefined, timeout: sandboxTimeout, }; } if (sandboxProvider === 'n8n-sandbox') { return { enabled: true, provider: 'n8n-sandbox', serviceUrl: n8nSandboxServiceUrl || undefined, apiKey: n8nSandboxServiceApiKey || undefined, timeout: sandboxTimeout, }; } return { enabled: true, provider: 'local', timeout: sandboxTimeout, }; } async resolveSandboxConfig(user) { const base = this.getSandboxConfigFromEnv(); if (!base.enabled) return base; if (base.provider === 'daytona') { if (this.aiService.isProxyEnabled()) { const client = await this.aiService.getClient(); const proxyConfig = await client.getSandboxProxyConfig(); return { ...base, daytonaApiUrl: client.getSandboxProxyBaseUrl(), image: proxyConfig.image, getAuthToken: async () => { const token = await client.getBuilderApiProxyToken({ id: user.id }, { userMessageId: (0, nanoid_1.nanoid)() }); return token.accessToken; }, }; } const daytona = await this.settingsService.resolveDaytonaConfig(user); return { ...base, daytonaApiUrl: daytona.apiUrl ?? base.daytonaApiUrl, daytonaApiKey: daytona.apiKey ?? base.daytonaApiKey, }; } if (base.provider === 'n8n-sandbox') { const sandbox = await this.settingsService.resolveN8nSandboxConfig(user); return { ...base, serviceUrl: sandbox.serviceUrl ?? base.serviceUrl, apiKey: sandbox.apiKey ?? base.apiKey, }; } return base; } async createBuilderFactory(user) { const config = await this.resolveSandboxConfig(user); if (!config.enabled) return undefined; if (config.provider === 'daytona') { return new instance_ai_1.BuilderSandboxFactory(config, new instance_ai_1.SnapshotManager(config.image, this.logger, config.n8nVersion, this.errorReporter), this.logger, this.errorReporter); } return new instance_ai_1.BuilderSandboxFactory(config, undefined, this.logger); } async getOrCreateWorkspace(threadId, user) { const existing = this.sandboxes.get(threadId); if (existing) return existing; const config = await this.resolveSandboxConfig(user); if (!config.enabled) return undefined; const sandbox = await (0, instance_ai_1.createSandbox)(config); const workspace = (0, instance_ai_1.createWorkspace)(sandbox); if (!sandbox || !workspace) return undefined; const entry = { sandbox, workspace }; this.sandboxes.set(threadId, entry); return entry; } async destroySandbox(threadId) { const entry = this.sandboxes.get(threadId); if (!entry?.sandbox) return; this.sandboxes.delete(threadId); try { if ('destroy' in entry.sandbox && typeof entry.sandbox.destroy === 'function') { await entry.sandbox.destroy(); } } catch (error) { this.logger.warn('Failed to destroy sandbox', { threadId, error: error instanceof Error ? error.message : String(error), }); } } async getProxyAuth(user) { const client = await this.aiService.getClient(); const token = await client.getBuilderApiProxyToken({ id: user.id }, { userMessageId: (0, nanoid_1.nanoid)() }); return { client, headers: { Authorization: `${token.tokenType} ${token.accessToken}` }, }; } async resolveAgentModelConfig(user) { if (this.aiService.isProxyEnabled()) { const client = await this.aiService.getClient(); const proxyBaseUrl = client.getApiProxyBaseUrl(); const tokenManager = new proxy_token_manager_1.ProxyTokenManager(async () => { return await client.getBuilderApiProxyToken({ id: user.id }, { userMessageId: (0, nanoid_1.nanoid)() }); }); return await this.resolveProxyModel(user, proxyBaseUrl, tokenManager); } const httpProxyModel = await this.resolveHttpProxyModel(user); if (httpProxyModel) return httpProxyModel; return await this.settingsService.resolveModelConfig(user); } async resolveProxyModel(user, proxyBaseUrl, tokenManager) { const modelName = await this.settingsService.resolveModelName(user); const { createAnthropic } = await Promise.resolve().then(() => __importStar(require('@ai-sdk/anthropic'))); const provider = createAnthropic({ baseURL: proxyBaseUrl + '/anthropic/v1', apiKey: 'proxy-managed', fetch: async (input, init) => { const headers = new Headers(init?.headers); const auth = await tokenManager.getAuthHeaders(); for (const [k, v] of Object.entries(auth)) { headers.set(k, v); } for (const [k, v] of Object.entries((0, api_types_1.buildProxyHeaders)({ feature: 'instance-ai', n8nVersion: constants_1.N8N_VERSION }))) { headers.set(k, v); } return await globalThis.fetch(input, { ...init, headers }); }, }); return provider(modelName); } async resolveHttpProxyModel(user) { const proxyFetch = getProxyFetch(); if (!proxyFetch) return undefined; const config = await this.settingsService.resolveModelConfig(user); const modelId = typeof config === 'string' ? config : 'id' in config ? config.id : null; if (!modelId) return undefined; const [provider, ...rest] = modelId.split('/'); const modelName = rest.join('/'); const apiKey = typeof config === 'object' && 'apiKey' in config ? config.apiKey : undefined; const baseURL = typeof config === 'object' && 'url' in config ? config.url : undefined; if (provider !== 'anthropic') return undefined; const { createAnthropic } = await Promise.resolve().then(() => __importStar(require('@ai-sdk/anthropic'))); return createAnthropic({ apiKey, baseURL: baseURL || undefined, fetch: proxyFetch, })(modelName); } async countCreditsIfFirst(user, threadId, runId) { if (!this.aiService.isProxyEnabled()) return; if (this.creditedThreads.has(threadId)) return; let thread; try { thread = await this.threadRepo.findOneBy({ id: threadId }); } catch (error) { this.logger.warn('Failed to check Instance AI credit status', { threadId, runId, error: getErrorMessage(error), }); return; } if (!thread) return; if (thread.metadata?.creditCounted) { this.creditedThreads.add(threadId); return; } try { this.creditedThreads.add(threadId); const { client, headers: authHeaders } = await this.getProxyAuth(user); const info = await client.markBuilderSuccess({ id: user.id }, authHeaders); if (info) { thread.metadata = { ...thread.metadata, creditCounted: true }; await this.threadRepo.save(thread); this.push.sendToUsers({ type: 'updateInstanceAiCredits', data: { creditsQuota: info.creditsQuota, creditsClaimed: info.creditsClaimed }, }, [user.id]); } } catch (error) { this.creditedThreads.delete(threadId); this.logger.warn('Failed to count Instance AI credits', { error: getErrorMessage(error), threadId, runId, }); } } isProxyEnabled() { return this.aiService.isProxyEnabled(); } async getCredits(user) { if (!this.aiService.isProxyEnabled()) { return { creditsQuota: api_types_1.UNLIMITED_CREDITS, creditsClaimed: 0 }; } const client = await this.aiService.getClient(); return await client.getBuilderInstanceCredits({ id: user.id }); } isEnabled() { return this.settingsService.isAgentEnabled() && !!this.instanceAiConfig.model; } hasActiveRun(threadId) { return this.runState.hasLiveRun(threadId); } getThreadStatus(threadId) { return this.runState.getThreadStatus(threadId, this.backgroundTasks.getTaskSnapshots(threadId)); } storeTraceContext(runId, threadId, tracing, messageGroupId) { this.traceContextsByRunId.set(runId, { threadId, messageGroupId, tracing, traceSlug: this.traceReplay.getActiveSlug(), }); } getTraceContext(runId) { return this.traceContextsByRunId.get(runId)?.tracing; } async configureTraceReplayMode(tracing) { await this.traceReplay.configureReplayMode(tracing); } async finalizeMessageTraceRoot(runId, tracing, options) { if (tracing.rootRun.endTime) return; const outputs = options.outputs ?? { status: options.status, runId, ...(options.outputText ? { response: options.outputText } : {}), ...(options.reason ? { reason: options.reason } : {}), }; const metadata = { final_status: options.status, ...(options.modelId !== undefined ? { model_id: options.modelId } : {}), ...options.metadata, }; try { await tracing.finishRun(tracing.rootRun, { outputs, metadata, ...(options.error ? { error: options.error } : options.status === 'error' && options.reason ? { error: options.reason } : {}), }); } catch (error) { this.logger.warn('Failed to finalize Instance AI message trace root', { runId, threadId: tracing.rootRun.metadata?.thread_id, error: getErrorMessage(error), }); } finally { (0, instance_ai_1.releaseTraceClient)(tracing.rootRun.traceId); } } async maybeFinalizeRunTraceRoot(runId, options) { const tracing = this.getTraceContext(runId); if (!tracing) return; await this.finalizeMessageTraceRoot(runId, tracing, options); } async finalizeRemainingMessageTraceRoots(threadId, options) { const finalizedMessageRuns = new Set(); for (const [runId, entry] of this.traceContextsByRunId) { if (entry.threadId !== threadId) continue; if (finalizedMessageRuns.has(entry.tracing.rootRun.id)) continue; finalizedMessageRuns.add(entry.tracing.rootRun.id); await this.finalizeMessageTraceRoot(runId, entry.tracing, options); } } deleteTraceContextsForThread(threadId) { for (const [runId, entry] of this.traceContextsByRunId) { if (entry.threadId === threadId) { (0, instance_ai_1.releaseTraceClient)(entry.tracing.rootRun.traceId); if (entry.tracing.traceWriter && entry.traceSlug) { this.traceReplay.preserveWriterEvents(entry.traceSlug, entry.tracing.traceWriter.getEvents()); } this.traceContextsByRunId.delete(runId); } } } async finalizeDetachedTraceRun(taskId, traceContext, options) { if (!traceContext) return; try { await traceContext.finishRun(traceContext.rootRun, { outputs: { status: options.status, ...options.outputs, }, metadata: { final_status: options.status, ...options.metadata, }, ...(options.error ? { error: options.error } : {}), }); } catch (error) { this.logger.warn('Failed to finalize Instance AI detached trace run', { taskId, traceRunId: traceContext.rootRun.id, error: getErrorMessage(error), }); } finally { (0, instance_ai_1.releaseTraceClient)(traceContext.rootRun.traceId); } } async finalizeRunTracing(runId, tracing, options) { if (!tracing) return; const outputs = { status: options.status, runId, ...(options.outputText ? { response: options.outputText } : {}), ...(options.reason ? { reason: options.reason } : {}), }; const metadata = { final_status: options.status, ...(options.modelId !== undefined ? { model_id: options.modelId } : {}), }; try { await tracing.finishRun(tracing.actorRun, { outputs, metadata, ...(options.status === 'error' && options.reason ? { error: options.reason } : {}), }); } catch (error) { this.logger.warn('Failed to finalize Instance AI run tracing', { runId, threadId: tracing.actorRun.metadata?.thread_id, error: getErrorMessage(error), }); } } async finalizeBackgroundTaskTracing(task, status) { await this.finalizeDetachedTraceRun(task.taskId, task.traceContext, { status, outputs: { taskId: task.taskId, agentId: task.agentId, role: task.role, ...(task.result ? { result: task.result } : {}), }, ...(status === 'failed' && task.error ? { error: task.error } : {}), metadata: { ...(task.plannedTaskId ? { planned_task_id: task.plannedTaskId } : {}), ...(task.workItemId ? { work_item_id: task.workItemId } : {}), }, }); } async submitLangsmithFeedback(user, threadId, responseId, payload) { const anchor = await this.dbSnapshotStorage.findLangsmithAnchor(threadId, responseId); if (!anchor) { this.logger.debug('No LangSmith anchor for feedback; skipping annotation', { threadId, responseId, }); return; } let tracingProxyConfig; if (this.aiService.isProxyEnabled()) { try { const client = await this.aiService.getClient(); const baseUrl = client.getApiProxyBaseUrl(); const manager = new proxy_token_manager_1.ProxyTokenManager(async () => await client.getBuilderApiProxyToken({ id: user.id }, { userMessageId: (0, nanoid_1.nanoid)() })); tracingProxyConfig = { apiUrl: baseUrl + '/langsmith', getAuthHeaders: async () => await manager.getAuthHeaders(), }; } catch (error) { this.logger.warn('Failed to build LangSmith proxy config for feedback', { threadId, responseId, error: getErrorMessage(error), }); return; } } const key = 'user_score'; const feedbackId = (0, uuid_1.v5)(`${key}:${responseId}`, INSTANCE_AI_FEEDBACK_NAMESPACE); try { await (0, instance_ai_1.submitLangsmithUserFeedback)({ langsmithRunId: anchor.langsmithRunId, langsmithTraceId: anchor.langsmithTraceId, key, score: payload.rating === 'up' ? 1 : 0, value: payload.rating, comment: payload.comment, feedbackId, sourceInfo: { thread_id: threadId, response_id: responseId, user_id: user.id, rating: payload.rating, }, proxyConfig: tracingProxyConfig, }); } catch (error) { this.logger.warn('Failed to submit LangSmith feedback', { threadId, responseId, error: getErrorMessage(error), }); } } startRun(user, threadId, message, researchMode, attachments, timeZone, pushRef) { this.liveness.clearThreadState(threadId); const { runId, abortController, messageGroupId } = this.runState.startRun({ threadId, user, researchMode, }); if (timeZone) { this.runState.setTimeZone(threadId, timeZone); } if (pushRef !== undefined) { this.threadPushRef.set(threadId, pushRef); } void this.executeRun(user, threadId, runId, message, abortController, researchMode, attachments, messageGroupId, timeZone); return runId; } getMessageGroupId(threadId) { return this.runState.getMessageGroupId(threadId); } getLiveMessageGroupId(threadId) { return this.runState.getLiveMessageGroupId(threadId, this.backgroundTasks.getTaskSnapshots(threadId)); } getRunIdsForMessageGroup(messageGroupId) { return this.runState.getRunIdsForMessageGroup(messageGroupId); } getActiveRunId(threadId) { return this.runState.getActiveRunId(threadId); } cancelRun(threadId, reason = 'user_cancelled') { const cancelledTasks = this.backgroundTasks.cancelThread(threadId); const user = this.runState.getThreadUser(threadId); for (const task of cancelledTasks) { void this.finalizeBackgroundTaskTracing(task, 'cancelled'); this.eventBus.publish(threadId, { type: 'agent-completed', runId: task.runId, agentId: task.agentId, payload: { role: task.role, result: '', error: reason === liveness_1.INSTANCE_AI_RUN_TIMEOUT_REASON ? 'Timed out' : 'Cancelled by user', }, }); void this.recordBackgroundTerminalOutcome(task).finally(() => { void this.saveAgentTreeSnapshot(threadId, task.runId, this.dbSnapshotStorage, true, task.messageGroupId); }); if (user) { void this.handlePlannedTaskSettlement(user, task, 'cancelled'); } } void this.cancelAwaitingApprovalPlan(threadId); const { active, suspended } = this.runState.cancelThread(threadId); if (active) { if (reason === liveness_1.INSTANCE_AI_RUN_TIMEOUT_REASON) this.liveness.markRunTimedOut(active.runId); active.abortController.abort(); return; } if (suspended) { if (reason === liveness_1.INSTANCE_AI_RUN_TIMEOUT_REASON) this.liveness.markRunTimedOut(suspended.runId); suspended.abortController.abort(); void this.finalizeCancelledSuspendedRun(suspended, reason); } } sendCorrectionToTask(threadId, taskId, correction) { return this.backgroundTasks.queueCorrection(threadId, taskId, correction); } cancelBackgroundTask(threadId, taskId) { const task = this.backgroundTasks.cancelTask(threadId, taskId); if (!task) return; void this.finalizeBackgroundTaskTracing(task, 'cancelled'); this.eventBus.publish(threadId, { type: 'agent-completed', runId: task.runId, agentId: task.agentId, payload: { role: task.role, result: '', error: 'Cancelled by user' }, }); void this.recordBackgroundTerminalOutcome(task).finally(() => { void this.saveAgentTreeSnapshot(threadId, task.runId, this.dbSnapshotStorage, true, task.messageGroupId); }); const user = this.runState.getThreadUser(threadId); if (user) { void this.handlePlannedTaskSettlement(user, task, 'cancelled'); } } async revalidateActiveUser(userId) { try { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['role'], }); if (!user || user.disabled) return null; if (!(0, permissions_1.hasGlobalScope)(user, 'instanceAi:message')) return null; return user; } catch (error) { this.logger.warn('Failed to revalidate user', { userId, error: getErrorMessage(error), }); return null; } } cancelAllBackgroundTasks() { const cancelled = this.backgroundTasks.cancelAll(); for (const task of cancelled) { void this.finalizeBackgroundTaskTracing(task, 'cancelled'); } return cancelled.length; } async startStuckBackgroundTaskForTest(user, threadId) { const messageId = `msg_${(0, nanoid_1.nanoid)()}`; const messageText = 'I started a background workflow-builder task.'; const { runId, messageGroupId } = this.runState.startRun({ threadId, user }); if (!messageGroupId) { throw new n8n_workflow_1.UnexpectedError('Failed to create message group for timeout simulation'); } const taskId = `task_${(0, nanoid_1.nanoid)()}`; const agentId = `agent_${(0, nanoid_1.nanoid)()}`; this.eventBus.publish(threadId, { type: 'run-start', runId, agentId: ORCHESTRATOR_AGENT_ID, userId: user.id, payload: { messageId, messageGroupId }, }); this.eventBus.publish(threadId, { type: 'text-delta', runId, agentId: ORCHESTRATOR_AGENT_ID, responseId: `test-background-start:${runId}`, payload: { text: messageText }, }); this.eventBus.publish(threadId, { type: 'agent-spawned', runId, agentId, payload: { parentId: ORCHESTRATOR_AGENT_ID, role: 'workflow-builder', tools: [], taskId, kind: 'builder', title: 'Building workflow', subtitle: 'Timeout simulation', goal: 'Simulate a stuck background task timeout', }, }); const memoryStorage = this.compositeStore.stores.memory; await memoryStorage.saveMessages({ messages: [ { id: messageId, threadId, resourceId: user.id, role: 'assistant', content: { format: 2, parts: [{ type: 'text', text: messageText }], content: messageText, }, createdAt: new Date(), }, ], }); const outcome = this.backgroundTasks.spawn({ taskId, threadId, runId, role: 'workflow-builder', agentId, messageGroupId, run: async (signal) => await new Promise((resolve) => { signal.addEventListener('abort', () => resolve('aborted'), { once: true }); }), onFailed: (task) => { this.eventBus.publish(threadId, { type: 'agent-completed', runId, agentId, payload: { role: task.role, result: '', error: task.error ?? 'Unknown error', }, }); }, onSettled: async (task) => { await this.recordBackgroundTerminalOutcome(task); await this.saveAgentTreeSnapshot(threadId, runId, this.dbSnapshotStorage, true, messageGroupId); }, }); if (outcome.status !== 'started') { throw new n8n_workflow_1.UnexpectedError('Failed to start stuck background task simulation'); } this.runState.clearActiveRun(threadId); this.eventBus.publish(threadId, { type: 'run-finish', runId, agentId: ORCHESTRATOR_AGENT_ID, userId: user.id, payload: { status: 'completed' }, }); return { threadId, runId, messageGroupId, taskId, agentId, timeoutAt: outcome.task.lastActivityAt + this.liveness.backgroundTaskIdleTimeoutMs + 1, }; } async runLivenessSweepForTest(now) { await this.liveness.sweepTimedOutWork(now); } loadTraceEvents(slug, events) { this.traceReplay.loadEvents(slug, events); } getTraceEvents(slug) { return this.traceReplay.getEventsWithWriterFallback(slug, this.traceContextsByRunId.values()); } activateTraceSlug(slug) { this.traceReplay.activateSlug(slug); } clearTraceEvents(slug) { this.traceReplay.clearEvents(slug); } getUserIdForApiKey(key) { return this.gatewayRegistry.getUserIdForApiKey(key); } generatePairingToken(userId) { return this.gatewayRegistry.generatePairingToken(userId); } getGatewayApiKeyExpiresAt(userId, key) { return this.gatewayRegistry.getApiKeyExpiresAt(userId, key); } getPairingToken(userId) { return this.gatewayRegistry.getPairingToken(userId); } consumePairingToken(userId, token) { return this.gatewayRegistry.consumePairingToken(userId, token); } getActiveSessionKey(userId) { return this.gatewayRegistry.getActiveSessionKey(userId); } clearActiveSessionKey(userId) { this.gatewayRegistry.clearActiveSessionKey(userId); } getLocalGateway(userId) { return this.gatewayRegistry.getGateway(userId); } initGateway(userId, data) { this.gatewayRegistry.initGateway(userId, data); this.telemetry.track('User connected to Computer Use', { user_id: userId, tool_groups: data.toolCategories.filter((c) => c.enabled).map((c) => c.name), }); } resolveGatewayRequest(userId, requestId, result, error) { return this.gatewayRegistry.resolveGatewayRequest(userId, requestId, result, error); } disconnectGateway(userId) { this.gatewayRegistry.disconnectGateway(userId); } disconnectAllGateways() { const connectedUserIds = this.gatewayRegistry.getConnectedUserIds(); this.gatewayRegistry.disconnectAll(); return connectedUserIds; } isLocalGatewayDisabled() { return this.settingsService.isLocalGatewayDisabled(); } getGatewayStatus(userId) { return this.gatewayRegistry.getGatewayStatus(userId); } startDisconnectTimer(userId, onDisconnect) { this.gatewayRegistry.startDisconnectTimer(userId, onDisconnect); } clearDisconnectTimer(userId) { this.gatewayRegistry.clearDisconnectTimer(userId); } async clearThreadState(threadId) { const { active, suspended } = this.runState.clearThread(threadId); if (active) { active.abortController.abort(); await this.finalizeRunTracing(active.runId, active.tracing, { status: 'cancelled', reason: 'thread_cleared', }); } if (suspended) { suspended.abortController.abort(); await this.finalizeRunTracing(suspended.runId, suspended.tracing, { status: 'cancelled', reason: 'thread_cleared', }); } for (const task of this.backgroundTasks.cancelThread(threadId)) { task.abortController.abort(); await this.finalizeBackgroundTaskTracing(task, 'cancelled'); } await this.finalizeRemainingMessageTraceRoots(threadId, { status: 'cancelled', reason: 'thread_cleared', metadata: { completion_source: 'service_cleanup' }, }); this.creditedThreads.delete(threadId); this.schedulerLocks.delete(threadId); this.liveness.clearThreadState(threadId); this.domainAccessTrackersByThread.delete(threadId); this.threadPushRef.delete(threadId); this.deleteTraceContextsForThread(threadId); await this.builderSandboxSessions.cleanupThread(threadId, 'thread_cleared'); await this.destroySandbox(threadId); await this.reapAiTemporaryForThreadCleanup(threadId); this.eventBus.clearThread(threadId); } async shutdown() { this.liveness.shutdown(); const { activeRuns, suspendedRuns } = this.runState.shutdown(); for (const run of activeRuns) { run.abortController.abort(); await this.finalizeRunTracing(run.runId, run.tracing, { status: 'cancelled', reason: 'service_shutdown', }); } for (const run of suspendedRuns) { run.abortController.abort(); await this.finalizeRunTracing(run.runId, run.tracing, { status: 'cancelled', reason: 'service_shutdown', }); } for (const task of this.backgroundTasks.cancelAll()) { task.abortController.abort(); await this.finalizeBackgroundTaskTracing(task, 'cancelled'); } const threadsWithTraces = new Set([...this.traceContextsByRunId.values()].map((entry) => entry.threadId)); for (const threadId of threadsWithTraces) { await this.finalizeRemainingMessageTraceRoots(threadId, { status: 'cancelled', reason: 'service_shutdown', metadata: { completion_source: 'service_cleanup' }, }); } this.gatewayRegistry.disconnectAll(); const sandboxCleanups = [...this.sandboxes.keys()].map(async (threadId) => await this.destroySandbox(threadId)); await Promise.allSettled([ ...sandboxCleanups, this.builderSandboxSessions.cleanupAll('service_shutdown'), ]); this.domainAccessTrackersByThread.clear(); this.traceContextsByRunId.clear(); this.eventBus.clear(); await this.mcpClientManager.disconnect(); this.logger.debug('Instance AI service shut down'); } createMemoryConfig() { return { storage: this.compositeStore, embedderModel: this.instanceAiConfig.embedderModel || undefined, lastMessages: this.instanceAiConfig.lastMessages, semanticRecallTopK: this.instanceAiConfig.semanticRecallTopK, }; } async ensureThreadExists(memory, threadId, resourceId) { const existingThread = await memory.getThreadById({ threadId }); if (existingThread) return; const now = new Date(); await memory.saveThread({ thread: { id: threadId, resourceId, title: '', createdAt: now, updatedAt: now, }, }); } projectPlannedTaskList(graph) { return { tasks: graph.tasks.map((task) => ({ id: task.id, description: task.title, status: task.status === 'planned' ? 'todo' : task.status === 'running' ? 'in_progress' : task.status === 'succeeded' ? 'done' : task.status, })), }; } buildPlannedTaskFollowUpMessage(type, graph, options = {}) { const payload = { tasks: graph.tasks.map((task) => ({ id: task.id, title: task.title, kind: task.kind, status: task.status, result: task.result, error: task.error, outcome: task.outcome, })), }; if (options.failedTask) { payload.failedTask = { id: options.failedTask.id, title: options.failedTask.title, kind: options.failedTask.kind, error: options.failedTask.error, result: options.failedTask.result, }; } if (options.checkpoint) { const depOutcomes = graph.tasks .filter((t) => options.checkpoint.deps.includes(t.id)) .map((t) => ({ id: t.id, title: t.title, kind: t.kind, status: t.status, result: t.result, outcome: t.outcome, })); payload.checkpoint = { id: options.checkpoint.id, title: options.checkpoint.title, instructions: options.checkpoint.spec, dependsOn: depOutcomes, }; } return `<planned-task-follow-up type="${type}">\n${JSON.stringify(payload, null, 2)}\n</planned-task-follow-up>\n\n${internal_messages_1.AUTO_FOLLOW_UP_MESSAGE}`; } async createPlannedTaskState() { const memory = (0, instance_ai_1.createMemory)(this.createMemoryConfig()); const taskStorage = new instance_ai_1.MastraTaskStorage(memory); const plannedTaskStorage = new instance_ai_1.PlannedTaskStorage(memory); const plannedTaskService = new instance_ai_1.PlannedTaskCoordinator(plannedTaskStorage); return { memory, taskStorage, plannedTaskServic