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

283 lines (240 loc) • 8.48 kB
/** * @claude-flow/browser - ReasoningBank Integration * Connects browser trajectories to agentic-flow's learning system */ import type { BrowserTrajectory, BrowserTrajectoryStep, Snapshot } from '../domain/types.js'; // ============================================================================ // ReasoningBank Pattern Types // ============================================================================ export interface BrowserPattern { id: string; type: 'navigation' | 'interaction' | 'extraction' | 'form' | 'auth' | 'test'; goal: string; steps: PatternStep[]; successRate: number; avgDuration: number; lastUsed: string; usageCount: number; embedding?: number[]; } export interface PatternStep { action: string; selector?: string; value?: string; condition?: string; } // ============================================================================ // ReasoningBank Adapter // ============================================================================ export class ReasoningBankAdapter { private patterns: Map<string, BrowserPattern> = new Map(); private trajectoryBuffer: BrowserTrajectory[] = []; private readonly maxBufferSize = 100; /** * Store a completed trajectory for learning */ async storeTrajectory(trajectory: BrowserTrajectory): Promise<void> { this.trajectoryBuffer.push(trajectory); // Process buffer when full if (this.trajectoryBuffer.length >= this.maxBufferSize) { await this.processBuffer(); } // Extract pattern from successful trajectories if (trajectory.success) { const pattern = this.extractPattern(trajectory); if (pattern) { this.patterns.set(pattern.id, pattern); } } } /** * Extract a reusable pattern from a trajectory */ private extractPattern(trajectory: BrowserTrajectory): BrowserPattern | null { if (trajectory.steps.length < 2) return null; const patternId = this.generatePatternId(trajectory.goal); const existing = this.patterns.get(patternId); const steps: PatternStep[] = trajectory.steps.map(step => ({ action: step.action, selector: this.normalizeSelector(step.input.target as string), value: step.input.value as string, condition: step.input.waitUntil as string || step.input.text as string, })); const avgDuration = trajectory.steps.reduce((sum, s) => sum + (s.result.duration || 0), 0) / trajectory.steps.length; if (existing) { // Update existing pattern return { ...existing, successRate: (existing.successRate * existing.usageCount + 1) / (existing.usageCount + 1), avgDuration: (existing.avgDuration * existing.usageCount + avgDuration) / (existing.usageCount + 1), lastUsed: new Date().toISOString(), usageCount: existing.usageCount + 1, }; } return { id: patternId, type: this.inferPatternType(trajectory), goal: trajectory.goal, steps, successRate: 1, avgDuration, lastUsed: new Date().toISOString(), usageCount: 1, }; } /** * Infer pattern type from trajectory */ private inferPatternType(trajectory: BrowserTrajectory): BrowserPattern['type'] { const actions = trajectory.steps.map(s => s.action); const goal = trajectory.goal.toLowerCase(); if (goal.includes('login') || goal.includes('auth') || actions.includes('state-save')) { return 'auth'; } if (goal.includes('test') || goal.includes('verify') || goal.includes('assert')) { return 'test'; } if (goal.includes('extract') || goal.includes('scrape') || actions.filter(a => a === 'getText').length > 3) { return 'extraction'; } if (goal.includes('form') || goal.includes('submit') || actions.filter(a => a === 'fill').length > 2) { return 'form'; } if (actions.filter(a => a === 'click').length > 3) { return 'interaction'; } return 'navigation'; } /** * Normalize selector for pattern matching */ private normalizeSelector(selector?: string): string | undefined { if (!selector) return undefined; // Keep refs as-is (they're from snapshots) if (selector.startsWith('@e')) { return '{ref}'; // Placeholder for any ref } // Keep semantic locators if (selector.startsWith('text=') || selector.startsWith('role=')) { return selector; } // Generalize CSS selectors return selector .replace(/\[data-testid="[^"]+"\]/g, '[data-testid="{testid}"]') .replace(/#\w+/g, '#{id}') .replace(/\.\w+/g, '.{class}'); } /** * Generate pattern ID from goal */ private generatePatternId(goal: string): string { return `pattern-${goal .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .slice(0, 50)}-${Date.now().toString(36)}`; } /** * Process buffered trajectories (batch learning) */ private async processBuffer(): Promise<void> { const successful = this.trajectoryBuffer.filter(t => t.success); const failed = this.trajectoryBuffer.filter(t => !t.success); // Learn from failures for (const failure of failed) { await this.analyzeFailure(failure); } // Clear buffer this.trajectoryBuffer = []; } /** * Analyze a failed trajectory to learn what went wrong */ private async analyzeFailure(trajectory: BrowserTrajectory): Promise<void> { const failedStep = trajectory.steps.find(s => !s.result.success); if (!failedStep) return; // Store failure pattern for avoidance const failureId = `failure-${failedStep.action}-${Date.now().toString(36)}`; console.log(`[ReasoningBank] Learned from failure: ${failureId} - ${failedStep.result.error}`); } /** * Find similar patterns for a goal */ async findSimilarPatterns(goal: string, limit = 5): Promise<BrowserPattern[]> { const patterns = Array.from(this.patterns.values()); // Simple text similarity for now // In production, use HNSW with embeddings const scored = patterns.map(p => ({ pattern: p, score: this.textSimilarity(goal.toLowerCase(), p.goal.toLowerCase()), })); return scored .filter(s => s.score > 0.3) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(s => s.pattern); } /** * Simple text similarity */ private textSimilarity(a: string, b: string): number { const wordsA = new Set(a.split(/\s+/)); const wordsB = new Set(b.split(/\s+/)); const intersection = new Set([...wordsA].filter(w => wordsB.has(w))); const union = new Set([...wordsA, ...wordsB]); return intersection.size / union.size; } /** * Get recommended steps for a goal */ async getRecommendedSteps(goal: string): Promise<PatternStep[]> { const similar = await this.findSimilarPatterns(goal, 1); if (similar.length === 0) return []; return similar[0].steps; } /** * Record verdict for SONA learning */ async recordVerdict(trajectoryId: string, success: boolean, feedback?: string): Promise<void> { // In production, this would update SONA weights console.log(`[ReasoningBank] Verdict for ${trajectoryId}: ${success ? 'SUCCESS' : 'FAILURE'}${feedback ? ` - ${feedback}` : ''}`); } /** * Get pattern stats */ getStats(): { totalPatterns: number; avgSuccessRate: number; bufferedTrajectories: number } { const patterns = Array.from(this.patterns.values()); const avgSuccessRate = patterns.length > 0 ? patterns.reduce((sum, p) => sum + p.successRate, 0) / patterns.length : 0; return { totalPatterns: patterns.length, avgSuccessRate, bufferedTrajectories: this.trajectoryBuffer.length, }; } /** * Export patterns for persistence */ exportPatterns(): BrowserPattern[] { return Array.from(this.patterns.values()); } /** * Import patterns from storage */ importPatterns(patterns: BrowserPattern[]): void { for (const pattern of patterns) { this.patterns.set(pattern.id, pattern); } } } // ============================================================================ // Singleton Instance // ============================================================================ let instance: ReasoningBankAdapter | null = null; export function getReasoningBank(): ReasoningBankAdapter { if (!instance) { instance = new ReasoningBankAdapter(); } return instance; } export default ReasoningBankAdapter;