n8n-nodes-better-ai-agent
Version:
A better AI Agent node for n8n with improved memory management and modern AI SDK integration
102 lines (101 loc) • 4.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChatArrayMemory = void 0;
const messages_1 = require("@langchain/core/messages");
class ChatArrayMemory {
memory;
maxMessages;
constructor(memory, // BufferMemory or BufferWindowMemory
/**
* Optional hard limit on the number of messages to keep. If provided, the
* stored conversation will be truncated to the *last* `maxMessages` items
* before persisting so that upstream BufferWindowMemory `k` setting is
* respected.
*/
maxMessages = null) {
this.memory = memory;
this.maxMessages = maxMessages;
}
/**
* Load the stored chat array by concatenating every JSON array that was
* persisted on previous turns. This keeps the database lean (one delta per
* turn) while still letting us reconstruct the full conversation for the
* next prompt. We apply an optional `maxMessages` window **after**
* reconstruction so the caller controls how much context is sent to the
* model without losing older history in the DB.
*/
async load() {
// Trigger potential BufferMemory wrappers so n8n logs the access
try {
if (typeof this.memory.loadMemoryVariables === 'function') {
await this.memory.loadMemoryVariables({ input: '__load__' });
}
}
catch { }
if (!this.memory.chatHistory || !this.memory.chatHistory.getMessages) {
return [];
}
const allStored = await this.memory.chatHistory.getMessages();
const combined = [];
for (const msg of allStored) {
if (msg._getType?.() !== 'ai')
continue;
const text = msg.content;
if (typeof text !== 'string')
continue;
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) {
combined.push(...parsed);
}
}
catch {
// ignore parse errors – could be non-JSON messages
}
}
// Apply runtime window if specified
let windowed = combined;
if (this.maxMessages !== null && this.maxMessages > 0 && combined.length > this.maxMessages) {
windowed = combined.slice(-this.maxMessages);
}
// Remove problematic messages at the very start of the window – those are the
// only ones that can become "orphaned" when we cut off history.
let startIndex = 0;
while (startIndex < windowed.length) {
const msg = windowed[startIndex];
// 1. Orphan tool result (first message is `tool` with no preceding assistant)
if (msg.role === 'tool') {
if (globalThis.__BAA_VERBOSE)
console.log('⚠️ Dropping leading orphan tool message');
startIndex += 1;
continue;
}
// 2. Assistant message that *only* contains tool-calls with no following tool
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
const hasToolCalls = msg.content.some(p => p.type === 'tool-call');
if (hasToolCalls) {
if (globalThis.__BAA_VERBOSE)
console.log('⚠️ Dropping leading assistant message with tool-call but no response');
startIndex += 1;
continue;
}
}
// First message is safe – stop trimming
break;
}
return windowed.slice(startIndex);
}
/**
* Persist the messages produced in the *current* run (delta). We expect the
* caller to pass **only** the new messages, so we just insert them as a JSON
* array. Nothing is truncated – historical context remains intact in the DB.
*/
async save(messages) {
if (!messages || messages.length === 0)
return;
if (this.memory.chatHistory && this.memory.chatHistory.addMessage) {
await this.memory.chatHistory.addMessage(new messages_1.AIMessage(JSON.stringify(messages)));
}
}
}
exports.ChatArrayMemory = ChatArrayMemory;