UNPKG

n8n

Version:

n8n Workflow Automation Tool

443 lines • 19.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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiMemoryService = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const di_1 = require("@n8n/di"); const instance_ai_1 = require("@n8n/instance-ai"); const db_snapshot_storage_1 = require("./storage/db-snapshot-storage"); 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 durable_log_metrics_1 = require("./event-bus/durable-log-metrics"); const message_parser_1 = require("./message-parser"); const instance_ai_checkpoint_repository_1 = require("./repositories/instance-ai-checkpoint.repository"); const instance_ai_event_log_repository_1 = require("./repositories/instance-ai-event-log.repository"); const instance_ai_pending_confirmation_repository_1 = require("./repositories/instance-ai-pending-confirmation.repository"); const typeorm_agent_memory_1 = require("./storage/typeorm-agent-memory"); function isAgentMessageLike(value) { return (typeof value === 'object' && value !== null && typeof value.id === 'string' && 'role' in value); } function isRestorableMessage(value) { if (typeof value.id !== 'string' || value.id.length === 0) return false; if (value.type === 'custom') return typeof value.data === 'object' && value.data !== null; return typeof value.role === 'string' && Array.isArray(value.content); } function toRestorableMessage(value) { const rawCreatedAt = value.createdAt; const createdAt = rawCreatedAt instanceof Date ? rawCreatedAt : typeof rawCreatedAt === 'string' ? new Date(rawCreatedAt) : undefined; if (!createdAt || Number.isNaN(createdAt.getTime())) return undefined; const candidate = { ...value, createdAt }; return isRestorableMessage(candidate) ? candidate : undefined; } function messageCreatedAtMs(message) { const at = message.createdAt; if (at instanceof Date) return at.getTime(); const parsed = new Date(at).getTime(); return Number.isNaN(parsed) ? 0 : parsed; } function collectInFlightCheckpointMessages(checkpoints) { const merged = []; const seen = new Set(); for (const checkpoint of checkpoints) { const stateMessages = checkpoint.state?.messageList?.messages ?? []; for (const candidate of stateMessages) { if (!isAgentMessageLike(candidate) || seen.has(candidate.id)) continue; seen.add(candidate.id); merged.push({ ...candidate, createdAt: candidate.createdAt instanceof Date ? candidate.createdAt : new Date(candidate.createdAt), }); } } return merged; } function mergeMessagesById(stored, extras) { if (extras.length === 0) return stored; const byId = new Map(); for (const message of stored) byId.set(message.id, message); for (const message of extras) if (!byId.has(message.id)) byId.set(message.id, message); return [...byId.values()].sort((a, b) => messageCreatedAtMs(a) - messageCreatedAtMs(b)); } function collectUnfinishedRunIds(rows) { const unfinished = new Set(); for (const row of rows) { if (row.event.type === 'run-start') unfinished.add(row.runId); else if (row.event.type === 'run-finish') unfinished.delete(row.runId); } return unfinished; } function collectSuspendedHostRunIds(checkpoints) { const suspended = new Set(); for (const checkpoint of checkpoints) { if (checkpoint.state?.status === 'suspended' && checkpoint.hostRunId) { suspended.add(checkpoint.hostRunId); } } return suspended; } function buildLogDerivedSnapshots(rows, skipRunIds, skipGroupIds) { const groupKeyByRun = new Map(); for (const row of rows) { if (row.event.type === 'run-start') { const groupId = row.event.payload.messageGroupId; if (typeof groupId === 'string' && groupId) groupKeyByRun.set(row.runId, groupId); } } const skipGroupKeys = new Set(skipGroupIds); for (const runId of skipRunIds) { const groupId = groupKeyByRun.get(runId); if (groupId) skipGroupKeys.add(groupId); } const groups = new Map(); let skippedInFlight = false; for (const row of rows) { if (!row.runId) continue; const messageGroupId = groupKeyByRun.get(row.runId); const key = messageGroupId ?? row.runId; if (skipRunIds.has(row.runId) || skipGroupKeys.has(key)) { skippedInFlight = true; continue; } let group = groups.get(key); if (!group) { group = { runIds: [], events: [], messageGroupId, anchorAt: row.createdAt, lastAt: row.createdAt, }; groups.set(key, group); } if (!group.runIds.includes(row.runId)) group.runIds.push(row.runId); group.events.push(row.event); if (row.runId === group.runIds[0] && row.createdAt > group.anchorAt) { group.anchorAt = row.createdAt; } if (row.createdAt > group.lastAt) group.lastAt = row.createdAt; } const entries = []; for (const group of groups.values()) { const hasContent = group.events.some((e) => e.type !== 'run-start' && e.type !== 'run-finish'); if (!hasContent) continue; entries.push({ tree: (0, instance_ai_1.buildAgentTreeFromEvents)(group.events), runId: group.runIds[group.runIds.length - 1], messageGroupId: group.messageGroupId, runIds: group.runIds, createdAt: group.anchorAt, updatedAt: group.lastAt, }); } return { entries, skippedInFlight }; } let InstanceAiMemoryService = class InstanceAiMemoryService { constructor(logger, globalConfig, agentMemory, dbSnapshotStorage, checkpointRepository, pendingConfirmationRepository, eventLogRepository, durableLogMetrics) { this.logger = logger; this.agentMemory = agentMemory; this.dbSnapshotStorage = dbSnapshotStorage; this.checkpointRepository = checkpointRepository; this.pendingConfirmationRepository = pendingConfirmationRepository; this.eventLogRepository = eventLogRepository; this.durableLogMetrics = durableLogMetrics; this.instanceAiConfig = globalConfig.instanceAi; } async listThreads(userId, page = 0, perPage = 100) { const result = await this.agentMemory.listThreads({ filter: { resourceId: userId }, perPage, page, orderBy: { field: 'updatedAt', direction: 'DESC' }, }); return { threads: result.threads.map((t) => this.toThreadInfo(t)), total: result.total, page: result.page, hasMore: result.hasMore, }; } async ensureThread(userId, threadId, projectId, launchMetadata) { const existing = await this.agentMemory.getThread(threadId); if (existing) { if (existing.resourceId !== userId) { throw new Error(`Thread ${threadId} is not owned by user ${userId}`); } return { thread: this.toThreadInfo(existing), created: false, }; } const created = await this.agentMemory.saveThreadWithProject({ id: threadId, resourceId: userId, title: '', metadata: { source: launchMetadata.source, origin: launchMetadata.origin, ...(launchMetadata.sourceContext ? { sourceContext: launchMetadata.sourceContext } : {}), }, }, projectId); return { thread: this.toThreadInfo(created), created: true, }; } async restoreThreadMessages(userId, threadId, messages) { const restorable = []; for (const [index, raw] of messages.entries()) { const message = toRestorableMessage(raw); if (!message) { throw new bad_request_error_1.BadRequestError(`Seed message at index ${index} is not a valid agent message (id, createdAt, and role+content or type:custom+data are required)`); } restorable.push(message); } await this.agentMemory.saveMessages({ threadId, resourceId: userId, messages: restorable }); return { restored: restorable.length }; } async getThreadProjectId(threadId) { return (await this.agentMemory.getThreadProjectId(threadId)) ?? undefined; } async getThreadMessages(_userId, threadId, options) { const result = await this.agentMemory.listMessages({ threadId, limit: options?.limit ?? 50, page: options?.page ?? 0, }); return { threadId, messages: result.messages.map((m) => this.toThreadMessage(m)), }; } async getRichMessages(_userId, threadId, options) { const result = await this.agentMemory.listMessages({ threadId, limit: options?.limit ?? 50, page: options?.page ?? 0, }); const loadStoredSnapshots = async () => { let snapshots = await this.dbSnapshotStorage.getAll(threadId).catch((error) => { this.logger.warn('Failed to load agent tree snapshots', { threadId, error: error instanceof Error ? error.message : String(error), }); return []; }); if (options?.excludeRunIds?.length) { const excluded = new Set(options.excludeRunIds); snapshots = snapshots.filter((s) => !excluded.has(s.runId)); } return snapshots; }; const activeCheckpoints = await this.loadActiveCheckpoints(threadId); const snapshots = this.instanceAiConfig.durableLog ? await this.foldSnapshotsFromLog(threadId, loadStoredSnapshots, collectSuspendedHostRunIds(activeCheckpoints), options?.excludeRunIds, options?.excludeMessageGroupIds) : await loadStoredSnapshots(); const checkpointMessages = collectInFlightCheckpointMessages(activeCheckpoints); const storedMessages = mergeMessagesById(result.messages, checkpointMessages); const fallbacksBefore = message_parser_1.messageParserStats.fallbackActivations; const messages = (0, message_parser_1.parseStoredMessages)(storedMessages, snapshots); this.durableLogMetrics.notifyParserFallbacks(message_parser_1.messageParserStats.fallbackActivations - fallbacksBefore); await this.flagExpiredConfirmations(messages); const projectId = await this.agentMemory.getThreadProjectId(threadId); return { threadId, projectId: projectId ?? undefined, messages }; } async foldSnapshotsFromLog(threadId, loadStoredSnapshots, suspendedRunIds, excludeRunIds, excludeMessageGroupIds) { const start = Date.now(); let rows; try { rows = await this.eventLogRepository.getForThread(threadId); } catch (error) { this.logger.warn('Failed to read Instance AI event log for history', { threadId, error: error instanceof Error ? error.message : String(error), }); return await loadStoredSnapshots(); } if (rows.length === 0) return await loadStoredSnapshots(); const skipRunIds = new Set(excludeRunIds ?? []); for (const runId of collectUnfinishedRunIds(rows)) { if (!suspendedRunIds.has(runId)) skipRunIds.add(runId); } const { entries, skippedInFlight } = buildLogDerivedSnapshots(rows, skipRunIds, new Set(excludeMessageGroupIds ?? [])); if (entries.length === 0) { if (skippedInFlight) return []; return await loadStoredSnapshots(); } entries.sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0)); this.durableLogMetrics.recordFoldRead(Date.now() - start, entries.length); return entries; } async flagExpiredConfirmations(messages) { const requestIds = (0, message_parser_1.collectConfirmationRequestIds)(messages); if (requestIds.length === 0) return; try { const live = await this.pendingConfirmationRepository.findLiveRequestIds(requestIds, new Date()); (0, message_parser_1.markExpiredConfirmations)(messages, live); } catch (error) { this.logger.warn('Failed to flag expired confirmation cards', { error: error instanceof Error ? error.message : String(error), }); } } async loadActiveCheckpoints(threadId) { try { return await this.checkpointRepository.findActiveByThreadId(threadId); } catch (error) { this.logger.warn('Failed to load in-flight checkpoints', { threadId, error: error instanceof Error ? error.message : String(error), }); return []; } } async getLatestRunSnapshot(threadId, options) { return await this.dbSnapshotStorage.getLatest(threadId, options); } async validateThreadOwnership(userId, threadId) { return (await this.checkThreadOwnership(userId, threadId)) === 'owned'; } async checkThreadOwnership(userId, threadId) { const thread = await this.agentMemory.getThread(threadId); if (!thread) return 'not_found'; return thread.resourceId === userId ? 'owned' : 'other_user'; } async deleteThread(threadId) { await this.agentMemory.deleteThreadsByResourceIdPrefix((0, instance_ai_1.createSubAgentResourceIdPrefix)(threadId)); await this.agentMemory.deleteThread(threadId); } async deleteThreadsForUser(userId) { return await this.agentMemory.deleteThreadsByResourceId(userId); } async renameThread(threadId, title) { return await this.updateThread(threadId, { title }); } async updateThread(threadId, updates) { const updated = await (0, instance_ai_1.patchThread)(this.agentMemory, { threadId, update: ({ metadata }) => { const patch = { metadata: { ...metadata, ...updates.metadata }, }; if (updates.title !== undefined) { patch.title = updates.title; patch.metadata.titleRefined = true; } return patch; }, }); if (!updated) { throw new not_found_error_1.NotFoundError(`Thread ${threadId} not found`); } return this.toThreadInfo(updated); } async getThreadMetadata(userId, threadId) { const thread = await this.agentMemory.getThread(threadId); if (!thread || thread.resourceId !== userId) return undefined; return thread.metadata; } async cleanupExpiredThreads(onThreadDeleted) { const ttlDays = this.instanceAiConfig.threadTtlDays; if (!ttlDays || ttlDays <= 0) return 0; const cutoff = new Date(Date.now() - ttlDays * 24 * 60 * 60 * 1000); let deletedCount = 0; const perPage = 100; let hasMore = true; while (hasMore) { const result = await this.agentMemory.listThreads({ perPage, page: 0, orderBy: { field: 'updatedAt', direction: 'ASC' }, }); let deletedInPage = 0; for (const thread of result.threads) { if (thread.updatedAt < cutoff) { try { await onThreadDeleted?.(thread.id); await this.deleteThread(thread.id); deletedCount++; deletedInPage++; } catch (error) { this.logger.warn('Failed to delete expired thread', { threadId: thread.id, error: error instanceof Error ? error.message : String(error), }); } } } hasMore = deletedInPage > 0 && result.hasMore; } if (deletedCount > 0) { this.logger.info(`Cleaned up ${deletedCount} expired conversation threads (TTL: ${ttlDays} days)`); } return deletedCount; } toThreadInfo(thread) { return { id: thread.id, title: thread.title, resourceId: thread.resourceId, createdAt: thread.createdAt.toISOString(), updatedAt: thread.updatedAt.toISOString(), metadata: thread.metadata, }; } toThreadMessage(message) { return { id: message.id, role: 'role' in message ? message.role : 'custom', content: 'content' in message ? message.content : message.data, type: message.type, createdAt: message.createdAt.toISOString(), }; } }; exports.InstanceAiMemoryService = InstanceAiMemoryService; exports.InstanceAiMemoryService = InstanceAiMemoryService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, config_1.GlobalConfig, typeorm_agent_memory_1.TypeORMAgentMemory, db_snapshot_storage_1.DbSnapshotStorage, instance_ai_checkpoint_repository_1.InstanceAiCheckpointRepository, instance_ai_pending_confirmation_repository_1.InstanceAiPendingConfirmationRepository, instance_ai_event_log_repository_1.InstanceAiEventLogRepository, durable_log_metrics_1.DurableLogMetrics]) ], InstanceAiMemoryService); //# sourceMappingURL=instance-ai-memory.service.js.map