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
779 lines • 28 kB
JavaScript
/**
* SONA (Self-Optimizing Neural Architecture) Optimizer
*
* Processes trajectory outcomes to learn optimal routing patterns.
* Integrates with Q-learning router and persistence layer.
*
* Features:
* - Processes trajectory outcomes from hooksTrajectoryEnd
* - Extracts keywords from tasks for pattern matching
* - Maintains learned routing patterns with confidence scoring
* - Persists patterns to .swarm/sona-patterns.json
* - Integrates with Q-learning router for combined routing
*
* @module v3/cli/memory/sona-optimizer
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_PERSISTENCE_PATH = '.swarm/sona-patterns.json';
const PATTERN_VERSION = '1.0.0';
const MIN_CONFIDENCE = 0.1;
const MAX_CONFIDENCE = 0.99;
const CONFIDENCE_INCREMENT = 0.1;
const CONFIDENCE_DECREMENT = 0.15;
const DECAY_RATE = 0.01; // Per day
const MAX_PATTERNS = 1000;
// ============================================================================
// Contrastive Trainer (lazy-loaded from @ruvector/ruvllm)
// ============================================================================
let contrastiveTrainer = null;
let trainerLoaded = false;
async function loadContrastiveTrainer() {
if (trainerLoaded)
return contrastiveTrainer;
trainerLoaded = true;
try {
const { createRequire } = await import('module');
const requireCjs = createRequire(import.meta.url);
const ruvllm = requireCjs('@ruvector/ruvllm');
contrastiveTrainer = new ruvllm.ContrastiveTrainer({ batchSize: 32, margin: 0.5 });
return contrastiveTrainer;
}
catch {
return null;
}
}
/**
* Common agent types for routing
*/
const AGENT_TYPES = [
'coder',
'tester',
'reviewer',
'architect',
'researcher',
'optimizer',
'debugger',
'documenter',
'security-architect',
'performance-engineer',
];
/**
* Task keywords for pattern extraction
*/
const KEYWORD_CATEGORIES = {
coder: [
'implement', 'code', 'write', 'create', 'build', 'develop', 'add',
'feature', 'function', 'class', 'module', 'api', 'endpoint',
],
tester: [
'test', 'spec', 'coverage', 'unit', 'integration', 'e2e', 'mock',
'assert', 'expect', 'verify', 'validate', 'scenario',
],
reviewer: [
'review', 'check', 'audit', 'analyze', 'inspect', 'evaluate',
'quality', 'standards', 'best-practices', 'lint',
],
architect: [
'architect', 'design', 'structure', 'pattern', 'system', 'schema',
'database', 'infrastructure', 'scalability', 'architecture',
],
researcher: [
'research', 'investigate', 'explore', 'find', 'search', 'discover',
'analyze', 'understand', 'learn', 'study',
],
optimizer: [
'optimize', 'performance', 'speed', 'memory', 'improve', 'enhance',
'faster', 'efficient', 'reduce', 'benchmark',
],
debugger: [
'debug', 'fix', 'bug', 'error', 'issue', 'problem', 'crash',
'exception', 'trace', 'diagnose', 'resolve',
],
documenter: [
'document', 'docs', 'readme', 'comment', 'explain', 'guide',
'tutorial', 'api-docs', 'specification', 'jsdoc',
],
'security-architect': [
'security', 'auth', 'authentication', 'authorization', 'encrypt',
'vulnerability', 'cve', 'secure', 'permission', 'role',
],
'performance-engineer': [
'profiling', 'bottleneck', 'latency', 'throughput', 'cache',
'scale', 'load', 'stress', 'concurrent', 'parallel',
],
};
// ============================================================================
// SONAOptimizer Class
// ============================================================================
/**
* SONA Optimizer for adaptive routing based on trajectory outcomes
*
* Learns from past task outcomes to improve future routing decisions.
* Integrates with Q-learning router for hybrid routing strategy.
*/
export class SONAOptimizer {
patterns = new Map();
trajectoriesProcessed = 0;
successfulRoutings = 0;
failedRoutings = 0;
lastUpdate = null;
persistencePath;
qLearningRouter = null;
qLearningEnabled = false;
/** Real @ruvector/sona engine — null if native not available, undefined if not yet tried */
sonaEngine = undefined;
constructor(options) {
this.persistencePath = options?.persistencePath || DEFAULT_PERSISTENCE_PATH;
}
/**
* Attempt to load the native @ruvector/sona engine (once).
* Sets `sonaEngine` to the engine instance or null if unavailable.
*/
async loadSonaEngine() {
if (this.sonaEngine !== undefined)
return; // already attempted
try {
// @ts-ignore — @ruvector/sona is in optionalDependencies and ships
// no .d.ts. Runtime is gated by try/catch; TS errors here on hosts
// without the module resolved (e.g. CI before postinstall).
const sona = await import('@ruvector/sona');
const EngineCtor = sona.SonaEngine || sona.default?.SonaEngine;
if (EngineCtor) {
this.sonaEngine = new EngineCtor({ mode: 'real-time' });
}
else {
this.sonaEngine = null;
}
}
catch {
this.sonaEngine = null; // native not available
}
}
/**
* Infer an agent type string from a SONA pattern result object.
*/
inferAgentFromPattern(pattern) {
if (typeof pattern.agent === 'string')
return pattern.agent;
if (typeof pattern.route === 'string')
return pattern.route;
if (typeof pattern.label === 'string')
return pattern.label;
return 'coder';
}
/**
* Initialize the optimizer and load persisted state
*/
async initialize() {
// Load persisted patterns
const loaded = this.loadFromDisk();
// Try to load Q-learning router lazily
try {
const { QLearningRouter } = await import('../ruvector/q-learning-router.js');
this.qLearningRouter = new QLearningRouter();
await this.qLearningRouter.initialize();
this.qLearningEnabled = true;
}
catch {
// Q-learning not available, continue without it
this.qLearningEnabled = false;
}
// Eagerly load ContrastiveTrainer so stats reflect backend status
await loadContrastiveTrainer();
return {
success: true,
patternsLoaded: loaded ? this.patterns.size : 0,
};
}
/**
* Process a trajectory outcome and learn from it
* Called by hooksTrajectoryEnd
*/
processTrajectoryOutcome(outcome) {
const { task, agent, success } = outcome;
// Extract keywords from task
const keywords = this.extractKeywords(task);
if (keywords.length === 0) {
return {
learned: false,
patternKey: '',
confidence: 0,
keywordsExtracted: [],
};
}
// Create pattern key from sorted keywords
const patternKey = this.createPatternKey(keywords, agent);
// Get or create pattern
let pattern = this.patterns.get(patternKey);
if (!pattern) {
pattern = {
keywords,
agent,
confidence: 0.5, // Start at neutral
successCount: 0,
failureCount: 0,
lastUsed: Date.now(),
createdAt: Date.now(),
};
}
// Update pattern based on outcome
if (success) {
pattern.successCount++;
pattern.confidence = Math.min(MAX_CONFIDENCE, pattern.confidence + CONFIDENCE_INCREMENT * (1 - pattern.confidence));
this.successfulRoutings++;
}
else {
pattern.failureCount++;
pattern.confidence = Math.max(MIN_CONFIDENCE, pattern.confidence - CONFIDENCE_DECREMENT * pattern.confidence);
this.failedRoutings++;
}
pattern.lastUsed = Date.now();
// Store pattern
this.patterns.set(patternKey, pattern);
this.trajectoriesProcessed++;
this.lastUpdate = Date.now();
// Prune old patterns if needed
this.prunePatterns();
// Update Q-learning router if available
if (this.qLearningRouter) {
const reward = success ? 1.0 : -0.5;
this.qLearningRouter.update(task, agent, reward);
}
// Feed outcome into contrastive trainer for agent embedding learning (fire-and-forget)
if (success) {
loadContrastiveTrainer().then(trainer => {
if (!trainer)
return;
// Use keyword vector as a lightweight embedding proxy
const embedding = this.keywordsToEmbedding(keywords);
trainer.addAgentEmbedding(agent, embedding);
}).catch(() => { });
}
// Persist to disk (debounced)
this.saveToDisk();
return {
learned: true,
patternKey,
confidence: pattern.confidence,
keywordsExtracted: keywords,
};
}
/**
* Get routing suggestion based on learned patterns.
*
* Priority order:
* 1. Real @ruvector/sona native engine (if available and has matches)
* 2. SONA learned pattern matching (keyword overlap + confidence)
* 3. Q-learning router (if enabled)
* 4. Keyword heuristic
* 5. Default fallback
*/
async getRoutingSuggestion(task) {
// Priority 1: Try real @ruvector/sona native engine
await this.loadSonaEngine();
if (this.sonaEngine) {
try {
const patterns = this.sonaEngine.findPatterns(task, 3);
if (patterns && patterns.length > 0) {
const best = patterns[0];
const agent = best.route || best.agent || this.inferAgentFromPattern(best);
return {
agent,
confidence: best.quality || 0.8,
usedQLearning: false,
source: 'sona-native',
alternatives: patterns.slice(1).map((p) => ({
agent: p.route || p.agent || this.inferAgentFromPattern(p),
score: p.quality || 0.5,
})),
matchedKeywords: best.keywords || [],
};
}
}
catch {
// Native SONA failed on this query — fall through to keyword matching
}
}
const keywords = this.extractKeywords(task);
// Priority 2: Try SONA learned pattern matching
const sonaResult = this.findBestPatternMatch(keywords);
if (sonaResult && sonaResult.confidence >= 0.6) {
return {
agent: sonaResult.agent,
confidence: sonaResult.confidence,
usedQLearning: false,
source: 'sona-pattern',
alternatives: this.getAlternatives(keywords, sonaResult.agent),
matchedKeywords: sonaResult.matchedKeywords,
};
}
// Priority 3: Try Q-learning router if available
if (this.qLearningRouter && this.qLearningEnabled) {
try {
const decision = this.qLearningRouter.route(task, false);
if (decision.confidence >= 0.5) {
return {
agent: decision.route,
confidence: decision.confidence,
usedQLearning: true,
source: 'q-learning',
alternatives: decision.alternatives,
};
}
}
catch {
// Q-learning failed, continue to fallback
}
}
// Priority 4: Keyword-based heuristic
const keywordMatch = this.matchKeywordsToAgent(keywords);
if (keywordMatch) {
return {
agent: keywordMatch.agent,
confidence: keywordMatch.confidence,
usedQLearning: false,
source: 'sona-keyword',
alternatives: this.getAlternatives(keywords, keywordMatch.agent),
matchedKeywords: keywordMatch.matchedKeywords,
};
}
// Priority 5: Default fallback
return {
agent: 'coder',
confidence: 0.3,
usedQLearning: false,
source: 'default',
alternatives: [
{ agent: 'researcher', score: 0.2 },
{ agent: 'architect', score: 0.15 },
],
};
}
/**
* Get optimizer statistics
*/
getStats() {
let totalConfidence = 0;
for (const pattern of this.patterns.values()) {
totalConfidence += pattern.confidence;
}
return {
totalPatterns: this.patterns.size,
successfulRoutings: this.successfulRoutings,
failedRoutings: this.failedRoutings,
trajectoriesProcessed: this.trajectoriesProcessed,
avgConfidence: this.patterns.size > 0 ? totalConfidence / this.patterns.size : 0,
qLearningEnabled: this.qLearningEnabled,
lastUpdate: this.lastUpdate,
_contrastiveTrainer: contrastiveTrainer
? { triplets: contrastiveTrainer.getTripletCount?.() ?? 0, agents: contrastiveTrainer.getAgentEmbeddings?.()?.size ?? 0 }
: 'unavailable',
};
}
/**
* Trigger contrastive training on accumulated agent embeddings.
* Returns training metrics or { trained: false } if insufficient data.
*
* @param _epochs - reserved for future use (epochs are set at ContrastiveTrainer construction)
*/
async trainAgentEmbeddings(_epochs = 5) {
const trainer = await loadContrastiveTrainer();
if (!trainer || (trainer.getTripletCount?.() ?? 0) < 3) {
return { trained: false };
}
const result = trainer.train();
return { trained: true, loss: result.finalLoss, triplets: result.tripletCount };
}
/**
* Apply temporal decay to pattern confidence
* Reduces confidence of unused patterns
*/
applyTemporalDecay() {
const now = Date.now();
let decayed = 0;
for (const [key, pattern] of this.patterns) {
const daysSinceUse = (now - pattern.lastUsed) / (1000 * 60 * 60 * 24);
if (daysSinceUse > 1) {
const decay = Math.exp(-DECAY_RATE * daysSinceUse);
const newConfidence = pattern.confidence * decay;
if (newConfidence < MIN_CONFIDENCE) {
// Remove patterns with very low confidence
this.patterns.delete(key);
}
else {
pattern.confidence = newConfidence;
}
decayed++;
}
}
if (decayed > 0) {
this.saveToDisk();
}
return decayed;
}
/**
* Reset all learned patterns
*/
reset() {
this.patterns.clear();
this.trajectoriesProcessed = 0;
this.successfulRoutings = 0;
this.failedRoutings = 0;
this.lastUpdate = null;
if (this.qLearningRouter) {
this.qLearningRouter.reset();
}
this.saveToDisk();
}
/**
* Export patterns for analysis
*/
exportPatterns() {
const result = {};
for (const [key, pattern] of this.patterns) {
result[key] = { ...pattern };
}
return result;
}
/**
* Import patterns (for migration or testing)
*/
importPatterns(patterns) {
let imported = 0;
for (const [key, pattern] of Object.entries(patterns)) {
if (this.validatePattern(pattern)) {
this.patterns.set(key, pattern);
imported++;
}
}
this.saveToDisk();
return imported;
}
// ============================================================================
// Private Methods
// ============================================================================
/**
* Convert extracted keywords into a lightweight 384-dim embedding proxy.
* Uses a deterministic hash-scatter so each keyword set maps to a
* consistent unit-length vector compatible with ContrastiveTrainer.
*/
keywordsToEmbedding(keywords) {
const dim = 384;
const vec = new Float32Array(dim);
for (const kw of keywords) {
// Simple FNV-1a-like hash per character to scatter energy across dims
let h = 0x811c9dc5;
for (let i = 0; i < kw.length; i++) {
h ^= kw.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
const idx = Math.abs(h) % dim;
vec[idx] += (h & 1) ? 1 : -1;
}
// L2-normalize
let norm = 0;
for (let i = 0; i < dim; i++)
norm += vec[i] * vec[i];
norm = Math.sqrt(norm) || 1;
for (let i = 0; i < dim; i++)
vec[i] /= norm;
return vec;
}
/**
* Extract meaningful keywords from task description
*/
extractKeywords(task) {
if (!task || typeof task !== 'string') {
return [];
}
const lower = task.toLowerCase();
const words = lower.split(/[\s\-_.,;:!?'"()\[\]{}]+/).filter(w => w.length > 2);
// Extract keywords that match our categories
const keywords = new Set();
for (const categoryKeywords of Object.values(KEYWORD_CATEGORIES)) {
for (const keyword of categoryKeywords) {
if (lower.includes(keyword)) {
keywords.add(keyword);
}
}
}
// Add any significant words not in categories
for (const word of words) {
if (word.length >= 4 && !this.isStopWord(word)) {
keywords.add(word);
}
}
return Array.from(keywords).slice(0, 10); // Limit to 10 keywords
}
/**
* Check if word is a stop word
*/
isStopWord(word) {
const stopWords = new Set([
'the', 'and', 'for', 'that', 'this', 'with', 'from', 'have', 'been',
'will', 'would', 'could', 'should', 'into', 'then', 'than', 'when',
'where', 'which', 'there', 'their', 'what', 'about', 'more', 'some',
'also', 'just', 'only', 'other', 'very', 'after', 'most', 'such',
]);
return stopWords.has(word);
}
/**
* Create a unique pattern key from keywords and agent
*/
createPatternKey(keywords, agent) {
const sortedKeywords = [...keywords].sort();
return `${agent}:${sortedKeywords.join('+')}`;
}
/**
* Find the best matching pattern for given keywords
*/
findBestPatternMatch(keywords) {
if (keywords.length === 0 || this.patterns.size === 0) {
return null;
}
let bestMatch = null;
let bestScore = 0;
for (const pattern of this.patterns.values()) {
const matchedKeywords = pattern.keywords.filter(k => keywords.includes(k));
const matchRatio = matchedKeywords.length / Math.max(pattern.keywords.length, keywords.length);
// Combine match ratio with confidence
const score = matchRatio * pattern.confidence;
if (score > bestScore && matchedKeywords.length >= 1) {
bestScore = score;
bestMatch = {
agent: pattern.agent,
confidence: pattern.confidence * matchRatio,
matchedKeywords,
};
}
}
return bestMatch;
}
/**
* Match keywords to agent using category heuristics
*/
matchKeywordsToAgent(keywords) {
const scores = {};
for (const [agent, categoryKeywords] of Object.entries(KEYWORD_CATEGORIES)) {
const matched = keywords.filter(k => categoryKeywords.includes(k));
if (matched.length > 0) {
scores[agent] = {
score: matched.length / categoryKeywords.length,
matched,
};
}
}
// Find best scoring agent
let bestAgent = '';
let bestScore = 0;
let bestMatched = [];
for (const [agent, data] of Object.entries(scores)) {
if (data.score > bestScore) {
bestScore = data.score;
bestAgent = agent;
bestMatched = data.matched;
}
}
if (bestAgent && bestScore > 0) {
return {
agent: bestAgent,
confidence: Math.min(0.7, 0.3 + bestScore),
matchedKeywords: bestMatched,
};
}
return null;
}
/**
* Get alternative agent suggestions
*/
getAlternatives(keywords, excludeAgent) {
const alternatives = [];
for (const [agent, categoryKeywords] of Object.entries(KEYWORD_CATEGORIES)) {
if (agent === excludeAgent)
continue;
const matched = keywords.filter(k => categoryKeywords.includes(k));
if (matched.length > 0) {
alternatives.push({
agent,
score: matched.length / Math.max(keywords.length, 1) * 0.5,
});
}
}
return alternatives
.sort((a, b) => b.score - a.score)
.slice(0, 3);
}
/**
* Prune old/low-confidence patterns if over limit
*/
prunePatterns() {
if (this.patterns.size <= MAX_PATTERNS) {
return;
}
// Sort patterns by score (confidence * recency)
const entries = Array.from(this.patterns.entries()).map(([key, pattern]) => {
const ageInDays = (Date.now() - pattern.lastUsed) / (1000 * 60 * 60 * 24);
const recency = Math.exp(-0.1 * ageInDays);
const score = pattern.confidence * recency;
return { key, pattern, score };
});
entries.sort((a, b) => a.score - b.score);
// Remove lowest-scoring patterns
const toRemove = entries.slice(0, entries.length - Math.floor(MAX_PATTERNS * 0.8));
for (const { key } of toRemove) {
this.patterns.delete(key);
}
}
/**
* Validate pattern structure
*/
validatePattern(pattern) {
if (!pattern || typeof pattern !== 'object')
return false;
const p = pattern;
return (Array.isArray(p.keywords) &&
typeof p.agent === 'string' &&
typeof p.confidence === 'number' &&
typeof p.successCount === 'number' &&
typeof p.failureCount === 'number');
}
/**
* Load patterns from disk
*/
loadFromDisk() {
try {
const fullPath = join(process.cwd(), this.persistencePath);
if (!existsSync(fullPath)) {
return false;
}
const data = readFileSync(fullPath, 'utf-8');
const state = JSON.parse(data);
// Validate version
if (!state.version || !state.version.startsWith('1.')) {
console.error('[SONA] Incompatible state version, starting fresh');
return false;
}
// Load patterns
this.patterns.clear();
for (const [key, pattern] of Object.entries(state.patterns)) {
if (this.validatePattern(pattern)) {
this.patterns.set(key, pattern);
}
}
// Load stats
if (state.stats) {
this.trajectoriesProcessed = state.stats.trajectoriesProcessed || 0;
this.successfulRoutings = state.stats.successfulRoutings || 0;
this.failedRoutings = state.stats.failedRoutings || 0;
this.lastUpdate = state.stats.lastUpdate || null;
}
return true;
}
catch (err) {
console.error(`[SONA] Failed to load state: ${err}`);
return false;
}
}
/**
* Save patterns to disk
*/
saveToDisk() {
try {
const fullPath = join(process.cwd(), this.persistencePath);
const dir = dirname(fullPath);
// Ensure directory exists
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const state = {
version: PATTERN_VERSION,
patterns: this.exportPatterns(),
stats: {
trajectoriesProcessed: this.trajectoriesProcessed,
successfulRoutings: this.successfulRoutings,
failedRoutings: this.failedRoutings,
lastUpdate: this.lastUpdate,
},
metadata: {
createdAt: new Date().toISOString(),
savedAt: new Date().toISOString(),
},
};
writeFileSync(fullPath, JSON.stringify(state, null, 2));
return true;
}
catch (err) {
console.error(`[SONA] Failed to save state: ${err}`);
return false;
}
}
}
// ============================================================================
// Singleton Instance
// ============================================================================
let sonaOptimizerInstance = null;
let initializationPromise = null;
/**
* Get the singleton SONAOptimizer instance
* Uses lazy initialization to avoid circular imports
*/
export async function getSONAOptimizer() {
if (sonaOptimizerInstance) {
return sonaOptimizerInstance;
}
// Prevent multiple concurrent initializations
if (initializationPromise) {
return initializationPromise;
}
initializationPromise = (async () => {
const optimizer = new SONAOptimizer();
await optimizer.initialize();
sonaOptimizerInstance = optimizer;
return optimizer;
})();
return initializationPromise;
}
/**
* Reset the singleton instance (for testing)
*/
export function resetSONAOptimizer() {
if (sonaOptimizerInstance) {
sonaOptimizerInstance.reset();
}
sonaOptimizerInstance = null;
initializationPromise = null;
}
/**
* Process a trajectory outcome (convenience function)
*/
export async function processTrajectory(outcome) {
const optimizer = await getSONAOptimizer();
return optimizer.processTrajectoryOutcome(outcome);
}
/**
* Get routing suggestion (convenience function)
*/
export async function getSuggestion(task) {
const optimizer = await getSONAOptimizer();
return optimizer.getRoutingSuggestion(task);
}
/**
* Get SONA statistics (convenience function)
*/
export async function getSONAStats() {
const optimizer = await getSONAOptimizer();
return optimizer.getStats();
}
export default {
SONAOptimizer,
getSONAOptimizer,
resetSONAOptimizer,
processTrajectory,
getSuggestion,
getSONAStats,
};
//# sourceMappingURL=sona-optimizer.js.map