UNPKG

n8n

Version:

n8n Workflow Automation Tool

1,080 lines 52.3 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.AgentsService = void 0; exports.chatThreadId = chatThreadId; const api_types_1 = require("@n8n/api-types"); const agents = __importStar(require("@n8n/agents")); const ai_utilities_1 = require("@n8n/ai-utilities"); const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const constants_1 = require("@n8n/constants"); const db_1 = require("@n8n/db"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const typeorm_1 = require("@n8n/typeorm"); const n8n_workflow_1 = require("n8n-workflow"); const uuid_1 = require("uuid"); const active_executions_1 = require("../../active-executions"); const credentials_service_1 = require("../../credentials/credentials.service"); const conflict_error_1 = require("../../errors/response-errors/conflict.error"); const not_found_error_1 = require("../../errors/response-errors/not-found.error"); const node_definition_resolver_1 = require("../../modules/instance-ai/node-definition-resolver"); const node_execution_1 = require("../../node-execution"); const publisher_service_1 = require("../../scaling/pubsub/publisher.service"); const url_service_1 = require("../../services/url.service"); const ttl_map_1 = require("../../utils/ttl-map"); const workflow_runner_1 = require("../../workflow-runner"); const workflow_finder_service_1 = require("../../workflows/workflow-finder.service"); const agents_credential_provider_1 = require("./adapters/agents-credential-provider"); const agent_draft_utils_1 = require("./utils/agent-draft.utils"); const agent_resource_id_1 = require("./utils/agent-resource-id"); const agent_execution_service_1 = require("./agent-execution.service"); const agent_skills_service_1 = require("./agent-skills.service"); const agents_tools_service_1 = require("./agents-tools.service"); const builder_tool_names_1 = require("./builder/builder-tool-names"); const execution_recorder_1 = require("./execution-recorder"); const agent_chat_integration_1 = require("./integrations/agent-chat-integration"); const integrations_sync_1 = require("./integrations/integrations-sync"); const n8n_checkpoint_storage_1 = require("./integrations/n8n-checkpoint-storage"); const n8n_memory_1 = require("./integrations/n8n-memory"); const agent_config_composition_1 = require("./json-config/agent-config-composition"); const agent_json_config_1 = require("./json-config/agent-json-config"); const from_json_config_1 = require("./json-config/from-json-config"); const agent_published_version_repository_1 = require("./repositories/agent-published-version.repository"); const agent_repository_1 = require("./repositories/agent.repository"); const agent_secure_runtime_1 = require("./runtime/agent-secure-runtime"); const tool_registry_1 = require("./tool-registry"); function chatThreadId(agentId, userId) { const baseThreadId = `${builder_tool_names_1.AGENT_THREAD_PREFIX.TEST}${agentId}`; return userId ? `${baseThreadId}:${userId}` : baseThreadId; } let AgentsService = class AgentsService { computeRuntimeCacheKey(params) { if (params.usePublishedVersion) { const parts = [params.agentId, 'published']; if (params.integrationType) parts.push(params.integrationType); return parts.join(':'); } const parts = [params.agentId, 'draft']; if (params.n8nUserId) parts.push(params.n8nUserId); return parts.join(':'); } clearRuntimes(agentId, options = {}) { for (const key of this.runtimes.keys()) { if (key === agentId || key.startsWith(`${agentId}:`)) { this.runtimes.delete(key); } } if (options.skipBroadcast) return; if (!this.globalConfig.multiMainSetup.enabled) return; void this.publisher .publishCommand({ command: 'agent-config-changed', payload: { agentId }, }) .catch((error) => { this.logger.warn(`[AgentsService] Failed to publish agent-config-changed for ${agentId}`, { error: error instanceof Error ? error.message : String(error), }); }); } handleAgentConfigChanged(payload) { this.clearRuntimes(payload.agentId, { skipBroadcast: true }); } constructor(logger, agentRepository, projectRelationRepository, workflowRunner, activeExecutions, executionRepository, workflowRepository, userRepository, workflowFinderService, urlService, n8nCheckpointStorage, secureRuntime, ephemeralNodeExecutor, agentsToolsService, n8nMemory, agentExecutionService, agentPublishedVersionRepository, agentSkillsService, publisher, agentsConfig, globalConfig) { this.logger = logger; this.agentRepository = agentRepository; this.projectRelationRepository = projectRelationRepository; this.workflowRunner = workflowRunner; this.activeExecutions = activeExecutions; this.executionRepository = executionRepository; this.workflowRepository = workflowRepository; this.userRepository = userRepository; this.workflowFinderService = workflowFinderService; this.urlService = urlService; this.n8nCheckpointStorage = n8nCheckpointStorage; this.secureRuntime = secureRuntime; this.ephemeralNodeExecutor = ephemeralNodeExecutor; this.agentsToolsService = agentsToolsService; this.n8nMemory = n8nMemory; this.agentExecutionService = agentExecutionService; this.agentPublishedVersionRepository = agentPublishedVersionRepository; this.agentSkillsService = agentSkillsService; this.publisher = publisher; this.agentsConfig = agentsConfig; this.globalConfig = globalConfig; this.runtimes = new ttl_map_1.TtlMap(30 * constants_1.Time.minutes.toMilliseconds); } isNodeToolsModuleEnabled() { return this.agentsConfig.modules.includes('node-tools-searcher'); } shouldAttachNodeTools(config) { return this.isNodeToolsModuleEnabled() && (0, agent_json_config_1.isNodeToolsEnabled)(config); } listChatIntegrations() { return di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry) .list() .map((i) => ({ type: i.type, label: i.displayLabel, icon: i.displayIcon, credentialTypes: i.credentialTypes, })); } async create(projectId, name) { const defaultConfig = { name, model: 'anthropic/claude-sonnet-4-5', credential: '', instructions: '', tools: [], skills: [], }; const agent = this.agentRepository.create({ name, projectId, schema: defaultConfig, versionId: (0, uuid_1.v4)(), }); const saved = await this.agentRepository.save(agent); this.logger.debug('Created SDK agent', { agentId: saved.id, projectId }); return saved; } async findByProjectId(projectId) { return await this.agentRepository.findByProjectId(projectId); } async findById(agentId, projectId) { return await this.agentRepository.findByIdAndProjectId(agentId, projectId); } async updateName(agentId, projectId, name) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { return null; } agent.name = name; if (agent.schema) { agent.schema = { ...agent.schema, name }; } (0, agent_draft_utils_1.markAgentDraftDirty)(agent); const saved = await this.agentRepository.save(agent); this.logger.debug('Updated SDK agent name', { agentId, projectId, name }); return saved; } async updateDescription(agentId, projectId, description, updatedAt) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { return null; } if (updatedAt && agent.updatedAt.toISOString() !== updatedAt) { throw new conflict_error_1.ConflictError('Agent has been modified'); } agent.description = description; if (agent.schema) { agent.schema = { ...agent.schema, description }; } (0, agent_draft_utils_1.markAgentDraftDirty)(agent); const saved = await this.agentRepository.save(agent); this.logger.debug('Updated SDK agent description', { agentId, projectId }); return saved; } async findByUser(userId) { const projectRelations = await this.projectRelationRepository.findAllByUser(userId); const projectIds = projectRelations.map((pr) => pr.projectId); if (projectIds.length === 0) return []; return await this.agentRepository.find({ where: { projectId: (0, typeorm_1.In)(projectIds) }, order: { updatedAt: 'DESC' }, }); } async findPublishedByUser(userId) { const projectRelations = await this.projectRelationRepository.findAllByUser(userId); const projectIds = projectRelations.map((pr) => pr.projectId); if (projectIds.length === 0) return []; const agents = await this.agentRepository.find({ where: { projectId: (0, typeorm_1.In)(projectIds) }, relations: { publishedVersion: true }, order: { updatedAt: 'DESC' }, }); return agents.filter((agent) => agent.publishedVersion); } async publishAgent(agentId, projectId, userId) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`); } await this.agentRepository.manager.transaction(async (trx) => { if (!agent.versionId) { agent.versionId = (0, uuid_1.v4)(); await trx.save(agent); } agent.publishedVersion = await this.agentPublishedVersionRepository.savePublishedVersion({ agentId: agent.id, schema: agent.schema, tools: this.snapshotConfiguredTools(agent.schema, agent.tools ?? {}), skills: this.agentSkillsService.snapshotConfiguredSkills(agent.schema, agent.skills ?? {}), publishedFromVersionId: agent.versionId, model: agent.model, provider: agent.provider, credentialId: agent.credentialId, publishedById: userId, }, trx); }); this.clearRuntimes(agentId); const credentialIntegrations = (agent.integrations ?? []).filter(api_types_1.isAgentCredentialIntegration); if (credentialIntegrations.length > 0) { const { ChatIntegrationService } = await Promise.resolve().then(() => __importStar(require('./integrations/chat-integration.service'))); await di_1.Container.get(ChatIntegrationService) .syncToConfig(agent, [], credentialIntegrations) .catch((error) => this.logger.warn('Failed to connect integrations on publish', { agentId, error, })); } this.logger.debug('Published SDK agent', { agentId, projectId, userId }); return agent; } async unpublishAgent(agentId, projectId) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`); } await this.agentRepository.manager.transaction(async (trx) => { await this.agentPublishedVersionRepository.deleteByAgentId(agentId, trx); agent.publishedVersion = null; const hasActiveSchedule = (agent.integrations ?? []).some((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) && integration.active); if (hasActiveSchedule) { agent.integrations = (agent.integrations ?? []).map((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) ? { ...integration, active: false } : integration); await trx.save(agent); } }); this.clearRuntimes(agentId); const { ChatIntegrationService } = await Promise.resolve().then(() => __importStar(require('./integrations/chat-integration.service'))); await di_1.Container.get(ChatIntegrationService).disconnect(agentId); const { AgentScheduleService } = await Promise.resolve().then(() => __importStar(require('./integrations/agent-schedule.service'))); di_1.Container.get(AgentScheduleService).deregister(agentId); this.logger.debug('Unpublished SDK agent', { agentId, projectId }); return agent; } async revertToPublishedAgent(agentId, projectId) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`); } const publishedVersion = agent.publishedVersion; if (!publishedVersion) { throw new conflict_error_1.ConflictError(`Agent "${agentId}" is not published`); } await this.agentRepository.manager.transaction(async (trx) => { agent.schema = publishedVersion.schema ? (0, n8n_workflow_1.deepCopy)(publishedVersion.schema) : null; agent.tools = (0, n8n_workflow_1.deepCopy)(publishedVersion.tools ?? {}); agent.skills = (0, n8n_workflow_1.deepCopy)(publishedVersion.skills ?? {}); agent.model = publishedVersion.model; agent.provider = publishedVersion.provider; agent.credentialId = publishedVersion.credentialId; agent.versionId = publishedVersion.publishedFromVersionId; if (agent.schema) { agent.name = agent.schema.name; agent.description = agent.schema.description ?? null; } await trx.save(agent); }); this.clearRuntimes(agentId); this.logger.debug('Reverted SDK agent to published version', { agentId, projectId }); return agent; } async delete(agentId, projectId) { const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agent) { return false; } await this.agentRepository.remove(agent); this.clearRuntimes(agentId); try { const { AgentScheduleService } = await Promise.resolve().then(() => __importStar(require('./integrations/agent-schedule.service'))); di_1.Container.get(AgentScheduleService).deregister(agentId); } catch (error) { this.logger.warn('Failed to stop schedule on agent delete', { agentId, error: error instanceof Error ? error.message : error, }); } try { await this.clearAllTestChatMessages(agentId); } catch (error) { this.logger.warn('Failed to clear test chat on agent delete', { agentId, error: error instanceof Error ? error.message : error, }); } this.logger.debug('Deleted SDK agent', { agentId, projectId }); return true; } async getChatMessages(threadId) { return await this.n8nMemory.getMessages(threadId); } getMemoryFactory() { return (params) => { if (params.storage === 'n8n') { return this.n8nMemory; } if (params.storage === 'sqlite') { return new agents.SqliteMemory(agents.SqliteMemoryConfigSchema.parse(params)); } throw new Error(`Unsupported memory storage: ${params.storage}`); }; } createCredentialProvider(projectId) { return new agents_credential_provider_1.AgentsCredentialProvider(di_1.Container.get(credentials_service_1.CredentialsService), projectId); } async getRuntime(params) { const { agentId, projectId, integrationType, usePublishedVersion } = params; const cacheKey = this.computeRuntimeCacheKey(params); const cached = this.runtimes.get(cacheKey); if (cached) return cached; const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agentEntity) throw new not_found_error_1.NotFoundError(`Agent ${agentId} not found`); let n8nUserId = params.n8nUserId; let agentData = agentEntity; if (usePublishedVersion) { const publishedSchema = agentEntity.publishedVersion?.schema; if (!publishedSchema) { throw new not_found_error_1.NotFoundError(`Agent ${agentId} is not published`); } agentData = { ...agentEntity, schema: publishedSchema, tools: agentEntity.publishedVersion?.tools ?? agentEntity.tools ?? {}, skills: agentEntity.publishedVersion?.skills ?? agentEntity.skills ?? {}, }; n8nUserId ??= agentEntity.publishedVersion?.publishedById ?? undefined; } if (!n8nUserId) { throw new n8n_workflow_1.UserError('Agent user owner id is required'); } const credentialProvider = this.createCredentialProvider(projectId); const { agent: agentInstance, toolRegistry } = await this.reconstructFromConfig(agentData, credentialProvider, n8nUserId, integrationType); this.runtimes.set(cacheKey, { agent: agentInstance, agentId, toolRegistry, projectId }); const runtime = this.runtimes.get(cacheKey); if (!runtime) throw new Error(`Agent ${agentId} failed to reconstruct`); return runtime; } makeToolResolver(projectId, userId) { return async (ref) => { if (ref.type === 'workflow') { if (!userId) { throw new n8n_workflow_1.UserError('userId is required when agent uses workflow tools'); } const { resolveWorkflowTool } = await Promise.resolve().then(() => __importStar(require('./tools/workflow-tool-factory'))); return await resolveWorkflowTool(ref, { workflowRepository: this.workflowRepository, workflowRunner: this.workflowRunner, activeExecutions: this.activeExecutions, executionRepository: this.executionRepository, workflowFinderService: this.workflowFinderService, userRepository: this.userRepository, userId, projectId, webhookBaseUrl: this.urlService.getWebhookBaseUrl(), }); } if (ref.type === 'node') { const { resolveNodeTool } = await Promise.resolve().then(() => __importStar(require('./tools/node-tool-factory'))); return await resolveNodeTool(ref, { executor: this.ephemeralNodeExecutor, projectId, }); } return null; }; } async injectRuntimeDependencies(params) { const { agent, agentId, projectId, credentialProvider, nodeToolsEnabled, integrationType } = params; try { const { createGetEnvironmentTool } = await Promise.resolve().then(() => __importStar(require('./tools/environment-tool'))); agent.tool(createGetEnvironmentTool()); } catch (toolError) { this.logger.warn('Failed to inject get_environment tool', { agentId, error: toolError instanceof Error ? toolError.message : String(toolError), }); } const integration = integrationType ? di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry).get(integrationType) : undefined; if (integration?.supportedComponents !== undefined) { try { const { createRichInteractionTool } = await Promise.resolve().then(() => __importStar(require('./integrations/rich-interaction-tool'))); agent.tool(createRichInteractionTool(integrationType)); } catch (toolError) { this.logger.warn('Failed to inject rich_interaction tool', { agentId, error: toolError instanceof Error ? toolError.message : String(toolError), }); } } if (nodeToolsEnabled) { this.attachNodeToolChain(agent, credentialProvider, projectId); } if (!agent.hasCheckpointStorage()) { agent.checkpoint(this.n8nCheckpointStorage); } } attachNodeToolChain(agent, credentialProvider, projectId) { agent.tool(this.agentsToolsService.getRuntimeTools(credentialProvider, projectId)); } async *resumeForChat(config) { const { agentId, projectId, runId, toolCallId, resumeData, integrationType } = config; const checkpointStatus = await this.n8nCheckpointStorage.getStatus(runId); if (checkpointStatus.status === 'expired') { throw new n8n_workflow_1.UserError(`Checkpoint ${runId} is expired and cannot be resumed`); } if (checkpointStatus.status === 'not-found') { throw new n8n_workflow_1.UserError(`Checkpoint ${runId} not found and cannot be resumed`); } const memoryScope = checkpointStatus.checkpoint?.persistence; if (!memoryScope) { throw new n8n_workflow_1.UserError(`Checkpoint ${runId} has no memory data and cannot be resumed`); } const threadId = memoryScope.threadId; const runtime = await this.getRuntime({ agentId, projectId, usePublishedVersion: true, integrationType, }); const { agent: agentInstance, toolRegistry } = runtime; const recorder = new execution_recorder_1.ExecutionRecorder(toolRegistry); const resultStream = await agentInstance.resume('stream', resumeData, { runId, toolCallId, }); const reader = resultStream.stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; recorder.record(value); yield value; } } finally { reader.releaseLock(); } const messageRecord = recorder.getMessageRecord(); void this.agentExecutionService .recordMessage({ threadId, agentId, agentName: agentInstance.name, projectId, userMessage: '', record: messageRecord, hitlStatus: 'resumed', }) .catch((error) => { this.logger.warn('Failed to record resumed agent execution', { agentId, threadId, error: error instanceof Error ? error.message : String(error), }); }); } async validateAgentIsRunnable(agentId, projectId, credentialProvider) { const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agentEntity) { return { missing: ['agent'] }; } const config = agentEntity.schema; const missing = []; if (!config) { return { missing: ['instructions', 'model'] }; } if (!config.instructions?.trim()) { missing.push('instructions'); } const modelSchema = agent_json_config_1.AgentJsonConfigSchema.shape.model; if (!config.model || !modelSchema.safeParse(config.model).success) { missing.push('model'); } if (config.credential) { try { const credentialName = config.credential; const creds = await credentialProvider.list(); const exists = creds.some((c) => c.id === credentialName || c.name.toLowerCase() === credentialName.toLowerCase()); if (!exists) missing.push('credential'); } catch { } } missing.push(...this.agentSkillsService .getMissingSkillIds(config, agentEntity.skills ?? {}) .map((skillId) => `skill:${skillId}`)); return { missing }; } async *executeForChat(config) { const { agentId, projectId, message, userId, memory } = config; const runtime = await this.getRuntime({ agentId, projectId, n8nUserId: userId }); yield* this.streamChatResponse({ agentInstance: runtime.agent, toolRegistry: runtime.toolRegistry, agentId, message, memory, projectId: runtime.projectId, }); } async getTestChatMessages(agentId, userId) { return await this.n8nMemory.getMessages(chatThreadId(agentId, userId), { resourceId: userId, }); } async clearTestChatMessages(agentId, userId) { await this.n8nMemory.deleteMessagesByThread(chatThreadId(agentId, userId), userId); } async clearAllTestChatMessages(agentId) { const threadId = chatThreadId(agentId); await this.n8nMemory.deleteThreadsByPrefix(threadId); await this.n8nMemory.deleteMessagesByThread(threadId); await this.n8nMemory.deleteThread(threadId); } async *executeForChatPublished(config) { const { agentId, projectId, message, memory, integrationType } = config; const runtime = await this.getRuntime({ agentId, projectId, integrationType, usePublishedVersion: true, }); yield* this.streamChatResponse({ agentInstance: runtime.agent, toolRegistry: runtime.toolRegistry, agentId, message, memory, projectId: runtime.projectId, source: integrationType, }); } async *executeForSchedulePublished(config) { const { agentId, projectId, message, memory } = config; const runtime = await this.getRuntime({ agentId, projectId, integrationType: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE, usePublishedVersion: true, }); yield* this.streamChatResponse({ agentInstance: runtime.agent, toolRegistry: runtime.toolRegistry, agentId, message, memory, projectId: runtime.projectId, source: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE, }); } async *streamChatResponse(config) { const { agentInstance, toolRegistry, agentId, message, memory, projectId, source } = config; const { threadId, resourceId } = memory; const recorder = new execution_recorder_1.ExecutionRecorder(toolRegistry); const resultStream = await agentInstance.stream(message, { persistence: { threadId, resourceId }, }); const reader = resultStream.stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; recorder.record(value); if (value.type === 'tool-call-suspended') { this.logger.info('Chat: tool-call-suspended chunk received', { agentId, toolCallId: value.toolCallId, toolName: value.toolName, }); } yield value; } } finally { reader.releaseLock(); } const messageRecord = recorder.getMessageRecord(); void this.agentExecutionService .recordMessage({ threadId, agentId, agentName: agentInstance.name, projectId, userMessage: message, record: messageRecord, hitlStatus: recorder.suspended ? 'suspended' : undefined, source, }) .catch((error) => { this.logger.warn('Failed to record agent execution', { agentId, threadId, error: error instanceof Error ? error.message : String(error), }); }); } async compileIsolated(agentEntity, credentialProvider, userId) { if (!agentEntity.schema) { return { ok: false, error: 'Agent has no JSON config. Create a config first.' }; } try { const { agent: reconstructed } = await this.reconstructFromConfig(agentEntity, credentialProvider, userId); return { ok: true, agent: reconstructed }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'Unknown compilation error', }; } } async executeForWorkflow(agentId, message, executionId, threadId, userId, projectId) { const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agentEntity) { throw new n8n_workflow_1.OperationalError('Agent not found or not accessible.'); } if (!agentEntity.publishedVersion) { throw new n8n_workflow_1.OperationalError('Agent is not published. Publish the agent before using it in a workflow.'); } const credentialProvider = new agents_credential_provider_1.AgentsCredentialProvider(di_1.Container.get(credentials_service_1.CredentialsService), projectId); const compiled = await this.compileIsolated(agentEntity, credentialProvider, userId); if (!compiled.ok || !compiled.agent) { throw new n8n_workflow_1.OperationalError(`Failed to compile agent: ${compiled.error ?? 'unknown error'}`); } const agentInstance = compiled.agent; const recorder = new execution_recorder_1.ExecutionRecorder(); let structuredOutput = null; const toolCalls = []; const toolInputs = new Map(); const resultStream = await agentInstance.stream(message, { persistence: { resourceId: executionId, threadId }, }); const reader = resultStream.stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; recorder.record(value); if (value.type === 'tool-call') { toolInputs.set(value.toolCallId, { toolName: value.toolName, input: value.input }); } else if (value.type === 'tool-result') { const pending = toolInputs.get(value.toolCallId); toolCalls.push({ toolName: value.toolName, input: pending?.input ?? null, result: value.output, }); toolInputs.delete(value.toolCallId); } else if (value.type === 'finish' && value.structuredOutput !== undefined) { structuredOutput = value.structuredOutput; } } } finally { reader.releaseLock(); } const messageRecord = recorder.getMessageRecord(); void this.agentExecutionService .recordMessage({ threadId, agentId, agentName: agentInstance.name, projectId, userMessage: message, record: messageRecord, source: api_types_1.AGENT_WORKFLOW_TRIGGER_TYPE, }) .catch((error) => { this.logger.warn('Failed to record agent execution from workflow', { agentId, threadId, error: error instanceof Error ? error.message : String(error), }); }); if (recorder.suspended) { throw new n8n_workflow_1.OperationalError('Agent execution suspended waiting for tool approval. ' + 'Suspend/resume is not supported in workflow execution context.'); } if (messageRecord.error) { throw new n8n_workflow_1.OperationalError(`Agent execution failed: ${messageRecord.error}`); } if (messageRecord.finishReason === 'error') { throw new n8n_workflow_1.OperationalError('Agent execution finished with an error.'); } return { response: messageRecord.assistantResponse, structuredOutput: structuredOutput ?? null, usage: messageRecord.usage ? { promptTokens: messageRecord.usage.promptTokens, completionTokens: messageRecord.usage.completionTokens, totalTokens: messageRecord.usage.totalTokens, } : null, toolCalls, finishReason: messageRecord.finishReason, session: { agentId, projectId, sessionId: threadId, }, }; } async getConfig(agentId, projectId) { const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!entity) throw new not_found_error_1.NotFoundError('Agent not found'); const config = (0, agent_config_composition_1.composeJsonConfig)(entity); if (!config) { throw new n8n_workflow_1.UserError('Agent has no JSON config yet.'); } return config; } async validateConfig(raw) { const parsed = agent_json_config_1.AgentJsonConfigSchema.safeParse(raw); if (!parsed.success) { return { valid: false, error: parsed.error.message }; } const config = parsed.data; if ((0, agent_json_config_1.isNodeToolsEnabled)(config.config) && !this.isNodeToolsModuleEnabled()) { return { valid: false, error: 'config.nodeTools.enabled requires the node-tools-searcher agents module to be enabled.', }; } try { this.validateNodeToolExpressions(config); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { valid: false, error: `Invalid $fromAI expression in node tool config: ${message}`, }; } const nodeError = await this.validateNodeToolConfigs(config); if (nodeError) { return { valid: false, error: nodeError }; } return { valid: true, config }; } validateNodeToolExpressions(config) { for (const tool of config.tools ?? []) { if (tool.type !== 'node') continue; (0, ai_utilities_1.extractFromAIParameters)((tool.node.nodeParameters ?? {})); } } async healIntegrationCredentialNames(rawConfig) { if (!rawConfig || typeof rawConfig !== 'object') return rawConfig; const cfg = rawConfig; if (!Array.isArray(cfg.integrations)) return rawConfig; const missingIds = new Set(); for (const integration of cfg.integrations) { if (!integration || typeof integration !== 'object') continue; const i = integration; if (typeof i.credentialId === 'string' && i.credentialName === undefined) { missingIds.add(i.credentialId); } } if (missingIds.size === 0) return rawConfig; const credentials = await di_1.Container.get(db_1.CredentialsRepository).findBy({ id: (0, typeorm_1.In)(Array.from(missingIds)), }); const namesById = new Map(credentials.map((c) => [c.id, c.name])); const integrations = cfg.integrations; return { ...cfg, integrations: integrations.map((integration) => { if (!integration || typeof integration !== 'object') return integration; const i = integration; if (typeof i.credentialId !== 'string' || i.credentialName !== undefined) { return integration; } const name = namesById.get(i.credentialId); return name ? { ...integration, credentialName: name } : integration; }), }; } async updateConfig(agentId, projectId, config) { const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!entity) throw new not_found_error_1.NotFoundError('Agent not found'); const healedConfig = await this.healIntegrationCredentialNames(config); const result = await this.validateConfig(healedConfig); if (!result.valid) { throw new n8n_workflow_1.UserError(`Invalid agent config: ${result.error}`); } this.validateConfigRefs(result.config, entity); const previousIntegrations = entity.integrations ?? []; const previousSchema = entity.schema ?? null; const integrationsProvided = result.config.integrations !== undefined; const toolsProvided = result.config.tools !== undefined; const skillsProvided = result.config.skills !== undefined; const descriptionProvided = result.config.description !== undefined; const credentialProvided = result.config.credential !== undefined; const memoryProvided = result.config.memory !== undefined; const providerToolsProvided = result.config.providerTools !== undefined; const configBlockProvided = result.config.config !== undefined; const { schemaConfig: decomposedSchema, integrations: decomposedIntegrations } = (0, agent_config_composition_1.decomposeJsonConfig)(result.config); const nextIntegrations = integrationsProvided ? decomposedIntegrations : previousIntegrations; const nextSchema = { ...(previousSchema ?? {}), name: decomposedSchema.name, model: decomposedSchema.model, instructions: decomposedSchema.instructions, ...(descriptionProvided ? { description: decomposedSchema.description } : {}), ...(credentialProvided ? { credential: decomposedSchema.credential } : {}), ...(memoryProvided ? { memory: decomposedSchema.memory } : {}), ...(toolsProvided ? { tools: decomposedSchema.tools } : {}), ...(skillsProvided ? { skills: decomposedSchema.skills } : {}), ...(providerToolsProvided ? { providerTools: decomposedSchema.providerTools } : {}), ...(configBlockProvided ? { config: decomposedSchema.config } : {}), }; entity.schema = nextSchema; entity.name = result.config.name; if (descriptionProvided) entity.description = result.config.description ?? null; entity.integrations = nextIntegrations; (0, agent_draft_utils_1.markAgentDraftDirty)(entity); if (toolsProvided) { const referencedIds = new Set((result.config.tools ?? []) .filter((t) => t.type === 'custom') .map((t) => t.id)); const orphanIds = Object.keys(entity.tools).filter((id) => !referencedIds.has(id)); if (orphanIds.length > 0) { const tools = { ...entity.tools }; for (const id of orphanIds) { delete tools[id]; } entity.tools = tools; } } if (skillsProvided) { this.agentSkillsService.removeUnreferencedSkills(entity, result.config); } this.clearRuntimes(agentId); const saved = await this.agentRepository.save(entity); this.logger.debug('Updated agent JSON config', { agentId, projectId }); if (integrationsProvided) { await (0, integrations_sync_1.syncAgentIntegrations)(saved, previousIntegrations, nextIntegrations, this.logger); } return { config: (0, agent_config_composition_1.composeJsonConfig)(saved) ?? result.config, updatedAt: saved.updatedAt.toISOString(), versionId: saved.versionId, }; } async buildCustomTool(agentId, projectId, code, descriptor) { const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!entity) throw new not_found_error_1.NotFoundError('Agent not found'); const toolId = (0, agent_resource_id_1.generateAgentResourceId)('tool', Object.keys(entity.tools ?? {})); entity.tools = { ...entity.tools, [toolId]: { code, descriptor }, }; (0, agent_draft_utils_1.markAgentDraftDirty)(entity); this.clearRuntimes(agentId); await this.agentRepository.save(entity); this.logger.debug('Built custom tool', { agentId, projectId, toolId }); return { ok: true, id: toolId, descriptor }; } async listSkills(agentId, projectId) { return await this.agentSkillsService.listSkills(agentId, projectId); } async getSkill(agentId, projectId, skillId) { return await this.agentSkillsService.getSkill(agentId, projectId, skillId); } async createSkill(agentId, projectId, skill) { const result = await this.agentSkillsService.createSkill(agentId, projectId, skill); this.clearRuntimes(agentId); return result; } async createAndAttachSkill(agentId, projectId, skill) { const result = await this.agentSkillsService.createAndAttachSkill(agentId, projectId, skill); this.clearRuntimes(agentId); return result; } async updateSkill(agentId, projectId, skillId, updates) { const result = await this.agentSkillsService.updateSkill(agentId, projectId, skillId, updates); this.clearRuntimes(agentId); return result; } async deleteCustomTool(agentId, projectId, toolId) { const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!entity) throw new not_found_error_1.NotFoundError('Agent not found'); const tools = { ...entity.tools }; delete tools[toolId]; entity.tools = tools; if (entity.schema?.tools) { entity.schema.tools = entity.schema.tools.filter((t) => !(t.type === 'custom' && 'id' in t && t.id === toolId)); } (0, agent_draft_utils_1.markAgentDraftDirty)(entity); this.clearRuntimes(agentId); await this.agentRepository.save(entity); this.logger.debug('Deleted custom tool', { agentId, projectId, toolId }); } async deleteSkill(agentId, projectId, skillId) { await this.agentSkillsService.deleteSkill(agentId, projectId, skillId); this.clearRuntimes(agentId); } async validateNodeToolConfigs(config) { const nodeTools = (config.tools ?? []).filter((t) => t.type === 'node'); if (nodeTools.length === 0) return null; const { setSchemaBaseDirs, validateNodeConfig } = await Promise.resolve().then(() => __importStar(require('@n8n/workflow-sdk'))); const dirs = (0, node_definition_resolver_1.resolveBuiltinNodeDefinitionDirs)(); if (dirs.length > 0) { setSchemaBaseDirs(dirs); } const errors = []; for (const tool of nodeTools) { const nodeType = tool.node.nodeType; const nodeTypeVersion = tool.node.nodeTypeVersion; const nodeParameters = tool.node.nodeParameters ?? {}; const result = validateNodeConfig(nodeType, nodeTypeVersion, { parameters: nodeParameters }, { isToolNode: true }); if (!result.valid) { const messages = result.errors .map((e) => e.message) .join('; '); errors.push(`Node tool "${tool.name}" (${nodeType}@${nodeTypeVersion}): ${messages}`); } } return errors.length > 0 ? errors.join('\n') : null; } validateConfigRefs(config, entity) { const missingSkillIds = this.agentSkillsService.getMissingSkillIds(config, entity.skills ?? {}); if (missingSkillIds.length > 0) { throw new n8n_workflow_1.UserError(`Invalid agent config: Missing skill bodies: ${missingSkillIds.join(', ')}`); } const missingToolIds = this.getMissingCustomToolIds(config, entity.tools ?? {}); if (missingToolIds.length > 0) { throw new n8n_workflow_1.UserError(`Invalid agent config: Missing custom tool definitions: ${missingToolIds.join(', ')}`); } const activeUnpublishedSchedule = (config.integrations ?? []).some((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) && integration.active); if (activeUnpublishedSchedule && !entity.publishedVersion) { throw new n8n_workflow_1.UserError('Invalid agent config: schedule integration cannot be active until the agent is published'); } } getMissingCustomToolIds(config, tools) { const refs = (config?.tools ?? []).filter((ref) => ref.type === 'custom'); const seen = new Set(); const missing = []; for (const ref of refs) { if (seen.has(ref.id)) continue; seen.add(ref.id); if (!tools[ref.id]) missing.push(ref.id); } return missing; } snapshotConfiguredTools(config, tools) { if (!config) return null; const missing = this.getMissingCustomToolIds(config, tools); if (missing.length > 0) { throw new n8n_workflow_1.UserError(`Cannot publish agent with missing custom tools: ${missing.join(', ')}`); } const snapshot = {}; for (const ref of config.tools ?? []) { if (ref.type !== 'custom') continue; const tool = tools[ref.id]; if (tool) snapshot[ref.id] = tool; } return snapshot; } async reconstructFromConfig(agentEntity, credentialProvider, userId, integrationType) { const config = agentEntity.schema; if (!config) { throw new n8n_workflow_1.UserError('Agent has no JSON config.'); } const toolsByName = {}; for (const [_toolId, toolEntry] of Object.entries(agentEntity.tools ?? {})) { toolsByName[toolEntry.descriptor.name] = toolEntry.code; } const toolDescriptors = {}; for (const [toolId, toolEntry] of Object.entries(agentEntity.tools ?? {})) { toolDescriptors[toolId] = toolEntry.descriptor; } const toolExecutor = this.secureRuntime.createToolExecutor(toolsByName); const toolResolver = this.makeToolResolver(agentEntity.projectId, userId); const resolvedTools = []; const reconstructed = await (0, from_json_config_1.buildFromJson)(config, toolDescriptors, { t