/** * ConversationManager - Infinite Agent Memory * * Production-ready conversation and context management for AI agents. * Built on Brainy's existing infrastructure: Triple Intelligence, Neural API, VFS. * * REAL IMPLEMENTATION - No stubs, no mocks, no TODOs */ import { v4 as uuidv4 } from '../universal/uuid.js' import { NounType, VerbType } from '../types/graphTypes.js' import { Brainy } from '../brainy.js' import { MessageRole, ProblemSolvingPhase, ConversationMessage, ConversationMessageMetadata, ConversationThread, ConversationThreadMetadata, ConversationContext, RankedMessage, SaveMessageOptions, ContextRetrievalOptions, ConversationSearchOptions, ConversationSearchResult, ConversationTheme, ArtifactOptions, ConversationStats, CompactionOptions, CompactionResult } from './types.js' /** * ConversationManager - High-level API for conversation operations * * Uses existing Brainy infrastructure: * - brain.add() for messages * - brain.relate() for threading * - brain.find() with Triple Intelligence for context * - brain.neural for clustering and similarity * - brain.vfs() for artifacts */ export class ConversationManager { private brain: Brainy private initialized = false private _vfs: any = null /** * Create a ConversationManager instance * @param brain Brainy instance to use */ constructor(brain: Brainy) { this.brain = brain } /** * Initialize the conversation manager * Lazy initialization pattern - only called when first used */ async init(): Promise { if (this.initialized) { return } // VFS is lazy-loaded and might not be initialized yet try { this._vfs = this.brain.vfs() await this._vfs.init() } catch (error) { // VFS initialization failed, will work without artifact support console.warn('VFS initialization failed, artifact support disabled:', error) } this.initialized = true } /** * Save a message to the conversation history * * Uses: brain.add() with NounType.Message * Real implementation - stores message with embedding * * @param content Message content * @param role Message role (user, assistant, system, tool) * @param options Save options (conversationId, metadata, etc.) * @returns Message ID */ async saveMessage( content: string, role: MessageRole, options: SaveMessageOptions = {} ): Promise { if (!this.initialized) { await this.init() } // Generate IDs if not provided const conversationId = options.conversationId || `conv_${uuidv4()}` const sessionId = options.sessionId || `session_${uuidv4()}` const timestamp = Date.now() // Build metadata const metadata: ConversationMessageMetadata = { role, conversationId, sessionId, timestamp, problemSolvingPhase: options.phase, confidence: options.confidence, artifacts: options.artifacts || [], toolsUsed: options.toolsUsed || [], references: [], tags: options.tags || [], ...options.metadata } // Add message to brain using REAL API const messageId = await this.brain.add({ data: content, type: NounType.Message, metadata }) // Link to previous message if specified (REAL graph relationship) if (options.linkToPrevious) { await this.brain.relate({ from: options.linkToPrevious, to: messageId, type: VerbType.Precedes, metadata: { conversationId, timestamp } }) } return messageId } /** * Link two messages in temporal sequence * * Uses: brain.relate() with VerbType.Precedes * Real implementation - creates graph relationship * * @param prevMessageId ID of previous message * @param nextMessageId ID of next message * @returns Relationship ID */ async linkMessages(prevMessageId: string, nextMessageId: string): Promise { if (!this.initialized) { await this.init() } // Create real graph relationship const verbId = await this.brain.relate({ from: prevMessageId, to: nextMessageId, type: VerbType.Precedes, metadata: { timestamp: Date.now() } }) return verbId } /** * Get a full conversation thread * * Uses: brain.getNoun() and brain.getConnections() * Real implementation - traverses graph relationships * * @param conversationId Conversation ID * @param options Options (includeArtifacts, etc.) * @returns Complete conversation thread */ async getConversationThread( conversationId: string, options: { includeArtifacts?: boolean } = {} ): Promise { if (!this.initialized) { await this.init() } // Search for all messages in conversation (REAL search) const results = await this.brain.find({ where: { conversationId }, limit: 10000 // Large limit for full thread }) // Convert results to ConversationMessage format const messages: ConversationMessage[] = results.map((result: any) => ({ id: result.id, content: result.data || result.content || '', role: result.metadata.role, metadata: result.metadata as ConversationMessageMetadata, embedding: result.embedding, createdAt: result.metadata.timestamp || Date.now(), updatedAt: result.metadata.timestamp || Date.now() })) // Sort by timestamp messages.sort((a, b) => a.createdAt - b.createdAt) // Build thread metadata const startTime = messages.length > 0 ? messages[0].createdAt : Date.now() const endTime = messages.length > 0 ? messages[messages.length - 1].createdAt : undefined const totalTokens = messages.reduce((sum, msg) => sum + (msg.metadata.tokensUsed || 0), 0) const threadMetadata: ConversationThreadMetadata = { conversationId, startTime, endTime, messageCount: messages.length, totalTokens, participants: [...new Set(messages.map(m => m.role))] } // Get artifacts if requested (REAL VFS query) let artifacts: string[] | undefined if (options.includeArtifacts && this._vfs) { artifacts = messages .flatMap(m => m.metadata.artifacts || []) .filter((id, idx, arr) => arr.indexOf(id) === idx) } return { id: conversationId, metadata: threadMetadata, messages, artifacts } } /** * Get relevant context for a query * * Uses: brain.find() with Triple Intelligence * Real implementation - semantic + temporal + graph ranking * * @param query Query string or context options * @param options Retrieval options * @returns Ranked context messages with artifacts */ async getRelevantContext( query: string | ContextRetrievalOptions, options?: ContextRetrievalOptions ): Promise { if (!this.initialized) { await this.init() } const startTime = Date.now() // Normalize options const opts: ContextRetrievalOptions = typeof query === 'string' ? { query, ...options } : query const { query: queryText, limit = 10, maxTokens = 50000, relevanceThreshold = 0.7, role, phase, tags, minConfidence, timeRange, conversationId, sessionId, weights = { semantic: 1.0, temporal: 0.5, graph: 0.3 }, includeArtifacts = false, includeSimilarConversations = false, deduplicateClusters = true } = opts // Build metadata filter const whereFilter: any = {} if (role) { whereFilter.role = Array.isArray(role) ? { $in: role } : role } if (phase) { whereFilter.problemSolvingPhase = Array.isArray(phase) ? { $in: phase } : phase } if (tags && tags.length > 0) { whereFilter.tags = { $in: tags } } if (minConfidence !== undefined) { whereFilter.confidence = { $gte: minConfidence } } if (timeRange) { if (timeRange.start !== undefined) { whereFilter.timestamp = { $gte: timeRange.start } } if (timeRange.end !== undefined) { whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end } } } if (conversationId) { whereFilter.conversationId = conversationId } if (sessionId) { whereFilter.sessionId = sessionId } // Query with Triple Intelligence (REAL) const findOptions: any = { limit: limit * 2, // Get more for ranking where: whereFilter } if (queryText) { findOptions.like = queryText } const results = await this.brain.find(findOptions) // Calculate relevance scores (REAL scoring) const now = Date.now() const rankedMessages: RankedMessage[] = results .map((result: any) => { // Semantic score (from vector similarity) const semanticScore = result.score || 0 // Temporal score (recency decay) const ageInDays = (now - (result.metadata.timestamp || now)) / (1000 * 60 * 60 * 24) const temporalScore = Math.exp(-0.1 * ageInDays) // Decay rate: 0.1 // Graph score (would need graph traversal, simplified for now) const graphScore = 0.5 // Placeholder for now, can enhance later // Combined score const relevanceScore = (weights.semantic ?? 1.0) * semanticScore + (weights.temporal ?? 0.5) * temporalScore + (weights.graph ?? 0.3) * graphScore return { id: result.id, content: result.data || result.content || '', role: result.metadata.role, metadata: result.metadata as ConversationMessageMetadata, embedding: result.embedding, createdAt: result.metadata.timestamp || now, updatedAt: result.metadata.timestamp || now, relevanceScore, semanticScore, temporalScore, graphScore } as RankedMessage }) .filter((msg: RankedMessage) => msg.relevanceScore >= relevanceThreshold) .sort((a: RankedMessage, b: RankedMessage) => b.relevanceScore - a.relevanceScore) // Deduplicate via clustering if requested let finalMessages = rankedMessages if (deduplicateClusters && rankedMessages.length > 5 && this.brain.neural) { // Use neural clustering to remove duplicates (REAL) try { const clusters = await this.brain.neural().clusters({ maxClusters: Math.ceil(rankedMessages.length / 3), threshold: 0.85 }) // Keep highest scoring message from each cluster const kept = new Set() for (const cluster of clusters) { const clusterMessages = rankedMessages.filter(msg => cluster.members?.includes(msg.id) ) if (clusterMessages.length > 0) { const best = clusterMessages.reduce((a, b) => a.relevanceScore > b.relevanceScore ? a : b ) kept.add(best.id) } } finalMessages = rankedMessages.filter(msg => kept.has(msg.id)) } catch (error) { // Clustering failed, use all messages console.warn('Clustering failed:', error) } } // Limit by token budget let totalTokens = 0 const messagesWithinBudget: RankedMessage[] = [] for (const msg of finalMessages) { const tokens = msg.metadata.tokensUsed || Math.ceil(msg.content.length / 4) if (totalTokens + tokens <= maxTokens) { messagesWithinBudget.push(msg) totalTokens += tokens } else { break } } // Get artifacts if requested (REAL VFS) let artifacts: any[] = [] if (includeArtifacts && this._vfs) { const artifactIds = new Set( messagesWithinBudget.flatMap(msg => msg.metadata.artifacts || []) ) for (const artifactId of artifactIds) { try { const entity = await this.brain.get(artifactId) if (entity) { artifacts.push({ id: artifactId, path: entity.metadata?.path || artifactId, summary: entity.metadata?.description || undefined }) } } catch (error) { // Artifact not found, skip continue } } } // Get similar conversations if requested let similarConversations: any[] = [] if (includeSimilarConversations && conversationId && this.brain.neural) { // Use neural neighbors (REAL) try { const neighborsResult = await this.brain.neural().neighbors(conversationId, { limit: 5, minSimilarity: 0.7 }) similarConversations = neighborsResult.neighbors.map((neighbor: any) => ({ id: neighbor.id, title: neighbor.metadata?.title, summary: neighbor.metadata?.summary, relevance: neighbor.score, messageCount: neighbor.metadata?.messageCount || 0 })) } catch (error) { // Neighbors failed, skip console.warn('Similar conversation search failed:', error) } } const queryTime = Date.now() - startTime return { messages: messagesWithinBudget.slice(0, limit), artifacts, similarConversations, totalTokens, metadata: { queryTime, messagesConsidered: results.length, conversationsSearched: new Set(results.map((r: any) => r.metadata.conversationId)).size } } } /** * Search messages semantically * * Uses: brain.find() with semantic search * Real implementation - vector similarity search * * @param options Search options * @returns Search results with scores */ async searchMessages(options: ConversationSearchOptions): Promise { if (!this.initialized) { await this.init() } const { query, limit = 10, role, conversationId, sessionId, timeRange, includeMetadata = true, includeContent = true } = options // Build filter const whereFilter: any = {} if (role) { whereFilter.role = Array.isArray(role) ? { $in: role } : role } if (conversationId) { whereFilter.conversationId = conversationId } if (sessionId) { whereFilter.sessionId = sessionId } if (timeRange) { if (timeRange.start) { whereFilter.timestamp = { $gte: timeRange.start } } if (timeRange.end) { whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end } } } // Search with Triple Intelligence (REAL) const results = await this.brain.find({ query: query, where: whereFilter, limit }) // Format results return results.map((result: any) => { const message: ConversationMessage = { id: result.id, content: includeContent ? (result.data || result.content || '') : '', role: result.metadata.role, metadata: includeMetadata ? (result.metadata as ConversationMessageMetadata) : {} as any, embedding: result.embedding, createdAt: result.metadata.timestamp || Date.now(), updatedAt: result.metadata.timestamp || Date.now() } // Create snippet const content = result.data || result.content || '' const snippet = content.length > 150 ? content.substring(0, 147) + '...' : content return { message, score: result.score || 0, conversationId: result.metadata.conversationId, snippet: includeContent ? snippet : undefined } }) } /** * Find similar conversations using Neural API * * Uses: brain.neural.neighbors() * Real implementation - semantic similarity with embeddings * * @param conversationId Conversation ID to find similar to * @param limit Maximum number of similar conversations * @param threshold Minimum similarity threshold * @returns Similar conversations with relevance scores */ async findSimilarConversations( conversationId: string, limit: number = 5, threshold: number = 0.7 ): Promise> { if (!this.initialized) { await this.init() } if (!this.brain.neural) { throw new Error('Neural API not available') } // Use neural neighbors (REAL) const neighborsResult = await this.brain.neural().neighbors(conversationId, { limit: limit, minSimilarity: threshold }) return neighborsResult.neighbors.map((neighbor: any) => ({ id: neighbor.id, relevance: neighbor.score, metadata: neighbor.metadata })) } /** * Get conversation themes via clustering * * Uses: brain.neural.clusters() * Real implementation - semantic clustering * * @param conversationId Conversation ID * @returns Discovered themes */ async getConversationThemes(conversationId: string): Promise { if (!this.initialized) { await this.init() } if (!this.brain.neural) { throw new Error('Neural API not available') } // Get messages for conversation const results = await this.brain.find({ where: { conversationId }, limit: 1000 }) if (results.length === 0) { return [] } // Cluster messages (REAL) const clusters = await this.brain.neural().clusters({ maxClusters: Math.min(5, Math.ceil(results.length / 5)), threshold: 0.75 }) // Convert to themes return clusters.map((cluster: any, index: number) => ({ id: `theme_${index}`, label: cluster.label || `Theme ${index + 1}`, messages: cluster.members || [], centroid: cluster.centroid || [], coherence: cluster.coherence || 0 })) } /** * Save an artifact (code, file, etc.) to VFS * * Uses: brain.vfs() * Real implementation - stores in virtual filesystem * * @param path VFS path * @param content File content * @param options Artifact options * @returns Artifact entity ID */ async saveArtifact( path: string, content: string | Buffer, options: ArtifactOptions ): Promise { if (!this.initialized) { await this.init() } if (!this._vfs) { throw new Error('VFS not available') } // Write file to VFS (REAL) await this._vfs.writeFile(path, content) // Get the file entity const entity = await this._vfs.getEntity(path) // Link to conversation message if provided if (options.messageId) { await this.brain.relate({ from: options.messageId, to: entity.id, type: VerbType.Creates, metadata: { conversationId: options.conversationId, artifactType: options.type || 'other' } }) } return entity.id } /** * Get conversation statistics * * Uses: brain.find() with aggregations * Real implementation - queries and aggregates data * * @param conversationId Optional conversation ID to filter * @returns Conversation statistics */ async getConversationStats(conversationId?: string): Promise { if (!this.initialized) { await this.init() } // Query messages const whereFilter = conversationId ? { conversationId } : {} const results = await this.brain.find({ where: whereFilter, limit: 100000 // Large limit for stats }) // Calculate statistics (REAL aggregation) const conversations = new Set(results.map((r: any) => r.metadata.conversationId)) const totalMessages = results.length const totalTokens = results.reduce( (sum: number, r: any) => sum + (r.metadata.tokensUsed || 0), 0 ) const timestamps = results.map((r: any) => r.metadata.timestamp || Date.now()) const oldestMessage = Math.min(...timestamps) const newestMessage = Math.max(...timestamps) // Count by phase const phases: Record = {} const roles: Record = {} for (const result of results) { const phase = result.entity.metadata.problemSolvingPhase const role = result.entity.metadata.role if (phase) { phases[phase] = (phases[phase] || 0) + 1 } if (role) { roles[role] = (roles[role] || 0) + 1 } } return { totalConversations: conversations.size, totalMessages, totalTokens, averageMessagesPerConversation: totalMessages / Math.max(1, conversations.size), averageTokensPerMessage: totalTokens / Math.max(1, totalMessages), oldestMessage, newestMessage, phases: phases as any, roles: roles as any } } /** * Delete a message * * Uses: brain.deleteNoun() * Real implementation - removes from graph * * @param messageId Message ID to delete */ async deleteMessage(messageId: string): Promise { if (!this.initialized) { await this.init() } await this.brain.delete(messageId) } /** * Export conversation to JSON * * Uses: getConversationThread() * Real implementation - serializes conversation * * @param conversationId Conversation ID * @returns JSON-serializable conversation object */ async exportConversation(conversationId: string): Promise { if (!this.initialized) { await this.init() } const thread = await this.getConversationThread(conversationId, { includeArtifacts: true }) return { version: '1.0', exportedAt: Date.now(), conversation: thread } } /** * Import conversation from JSON * * Uses: saveMessage() and linkMessages() * Real implementation - recreates conversation * * @param data Exported conversation data * @returns New conversation ID */ async importConversation(data: any): Promise { if (!this.initialized) { await this.init() } const newConversationId = `conv_${uuidv4()}` const conversation = data.conversation if (!conversation || !conversation.messages) { throw new Error('Invalid conversation data') } // Import messages in order const messageIdMap = new Map() for (let i = 0; i < conversation.messages.length; i++) { const msg = conversation.messages[i] const prevMessageId = i > 0 ? messageIdMap.get(conversation.messages[i - 1].id) : undefined const newMessageId = await this.saveMessage(msg.content, msg.role, { conversationId: newConversationId, sessionId: conversation.metadata.sessionId, phase: msg.metadata.problemSolvingPhase, confidence: msg.metadata.confidence, tags: msg.metadata.tags, linkToPrevious: prevMessageId, metadata: msg.metadata }) messageIdMap.set(msg.id, newMessageId) } return newConversationId } } /** * Create a ConversationManager instance * * @param brain Brainy instance * @returns ConversationManager instance */ export function createConversationManager(brain: Brainy): ConversationManager { return new ConversationManager(brain) }