n8n
Version:
n8n Workflow Automation Tool
477 lines • 20.8 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 decorators_1 = require("@n8n/decorators");
const di_1 = require("@n8n/di");
const typeorm_1 = require("@n8n/typeorm");
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 agent_execution_orchestrator_service_1 = require("./agent-execution-orchestrator.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;
const AGENT_TASK_SCHEDULE_GROUP_TYPE = 'agent-task';
const agentTaskScheduleGroup = (agentId) => ({
type: AGENT_TASK_SCHEDULE_GROUP_TYPE,
id: agentId,
});
let AgentTaskService = class AgentTaskService {
constructor(logger, globalConfig, taskRepository, taskSnapshotRepository, taskRunLockRepository, agentRepository, agentExecutionOrchestratorService, instanceSettings, scheduledTaskManager, publisher) {
this.logger = logger;
this.globalConfig = globalConfig;
this.taskRepository = taskRepository;
this.taskSnapshotRepository = taskSnapshotRepository;
this.taskRunLockRepository = taskRunLockRepository;
this.agentRepository = agentRepository;
this.agentExecutionOrchestratorService = agentExecutionOrchestratorService;
this.instanceSettings = instanceSettings;
this.scheduledTaskManager = scheduledTaskManager;
this.publisher = publisher;
}
async list(agentId) {
const tasks = await this.taskRepository.findByAgentId(agentId);
return tasks.map((task) => this.toDto(task));
}
async create(agentId, dto) {
const [task] = await this.createTasksBatch(agentId, undefined, [dto]);
return task;
}
async createTasks(agentId, projectId, dtos) {
return await this.createTasksBatch(agentId, projectId, dtos);
}
async createTasksBatch(agentId, projectId, dtos) {
if (dtos.length === 0) {
throw new bad_request_error_1.BadRequestError('At least one task is required');
}
for (const dto of dtos) {
this.assertValidCron(dto.cronExpression);
}
const agent = projectId
? await this.agentRepository.findByIdAndProjectId(agentId, projectId)
: 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 tasks = dtos.map((dto) => {
const taskId = (0, agent_resource_id_1.generateAgentResourceId)('task', (agent.schema?.tasks ?? []).map((ref) => ref.id));
this.attachTaskRef(agent, taskId, dto.enabled ?? true);
return this.taskRepository.create({
id: taskId,
agentId,
name: dto.name,
objective: dto.objective,
cronExpression: dto.cronExpression,
});
});
(0, agent_draft_utils_1.markAgentDraftDirty)(agent);
await this.agentRepository.manager.transaction(async (em) => {
for (const task of tasks) {
await em.save(task);
}
await em.save(agent);
});
this.logger.debug('[AgentTaskService] Created tasks', {
agentId,
taskIds: tasks.map((task) => task.id),
});
return tasks.map((task) => 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 group = agentTaskScheduleGroup(agentId);
const taskIds = this.scheduledTaskManager.getTargetIds(group);
if (!this.scheduledTaskManager.deregisterGroup(group))
return;
for (const taskId of taskIds) {
this.logger.info('[AgentTaskService] Deregistered task', { 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() {
this.scheduledTaskManager.deregisterGroups(AGENT_TASK_SCHEDULE_GROUP_TYPE);
}
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 of this.scheduledTaskManager.getTargetIds(agentTaskScheduleGroup(agent.id))) {
if (!enabledIds.has(taskId))
this.deregister(agent.id, 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(agentId, taskId);
return;
}
this.deregister(agentId, taskId);
const timezone = this.globalConfig.generic.timezone;
const registered = this.scheduledTaskManager.register({
group: agentTaskScheduleGroup(agentId),
targetId: taskId,
expression: cronExpression,
timezone,
}, () => {
void this.runScheduledTask(agentId, taskId);
});
if (!registered)
return;
this.logger.info('[AgentTaskService] Registered task', {
taskId,
agentId,
cronExpression,
timezone,
});
}
deregister(agentId, taskId) {
const group = agentTaskScheduleGroup(agentId);
if (!this.scheduledTaskManager.hasTarget(group, taskId))
return;
this.scheduledTaskManager.deregisterTarget(group, taskId);
this.logger.info('[AgentTaskService] Deregistered task', { taskId });
}
async runScheduledTask(agentId, taskId) {
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(agentId, 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(agentId, taskId) {
let projectId;
try {
const agent = await this.agentRepository.findOne({
where: { id: agentId },
relations: { activeVersion: true },
});
if (!agent) {
this.deregister(agentId, taskId);
return;
}
projectId = agent.projectId;
if (!agent.activeVersionId) {
this.logger.warn('[AgentTaskService] Task fired for unpublished agent', {
taskId,
agentId,
});
this.deregister(agentId, 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(agentId, taskId);
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.agentExecutionOrchestratorService.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, user) {
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, user);
}
async executeNow(task, projectId, user) {
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.agentExecutionOrchestratorService.executeForTaskNow({
agentId: task.agentId,
projectId,
user,
message,
memory: { threadId, resourceId: (0, agent_memory_scope_1.taskRunMemoryResourceId)(task.id) },
taskId: task.id,
}));
}
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, agent_execution_orchestrator_service_1.AgentExecutionOrchestratorService, n8n_core_1.InstanceSettings, n8n_core_1.ScheduledTaskManager, publisher_service_1.Publisher])
], AgentTaskService);
//# sourceMappingURL=agent-task.service.js.map