UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

769 lines (673 loc) • 24.7 kB
/** * Reflection Engine * * Purpose: Enable self-correcting agents through systematic reflection * and iterative improvement * * Key Features: * - Multi-criteria output analysis * - Self-correction with iterative improvement * - Quality scoring and threshold enforcement * - Issue identification and resolution * - Reflection history tracking * * @module ReflectionEngine * @version 1.0.0 * @date 2025-11-25 */ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); class ReflectionEngine { constructor(rootDir = process.cwd()) { this.rootDir = rootDir; this.configPath = path.join(rootDir, 'sf-core', 'config', 'reflection-engine.yaml'); this.reflectionsDir = path.join(rootDir, '.sf-agent', 'reflections'); // Default configuration this.config = { enabled: true, max_iterations: 3, improvement_threshold: 0.8, // 80% quality score required reflection_triggers: ['on-completion', 'on-error', 'on-user-request', 'on-low-quality'], // Reflection checklist categories reflection_checklist: { code_quality: { weight: 0.25, checks: [ 'follows-coding-standards', 'has-appropriate-comments', 'handles-edge-cases', 'includes-error-handling', 'no-code-smells', ], }, salesforce_best_practices: { weight: 0.3, checks: [ 'bulkified', 'within-governor-limits', 'secure-coding', 'test-coverage-adequate', 'uses-platform-features', ], }, architecture: { weight: 0.25, checks: [ 'scalable-design', 'maintainable', 'follows-patterns', 'properly-documented', 'loose-coupling', ], }, completeness: { weight: 0.2, checks: [ 'requirements-met', 'all-use-cases-covered', 'no-todos-remaining', 'deployment-ready', 'user-stories-complete', ], }, }, self_correction: { enabled: true, auto_apply: false, // Require user approval max_auto_iterations: 2, }, }; // Reflection state this.state = { currentReflection: null, history: [], }; } /** * Initialize reflection engine */ async initialize() { await fs.ensureDir(this.reflectionsDir); // Load custom configuration if exists if (await fs.pathExists(this.configPath)) { const customConfig = yaml.load(await fs.readFile(this.configPath, 'utf8')); this.config = this.deepMerge(this.config, customConfig); } console.log('āœ“ Reflection engine initialized'); return true; } /** * Main reflection entry point */ async reflect(agentOutput, options = {}) { console.log('\nšŸ” Starting reflection process...\n'); const reflection = { id: this.generateReflectionId(), timestamp: new Date().toISOString(), agent: options.agent || 'unknown', originalOutput: agentOutput, iterations: [], finalOutput: null, finalScore: 0, status: 'in-progress', }; this.state.currentReflection = reflection; try { // Run reflection iterations let currentOutput = agentOutput; let iteration = 0; while (iteration < this.config.max_iterations) { iteration++; console.log(`šŸ”„ Reflection iteration ${iteration}/${this.config.max_iterations}\n`); // Analyze current output const analysis = await this.analyzeOutput(currentOutput, options); reflection.iterations.push({ iteration, analysis, timestamp: new Date().toISOString(), }); console.log(` Overall score: ${(analysis.score * 100).toFixed(1)}%`); console.log(` Issues found: ${analysis.issues.length}\n`); // Check if quality threshold met if (analysis.score >= this.config.improvement_threshold) { console.log( `āœ… Quality threshold met (${(analysis.score * 100).toFixed(1)}% >= ${this.config.improvement_threshold * 100}%)\n` ); reflection.finalOutput = currentOutput; reflection.finalScore = analysis.score; reflection.status = 'success'; break; } // Check if max iterations reached if (iteration >= this.config.max_iterations) { console.log(`āš ļø Max iterations reached without meeting threshold\n`); reflection.finalOutput = currentOutput; reflection.finalScore = analysis.score; reflection.status = 'max-iterations'; break; } // Generate improvements const improvements = await this.suggestImprovements(analysis); console.log(` Generated ${improvements.length} improvement suggestions\n`); // Apply improvements (if auto-correction enabled and within limits) if ( this.config.self_correction.enabled && iteration <= this.config.self_correction.max_auto_iterations ) { currentOutput = await this.applyImprovements(currentOutput, improvements, analysis); console.log(` āœ“ Improvements applied automatically\n`); } else { // Return improvements for manual review reflection.finalOutput = currentOutput; reflection.finalScore = analysis.score; reflection.pendingImprovements = improvements; reflection.status = 'pending-approval'; break; } } // Save reflection history await this.saveReflection(reflection); this.state.history.push(reflection); return { success: reflection.status === 'success', reflection, output: reflection.finalOutput, score: reflection.finalScore, iterations: reflection.iterations.length, improvements: reflection.pendingImprovements, }; } catch (error) { console.error('āœ— Reflection failed:', error.message); reflection.status = 'error'; reflection.error = error.message; return { success: false, error: error.message, reflection, }; } } /** * Analyze output against reflection checklist */ async analyzeOutput(output, options = {}) { const results = {}; const issues = []; let totalScore = 0; let totalWeight = 0; // Run checks for each category for (const [category, config] of Object.entries(this.config.reflection_checklist)) { const categoryResult = await this.runCategoryChecks(output, category, config); results[category] = categoryResult; // Calculate weighted score totalScore += categoryResult.score * config.weight; totalWeight += config.weight; // Collect issues issues.push(...categoryResult.issues); } const overallScore = totalWeight > 0 ? totalScore / totalWeight : 0; return { score: overallScore, categoryResults: results, issues, timestamp: new Date().toISOString(), passesThreshold: overallScore >= this.config.improvement_threshold, }; } /** * Run checks for a category */ async runCategoryChecks(output, category, config) { const checkResults = []; let passedChecks = 0; for (const check of config.checks) { const result = await this.runSingleCheck(output, category, check); checkResults.push(result); if (result.passed) passedChecks++; } const categoryScore = config.checks.length > 0 ? passedChecks / config.checks.length : 0; const issues = checkResults .filter((r) => !r.passed) .map((r) => ({ category, check: r.check, severity: r.severity || 'medium', description: r.description, suggestion: r.suggestion, })); return { score: categoryScore, checks: checkResults, issues, passedChecks, totalChecks: config.checks.length, }; } /** * Run a single check */ async runSingleCheck(output, category, check) { // This would integrate with actual code analysis tools // For now, we'll implement pattern-based checks const result = { check, category, passed: false, confidence: 0, description: '', suggestion: '', }; // Code quality checks if (category === 'code_quality') { switch (check) { case 'follows-coding-standards': result.passed = this.checkCodingStandards(output); result.description = result.passed ? 'Code follows naming conventions and formatting standards' : 'Code does not follow standard conventions'; result.suggestion = 'Review naming conventions, indentation, and code organization'; break; case 'has-appropriate-comments': result.passed = this.checkComments(output); result.description = result.passed ? 'Code has adequate comments and documentation' : 'Code lacks sufficient comments'; result.suggestion = 'Add comments for complex logic and public methods'; break; case 'handles-edge-cases': result.passed = this.checkEdgeCases(output); result.description = result.passed ? 'Code handles edge cases and null checks' : 'Missing edge case handling'; result.suggestion = 'Add null checks and boundary condition handling'; break; case 'includes-error-handling': result.passed = this.checkErrorHandling(output); result.description = result.passed ? 'Proper error handling implemented' : 'Missing or inadequate error handling'; result.suggestion = 'Add try-catch blocks and meaningful error messages'; break; case 'no-code-smells': result.passed = this.checkCodeSmells(output); result.description = result.passed ? 'No major code smells detected' : 'Code smells detected (duplicated code, long methods, etc.)'; result.suggestion = 'Refactor to eliminate code smells'; break; } } // Salesforce best practices checks else if (category === 'salesforce_best_practices') { switch (check) { case 'bulkified': result.passed = this.checkBulkification(output); result.description = result.passed ? 'Code is properly bulkified for governor limits' : 'Code contains non-bulkified patterns'; result.suggestion = 'Process records in collections, not one at a time'; result.severity = 'high'; break; case 'within-governor-limits': result.passed = this.checkGovernorLimits(output); result.description = result.passed ? 'Code respects Salesforce governor limits' : 'Potential governor limit violations detected'; result.suggestion = 'Review SOQL queries in loops and DML operations'; result.severity = 'critical'; break; case 'secure-coding': result.passed = this.checkSecureCoding(output); result.description = result.passed ? 'Code follows security best practices' : 'Security concerns detected'; result.suggestion = 'Add WITH SECURITY_ENFORCED, validate user input'; result.severity = 'high'; break; case 'test-coverage-adequate': result.passed = this.checkTestCoverage(output); result.description = result.passed ? 'Test coverage appears adequate' : 'Insufficient test coverage'; result.suggestion = 'Add test classes with >75% coverage'; result.severity = 'high'; break; case 'uses-platform-features': result.passed = this.checkPlatformFeatures(output); result.description = result.passed ? 'Leverages Salesforce platform features appropriately' : 'Could better utilize platform features'; result.suggestion = 'Consider using platform features instead of custom code'; break; } } // Architecture checks else if (category === 'architecture') { switch (check) { case 'scalable-design': result.passed = this.checkScalability(output); result.description = result.passed ? 'Design appears scalable' : 'Design may not scale well'; result.suggestion = 'Review data model and query patterns for scale'; break; case 'maintainable': result.passed = this.checkMaintainability(output); result.description = result.passed ? 'Code is maintainable' : 'Code may be difficult to maintain'; result.suggestion = 'Simplify complex methods and improve readability'; break; case 'follows-patterns': result.passed = this.checkPatterns(output); result.description = result.passed ? 'Follows established patterns' : 'Does not follow standard patterns'; result.suggestion = 'Use design patterns like Trigger Handler, Service Layer'; break; case 'properly-documented': result.passed = this.checkDocumentation(output); result.description = result.passed ? 'Code is well documented' : 'Documentation is lacking'; result.suggestion = 'Add class/method documentation and inline comments'; break; case 'loose-coupling': result.passed = this.checkCoupling(output); result.description = result.passed ? 'Components are loosely coupled' : 'Tight coupling detected'; result.suggestion = 'Use interfaces and dependency injection'; break; } } // Completeness checks else if (category === 'completeness') { switch (check) { case 'requirements-met': result.passed = this.checkRequirements(output); result.description = result.passed ? 'All requirements appear to be met' : 'Some requirements may not be met'; result.suggestion = 'Review requirements checklist'; result.severity = 'high'; break; case 'all-use-cases-covered': result.passed = this.checkUseCases(output); result.description = result.passed ? 'Use cases are covered' : 'Some use cases may not be covered'; result.suggestion = 'Review use case scenarios'; break; case 'no-todos-remaining': result.passed = !this.checkTodos(output); result.description = result.passed ? 'No TODO items remaining' : 'TODO items found in code'; result.suggestion = 'Complete or remove TODO items'; break; case 'deployment-ready': result.passed = this.checkDeploymentReady(output); result.description = result.passed ? 'Code appears deployment ready' : 'Code may not be ready for deployment'; result.suggestion = 'Complete testing and documentation'; result.severity = 'high'; break; case 'user-stories-complete': result.passed = true; // Would check against user stories result.description = 'User stories status not verified'; result.suggestion = 'Verify all user stories are complete'; break; } } result.confidence = result.passed ? 0.8 : 0.7; return result; } // ======================================================================== // CHECK IMPLEMENTATIONS (Pattern-based heuristics) // ======================================================================== checkCodingStandards(output) { const content = this.extractContent(output); // Check for consistent naming, indentation, etc. const hasConsistentNaming = /^[A-Z][a-zA-Z0-9]*/.test(content); // Class names const hasIndentation = /\n {2,}/.test(content); return hasConsistentNaming && hasIndentation; } checkComments(output) { const content = this.extractContent(output); const commentRatio = (content.match(/\/\*|\*\/|\/\//g) || []).length / content.split('\n').length; return commentRatio > 0.1; // At least 10% lines with comments } checkEdgeCases(output) { const content = this.extractContent(output); const hasNullChecks = /if\s*\([^)]*!=\s*null|isEmpty\(/i.test(content); const hasBoundaryChecks = /if\s*\([^)]*[<>]=?/i.test(content); return hasNullChecks || hasBoundaryChecks; } checkErrorHandling(output) { const content = this.extractContent(output); const hasTryCatch = /try\s*{[\s\S]*catch\s*\(/i.test(content); return hasTryCatch || content.includes('throw '); } checkCodeSmells(output) { const content = this.extractContent(output); const lines = content.split('\n'); const longMethods = lines.filter((_, i) => { const methodStart = lines[i].match(/\b(?:public|private|protected)\s+\w+\s+\w+\s*\(/); if (!methodStart) return false; // Check if method is too long (>50 lines) let braceDepth = 0; for (let j = i; j < Math.min(i + 60, lines.length); j++) { braceDepth += (lines[j].match(/{/g) || []).length; braceDepth -= (lines[j].match(/}/g) || []).length; if (braceDepth === 0 && j > i + 50) return true; } return false; }); return longMethods.length === 0; } checkBulkification(output) { const content = this.extractContent(output); // Check for SOQL/DML in loops const hasSoqlInLoop = /for\s*\([^)]*\)\s*{[^}]*\[SELECT/i.test(content); const hasDmlInLoop = /for\s*\([^)]*\)\s*{[^}]*(insert|update|delete|upsert)\s+/i.test(content); return !hasSoqlInLoop && !hasDmlInLoop; } checkGovernorLimits(output) { const content = this.extractContent(output); // Check for common governor limit issues const hasMapForQueries = /Map<.*>\s+\w+\s*=\s*new\s+Map<.*>\(\[SELECT/i.test(content); const hasBulkDml = /\b(insert|update|delete|upsert)\s+\w+List/i.test(content); return hasMapForQueries || hasBulkDml || !content.includes('[SELECT'); } checkSecureCoding(output) { const content = this.extractContent(output); const hasSecurityEnforced = /WITH SECURITY_ENFORCED/i.test(content); const hasSharing = /with sharing|without sharing/i.test(content); return hasSecurityEnforced || hasSharing || !content.includes('[SELECT'); } checkTestCoverage(output) { const content = this.extractContent(output); const hasTestClass = /@isTest|testMethod/i.test(content); const hasAssertions = /System\.assert/i.test(content); return hasTestClass && hasAssertions; } checkPlatformFeatures(output) { const content = this.extractContent(output); // Check if using platform features vs custom solutions const usesPlatformFeatures = /Schema\.|Database\.|System\./i.test(content); return usesPlatformFeatures; } checkScalability(output) { const content = this.extractContent(output); // Check for scalable patterns const usesLimits = /Limits\./i.test(content); const usesBatching = /Database\.Batchable|Queueable/i.test(content); return usesLimits || usesBatching || content.length < 500; } checkMaintainability(output) { const content = this.extractContent(output); const avgLineLength = content.length / content.split('\n').length; const hasModularDesign = /class.*{[\s\S]*?public.*{/i.test(content); return avgLineLength < 80 && hasModularDesign; } checkPatterns(output) { const content = this.extractContent(output); // Check for common Salesforce patterns const hasTriggerHandler = /TriggerHandler|Handler/i.test(content); const hasServiceLayer = /Service\.cls|Service\s+class/i.test(content); return hasTriggerHandler || hasServiceLayer || content.length < 200; } checkDocumentation(output) { const content = this.extractContent(output); const hasClassDoc = /\/\*\*[\s\S]*?\*\/[\s\S]*?class/i.test(content); const hasMethodDoc = /\/\*\*[\s\S]*?\*\/[\s\S]*?(?:public|private|protected)/i.test(content); return hasClassDoc || hasMethodDoc; } checkCoupling(output) { const content = this.extractContent(output); // Check for tight coupling indicators const hasDirectInstantiation = /new\s+[A-Z]\w+\(/g.test(content); const instanceCount = (content.match(/new\s+[A-Z]\w+\(/g) || []).length; return instanceCount < 5; // Loose threshold } checkRequirements(output) { // Would check against actual requirements return true; } checkUseCases(output) { // Would check against use case documentation return true; } checkTodos(output) { const content = this.extractContent(output); return /TODO|FIXME|HACK/i.test(content); } checkDeploymentReady(output) { const content = this.extractContent(output); const hasTests = /@isTest/i.test(content); const hasNoTodos = !this.checkTodos(output); return (hasTests || content.length < 100) && hasNoTodos; } /** * Extract content from output (handle various formats) */ extractContent(output) { if (typeof output === 'string') return output; if (output.content) return output.content; if (output.code) return output.code; return JSON.stringify(output); } /** * Suggest improvements based on analysis */ async suggestImprovements(analysis) { const improvements = []; for (const issue of analysis.issues) { improvements.push({ category: issue.category, check: issue.check, severity: issue.severity, description: issue.description, suggestion: issue.suggestion, priority: this.calculatePriority(issue.severity), action: this.generateAction(issue), }); } // Sort by priority improvements.sort((a, b) => a.priority - b.priority); return improvements; } /** * Calculate priority (lower number = higher priority) */ calculatePriority(severity) { const priorities = { critical: 1, high: 2, medium: 3, low: 4, }; return priorities[severity] || 3; } /** * Generate action for improvement */ generateAction(issue) { return { type: 'refactor', target: issue.check, description: issue.suggestion, }; } /** * Apply improvements to output */ async applyImprovements(output, improvements, analysis) { // This would integrate with actual code modification tools // For now, return output with improvement notes let improvedOutput = this.extractContent(output); // Add improvement comments const improvementNotes = improvements .map((imp) => `// IMPROVEMENT NEEDED: ${imp.check} - ${imp.suggestion}`) .join('\n'); return { content: improvedOutput, improvements: improvementNotes, metadata: { originalScore: analysis.score, improvementsApplied: improvements.length, timestamp: new Date().toISOString(), }, }; } /** * Save reflection to history */ async saveReflection(reflection) { const reflectionPath = path.join(this.reflectionsDir, `${reflection.id}.json`); await fs.writeJson(reflectionPath, reflection, { spaces: 2 }); } /** * Get reflection history */ getHistory(limit = 10) { return this.state.history.slice(-limit); } /** * Generate reflection ID */ generateReflectionId() { return `reflection_${Date.now()}_${Math.floor(Math.random() * 1000)}`; } /** * Deep merge objects */ deepMerge(target, source) { const output = { ...target }; if (this.isObject(target) && this.isObject(source)) { Object.keys(source).forEach((key) => { if (this.isObject(source[key])) { if (!(key in target)) { output[key] = source[key]; } else { output[key] = this.deepMerge(target[key], source[key]); } } else { output[key] = source[key]; } }); } return output; } /** * Check if value is object */ isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } } module.exports = ReflectionEngine;