UNPKG

n8n

Version:

n8n Workflow Automation Tool

487 lines • 20.5 kB
"use strict"; 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 __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.AgentTaskService = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); 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 cron_1 = require("cron"); const crypto_1 = require("crypto"); const luxon_1 = require("luxon"); const n8n_core_1 = require("n8n-core"); const bad_request_error_1 = require("../../errors/response-errors/bad-request.error"); const not_found_error_1 = require("../../errors/response-errors/not-found.error"); const publisher_service_1 = require("../../scaling/pubsub/publisher.service"); const agents_service_1 = require("./agents.service"); const cron_validation_1 = require("./integrations/cron-validation"); const agent_repository_1 = require("./repositories/agent.repository"); const agent_task_run_lock_repository_1 = require("./repositories/agent-task-run-lock.repository"); const agent_task_snapshot_repository_1 = require("./repositories/agent-task-snapshot.repository"); const agent_task_repository_1 = require("./repositories/agent-task.repository"); const agent_draft_utils_1 = require("./utils/agent-draft.utils"); const agent_memory_scope_1 = require("./utils/agent-memory-scope"); const agent_resource_id_1 = require("./utils/agent-resource-id"); const TASK_RUN_LOCK_TTL_MS = 5 * 60 * 1000; const TASK_RUN_LOCK_RENEW_MS = 60 * 1000; let AgentTaskService = class AgentTaskService { constructor(logger, globalConfig, taskRepository, taskSnapshotRepository, taskRunLockRepository, agentRepository, projectRelationRepository, agentsService, instanceSettings, publisher) { this.logger = logger; this.globalConfig = globalConfig; this.taskRepository = taskRepository; this.taskSnapshotRepository = taskSnapshotRepository; this.taskRunLockRepository = taskRunLockRepository; this.agentRepository = agentRepository; this.projectRelationRepository = projectRelationRepository; this.agentsService = agentsService; this.instanceSettings = instanceSettings; this.publisher = publisher; this.jobs = new Map(); } async list(agentId) { const tasks = await this.taskRepository.findByAgentId(agentId); return tasks.map((task) => this.toDto(task)); } async create(agentId, dto) { this.assertValidCron(dto.cronExpression); const agent = await this.agentRepository.findOne({ where: { id: agentId } }); if (!agent) throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`); if (!agent.schema) throw new bad_request_error_1.BadRequestError('Agent has no config yet'); const taskId = (0, agent_resource_id_1.generateAgentResourceId)('task', (agent.schema.tasks ?? []).map((ref) => ref.id)); const task = this.taskRepository.create({ id: taskId, agentId, name: dto.name, objective: dto.objective, cronExpression: dto.cronExpression, }); this.attachTaskRef(agent, taskId, dto.enabled ?? true); (0, agent_draft_utils_1.markAgentDraftDirty)(agent); await this.agentRepository.manager.transaction(async (em) => { await em.save(task); await em.save(agent); }); this.logger.debug('[AgentTaskService] Created task', { agentId, taskId }); return this.toDto(task); } async update(agentId, taskId, dto) { const task = await this.getOrThrow(agentId, taskId); let changed = false; if (dto.cronExpression !== undefined) { this.assertValidCron(dto.cronExpression); if (dto.cronExpression !== task.cronExpression) { task.cronExpression = dto.cronExpression; changed = true; } } if (dto.name !== undefined && dto.name !== task.name) { task.name = dto.name; changed = true; } if (dto.objective !== undefined && dto.objective !== task.objective) { task.objective = dto.objective; changed = true; } if (!changed) return this.toDto(task); const agent = await this.agentRepository.findOne({ where: { id: agentId } }); const saved = await this.agentRepository.manager.transaction(async (em) => { const savedTask = await em.save(task); if (agent) { (0, agent_draft_utils_1.markAgentDraftDirty)(agent); await em.save(agent); } return savedTask; }); return this.toDto(saved); } async delete(agentId, taskId) { const task = await this.getOrThrow(agentId, taskId); const agent = await this.agentRepository.findOne({ where: { id: agentId } }); if (agent?.schema?.tasks) { agent.schema.tasks = agent.schema.tasks.filter((ref) => ref.id !== taskId); (0, agent_draft_utils_1.markAgentDraftDirty)(agent); } await this.agentRepository.manager.transaction(async (em) => { await em.remove(task); if (agent) await em.save(agent); }); this.logger.debug('[AgentTaskService] Deleted task', { agentId, taskId }); } attachTaskRef(agent, taskId, enabled) { if (!agent.schema) throw new bad_request_error_1.BadRequestError('Agent has no config yet'); agent.schema.tasks = [ ...(agent.schema.tasks ?? []).filter((ref) => ref.id !== taskId), { type: 'task', id: taskId, enabled }, ]; } async requestReconcile(agentId) { await this.registerEnabledForAgent(agentId); this.broadcastTasksChanged(agentId); } async registerEnabledForAgent(agentId) { const agent = await this.agentRepository.findOne({ where: { id: agentId }, relations: { activeVersion: true }, }); if (!agent?.activeVersionId) { this.deregisterAgentTasks(agentId); return; } await this.reconcileAgent(agent); } async handleTasksChanged(payload) { await this.registerEnabledForAgent(payload.agentId); } broadcastTasksChanged(agentId) { if (!this.globalConfig.multiMainSetup.enabled) return; void this.publisher .publishCommand({ command: 'agent-tasks-changed', payload: { agentId } }) .catch((error) => this.logger.warn('[AgentTaskService] Failed to publish agent-tasks-changed', { agentId, error: error instanceof Error ? error.message : String(error), })); } deregisterAgentTasks(agentId) { const taskIds = [...this.jobs.entries()] .filter(([, entry]) => entry.agentId === agentId) .map(([taskId]) => taskId); for (const taskId of taskIds) this.deregister(taskId); } async reconnectAll() { const agents = await this.agentRepository.find({ where: { activeVersionId: (0, typeorm_1.Not)((0, typeorm_1.IsNull)()) }, relations: { activeVersion: true }, }); this.logger.debug('[AgentTaskService] Reconnecting published agents', { count: agents.length }); for (const agent of agents) { try { await this.reconcileAgent(agent); } catch (error) { this.logger.error('[AgentTaskService] Failed to reconnect agent tasks', { agentId: agent.id, error: error instanceof Error ? error.message : String(error), }); } } } stopAll() { for (const taskId of [...this.jobs.keys()]) { this.deregister(taskId); } } async reconcileAgent(agent) { if (!this.instanceSettings.isLeader) return; if (!agent.activeVersionId) { this.deregisterAgentTasks(agent.id); return; } const snapshots = await this.taskSnapshotRepository.findEnabledByVersionId(agent.activeVersionId); const enabledIds = new Set(snapshots.map((snapshot) => snapshot.taskId)); for (const [taskId, entry] of this.jobs.entries()) { if (entry.agentId === agent.id && !enabledIds.has(taskId)) this.deregister(taskId); } if (enabledIds.size === 0) return; for (const snapshot of snapshots) { this.registerOrRefresh(snapshot.taskId, agent.id, snapshot.cronExpression); } } registerOrRefresh(taskId, agentId, cronExpression) { if (!(0, cron_validation_1.isValidCronExpression)(cronExpression)) { this.logger.warn('[AgentTaskService] Skipping task with invalid cron', { taskId }); this.deregister(taskId); return; } this.deregister(taskId); const timezone = this.globalConfig.generic.timezone; const job = new cron_1.CronJob(cronExpression, () => { void this.runScheduledTask(taskId); }, null, true, timezone); this.jobs.set(taskId, { agentId, job }); this.logger.info('[AgentTaskService] Registered task', { taskId, agentId, cronExpression, timezone, }); } deregister(taskId) { const existing = this.jobs.get(taskId); if (!existing) return; void existing.job.stop(); this.jobs.delete(taskId); this.logger.info('[AgentTaskService] Deregistered task', { taskId }); } async runScheduledTask(taskId) { const agentId = this.jobs.get(taskId)?.agentId; if (!agentId) { await this.runTask(taskId); return; } const holderId = (0, crypto_1.randomUUID)(); let lock = null; let renewInterval; try { lock = await this.taskRunLockRepository.acquire(agentId, taskId, { holderId, ttlMs: TASK_RUN_LOCK_TTL_MS, }); if (!lock) { this.logger.info('[AgentTaskService] Skipping task because previous run is still active', { taskId, agentId, }); return; } renewInterval = this.startTaskRunLockRenewal(lock); await this.runTask(taskId); } catch (error) { this.logger.error('[AgentTaskService] Scheduled task lock failed', { taskId, agentId, error: error instanceof Error ? error.message : String(error), }); } finally { if (renewInterval) clearInterval(renewInterval); if (lock) { await this.taskRunLockRepository.release(lock).catch((error) => { this.logger.warn('[AgentTaskService] Failed to release task run lock', { taskId, agentId, error: error instanceof Error ? error.message : String(error), }); }); } } } startTaskRunLockRenewal(lock) { return setInterval(() => { void this.taskRunLockRepository .renew(lock, TASK_RUN_LOCK_TTL_MS) .then((renewed) => { if (!renewed) { this.logger.warn('[AgentTaskService] Failed to renew task run lock', { taskId: lock.taskId, agentId: lock.agentId, }); } }) .catch((error) => { this.logger.warn('[AgentTaskService] Failed to renew task run lock', { taskId: lock.taskId, agentId: lock.agentId, error: error instanceof Error ? error.message : String(error), }); }); }, TASK_RUN_LOCK_RENEW_MS); } async runTask(taskId) { const agentId = this.jobs.get(taskId)?.agentId; let projectId; try { if (!agentId) { this.deregister(taskId); return; } const agent = await this.agentRepository.findOne({ where: { id: agentId }, relations: { activeVersion: true }, }); if (!agent) { this.deregister(taskId); return; } projectId = agent.projectId; if (!agent.activeVersionId) { this.logger.warn('[AgentTaskService] Task fired for unpublished agent', { taskId, agentId, }); this.deregister(taskId); return; } const snapshot = await this.taskSnapshotRepository.findByVersionAndTaskId(agent.activeVersionId, taskId); if (!snapshot?.enabled) { this.logger.warn('[AgentTaskService] Task fired but has no enabled published snapshot', { taskId, agentId, }); this.deregister(taskId); return; } const executionUserId = await this.resolveExecutionUserId(agent); if (!executionUserId) { this.logger.warn('[AgentTaskService] No project member available for task run', { taskId, agentId, projectId, }); return; } const { message, threadId } = this.buildTaskRunMessage(taskId, snapshot.objective); this.logger.info('[AgentTaskService] Task fired', { taskId, agentId, projectId, cronExpression: snapshot.cronExpression, }); await this.consumeTaskRun('Task run', { taskId, agentId, projectId }, this.agentsService.executeForTaskPublished({ agentId: agent.id, projectId: agent.projectId, message, memory: { threadId, resourceId: (0, agent_memory_scope_1.taskRunMemoryResourceId)(taskId) }, taskId, taskVersionId: agent.activeVersionId, })); } catch (error) { this.logger.error('[AgentTaskService] Task run failed', { taskId, agentId, projectId, error: error instanceof Error ? error.message : String(error), }); } } buildTaskRunMessage(taskId, objective) { const timezone = this.globalConfig.generic.timezone; const timestamp = luxon_1.DateTime.now().setZone(timezone).toISO() ?? new Date().toISOString(); const message = `${objective}\n\nCurrent date and time: ${timestamp} (timezone: ${timezone})`; const threadId = `task-${taskId}-${(0, crypto_1.randomUUID)()}`; return { message, threadId }; } async consumeTaskRun(kind, context, stream) { const startedAt = Date.now(); try { let chunkCount = 0; for await (const _chunk of stream) { chunkCount += 1; } this.logger.info(`[AgentTaskService] ${kind} completed`, { ...context, chunkCount, durationMs: Date.now() - startedAt, }); } catch (error) { this.logger.error(`[AgentTaskService] ${kind} failed`, { ...context, error: error instanceof Error ? error.message : String(error), }); } } async runNow(agentId, taskId, userId) { const task = await this.getOrThrow(agentId, taskId); const agent = await this.agentRepository.findOne({ where: { id: agentId } }); if (!agent) { throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`); } void this.executeNow(task, agent.projectId, userId); } async executeNow(task, projectId, userId) { const { message, threadId } = this.buildTaskRunMessage(task.id, task.objective); this.logger.info('[AgentTaskService] Manual task run started', { taskId: task.id, agentId: task.agentId, projectId, }); await this.consumeTaskRun('Manual task run', { taskId: task.id, agentId: task.agentId, projectId }, this.agentsService.executeForTaskNow({ agentId: task.agentId, projectId, userId, message, memory: { threadId, resourceId: (0, agent_memory_scope_1.taskRunMemoryResourceId)(task.id) }, taskId: task.id, })); } async resolveExecutionUserId(agent) { const userIds = await this.projectRelationRepository.findUserIdsByProjectId(agent.projectId); if (userIds.length === 0) return undefined; const publishedById = agent.activeVersion?.publishedById; if (publishedById && userIds.includes(publishedById)) { return publishedById; } return undefined; } async getOrThrow(agentId, taskId) { const task = await this.taskRepository.findByIdAndAgentId(taskId, agentId); if (!task) { throw new not_found_error_1.NotFoundError(`Task "${taskId}" not found`); } return task; } assertValidCron(cronExpression) { if (!(0, cron_validation_1.isValidCronExpression)(cronExpression)) { throw new bad_request_error_1.BadRequestError('Invalid cron expression'); } } toDto(task) { return { id: task.id, name: task.name, objective: task.objective, cronExpression: task.cronExpression, createdAt: task.createdAt.toISOString(), updatedAt: task.updatedAt.toISOString(), }; } }; exports.AgentTaskService = AgentTaskService; __decorate([ (0, decorators_1.OnPubSubEvent)('agent-tasks-changed', { instanceType: 'main' }), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], AgentTaskService.prototype, "handleTasksChanged", null); __decorate([ (0, decorators_1.OnLeaderTakeover)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], AgentTaskService.prototype, "reconnectAll", null); __decorate([ (0, decorators_1.OnLeaderStepdown)(), (0, decorators_1.OnShutdown)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], AgentTaskService.prototype, "stopAll", null); exports.AgentTaskService = AgentTaskService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, config_1.GlobalConfig, agent_task_repository_1.AgentTaskRepository, agent_task_snapshot_repository_1.AgentTaskSnapshotRepository, agent_task_run_lock_repository_1.AgentTaskRunLockRepository, agent_repository_1.AgentRepository, db_1.ProjectRelationRepository, agents_service_1.AgentsService, n8n_core_1.InstanceSettings, publisher_service_1.Publisher]) ], AgentTaskService); //# sourceMappingURL=agent-task.service.js.map