n8n
Version:
n8n Workflow Automation Tool
487 lines • 20.5 kB
JavaScript
"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