n8n
Version:
n8n Workflow Automation Tool
427 lines • 19.6 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);
};
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