UNPKG

n8n

Version:

n8n Workflow Automation Tool

251 lines • 12.1 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_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"); let AgentExecutionService = AgentExecutionService_1 = class AgentExecutionService { constructor(logger, agentExecutionRepository, agentExecutionThreadRepository, n8nMemory, telemetry, agentExecutionLogStore, storageConfig, errorReporter) { this.logger = logger; this.agentExecutionRepository = agentExecutionRepository; this.agentExecutionThreadRepository = agentExecutionThreadRepository; this.n8nMemory = n8nMemory; this.telemetry = telemetry; this.agentExecutionLogStore = agentExecutionLogStore; this.storageConfig = storageConfig; this.errorReporter = errorReporter; } async recordMessage(params) { const { threadId, agentId, agentName, projectId, record, source, hitlStatus, threadMetadata, taskId, taskVersionId, } = params; const { thread, created } = await this.agentExecutionThreadRepository.findOrCreate(threadId, agentId, agentName, projectId, threadMetadata, taskId, taskVersionId); if (!created) { await this.agentExecutionThreadRepository.bumpUpdatedAt(threadId); if (!thread.title) { await this.syncTitleFromMemory(threadId, agentId); } } const userMessage = params.userMessage === null ? null : (() => { const cleanedMessage = params.userMessage .replace(/<@[A-Z0-9]+>/gi, `@${agentName}`) .replace(/@[A-Z0-9]{8,}/gi, `@${agentName}`) .trim(); return cleanedMessage.length > 0 ? cleanedMessage : null; })(); const status = record.error ? 'error' : 'success'; const startedAt = new Date(record.startTime); const stoppedAt = new Date(record.startTime + record.duration); const storedAt = record.timeline.length > 0 ? this.storageConfig.modeTag : 'db'; const inserted = await this.agentExecutionRepository.save(this.agentExecutionRepository.create({ threadId, status, startedAt, stoppedAt, duration: record.duration, userMessage, 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, source: source ?? null, })); if (storedAt !== 'db') { try { await this.agentExecutionLogStore.write({ agentId, threadId, executionId: inserted.id }, { timeline: record.timeline }, storedAt); } catch (error) { this.errorReporter.error(error); } } 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: record.error !== null || record.finishReason === 'error' ? 'failed' : 'succeeded', configuration: params.telemetry.configuration, latency_ms: record.duration, cost: record.totalCost ?? 0, tool_call_count: record.timeline.filter((t) => t.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: inserted.id, threadId, agentId, status, duration: record.duration, }); if (created) { await this.syncTitleFromMemory(threadId, agentId); } return inserted.id; } 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 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; 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_execution_log_store_1.AgentExecutionLogStore, n8n_core_1.StorageConfig, n8n_core_1.ErrorReporter]) ], AgentExecutionService); 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