UNPKG

n8n

Version:

n8n Workflow Automation Tool

427 lines • 19.6 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); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var AgentExecutionService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentExecutionService = void 0; exports.threadBelongsTo = threadBelongsTo; const backend_common_1 = require("@n8n/backend-common"); const di_1 = require("@n8n/di"); const chunk_1 = __importDefault(require("lodash/chunk")); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const telemetry_1 = require("../../telemetry"); const agent_chat_attachment_service_1 = require("./agent-chat-attachment.service"); const agent_execution_update_broadcaster_1 = require("./agent-execution-update-broadcaster"); const agent_execution_log_store_1 = require("./execution-log/agent-execution-log-store"); const n8n_memory_1 = require("./integrations/n8n-memory"); const agent_execution_thread_repository_1 = require("./repositories/agent-execution-thread.repository"); const agent_execution_repository_1 = require("./repositories/agent-execution.repository"); const TIMELINE_SNAPSHOT_RETRY_DELAY_MS = 1_000; let AgentExecutionService = AgentExecutionService_1 = class AgentExecutionService { constructor(logger, agentExecutionRepository, agentExecutionThreadRepository, n8nMemory, telemetry, agentChatAttachmentService, agentExecutionLogStore, storageConfig, errorReporter, executionUpdateBroadcaster) { this.logger = logger; this.agentExecutionRepository = agentExecutionRepository; this.agentExecutionThreadRepository = agentExecutionThreadRepository; this.n8nMemory = n8nMemory; this.telemetry = telemetry; this.agentChatAttachmentService = agentChatAttachmentService; this.agentExecutionLogStore = agentExecutionLogStore; this.storageConfig = storageConfig; this.errorReporter = errorReporter; this.executionUpdateBroadcaster = executionUpdateBroadcaster; this.heartbeatTimers = new Map(); this.pendingTimelineSnapshots = new Map(); this.timelineSnapshotWrites = new Map(); this.executionsNeedingTitleSync = new Set(); } async startExecutionRecording(params, startedAt) { const { userMessage, created } = await this.prepareThread(params); const inserted = await this.agentExecutionRepository.save(this.agentExecutionRepository.create({ threadId: params.threadId, status: 'running', startedAt, stoppedAt: null, duration: 0, userMessage, model: null, promptTokens: null, completionTokens: null, totalTokens: null, cost: null, timeline: null, storedAt: 'db', error: null, hitlStatus: null, source: params.source ?? null, attachments: params.attachments?.length ? params.attachments : null, })); if (created) this.executionsNeedingTitleSync.add(inserted.id); this.startHeartbeat(inserted.id); this.executionUpdateBroadcaster.notify({ projectId: params.projectId, agentId: params.agentId, threadId: params.threadId, executionId: inserted.id, }); return inserted.id; } recordTimelineSnapshot({ executionId, ...snapshot }) { this.pendingTimelineSnapshots.set(executionId, snapshot); this.ensureTimelineSnapshotWrite(executionId); } async finalizeExecution(executionId, params) { const { record, hitlStatus } = params; const status = executionStatus(record); let storedAt = record.timeline.length > 0 ? this.storageConfig.modeTag : 'db'; try { if (storedAt !== 'db') { try { await this.agentExecutionLogStore.write({ agentId: params.agentId, threadId: params.threadId, executionId }, { timeline: record.timeline }, storedAt); } catch (error) { this.errorReporter.error(error); storedAt = 'db'; } } const finalized = await this.agentExecutionRepository.updateIfRunning(executionId, { status, stoppedAt: new Date(record.startTime + record.duration), duration: record.duration, model: record.model, promptTokens: record.usage?.promptTokens ?? null, completionTokens: record.usage?.completionTokens ?? null, totalTokens: record.usage?.totalTokens ?? null, cost: record.totalCost, timeline: storedAt === 'db' && record.timeline.length > 0 ? record.timeline : null, storedAt, error: record.error, hitlStatus: hitlStatus ?? null, }); if (!finalized) return executionId; this.executionUpdateBroadcaster.notify({ projectId: params.projectId, agentId: params.agentId, threadId: params.threadId, executionId, }); await this.completeRecordedExecution(params, executionId, status); return executionId; } catch (error) { this.errorReporter.error(error); throw error; } finally { this.stopHeartbeat(executionId); this.executionsNeedingTitleSync.delete(executionId); } } async finalizeInterruptedExecution(execution) { const timeline = execution.timeline ?? []; const stoppedAt = new Date(); const duration = execution.startedAt ? Math.max(0, stoppedAt.getTime() - execution.startedAt.getTime()) : 0; const finalized = await this.agentExecutionRepository.updateIfRunning(execution.id, { status: 'interrupted', stoppedAt, duration, timeline: timeline.length > 0 ? timeline : null, storedAt: 'db', error: 'Agent execution was interrupted by a process restart.', }); if (finalized) void this.notifyInterruptedExecution(execution); return finalized; } async notifyInterruptedExecution(execution) { try { const thread = await this.agentExecutionThreadRepository.findOneBy({ id: execution.threadId, }); if (!thread) return; this.executionUpdateBroadcaster.notify({ projectId: thread.projectId, agentId: thread.agentId, threadId: execution.threadId, executionId: execution.id, }); } catch (error) { this.logger.warn('Failed to resolve an interrupted agent execution update', { executionId: execution.id, threadId: execution.threadId, error: error instanceof Error ? error.message : String(error), }); } } startHeartbeat(executionId) { const timer = setInterval(() => { void this.agentExecutionRepository.touchRunning(executionId).catch((error) => { this.logger.warn('Failed to heartbeat a running agent execution', { executionId, error: error instanceof Error ? error.message : String(error), }); }); }, AgentExecutionService_1.heartbeatIntervalMs); timer.unref(); this.heartbeatTimers.set(executionId, timer); } stopHeartbeat(executionId) { const timer = this.heartbeatTimers.get(executionId); if (timer) clearInterval(timer); this.heartbeatTimers.delete(executionId); } ensureTimelineSnapshotWrite(executionId) { if (this.timelineSnapshotWrites.has(executionId) || !this.pendingTimelineSnapshots.has(executionId)) { return; } const write = this.drainTimelineSnapshots(executionId).finally(() => { this.timelineSnapshotWrites.delete(executionId); this.ensureTimelineSnapshotWrite(executionId); }); this.timelineSnapshotWrites.set(executionId, write); } async drainTimelineSnapshots(executionId) { while (true) { const snapshot = this.pendingTimelineSnapshots.get(executionId); if (!snapshot) return; this.pendingTimelineSnapshots.delete(executionId); try { if (!(await this.agentExecutionRepository.updateTimelineIfRunning(executionId, snapshot.timeline))) { this.pendingTimelineSnapshots.delete(executionId); return; } this.executionUpdateBroadcaster.notify({ projectId: snapshot.projectId, agentId: snapshot.agentId, threadId: snapshot.threadId, executionId, }); } catch (error) { if (!this.pendingTimelineSnapshots.has(executionId)) { this.pendingTimelineSnapshots.set(executionId, snapshot); } this.logger.warn('Failed to persist an agent execution timeline snapshot; retrying', { executionId, error: error instanceof Error ? error.message : String(error), }); await new Promise((resolve) => { const timer = setTimeout(resolve, TIMELINE_SNAPSHOT_RETRY_DELAY_MS); timer.unref(); }); } } } async prepareThread(params) { const { thread, created } = await this.agentExecutionThreadRepository.findOrCreate(params.threadId, params.agentId, params.agentName, params.projectId, params.threadMetadata, params.taskId, params.taskVersionId); if (!created) { await this.agentExecutionThreadRepository.bumpUpdatedAt(params.threadId); if (!thread.title) await this.syncTitleFromMemory(params.threadId, params.agentId); } return { userMessage: cleanUserMessage(params.userMessage, params.agentName), created }; } async completeRecordedExecution(params, executionId, status) { const { threadId, agentId, record, hitlStatus } = params; if (hitlStatus === 'resumed' && record.model) { await this.backfillSuspendedExecutions(threadId, record.model); } if (record.usage) { await this.agentExecutionThreadRepository.incrementUsage(threadId, record.usage.promptTokens, record.usage.completionTokens, record.totalCost ?? 0, record.duration); } if (params.telemetry) { try { this.telemetry.trackAgentTurnFinished({ agent_id: agentId, thread_id: threadId, run_type: params.telemetry.runType, turn_status: status === 'success' ? 'succeeded' : 'failed', configuration: params.telemetry.configuration, latency_ms: record.duration, cost: record.totalCost ?? 0, token_count: record.usage?.totalTokens ?? 0, tool_call_count: record.timeline.filter((event) => event.type === 'tool-call').length, }); } catch (error) { this.logger.warn('Failed to track agent execution telemetry', { agentId, threadId, error: error instanceof Error ? error.message : String(error), }); } } this.logger.debug('Recorded agent execution', { executionId, threadId, agentId, status, duration: record.duration, }); if (this.executionsNeedingTitleSync.has(executionId)) { await this.syncTitleFromMemory(threadId, agentId); } } async findLatestSuspendedRun(threadId) { return await this.agentExecutionRepository.findLatestSuspendedByThreadId(threadId); } async backfillSuspendedExecutions(threadId, model) { const candidates = await this.agentExecutionRepository.findSuspendedWithoutModel(threadId); if (candidates.length === 0) return; await this.agentExecutionRepository.backfillModel(candidates.map((c) => c.id), model); } async syncTitleFromMemory(threadId, agentId) { try { const memoryThread = await this.n8nMemory.getImplementation(agentId).getThread(threadId); if (memoryThread?.title) { const emoji = memoryThread.metadata && typeof memoryThread.metadata.emoji === 'string' ? memoryThread.metadata.emoji : null; await this.agentExecutionThreadRepository.update(threadId, { title: memoryThread.title, ...(emoji && { emoji }), }); } } catch { } } async deleteThread(projectId, agentId, threadId) { const thread = await this.agentExecutionThreadRepository.findOneBy({ id: threadId, projectId, agentId, }); if (!thread) return false; const blobRefs = this.toBlobRefs(await this.agentExecutionRepository.findBlobRefsByThreadId(threadId)); await this.n8nMemory.getImplementation(agentId).deleteThread(threadId); await this.agentChatAttachmentService.deleteByThread(threadId, { projectId }); await Promise.all([ this.agentExecutionThreadRepository.delete({ id: threadId }), this.agentExecutionLogStore.delete(blobRefs.map((r) => ({ agentId, threadId, executionId: r.id, storedAt: r.storedAt }))), ]); return true; } async deleteExecutionLogsForAgent(agentId) { const refs = this.toBlobRefs(await this.agentExecutionRepository.findBlobRefsByAgentId(agentId)); for (const batch of (0, chunk_1.default)(refs, AgentExecutionService_1.logDeletionBatchSize)) { await this.agentExecutionLogStore.delete(batch.map((r) => ({ agentId, threadId: r.threadId, executionId: r.id, storedAt: r.storedAt, }))); } } async getThreads(projectId, agentId, limit, cursor) { const page = await this.agentExecutionThreadRepository.findByProjectIdPaginated(projectId, agentId, limit, cursor); if (page.threads.length === 0) { return { threads: [], nextCursor: page.nextCursor }; } const threadIds = page.threads.map((t) => t.id); const [messageMap, sourceMap] = await Promise.all([ this.agentExecutionRepository.findFirstUserMessageByThreadIds(threadIds), this.agentExecutionRepository.findFirstSourceByThreadIds(threadIds), ]); return { ...page, threads: page.threads.map((t) => ({ ...t, firstMessage: messageMap.get(t.id) ?? null, source: sourceMap.get(t.id) ?? null, })), }; } async getThreadDetail(threadId, projectId, agentId) { const thread = await this.agentExecutionThreadRepository.findOneBy({ id: threadId }); if (!thread || !threadBelongsTo(thread, projectId, agentId)) return null; const executions = await this.agentExecutionRepository.findByThreadIdOrdered(threadId); await this.hydrateTimelines(agentId, threadId, executions); return { thread, executions }; } async hydrateTimelines(agentId, threadId, executions) { const blobStored = this.toBlobRefs(executions); if (blobStored.length === 0) return; const readable = blobStored.filter((e) => this.agentExecutionLogStore.hasLocation(e.storedAt)); if (readable.length < blobStored.length) { this.errorReporter.error(new n8n_workflow_1.UnexpectedError('Skipped reading agent execution logs for unconfigured storage location', { extra: { threadId, skipped: blobStored.length - readable.length }, })); } if (readable.length === 0) return; let entries; try { entries = await this.agentExecutionLogStore.readMany(readable.map((e) => ({ agentId, threadId, executionId: e.id, storedAt: e.storedAt }))); } catch (error) { this.errorReporter.error(error); return; } for (const execution of readable) { const entry = entries.get(execution.id); if (entry) execution.timeline = entry.timeline; } } async findThreadById(threadId) { return await this.agentExecutionThreadRepository.findOneBy({ id: threadId }); } toBlobRefs(refs) { return refs.filter((r) => r.storedAt !== 'db'); } }; exports.AgentExecutionService = AgentExecutionService; AgentExecutionService.logDeletionBatchSize = 500; AgentExecutionService.heartbeatIntervalMs = 30_000; exports.AgentExecutionService = AgentExecutionService = AgentExecutionService_1 = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, agent_execution_repository_1.AgentExecutionRepository, agent_execution_thread_repository_1.AgentExecutionThreadRepository, n8n_memory_1.N8nMemory, telemetry_1.Telemetry, agent_chat_attachment_service_1.AgentChatAttachmentService, agent_execution_log_store_1.AgentExecutionLogStore, n8n_core_1.StorageConfig, n8n_core_1.ErrorReporter, agent_execution_update_broadcaster_1.AgentExecutionUpdateBroadcaster]) ], AgentExecutionService); function cleanUserMessage(message, agentName) { if (message === null) return null; const cleaned = message .replace(/<@[A-Z0-9]+>/gi, `@${agentName}`) .replace(/@[A-Z0-9]{8,}/gi, `@${agentName}`) .trim(); return cleaned.length > 0 ? cleaned : null; } function executionStatus(record) { if (record.error !== null || record.finishReason === 'error') return 'error'; if (record.finishReason === 'cancelled') return 'cancelled'; return 'success'; } function threadBelongsTo(thread, projectId, agentId) { if (thread.projectId !== projectId) return false; if (thread.agentId !== agentId) return false; return true; } //# sourceMappingURL=agent-execution.service.js.map