UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

450 lines (392 loc) 12.5 kB
/** * @claude-flow/browser - Memory Integration * Persistent memory storage with HNSW semantic search for browser patterns */ import type { BrowserTrajectory, BrowserTrajectoryStep, Snapshot, ActionResult } from '../domain/types.js'; // ============================================================================ // Memory Types // ============================================================================ export interface BrowserMemoryEntry { id: string; type: 'trajectory' | 'pattern' | 'snapshot' | 'session' | 'error'; key: string; value: Record<string, unknown>; metadata: { sessionId: string; url?: string; goal?: string; success?: boolean; duration?: number; timestamp: string; embedding?: number[]; }; } export interface MemorySearchResult { entry: BrowserMemoryEntry; score: number; distance: number; } export interface MemoryStats { totalEntries: number; byType: Record<string, number>; bySession: Record<string, number>; avgEmbeddingDim: number; indexSize: number; } // ============================================================================ // Memory Adapter Interface // ============================================================================ export interface IMemoryAdapter { store(entry: BrowserMemoryEntry): Promise<void>; retrieve(key: string): Promise<BrowserMemoryEntry | null>; search(query: string, options?: MemorySearchOptions): Promise<MemorySearchResult[]>; delete(key: string): Promise<boolean>; list(filter?: MemoryFilter): Promise<BrowserMemoryEntry[]>; getStats(): Promise<MemoryStats>; } export interface MemorySearchOptions { topK?: number; minScore?: number; type?: BrowserMemoryEntry['type']; sessionId?: string; namespace?: string; } export interface MemoryFilter { type?: BrowserMemoryEntry['type']; sessionId?: string; startTime?: string; endTime?: string; success?: boolean; } // ============================================================================ // Claude Flow Memory Adapter // ============================================================================ /** * Adapter for claude-flow memory system with HNSW indexing */ export class ClaudeFlowMemoryAdapter implements IMemoryAdapter { private namespace: string; private cache: Map<string, BrowserMemoryEntry> = new Map(); private embeddingCache: Map<string, number[]> = new Map(); constructor(namespace = 'browser') { this.namespace = namespace; } /** * Store a browser memory entry with optional embedding */ async store(entry: BrowserMemoryEntry): Promise<void> { const key = `${this.namespace}:${entry.type}:${entry.key}`; // Generate text for embedding const embeddingText = this.generateEmbeddingText(entry); // Store in memory via MCP (when available) try { // This would call claude-flow memory_store MCP tool // For now, store in local cache this.cache.set(key, { ...entry, metadata: { ...entry.metadata, timestamp: entry.metadata.timestamp || new Date().toISOString(), }, }); // Store embedding text for search if (embeddingText) { this.embeddingCache.set(key, this.simpleHash(embeddingText)); } } catch (error) { console.error('[memory] Failed to store entry:', error); throw error; } } /** * Retrieve a specific memory entry */ async retrieve(key: string): Promise<BrowserMemoryEntry | null> { const fullKey = key.includes(':') ? key : `${this.namespace}:${key}`; return this.cache.get(fullKey) || null; } /** * Semantic search using HNSW index (falls back to keyword search) */ async search(query: string, options: MemorySearchOptions = {}): Promise<MemorySearchResult[]> { const { topK = 10, minScore = 0.3, type, sessionId } = options; const results: MemorySearchResult[] = []; const queryTerms = query.toLowerCase().split(/\s+/); for (const [key, entry] of this.cache.entries()) { // Apply filters if (type && entry.type !== type) continue; if (sessionId && entry.metadata.sessionId !== sessionId) continue; // Calculate relevance score const entryText = this.generateEmbeddingText(entry).toLowerCase(); let matches = 0; for (const term of queryTerms) { if (entryText.includes(term)) matches++; } const score = matches / queryTerms.length; if (score >= minScore) { results.push({ entry, score, distance: 1 - score, }); } } // Sort by score and limit return results .sort((a, b) => b.score - a.score) .slice(0, topK); } /** * Delete a memory entry */ async delete(key: string): Promise<boolean> { const fullKey = key.includes(':') ? key : `${this.namespace}:${key}`; const deleted = this.cache.delete(fullKey); this.embeddingCache.delete(fullKey); return deleted; } /** * List entries with optional filters */ async list(filter: MemoryFilter = {}): Promise<BrowserMemoryEntry[]> { const entries: BrowserMemoryEntry[] = []; for (const entry of this.cache.values()) { if (filter.type && entry.type !== filter.type) continue; if (filter.sessionId && entry.metadata.sessionId !== filter.sessionId) continue; if (filter.success !== undefined && entry.metadata.success !== filter.success) continue; if (filter.startTime && entry.metadata.timestamp < filter.startTime) continue; if (filter.endTime && entry.metadata.timestamp > filter.endTime) continue; entries.push(entry); } return entries; } /** * Get memory statistics */ async getStats(): Promise<MemoryStats> { const byType: Record<string, number> = {}; const bySession: Record<string, number> = {}; for (const entry of this.cache.values()) { byType[entry.type] = (byType[entry.type] || 0) + 1; bySession[entry.metadata.sessionId] = (bySession[entry.metadata.sessionId] || 0) + 1; } return { totalEntries: this.cache.size, byType, bySession, avgEmbeddingDim: 0, // Would be calculated from actual embeddings indexSize: this.embeddingCache.size, }; } /** * Generate text for embedding from entry */ private generateEmbeddingText(entry: BrowserMemoryEntry): string { const parts: string[] = []; if (entry.metadata.goal) parts.push(entry.metadata.goal); if (entry.metadata.url) parts.push(entry.metadata.url); if (entry.type === 'trajectory') { const trajectory = entry.value as unknown as BrowserTrajectory; parts.push(trajectory.goal); trajectory.steps?.forEach((step) => { parts.push(`${step.action} ${JSON.stringify(step.input)}`); }); } if (entry.type === 'error') { parts.push(String(entry.value.message || '')); parts.push(String(entry.value.stack || '')); } return parts.join(' '); } /** * Simple hash for embedding placeholder (real implementation would use ONNX) */ private simpleHash(text: string): number[] { const hash: number[] = new Array(128).fill(0); for (let i = 0; i < text.length; i++) { hash[i % 128] += text.charCodeAt(i); } const max = Math.max(...hash); return hash.map((v) => v / max); } } // ============================================================================ // Browser Memory Manager // ============================================================================ /** * High-level memory manager for browser automation */ export class BrowserMemoryManager { private adapter: IMemoryAdapter; private sessionId: string; constructor(sessionId: string, adapter?: IMemoryAdapter) { this.sessionId = sessionId; this.adapter = adapter || new ClaudeFlowMemoryAdapter(); } /** * Store a completed trajectory */ async storeTrajectory(trajectory: BrowserTrajectory): Promise<void> { await this.adapter.store({ id: trajectory.id, type: 'trajectory', key: trajectory.id, value: trajectory as unknown as Record<string, unknown>, metadata: { sessionId: this.sessionId, url: trajectory.steps[0]?.input?.url as string, goal: trajectory.goal, success: trajectory.success, duration: this.calculateDuration(trajectory), timestamp: trajectory.completedAt || new Date().toISOString(), }, }); } /** * Store a learned pattern */ async storePattern( patternId: string, goal: string, steps: Array<{ action: string; selector?: string; value?: string }>, success: boolean ): Promise<void> { await this.adapter.store({ id: patternId, type: 'pattern', key: patternId, value: { goal, steps, success }, metadata: { sessionId: this.sessionId, goal, success, timestamp: new Date().toISOString(), }, }); } /** * Store a snapshot for later retrieval */ async storeSnapshot(snapshotId: string, snapshot: Snapshot): Promise<void> { await this.adapter.store({ id: snapshotId, type: 'snapshot', key: snapshotId, value: snapshot as unknown as Record<string, unknown>, metadata: { sessionId: this.sessionId, url: snapshot.url, timestamp: snapshot.timestamp, }, }); } /** * Store an error for learning */ async storeError( errorId: string, error: Error, context: { action?: string; selector?: string; url?: string } ): Promise<void> { await this.adapter.store({ id: errorId, type: 'error', key: errorId, value: { message: error.message, stack: error.stack, name: error.name, ...context, }, metadata: { sessionId: this.sessionId, url: context.url, success: false, timestamp: new Date().toISOString(), }, }); } /** * Find similar trajectories for a given goal */ async findSimilarTrajectories(goal: string, topK = 5): Promise<BrowserTrajectory[]> { const results = await this.adapter.search(goal, { topK, type: 'trajectory', minScore: 0.3, }); return results.map((r) => r.entry.value as unknown as BrowserTrajectory); } /** * Find patterns for a given goal */ async findPatterns(goal: string, successfulOnly = true): Promise<MemorySearchResult[]> { const results = await this.adapter.search(goal, { topK: 10, type: 'pattern', minScore: 0.2, }); if (successfulOnly) { return results.filter((r) => r.entry.metadata.success === true); } return results; } /** * Get session memory stats */ async getSessionStats(): Promise<{ trajectories: number; patterns: number; snapshots: number; errors: number; successRate: number; }> { const entries = await this.adapter.list({ sessionId: this.sessionId }); let trajectories = 0; let patterns = 0; let snapshots = 0; let errors = 0; let successCount = 0; for (const entry of entries) { switch (entry.type) { case 'trajectory': trajectories++; if (entry.metadata.success) successCount++; break; case 'pattern': patterns++; break; case 'snapshot': snapshots++; break; case 'error': errors++; break; } } return { trajectories, patterns, snapshots, errors, successRate: trajectories > 0 ? successCount / trajectories : 0, }; } private calculateDuration(trajectory: BrowserTrajectory): number { if (!trajectory.startedAt || !trajectory.completedAt) return 0; return new Date(trajectory.completedAt).getTime() - new Date(trajectory.startedAt).getTime(); } } // ============================================================================ // Singleton Factory // ============================================================================ let defaultAdapter: IMemoryAdapter | null = null; export function getMemoryAdapter(): IMemoryAdapter { if (!defaultAdapter) { defaultAdapter = new ClaudeFlowMemoryAdapter(); } return defaultAdapter; } export function createMemoryManager(sessionId: string): BrowserMemoryManager { return new BrowserMemoryManager(sessionId, getMemoryAdapter()); }