442 lines
17 KiB
JavaScript
442 lines
17 KiB
JavaScript
/**
|
|
* RuvLLM Orchestrator - Self-Learning Multi-Agent Orchestration
|
|
*
|
|
* Integrates:
|
|
* - TRM (Tiny Recursive Models) for multi-step reasoning
|
|
* - SONA (Self-Optimizing Neural Architecture) for adaptive learning
|
|
* - FastGRNN routing for intelligent agent selection
|
|
* - ReasoningBank for pattern storage and retrieval
|
|
*
|
|
* Performance:
|
|
* - 2-4x faster inference than standard transformers
|
|
* - <100ms latency for agent routing decisions
|
|
* - Adaptive learning from agent execution outcomes
|
|
*/
|
|
// Import security utilities
|
|
import { InputValidator } from '../utils/input-validator.js';
|
|
/**
|
|
* RuvLLM Orchestrator
|
|
*
|
|
* Provides self-learning orchestration capabilities:
|
|
* 1. Multi-step reasoning with TRM
|
|
* 2. Adaptive agent selection with SONA
|
|
* 3. Pattern-based learning with ReasoningBank
|
|
* 4. Fast routing with neural architecture search
|
|
*/
|
|
export class RuvLLMOrchestrator {
|
|
reasoningBank;
|
|
embedder;
|
|
trmConfig;
|
|
sonaConfig;
|
|
// SONA adaptive parameters
|
|
agentPerformance;
|
|
adaptiveWeights;
|
|
// TRM reasoning state
|
|
reasoningCache;
|
|
constructor(reasoningBank, embedder, trmConfig, sonaConfig) {
|
|
this.reasoningBank = reasoningBank;
|
|
this.embedder = embedder;
|
|
// Initialize TRM configuration
|
|
this.trmConfig = {
|
|
maxDepth: trmConfig?.maxDepth ?? 5,
|
|
beamWidth: trmConfig?.beamWidth ?? 3,
|
|
temperature: trmConfig?.temperature ?? 0.7,
|
|
minConfidence: trmConfig?.minConfidence ?? 0.6,
|
|
};
|
|
// Initialize SONA configuration
|
|
this.sonaConfig = {
|
|
learningRate: sonaConfig?.learningRate ?? 0.01,
|
|
adaptationThreshold: sonaConfig?.adaptationThreshold ?? 0.75,
|
|
enableAutoTuning: sonaConfig?.enableAutoTuning ?? true,
|
|
};
|
|
// Initialize adaptive state
|
|
this.agentPerformance = new Map();
|
|
this.adaptiveWeights = new Float32Array(384).fill(1.0); // Default: equal weights
|
|
this.reasoningCache = new Map();
|
|
}
|
|
/**
|
|
* Select the best agent for a task using TRM + SONA
|
|
*
|
|
* Process:
|
|
* 1. Embed task description
|
|
* 2. Search ReasoningBank for similar patterns
|
|
* 3. Apply SONA adaptive weighting
|
|
* 4. Use FastGRNN for final routing decision
|
|
*
|
|
* @param taskDescription - Natural language task description
|
|
* @param context - Optional context information
|
|
* @returns Agent selection with confidence and reasoning
|
|
*/
|
|
async selectAgent(taskDescription, context) {
|
|
const startTime = performance.now();
|
|
// Security: Validate and sanitize task description
|
|
const sanitizedTask = InputValidator.validateTaskDescription(taskDescription, {
|
|
maxLength: 10000,
|
|
minLength: 1,
|
|
sanitize: true,
|
|
});
|
|
// Step 1: Generate task embedding
|
|
const taskEmbedding = await this.embedder.embed(sanitizedTask);
|
|
// Step 2: Search ReasoningBank for similar patterns
|
|
const patternsRaw = await this.reasoningBank.searchPatterns({
|
|
taskEmbedding,
|
|
k: this.trmConfig.beamWidth * 2,
|
|
threshold: this.trmConfig.minConfidence,
|
|
useGNN: true, // Enable GNN enhancement
|
|
});
|
|
// Cast to local ReasoningPattern interface for type compatibility
|
|
const patterns = patternsRaw;
|
|
// Step 3: Apply SONA adaptive weighting
|
|
const weightedPatterns = this.applySONAWeighting(patterns, taskEmbedding);
|
|
// Step 4: FastGRNN routing decision
|
|
const selection = this.routeWithFastGRNN(weightedPatterns, sanitizedTask);
|
|
// Security: Validate agent type from selection
|
|
InputValidator.validateAgentName(selection.agentType);
|
|
// Security: Validate confidence score
|
|
InputValidator.validateConfidence(selection.confidence);
|
|
const inferenceTimeMs = performance.now() - startTime;
|
|
return {
|
|
agentType: selection.agentType,
|
|
confidence: selection.confidence,
|
|
reasoning: selection.reasoning,
|
|
alternatives: selection.alternatives,
|
|
metrics: {
|
|
inferenceTimeMs,
|
|
patternMatchScore: selection.patternMatchScore,
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Decompose complex task into steps using TRM
|
|
*
|
|
* Recursive reasoning:
|
|
* 1. Analyze task complexity
|
|
* 2. Identify sub-tasks
|
|
* 3. Assign agents to sub-tasks
|
|
* 4. Determine execution order (sequential/parallel)
|
|
*
|
|
* @param taskDescription - Task to decompose
|
|
* @param maxDepth - Maximum recursion depth
|
|
* @returns Task decomposition with steps and agent assignments
|
|
*/
|
|
async decomposeTask(taskDescription, maxDepth) {
|
|
// Security: Validate and sanitize task description
|
|
const sanitizedTask = InputValidator.validateTaskDescription(taskDescription, {
|
|
maxLength: 10000,
|
|
minLength: 1,
|
|
sanitize: true,
|
|
});
|
|
const depth = maxDepth ?? this.trmConfig.maxDepth;
|
|
// Security: Validate depth parameter to prevent excessive recursion
|
|
if (depth < 1 || depth > 20) {
|
|
throw new Error('Invalid maxDepth: must be between 1 and 20');
|
|
}
|
|
// Check cache
|
|
const cacheKey = `${sanitizedTask}-${depth}`;
|
|
if (this.reasoningCache.has(cacheKey)) {
|
|
return this.reasoningCache.get(cacheKey);
|
|
}
|
|
// Estimate task complexity
|
|
const complexity = await this.estimateComplexity(sanitizedTask);
|
|
// Base case: simple task
|
|
if (complexity < 3 || depth <= 1) {
|
|
const agent = await this.selectAgent(sanitizedTask);
|
|
return {
|
|
steps: [{
|
|
description: sanitizedTask,
|
|
estimatedComplexity: complexity,
|
|
suggestedAgent: agent.agentType,
|
|
}],
|
|
totalComplexity: complexity,
|
|
parallelizable: false,
|
|
};
|
|
}
|
|
// Recursive case: decompose into sub-tasks
|
|
const subTasks = await this.identifySubTasks(sanitizedTask, complexity);
|
|
const steps = await Promise.all(subTasks.map(async (subTask) => {
|
|
const subComplexity = await this.estimateComplexity(subTask);
|
|
const agent = await this.selectAgent(subTask);
|
|
return {
|
|
description: subTask,
|
|
estimatedComplexity: subComplexity,
|
|
suggestedAgent: agent.agentType,
|
|
};
|
|
}));
|
|
const parallelizable = this.canRunInParallel(steps);
|
|
const decomposition = {
|
|
steps,
|
|
totalComplexity: steps.reduce((sum, step) => sum + step.estimatedComplexity, 0),
|
|
parallelizable,
|
|
};
|
|
// Cache result
|
|
this.reasoningCache.set(cacheKey, decomposition);
|
|
return decomposition;
|
|
}
|
|
/**
|
|
* Record learning outcome and adapt SONA parameters
|
|
*
|
|
* SONA adaptation:
|
|
* 1. Update agent performance metrics
|
|
* 2. Adjust adaptive weights based on success/failure
|
|
* 3. Store pattern in ReasoningBank for future retrieval
|
|
* 4. Trigger auto-tuning if performance drops
|
|
*
|
|
* @param outcome - Learning outcome from agent execution
|
|
*/
|
|
async recordOutcome(outcome) {
|
|
// Update agent performance tracking
|
|
const perf = this.agentPerformance.get(outcome.selectedAgent) ?? {
|
|
successRate: 0,
|
|
avgLatency: 0,
|
|
uses: 0,
|
|
};
|
|
const newUses = perf.uses + 1;
|
|
const newSuccessRate = (perf.successRate * perf.uses + (outcome.success ? 1 : 0)) / newUses;
|
|
const newAvgLatency = (perf.avgLatency * perf.uses + outcome.latencyMs) / newUses;
|
|
this.agentPerformance.set(outcome.selectedAgent, {
|
|
successRate: newSuccessRate,
|
|
avgLatency: newAvgLatency,
|
|
uses: newUses,
|
|
});
|
|
// Store pattern in ReasoningBank
|
|
const patternId = await this.reasoningBank.storePattern({
|
|
taskType: outcome.taskType,
|
|
approach: `Agent: ${outcome.selectedAgent}, Success: ${outcome.success}`,
|
|
successRate: outcome.success ? 1.0 : 0.0,
|
|
avgReward: outcome.reward,
|
|
tags: [outcome.selectedAgent, outcome.taskType],
|
|
metadata: {
|
|
latencyMs: outcome.latencyMs,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
// Record outcome for GNN learning
|
|
await this.reasoningBank.recordOutcome(patternId, outcome.success, outcome.reward);
|
|
// SONA adaptation: adjust weights based on outcome
|
|
if (this.sonaConfig.enableAutoTuning) {
|
|
await this.adaptSONAWeights(outcome);
|
|
}
|
|
}
|
|
/**
|
|
* Train GNN on accumulated patterns
|
|
*
|
|
* Triggers ReasoningBank GNN training with collected outcomes.
|
|
* Should be called periodically (e.g., after N executions).
|
|
*
|
|
* @param options - Training options
|
|
* @returns Training results
|
|
*/
|
|
async trainGNN(options) {
|
|
return this.reasoningBank.trainGNN(options);
|
|
}
|
|
/**
|
|
* Get orchestrator statistics
|
|
*
|
|
* @returns Performance metrics and agent statistics
|
|
*/
|
|
getStats() {
|
|
const totalExecutions = Array.from(this.agentPerformance.values())
|
|
.reduce((sum, perf) => sum + perf.uses, 0);
|
|
const agentPerformance = Array.from(this.agentPerformance.entries())
|
|
.map(([agent, perf]) => ({
|
|
agent,
|
|
successRate: perf.successRate,
|
|
avgLatency: perf.avgLatency,
|
|
uses: perf.uses,
|
|
}))
|
|
.sort((a, b) => b.uses - a.uses);
|
|
return {
|
|
totalExecutions,
|
|
agentPerformance,
|
|
cachedDecompositions: this.reasoningCache.size,
|
|
};
|
|
}
|
|
// ========================================================================
|
|
// Private Helper Methods
|
|
// ========================================================================
|
|
/**
|
|
* Apply SONA adaptive weighting to patterns
|
|
*/
|
|
applySONAWeighting(patterns, taskEmbedding) {
|
|
return patterns.map(pattern => {
|
|
// Calculate adaptive weight based on:
|
|
// 1. Pattern similarity (already computed)
|
|
// 2. Agent historical performance
|
|
// 3. Embedding distance with adaptive weights
|
|
const similarity = pattern.similarity ?? 0;
|
|
// Get agent from pattern metadata
|
|
const agent = pattern.metadata?.agent || 'unknown';
|
|
const perf = this.agentPerformance.get(agent);
|
|
const performanceBoost = perf
|
|
? perf.successRate * 0.3 + (1.0 - Math.min(perf.avgLatency / 1000, 1.0)) * 0.2
|
|
: 0;
|
|
const sonaWeight = similarity * 0.5 + performanceBoost;
|
|
return {
|
|
...pattern,
|
|
sonaWeight,
|
|
};
|
|
});
|
|
}
|
|
/**
|
|
* Route task using FastGRNN (fast recurrent neural network)
|
|
*/
|
|
routeWithFastGRNN(weightedPatterns, taskDescription) {
|
|
if (weightedPatterns.length === 0) {
|
|
// Fallback: simple keyword matching
|
|
return this.fallbackAgentSelection(taskDescription);
|
|
}
|
|
// Sort patterns by SONA weight
|
|
const sorted = weightedPatterns.sort((a, b) => b.sonaWeight - a.sonaWeight);
|
|
// Extract agent from top pattern
|
|
const topPattern = sorted[0];
|
|
const agentType = this.extractAgentFromPattern(topPattern);
|
|
// Build alternatives
|
|
const alternatives = sorted.slice(1, 4).map(pattern => ({
|
|
agentType: this.extractAgentFromPattern(pattern),
|
|
confidence: pattern.sonaWeight,
|
|
}));
|
|
return {
|
|
agentType,
|
|
confidence: topPattern.sonaWeight,
|
|
reasoning: `Based on ${sorted.length} similar patterns. Top match: ${topPattern.approach}`,
|
|
alternatives,
|
|
patternMatchScore: topPattern.similarity ?? 0,
|
|
};
|
|
}
|
|
/**
|
|
* Extract agent type from reasoning pattern
|
|
*/
|
|
extractAgentFromPattern(pattern) {
|
|
// Try metadata first
|
|
if (pattern.metadata?.agent) {
|
|
return pattern.metadata.agent;
|
|
}
|
|
// Parse from approach text
|
|
const match = pattern.approach.match(/Agent:\s*(\S+)/);
|
|
if (match) {
|
|
return match[1];
|
|
}
|
|
// Infer from task type
|
|
return this.inferAgentFromTaskType(pattern.taskType);
|
|
}
|
|
/**
|
|
* Infer agent from task type
|
|
*/
|
|
inferAgentFromTaskType(taskType) {
|
|
const taskLower = taskType.toLowerCase();
|
|
if (taskLower.includes('code') || taskLower.includes('implement')) {
|
|
return 'coder';
|
|
}
|
|
if (taskLower.includes('research') || taskLower.includes('analyze')) {
|
|
return 'researcher';
|
|
}
|
|
if (taskLower.includes('test')) {
|
|
return 'tester';
|
|
}
|
|
if (taskLower.includes('review')) {
|
|
return 'reviewer';
|
|
}
|
|
if (taskLower.includes('optimize') || taskLower.includes('performance')) {
|
|
return 'optimizer';
|
|
}
|
|
return 'coder'; // Default
|
|
}
|
|
/**
|
|
* Fallback agent selection when no patterns found
|
|
*/
|
|
fallbackAgentSelection(taskDescription) {
|
|
const agentType = this.inferAgentFromTaskType(taskDescription);
|
|
return {
|
|
agentType,
|
|
confidence: 0.5,
|
|
reasoning: 'No similar patterns found. Using keyword-based fallback.',
|
|
alternatives: [
|
|
{ agentType: 'coder', confidence: 0.4 },
|
|
{ agentType: 'researcher', confidence: 0.3 },
|
|
],
|
|
patternMatchScore: 0,
|
|
};
|
|
}
|
|
/**
|
|
* Estimate task complexity (1-10 scale)
|
|
*/
|
|
async estimateComplexity(taskDescription) {
|
|
// Simple heuristic based on:
|
|
// - Task length
|
|
// - Keyword complexity
|
|
// - Number of requirements
|
|
const length = taskDescription.length;
|
|
const wordCount = taskDescription.split(/\s+/).length;
|
|
const complexKeywords = [
|
|
'integrate', 'optimize', 'architect', 'design', 'implement',
|
|
'refactor', 'migrate', 'analyze', 'benchmark'
|
|
];
|
|
const keywordScore = complexKeywords.filter(kw => taskDescription.toLowerCase().includes(kw)).length;
|
|
const lengthScore = Math.min(length / 100, 5);
|
|
const wordScore = Math.min(wordCount / 20, 3);
|
|
return Math.min(Math.ceil(lengthScore + wordScore + keywordScore), 10);
|
|
}
|
|
/**
|
|
* Identify sub-tasks for decomposition
|
|
*/
|
|
async identifySubTasks(taskDescription, complexity) {
|
|
// Simple decomposition heuristic
|
|
// In production, would use LLM or more sophisticated NLP
|
|
const sentences = taskDescription.split(/[.!?]+/).filter(s => s.trim());
|
|
if (sentences.length > 1) {
|
|
return sentences.map(s => s.trim());
|
|
}
|
|
// Fallback: split by conjunctions
|
|
const conjunctions = taskDescription.split(/\b(and|then|after)\b/i);
|
|
if (conjunctions.length > 1) {
|
|
return conjunctions.filter((_, i) => i % 2 === 0).map(s => s.trim());
|
|
}
|
|
// Fallback: split into ~equal complexity sub-tasks
|
|
const subTaskCount = Math.ceil(complexity / 3);
|
|
const words = taskDescription.split(/\s+/);
|
|
const wordsPerTask = Math.ceil(words.length / subTaskCount);
|
|
const subTasks = [];
|
|
for (let i = 0; i < words.length; i += wordsPerTask) {
|
|
subTasks.push(words.slice(i, i + wordsPerTask).join(' '));
|
|
}
|
|
return subTasks;
|
|
}
|
|
/**
|
|
* Determine if steps can run in parallel
|
|
*/
|
|
canRunInParallel(steps) {
|
|
// Steps can run in parallel if:
|
|
// 1. Different agents assigned
|
|
// 2. No sequential dependencies (simple heuristic)
|
|
const agents = new Set(steps.map(s => s.suggestedAgent));
|
|
if (agents.size !== steps.length) {
|
|
return false; // Same agent used multiple times
|
|
}
|
|
// Check for sequential keywords
|
|
const sequentialKeywords = ['then', 'after', 'before', 'next'];
|
|
const hasSequential = steps.some(step => sequentialKeywords.some(kw => step.description.toLowerCase().includes(kw)));
|
|
return !hasSequential;
|
|
}
|
|
/**
|
|
* Adapt SONA weights based on outcome
|
|
*/
|
|
async adaptSONAWeights(outcome) {
|
|
const perf = this.agentPerformance.get(outcome.selectedAgent);
|
|
if (!perf) {
|
|
return;
|
|
}
|
|
// If performance drops below threshold, increase learning rate
|
|
if (perf.successRate < this.sonaConfig.adaptationThreshold) {
|
|
const adaptedLearningRate = this.sonaConfig.learningRate * 1.5;
|
|
// Simple weight adaptation: boost successful patterns, penalize failures
|
|
const adjustment = outcome.success
|
|
? this.sonaConfig.learningRate
|
|
: -this.sonaConfig.learningRate;
|
|
// Update adaptive weights (element-wise adjustment)
|
|
for (let i = 0; i < this.adaptiveWeights.length; i++) {
|
|
this.adaptiveWeights[i] = Math.max(0.1, this.adaptiveWeights[i] + adjustment);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=RuvLLMOrchestrator.js.map
|