UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

523 lines (522 loc) 20.6 kB
export class MessageHistory { messages; options; estimatedTokens = 0; microLog = []; extractedInfo = { entities: new Set(), decisions: [], todos: [], tools: [], }; constructor(initialMessages = [], options = {}) { this.messages = initialMessages.map(msg => ({ ...msg })); this.options = { maxMessages: options.maxMessages, maxTokens: options.maxTokens, preserveSystemMessages: options.preserveSystemMessages ?? true, compactToolCalls: options.compactToolCalls ?? true, compactionThreshold: options.compactionThreshold ?? 0.7, }; const firstSystemMsg = this.messages.find(m => m.type === 'message' && m.role === 'system'); if (firstSystemMsg) { firstSystemMsg.pinned = true; } this.updateTokenEstimate(); } async add(message) { const pinnableMsg = message; this.messages.push(pinnableMsg); this.addToMicroLog(pinnableMsg); this.extractInformation(pinnableMsg); this.updateTokenEstimate(); this.trim(); } async getMessages(model) { await this.checkAndCompact(model); this.ensureToolResultSequence(); return [...this.messages]; } pinMessage(index) { if (index >= 0 && index < this.messages.length) { this.messages[index].pinned = true; } } getMicroLog() { return [...this.microLog]; } getExtractedInfo() { return { entities: new Set(this.extractedInfo.entities), decisions: [...this.extractedInfo.decisions], todos: [...this.extractedInfo.todos], tools: [...this.extractedInfo.tools], }; } addToMicroLog(msg) { if (msg.type === 'message') { const content = this.getMessageContent(msg); const summary = this.createMicroLogSummary(msg.role, content); this.microLog.push({ timestamp: msg.timestamp || Date.now(), role: msg.role, summary, }); } else if (msg.type === 'function_call') { this.microLog.push({ timestamp: msg.timestamp || Date.now(), role: 'tool', summary: `Called ${msg.name}()`, }); } } createMicroLogSummary(role, content) { const firstLine = content.split('\n')[0].trim(); const maxLength = 80; if (firstLine.length <= maxLength) { return firstLine; } return firstLine.substring(0, maxLength - 3) + '...'; } extractInformation(msg) { if (msg.type === 'message') { const content = this.getMessageContent(msg); this.extractEntities(content); if (msg.role === 'assistant') { this.extractDecisions(content); this.extractTodos(content); } } else if (msg.type === 'function_call') { const existingTool = this.extractedInfo.tools.find(t => t.name === msg.name); if (!existingTool) { this.extractedInfo.tools.push({ name: msg.name, purpose: this.inferToolPurpose(msg.name, msg.arguments), }); } } } extractEntities(content) { const filePathRegex = /(?:\/[\w.-]+)+(?:\.\w+)?/g; const filePaths = content.match(filePathRegex) || []; filePaths.forEach(path => this.extractedInfo.entities.add(path)); const urlRegex = /https?:\/\/[^\s]+/g; const urls = content.match(urlRegex) || []; urls.forEach(url => this.extractedInfo.entities.add(url)); const quotedRegex = /["']([^"']+)["']/g; let match; while ((match = quotedRegex.exec(content)) !== null) { if (match[1].length > 3 && match[1].length < 50) { this.extractedInfo.entities.add(match[1]); } } } extractDecisions(content) { const decisionPatterns = [ /I (?:will|'ll|am going to) ([^.!?]+)[.!?]/gi, /(?:Decided|Choosing|Selected) to ([^.!?]+)[.!?]/gi, /The (?:solution|approach|strategy) is to ([^.!?]+)[.!?]/gi, ]; for (const pattern of decisionPatterns) { pattern.lastIndex = 0; let match; while ((match = pattern.exec(content)) !== null) { const decision = match[1].trim(); if (decision.length > 10 && decision.length < 200) { this.extractedInfo.decisions.push(decision); } } } } extractTodos(content) { const todoPatterns = [ /(?:TODO|FIXME|NOTE):\s*([^.!?\n]+)/g, /(?:Need to|Should|Must) ([^.!?]+)[.!?]/g, /(?:Next step|Then).*?(?:is to|will be) ([^.!?]+)[.!?]/g, ]; for (const pattern of todoPatterns) { let match; while ((match = pattern.exec(content)) !== null) { const todo = match[1].trim(); if (todo.length > 10 && todo.length < 200) { this.extractedInfo.todos.push(todo); } } } } inferToolPurpose(toolName, args) { try { JSON.parse(args); if (toolName.includes('read') || toolName.includes('get')) { return 'Information retrieval'; } else if (toolName.includes('write') || toolName.includes('create')) { return 'Content creation'; } else if (toolName.includes('search') || toolName.includes('find')) { return 'Search operation'; } else if (toolName.includes('execute') || toolName.includes('run')) { return 'Code execution'; } return 'General operation'; } catch { return 'General operation'; } } getMessageContent(msg) { if (msg.type === 'message' && 'content' in msg) { if (typeof msg.content === 'string') { return msg.content; } else if (Array.isArray(msg.content)) { return msg.content.map(item => ('text' in item ? item.text : '')).join(' '); } } return ''; } count() { return this.messages.length; } clear() { const systemMessages = this.options.preserveSystemMessages ? this.messages.filter(m => m.type === 'message' && m.role === 'system') : []; this.messages = systemMessages; } trim() { if (this.options.maxMessages && this.messages.length > this.options.maxMessages) { const systemMessages = this.options.preserveSystemMessages ? this.messages.filter(m => m.type === 'message' && m.role === 'system') : []; const nonSystemMessages = this.messages.filter(m => !(m.type === 'message' && m.role === 'system')); const trimmedMessages = nonSystemMessages.slice(-this.options.maxMessages); this.messages = [...systemMessages, ...trimmedMessages]; } if (this.options.compactToolCalls) { this.compactToolCalls(); } } compactToolCalls() { const compacted = []; let i = 0; while (i < this.messages.length) { const msg = this.messages[i]; if (msg.type === 'message' && msg.role === 'assistant') { compacted.push(msg); i++; const toolCalls = []; while (i < this.messages.length && (this.messages[i].type === 'function_call' || this.messages[i].type === 'function_call_output')) { toolCalls.push(this.messages[i]); i++; } compacted.push(...toolCalls); } else { compacted.push(msg); i++; } } this.messages = compacted; } getSummary() { const counts = { user: 0, assistant: 0, system: 0, toolCalls: 0, toolOutputs: 0, }; for (const msg of this.messages) { if (msg.type === 'message' && msg.role) { const role = msg.role; if (role in counts) { counts[role]++; } } else if (msg.type === 'function_call') { counts.toolCalls++; } else if (msg.type === 'function_call_output') { counts.toolOutputs++; } } return `Messages: ${this.messages.length} (User: ${counts.user}, Assistant: ${counts.assistant}, System: ${counts.system}, Tools: ${counts.toolCalls}/${counts.toolOutputs})`; } findLast(predicate) { for (let i = this.messages.length - 1; i >= 0; i--) { if (predicate(this.messages[i])) { return this.messages[i]; } } return undefined; } lastAssistantHadToolCalls() { let foundAssistant = false; for (let i = this.messages.length - 1; i >= 0; i--) { const msg = this.messages[i]; if (msg.type === 'function_call' && !foundAssistant) { for (let j = i - 1; j >= 0; j--) { const msg = this.messages[j]; if (msg.type === 'message' && msg.role === 'assistant') { return true; } if (msg.type === 'message' && msg.role === 'user') { break; } } } if (msg.type === 'message' && msg.role === 'assistant') { foundAssistant = true; } } return false; } estimateMessageTokens(msg) { let charCount = 0; if (msg.type === 'message' && 'content' in msg) { if (typeof msg.content === 'string') { charCount += msg.content.length; } else if (Array.isArray(msg.content)) { for (const item of msg.content) { if ('text' in item) { charCount += item.text.length; } } } } else if (msg.type === 'function_call') { charCount += msg.name.length + msg.arguments.length; } else if (msg.type === 'function_call_output') { charCount += msg.output.length; } return Math.ceil(charCount / 4); } updateTokenEstimate() { this.estimatedTokens = 0; for (const msg of this.messages) { this.estimatedTokens += this.estimateMessageTokens(msg); } } async checkAndCompact(modelId) { if (!modelId || this.options.compactionThreshold === 0) { return; } const { findModel } = await import('../data/model_data.js'); const model = findModel(modelId); if (!model || !model.features.context_length) { return; } const contextLength = model.features.context_length; const threshold = contextLength * this.options.compactionThreshold; if (this.estimatedTokens > threshold) { await this.performCompaction(contextLength); } } async performCompaction(contextLength) { await this.compactHistoryHybrid(contextLength); } async compactHistoryHybrid(contextLength) { const pinnedMessages = this.messages.filter(m => m.pinned); const unpinnedMessages = this.messages.filter(m => !m.pinned); if (unpinnedMessages.length < 4) { return; } const targetTokens = contextLength * 0.7; let currentTokens = 0; for (const msg of pinnedMessages) { currentTokens += this.estimateMessageTokens(msg); } const remainingBudget = targetTokens - currentTokens; const tailBudget = remainingBudget * 0.3; let tailTokens = 0; let tailStartIndex = unpinnedMessages.length; for (let i = unpinnedMessages.length - 1; i >= 0; i--) { const msgTokens = this.estimateMessageTokens(unpinnedMessages[i]); if (tailTokens + msgTokens > tailBudget) { tailStartIndex = i + 1; break; } tailTokens += msgTokens; } tailStartIndex = Math.max(0, Math.min(tailStartIndex, unpinnedMessages.length - 2)); const messagesToCompact = unpinnedMessages.slice(0, tailStartIndex); const tailMessages = unpinnedMessages.slice(tailStartIndex); if (messagesToCompact.length === 0) { return; } const hybridSummary = await this.createHybridSummary(messagesToCompact); const summaryMessage = { type: 'message', role: 'system', content: hybridSummary, pinned: false, }; this.messages = [...pinnedMessages, summaryMessage, ...tailMessages]; const recentTimestamp = tailMessages[0]?.timestamp || Date.now() - 3600000; this.microLog = this.microLog.filter(entry => (entry.timestamp || 0) >= recentTimestamp); this.updateTokenEstimate(); console.log(`MessageHistory: Compacted ${messagesToCompact.length} messages using hybrid approach. New token estimate: ${this.estimatedTokens}`); } async createHybridSummary(messages) { const sections = []; const microLogText = this.createMicroLogSection(messages); if (microLogText) { sections.push(`## Conversation Flow\n${microLogText}`); } const structuredInfo = this.createStructuredInfoSection(); if (structuredInfo) { sections.push(`## Key Information\n${structuredInfo}`); } const detailedSummary = await this.createDetailedSummary(messages); if (detailedSummary) { sections.push(`## Detailed Summary\n${detailedSummary}`); } return `[Previous Conversation Summary]\n\n${sections.join('\n\n')}`; } createMicroLogSection(messages) { const startTime = messages[0]?.timestamp || 0; const endTime = messages[messages.length - 1]?.timestamp || Date.now(); const relevantLogs = this.microLog.filter(entry => { const timestamp = entry.timestamp || 0; return timestamp >= startTime && timestamp <= endTime; }); if (relevantLogs.length === 0) { return ''; } return relevantLogs.map(entry => `- ${entry.role}: ${entry.summary}`).join('\n'); } createStructuredInfoSection() { const parts = []; if (this.extractedInfo.entities.size > 0) { const entities = Array.from(this.extractedInfo.entities).slice(-20); parts.push(`### Entities\n${entities.map(e => `- ${e}`).join('\n')}`); } if (this.extractedInfo.decisions.length > 0) { const decisions = this.extractedInfo.decisions.slice(-10); parts.push(`### Decisions\n${decisions.map(d => `- ${d}`).join('\n')}`); } if (this.extractedInfo.todos.length > 0) { const todos = this.extractedInfo.todos.slice(-10); parts.push(`### Pending Tasks\n${todos.map(t => `- ${t}`).join('\n')}`); } if (this.extractedInfo.tools.length > 0) { parts.push(`### Tools Used\n${this.extractedInfo.tools.map(t => `- ${t.name}: ${t.purpose}`).join('\n')}`); } return parts.join('\n\n'); } async createDetailedSummary(messages) { let conversationText = ''; let tokenCount = 0; const maxTokensForSummary = 2000; for (const msg of messages) { if (msg.type === 'message') { const content = this.getMessageContent(msg); const preview = content.substring(0, 500); const msgText = `${msg.role.toUpperCase()}: ${preview}${content.length > 500 ? '...' : ''}\n\n`; const msgTokens = this.estimateTextTokens(msgText); if (tokenCount + msgTokens > maxTokensForSummary) { break; } conversationText += msgText; tokenCount += msgTokens; } else if (msg.type === 'function_call') { const msgText = `TOOL: ${msg.name}()\n`; conversationText += msgText; tokenCount += this.estimateTextTokens(msgText); } } if (!conversationText.trim()) { return ''; } try { const { createSummary } = await import('./tool_result_processor.js'); const summaryPrompt = `Create a concise summary of this conversation, focusing on: 1. Main objectives and goals 2. Key decisions and outcomes 3. Current status and context 4. Any unresolved issues or next steps Keep the summary focused and relevant for continuing the conversation.`; return await createSummary(conversationText, summaryPrompt); } catch (error) { console.error('Error creating AI summary:', error); return ''; } } estimateTextTokens(text) { return Math.ceil(text.length / 4); } ensureToolResultSequence() { const reorderedMessages = []; let i = 0; while (i < this.messages.length) { const currentMsg = this.messages[i]; if (currentMsg.type === 'function_call') { reorderedMessages.push(currentMsg); const callId = currentMsg.call_id; let foundOutput = false; for (let j = i + 1; j < this.messages.length; j++) { const potentialOutput = this.messages[j]; if (potentialOutput.type === 'function_call_output' && potentialOutput.call_id === callId) { reorderedMessages.push(potentialOutput); this.messages.splice(j, 1); foundOutput = true; break; } } if (!foundOutput) { console.warn(`[MessageHistory] No matching output found for tool call ${callId}. Creating error output.`); const errorOutput = { type: 'function_call_output', call_id: callId, name: currentMsg.name, output: JSON.stringify({ error: 'Tool call did not complete or output was missing.', }), status: 'incomplete', model: currentMsg.model, }; reorderedMessages.push(errorOutput); } i++; } else if (currentMsg.type === 'function_call_output') { const callId = currentMsg.call_id; let hasMatchingCall = false; for (let j = reorderedMessages.length - 1; j >= 0; j--) { const msg = reorderedMessages[j]; if (msg.type === 'function_call' && msg.call_id === callId) { hasMatchingCall = true; break; } } if (!hasMatchingCall) { console.warn(`[MessageHistory] Found orphaned function_call_output with call_id ${callId}. Converting to regular message.`); const regularMessage = { type: 'message', role: 'user', content: `Tool result (${currentMsg.name || 'unknown_tool'}): ${currentMsg.output}`, status: 'completed', model: currentMsg.model, }; reorderedMessages.push(regularMessage); } i++; } else { reorderedMessages.push(currentMsg); i++; } } this.messages = reorderedMessages; } } //# sourceMappingURL=message_history.js.map