diff options
Diffstat (limited to 'modules/core/module-predictor.js')
| -rw-r--r-- | modules/core/module-predictor.js | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/modules/core/module-predictor.js b/modules/core/module-predictor.js new file mode 100644 index 0000000..312325b --- /dev/null +++ b/modules/core/module-predictor.js @@ -0,0 +1,279 @@ +// module-predictor.js — Predictive resource allocation engine +// Analyzes historical telemetry + memory graph data to predict task duration/turns +// Commands: predict, predictor-status, predictor-config +class PredictorModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._dataPoints = []; + this._predictions = 0; + this._accuracy = { correct: 0, total: 0 }; + this._CONFIDENCE_THRESHOLD = 0.5; + this._MIN_DATA_POINTS = 5; + this._loaded = false; + } + + getHelpInfo() { + return { + 'predict': 'predict <task description> — Predict task duration, tool turns, and confidence', + 'predictor-status': 'predictor-status — Model accuracy, data points, prediction count', + 'predictor-config': 'predictor-config <key> <value> — Tune: confidence-threshold, min-data-points', + }; + } + + registerCommands() { + this.commands['predict'] = (args) => this.predict(args.join(' ')); + this.commands['predictor-status'] = () => this.showStatus(); + this.commands['predictor-config'] = (args) => this.setConfig(args); + } + + init(terminal) { + this.terminal = terminal; + this._loadFromVFS(); + this._loaded = true; + this.terminal.print('📊 Predictor loaded — analyzing task patterns', 'info'); + } + + // ─── Prediction Engine ─── + + async predict(task) { + if (!task) return this.terminal.print('Usage: predict <task description>', 'error'); + + const startTime = performance.now(); + + // Extract features + const features = this._extractFeatures(task); + + // Find similar historical tasks + const similar = this._findSimilar(features); + + // Generate prediction + const prediction = this._computePrediction(similar, features); + + // Record this prediction for accuracy tracking + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + + this._predictions++; + this._dataPoints.push({ + features, + predictedDuration: prediction.duration, + predictedTurns: prediction.toolTurns, + confidence: prediction.confidence, + similarCount: similar.length, + timestamp: new Date().toISOString() + }); + + this._debounceSave(); + + // Display result + this.terminal.print('📈 Task Prediction', 'header'); + this.terminal.print(` Task: ${task.substring(0, 50)}${task.length > 50 ? '...' : ''}`, 'info'); + + if (prediction.confidence >= this._CONFIDENCE_THRESHOLD) { + this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s`, 'info'); + this.terminal.print(` 🔧 Estimated tool turns: ${prediction.toolTurns}`, 'info'); + this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% (based on ${similar.length} similar tasks)`, 'success'); + } else if (similar.length > 0) { + this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s (low confidence)`, 'warning'); + this.terminal.print(` 🔧 Tool turns: ~${prediction.toolTurns}`, 'warning'); + this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% — only ${similar.length} similar data point(s)`, 'warning'); + this.terminal.print(' Tip: More task data will improve predictions.', 'info'); + } else { + this.terminal.print(' ❓ No historical data to base prediction on.', 'warning'); + this.terminal.print(' Tip: Run `orchestrate` with various tasks to build a baseline.', 'info'); + // Provide hardcoded defaults + this.terminal.print(' Default estimate: ~30s duration, ~5 tool turns', 'info'); + } + + // If got memory graph, offer recommendations + if (prediction.confidence >= this._CONFIDENCE_THRESHOLD && prediction.duration > 30) { + this.terminal.print(` 💡 Recommendation: Set TTL to ${Math.ceil(prediction.duration * 1.5)}s for predictable completion`, 'info'); + } + + return prediction; + } + + _extractFeatures(task) { + const lower = task.toLowerCase(); + return { + length: task.length, + wordCount: task.split(/\s+/).length, + hasSearch: /\b(search|find|look)\b/.test(lower), + hasWrite: /\b(write|create|save|edit)\b/.test(lower), + hasDelete: /\b(delete|remove|rm)\b/.test(lower), + hasInstall: /\b(install|wpm|package)\b/.test(lower), + hasRead: /\b(read|cat|view|list)\b/.test(lower), + hasWeb: /\b(fetch|url|http|web)\b/.test(lower), + hasOrchestrate: /\b(orchestrat|delegate|agent)\b/.test(lower), + hasModule: /\b(module|edit_module|view_module)\b/.test(lower), + complexity: Math.min(task.length / 50, 10), // 0-10 scale + keywords: [...new Set(lower.split(/\s+/).filter(w => w.length > 3))].slice(0, 10) + }; + } + + _findSimilar(features) { + return this._dataPoints.filter(dp => { + // Skip predictions (only use actual executions with observed results) + if (!dp.observedDuration) return false; + const f = dp.features; + if (!f) return false; + // Feature similarity scoring + let score = 0; + const matches = [ + features.hasSearch === f.hasSearch, + features.hasWrite === f.hasWrite, + features.hasDelete === f.hasDelete, + features.hasInstall === f.hasInstall, + features.hasRead === f.hasRead, + features.hasWeb === f.hasWeb, + features.hasOrchestrate === f.hasOrchestrate, + features.hasModule === f.hasModule, + ]; + score = matches.filter(Boolean).length / matches.length; + // Keyword overlap bonus + const kOverlap = features.keywords.filter(k => f.keywords?.includes(k)).length; + const kScore = kOverlap / Math.max(features.keywords.length, 1); + score = score * 0.6 + kScore * 0.4; + return score > 0.3; + }); + } + + _computePrediction(similar, features) { + if (similar.length === 0) { + // Hardcoded defaults + let baseDuration = 15; + let baseTurns = 3; + if (features.hasSearch || features.hasWeb) { baseDuration = 25; baseTurns = 5; } + if (features.hasWrite) { baseDuration = 20; baseTurns = 4; } + if (features.hasOrchestrate) { baseDuration = 40; baseTurns = 8; } + if (features.complexity > 5) baseDuration *= 1.5; + return { + duration: baseDuration, + toolTurns: baseTurns, + confidence: 0.1, + similarCount: 0 + }; + } + + const avgDuration = similar.reduce((s, dp) => s + (dp.observedDuration || 30), 0) / similar.length; + const avgTurns = similar.reduce((s, dp) => s + (dp.observedTurns || 3), 0) / similar.length; + const confidence = Math.min(similar.length / 20, 1) * 0.8 + 0.1; + + // Apply task complexity multiplier + const complexityMultiplier = 1 + (features.complexity - 3) * 0.1; + const adjustedDuration = Math.round(avgDuration * Math.max(complexityMultiplier, 0.5)); + + return { + duration: adjustedDuration, + toolTurns: Math.round(avgTurns), + confidence: Math.min(confidence, 0.95), + similarCount: similar.length + }; + } + + // ─── Learning from Results ─── + + recordResult(features, duration, toolTurns, success) { + this._dataPoints.push({ + features, + observedDuration: duration, + observedTurns: toolTurns, + success, + timestamp: new Date().toISOString() + }); + this._accuracy.total++; + // Compare with last prediction for this feature set + const lastPrediction = [...this._dataPoints] + .reverse() + .find(dp => dp.predictedDuration && dp.features?.hasSearch === features.hasSearch); + if (lastPrediction) { + const error = Math.abs(lastPrediction.predictedDuration - duration) / duration; + if (error < 0.3) this._accuracy.correct++; + } + this._debounceSave(); + } + + // ─── Commands ─── + + showStatus() { + const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration).length; + const accPct = this._accuracy.total > 0 + ? ((this._accuracy.correct / this._accuracy.total) * 100).toFixed(0) + : 'N/A'; + + this.terminal.print('📊 Predictor Status', 'header'); + this.terminal.print(` Total data points: ${this._dataPoints.length}`, 'info'); + this.terminal.print(` Actual executions: ${actualExecutions}`, 'info'); + this.terminal.print(` Predictions made: ${this._predictions}`, 'info'); + this.terminal.print(` Accuracy: ${accPct}% (${this._accuracy.correct}/${this._accuracy.total})`, 'info'); + this.terminal.print(` Confidence threshold: ${this._CONFIDENCE_THRESHOLD}`, 'info'); + this.terminal.print(` Min data points: ${this._MIN_DATA_POINTS}`, 'info'); + } + + setConfig(args) { + if (args.length < 2) return this.terminal.print('Usage: predictor-config <key> <value>', 'error'); + const key = args[0]; + const val = args[1]; + + const configMap = { + 'confidence-threshold': () => { + const n = parseFloat(val); + if (isNaN(n) || n < 0 || n > 1) return 'Must be 0.0-1.0'; + this._CONFIDENCE_THRESHOLD = n; + return `confidence-threshold set to ${n}`; + }, + 'min-data-points': () => { + const n = parseInt(val); + if (isNaN(n) || n < 1) return 'Must be >= 1'; + this._MIN_DATA_POINTS = n; + return `min-data-points set to ${n}`; + }, + }; + + if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); + const msg = configMap[key](); + this.terminal.print(`✅ ${msg}`, 'success'); + } + + // ─── Persistence ─── + + _loadFromVFS() { + try { + const fs = this.terminal.fs; + if (!fs) return; + const data = fs.fsReadJSON('/system/metrics/predictor.json'); + if (data) { + this._dataPoints = data.dataPoints || []; + this._predictions = data.predictions || 0; + this._accuracy = data.accuracy || { correct: 0, total: 0 }; + } + } catch (e) { + console.warn('[Predictor] Load error (fresh start):', e.message); + } + } + + _saveNow() { + try { + const fs = this.terminal.fs; + if (!fs) return; + if (!fs.readDir('/system/metrics')) fs.mkdirRecursive('/system/metrics'); + fs.fsWriteJSON('/system/metrics/predictor.json', { + dataPoints: this._dataPoints.slice(-500), // keep last 500 + predictions: this._predictions, + accuracy: this._accuracy + }); + } catch (e) { + console.error('[Predictor] Save error:', e.message); + } + } + + _debounceSave() { + clearTimeout(this._saveTimer); + this._saveTimer = setTimeout(() => this._saveNow(), 3000); + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.PredictorModule = PredictorModule; +}
\ No newline at end of file |