n8n
Version:
n8n Workflow Automation Tool
251 lines • 12.1 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_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