n8n
Version:
n8n Workflow Automation Tool
1,204 lines • 140 kB
JavaScript
"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