n8n
Version:
n8n Workflow Automation Tool
1,204 lines • 177 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 decorators_1 = require("@n8n/decorators");
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 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 db_snapshot_storage_1 = require("./storage/db-snapshot-storage");
const db_iteration_log_storage_1 = require("./storage/db-iteration-log-storage");
const typeorm_agent_checkpoint_store_1 = require("./storage/typeorm-agent-checkpoint-store");
const typeorm_agent_memory_1 = require("./storage/typeorm-agent-memory");
const proxy_token_manager_1 = require("../../services/proxy-token-manager");
const instance_ai_pending_confirmation_repository_1 = require("./repositories/instance-ai-pending-confirmation.repository");
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");
const run_trace_metadata_1 = require("./run-trace-metadata");
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function isTelemetryConfigurableAgent(agent) {
return (typeof agent === 'object' &&
agent !== null &&
typeof Reflect.get(agent, 'telemetry') === 'function');
}
const INSTANCE_AI_CHECKPOINT_PRUNE_RETRY_MS = 30 * 1000;
const INSTANCE_AI_SHUTDOWN_DRAIN_TIMEOUT_MS = 5 * 1000;
function isTextMessagePart(part) {
return (typeof part === 'object' &&
part !== null &&
'type' in part &&
part.type === 'text' &&
'text' in part &&
typeof part.text === 'string');
}
const ORCHESTRATOR_AGENT_ID = 'agent-001';
const SANDBOX_NAME_MAX_LEN = 63;
const SANDBOX_LABEL_MAX_LEN = 63;
const NAME_PREFIX_SLUG_MAX_LEN = 24;
const DEFAULT_SANDBOX_TTL_MS = 15 * 60 * 1000;
function slugifySandboxName(value, maxLen) {
const slug = value
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return slug.slice(0, maxLen).replace(/-+$/, '');
}
function slugifySandboxLabel(value, maxLen) {
return value
.replace(/[^A-Za-z0-9_.-]+/g, '-')
.replace(/^[-.]+|[-.]+$/g, '')
.slice(0, maxLen)
.replace(/[-.]+$/, '');
}
function getThreadScopedSandboxName(threadId) {
return `instance-ai-thread-${threadId}`;
}
function buildThreadScopedSandboxName(threadId, namePrefix) {
const parts = [];
if (namePrefix) {
const prefixSlug = slugifySandboxName(namePrefix, NAME_PREFIX_SLUG_MAX_LEN);
if (prefixSlug)
parts.push(prefixSlug);
}
const threadSlug = slugifySandboxName(getThreadScopedSandboxName(threadId), SANDBOX_NAME_MAX_LEN);
if (threadSlug)
parts.push(threadSlug);
const name = slugifySandboxName(parts.join('-'), SANDBOX_NAME_MAX_LEN);
if (!name)
throw new n8n_workflow_1.UnexpectedError('Failed to build thread-scoped sandbox name');
return name;
}
function buildThreadScopedSandboxLabels(threadId, namePrefix) {
const baseName = getThreadScopedSandboxName(threadId);
const labels = {
'n8n-builder': slugifySandboxLabel(baseName, SANDBOX_LABEL_MAX_LEN),
thread_id: slugifySandboxLabel(threadId, SANDBOX_LABEL_MAX_LEN),
};
if (namePrefix)
labels.name_prefix = slugifySandboxLabel(namePrefix, SANDBOX_LABEL_MAX_LEN);
return labels;
}
function withThreadScopedSandboxIdentity(config, threadId) {
if (!config.enabled || config.provider !== 'daytona')
return config;
const name = buildThreadScopedSandboxName(threadId, config.namePrefix);
return {
...config,
id: name,
name,
labels: {
...buildThreadScopedSandboxLabels(threadId, config.namePrefix),
...config.labels,
},
};
}
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 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 'planDeny':
return { approved: false, denied: true };
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 {
get mcpClientManager() {
if (!this._mcpClientManager) {
this._mcpClientManager = new instance_ai_1.McpClientManager(this._ssrfProtectionConfig.enabled ? this._ssrfProtectionService : undefined);
}
return this._mcpClientManager;
}
constructor(logger, globalConfig, instanceSettings, adapterService, eventBus, settingsService, agentMemory, checkpointStore, aiService, push, threadRepo, pendingConfirmationRepo, urlService, dbSnapshotStorage, dbIterationLogStorage, sourceControlPreferencesService, telemetry, userRepository, aiBuilderTemporaryWorkflowRepository, errorReporter, ssrfProtectionConfig, ssrfProtectionService, eventService) {
this.instanceSettings = instanceSettings;
this.adapterService = adapterService;
this.eventBus = eventBus;
this.settingsService = settingsService;
this.agentMemory = agentMemory;
this.checkpointStore = checkpointStore;
this.aiService = aiService;
this.push = push;
this.threadRepo = threadRepo;
this.pendingConfirmationRepo = pendingConfirmationRepo;
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.sandboxCreations = new Map();
this.gatewayRegistry = new filesystem_1.LocalGatewayRegistry();
this.domainAccessTrackersByThread = new Map();
this.threadPushRef = new Map();
this.planRequestsByThread = 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.checkpointPruningStopped = true;
this.inFlightExecutions = new Set();
this.preserveHitlOnShutdown = new Set();
this.userMessagePersistenceByRun = new Map();
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);
},
onPendingConfirmationRejected: (requestId) => {
void this.dropPendingConfirmation(requestId);
},
});
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._ssrfProtectionConfig = ssrfProtectionConfig;
this._ssrfProtectionService = ssrfProtectionService;
this.eventService.on('instance-ai-settings-updated', ({ mcpSettingsChanged }) => {
if (!mcpSettingsChanged)
return;
if (!this._mcpClientManager)
return;
this._mcpClientManager.disconnect().catch((error) => {
this.logger.warn('Failed to disconnect MCP clients after settings change', {
error: getErrorMessage(error),
});
});
});
this.liveness.start();
if (this.instanceSettings.isLeader)
this.startCheckpointPruning();
}
getSandboxConfigFromEnv() {
const { sandboxEnabled, sandboxProvider, daytonaApiUrl, daytonaApiKey, n8nSandboxServiceUrl, n8nSandboxServiceApiKey, sandboxImage, sandboxTimeout, sandboxNamePrefix, daytonaTokenRefreshSkewMs, } = 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,
namePrefix: sandboxNamePrefix || undefined,
refreshSkewMs: daytonaTokenRefreshSkewMs,
};
}
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,
logger: this.logger,
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 getOrCreateWorkspaceEntry(threadId, user) {
const existing = this.sandboxes.get(threadId);
if (existing) {
if (this.isSandboxEntryExpired(existing) && !this.isSandboxInUse(threadId)) {
this.evictSandboxEntry(threadId, existing);
}
else {
this.touchSandboxEntry(threadId, existing);
return existing;
}
}
const pending = this.sandboxCreations.get(threadId);
if (pending)
return await pending;
const creation = this.createWorkspaceEntry(threadId, user);
this.sandboxCreations.set(threadId, creation);
try {
return await creation;
}
finally {
this.sandboxCreations.delete(threadId);
}
}
async getOrCreateWorkspace(threadId, user, context) {
const entry = await this.getOrCreateWorkspaceEntry(threadId, user);
if (entry)
await this.ensureWorkspaceSetup(entry, context);
return entry;
}
async ensureWorkspaceSetup(entry, context) {
if (entry.setupComplete)
return;
entry.setupPromise ??= (0, instance_ai_1.setupSandboxWorkspace)(entry.workspace, context)
.then(() => {
entry.setupComplete = true;
})
.finally(() => {
entry.setupPromise = undefined;
});
await entry.setupPromise;
}
async createWorkspaceEntry(threadId, user) {
const config = withThreadScopedSandboxIdentity(await this.resolveSandboxConfig(user), threadId);
if (!config.enabled)
return undefined;
const sandbox = await (0, instance_ai_1.createSandbox)(config, {
logger: this.logger,
errorReporter: this.errorReporter,
useSnapshotFallback: true,
});
const workspace = (0, instance_ai_1.createWorkspace)(sandbox);
if (!sandbox || !workspace)
return undefined;
try {
await workspace.init();
}
catch (error) {
try {
await workspace.destroy();
}
catch {
}
throw error;
}
const entry = {
sandbox,
workspace,
setupComplete: false,
setupPromise: undefined,
expiresAt: this.nextSandboxExpiry(),
};
this.sandboxes.set(threadId, entry);
this.scheduleSandboxExpiry(threadId, entry);
return entry;
}
evictSandboxEntry(threadId, entry) {
if (this.sandboxes.get(threadId) !== entry)
return;
this.sandboxes.delete(threadId);
if (entry.cleanupTimer) {
clearTimeout(entry.cleanupTimer);
entry.cleanupTimer = undefined;
}
}
async destroySandbox(threadId, reason = 'thread_cleanup') {
const entry = this.sandboxes.get(threadId);
if (!entry?.sandbox)
return;
this.evictSandboxEntry(threadId, entry);
try {
await entry.workspace?.destroy();
}
catch (error) {
this.logger.warn('Failed to destroy sandbox', {
threadId,
reason,
error: error instanceof Error ? error.message : String(error),
});
}
}
get sandboxTtlMs() {
return this.instanceAiConfig?.builderSandboxTtlMs ?? DEFAULT_SANDBOX_TTL_MS;
}
nextSandboxExpiry() {
return Date.now() + this.sandboxTtlMs;
}
isSandboxEntryExpired(entry) {
return this.sandboxTtlMs > 0 && entry.expiresAt <= Date.now();
}
touchSandboxEntry(threadId, entry) {
if (this.sandboxTtlMs <= 0)
return;
entry.expiresAt = this.nextSandboxExpiry();
this.scheduleSandboxExpiry(threadId, entry);
}
isSandboxInUse(threadId) {
return Boolean(this.runState.getActiveRunId(threadId) ||
this.runState.hasSuspendedRun(threadId) ||
this.backgroundTasks.getRunningTasks(threadId).length > 0);
}
scheduleSandboxExpiry(threadId, entry) {
if (this.sandboxTtlMs <= 0)
return;
if (entry.cleanupTimer)
clearTimeout(entry.cleanupTimer);
const delay = Math.max(0, entry.expiresAt - Date.now());
entry.cleanupTimer = setTimeout(() => {
const current = this.sandboxes.get(threadId);
if (current !== entry)
return;
if (this.isSandboxInUse(threadId)) {
this.touchSandboxEntry(threadId, entry);
return;
}
this.evictSandboxEntry(threadId, entry);
}, delay);
entry.cleanupTimer.unref();
}
stopSandboxExpiryTimers() {
for (const entry of this.sandboxes.values()) {
if (!entry.cleanupTimer)
continue;
clearTimeout(entry.cleanupTimer);
entry.cleanupTimer = undefined;
}
}
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 = 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;
}
getTraceContextForContinuation(threadId, messageGroupId) {
const entries = [...this.traceContextsByRunId.values()].reverse();
const sameGroup = messageGroupId === undefined
? undefined
: entries.find((entry) => entry.threadId === threadId && entry.messageGroupId === messageGroupId)?.tracing;
return sameGroup ?? entries.find((entry) => entry.threadId === threadId)?.tracing;
}
async createOrchestratorResumeTraceContext(options) {
const baseTracing = options.baseTracing ??
this.getTraceContextForContinuation(options.threadId, options.messageGroupId);
if (!baseTracing)
return undefined;
const tracing = await (0, instance_ai_1.continueInstanceAiTraceContext)(baseTracing, {
threadId: options.threadId,
messageId: options.messageId,
messageGroupId: options.messageGroupId,
runId: options.runId,
userId: options.userId,
modelId: options.modelId,
input: options.input,
proxyConfig: options.proxyConfig ?? baseTracing?.proxyConfig,
metadata: {
resume_reason: options.resumeReason,
agent_id: ORCHESTRATOR_AGENT_ID,
...options.metadata,
},
n8nVersion: constants_1.N8N_VERSION,
workflowSdkVersion: constants_1.WORKFLOW_SDK_VERSION,
});
if (tracing) {
await this.configureTraceReplayMode(tracing);
this.storeTraceContext(options.runId, options.threadId, tracing, options.messageGroupId);
this.runState.attachTracing(options.threadId, tracing);
}
return 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);
}
buildMessageTraceMetadata(threadId, runId, options) {
const traceOptions = {
status: options.status,
...(options.cancellationReason !== undefined
? { cancellationReason: options.cancellationReason }
: {}),
...(options.runTimeout !== undefined ? { runTimeout: options.runTimeout } : {}),
};
return {
completion_source: 'orchestrator',
...(0, run_trace_metadata_1.buildInstanceAiRunTraceMetadata)(this.eventBus.getEventsForRun(threadId, runId), traceOptions),
};
}
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 {
if (traceContext.actorRun.id !== traceContext.rootRun.id &&
traceContext.actorRun.endTime === undefined) {
await traceContext.finishRun(traceContext.actorRun, {
outputs: {
status: options.status,
...options.outputs,
},
metadata: {
final_status: options.status,
...options.metadata,
},
...(options.error ? { error: options.error } : {}),
});
}
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;
if (tracing.actorRun.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.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, attachments, timeZone, pushRef) {
this.liveness.clearThreadState(threadId);
const { runId, abortController, messageGroupId } = this.runState.startRun({
threadId,
user,
});
if (timeZone) {
this.runState.setTimeZone(threadId, timeZone);
}
if (pushRef !== undefined) {
this.threadPushRef.set(threadId, pushRef);
}
const userMessageId = (0, nanoid_1.nanoid)();
this.userMessagePersistenceByRun.set(runId, {
userId: user.id,
message: { id: userMessageId, text: message },
});
this.startExecuteRun(user, threadId, runId, message, abortController, attachments, messageGroupId, timeZone, false, undefined, undefined);
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();
void this.dropPendingConfirmationsForThread(threadId);
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);
}
void this.dropPendingConfirmationsForThread(threadId);
}
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');
}
}
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',
},
});
await this.agentMemory.saveMessages({
threadId,
resourceId: user.id,
messages: [
{
id: messageId,
createdAt: new Date(),
type: 'llm',
role: 'assistant',
content: [{ type: 'text', text: messageText }],
},
],
});
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, messageGroupI