tasq/node_modules/@claude-flow/hooks/dist/reasoningbank/index.js

785 lines
29 KiB
JavaScript

/**
* V3 ReasoningBank - Pattern Learning with AgentDB
*
* Connects hooks to persistent vector storage using AgentDB adapter.
* No JSON - all patterns stored as vectors in memory.db
*
* Features:
* - Real HNSW indexing (M=16, efConstruction=200) for 150x+ faster search
* - ONNX embeddings via @claude-flow/embeddings (MiniLM-L6 384-dim)
* - AgentDB backend for persistence
* - Pattern promotion from short-term to long-term memory
*
* @module @claude-flow/hooks/reasoningbank
*/
import { EventEmitter } from 'node:events';
// Dynamic imports for optional dependencies
let AgentDBAdapter = null;
let HNSWIndex = null;
let EmbeddingServiceImpl = null;
const DEFAULT_CONFIG = {
dimensions: 384, // MiniLM-L6
hnswM: 16,
hnswEfConstruction: 200,
hnswEfSearch: 100,
maxShortTerm: 1000,
maxLongTerm: 5000,
promotionThreshold: 3,
qualityThreshold: 0.6,
dedupThreshold: 0.95,
dbPath: '.claude-flow/memory.db',
useMockEmbeddings: false,
};
/**
* Agent mapping for routing
*/
const AGENT_PATTERNS = {
'security-architect': /security|auth|cve|vuln|encrypt|password|token/i,
'test-architect': /test|spec|mock|coverage|tdd|assert/i,
'performance-engineer': /perf|optim|fast|memory|cache|speed|slow/i,
'core-architect': /architect|design|ddd|domain|refactor|struct/i,
'swarm-specialist': /swarm|agent|coordinate|orchestrat|parallel/i,
'memory-specialist': /memory|agentdb|hnsw|vector|embedding/i,
'coder': /fix|bug|implement|create|add|build|error|code/i,
'reviewer': /review|quality|lint|check|audit/i,
};
/**
* Domain-specific guidance templates
*/
const DOMAIN_GUIDANCE = {
security: [
'Validate all inputs at system boundaries',
'Use parameterized queries (no string concatenation)',
'Store secrets in environment variables only',
'Apply principle of least privilege',
'Check OWASP Top 10 patterns',
],
testing: [
'Write test first, then implementation (TDD)',
'Mock external dependencies',
'Test behavior, not implementation',
'One assertion per test concept',
'Use descriptive test names',
],
performance: [
'Use HNSW for vector search (not brute-force)',
'Batch database operations',
'Implement caching at appropriate layers',
'Profile before optimizing',
'Target: <1ms searches, <100ms operations',
],
architecture: [
'Respect bounded context boundaries',
'Use domain events for cross-module communication',
'Keep domain logic in domain layer',
'Infrastructure adapters for external services',
'Follow ADR decisions (ADR-001 through ADR-010)',
],
debugging: [
'Reproduce the issue first',
'Check recent changes in git log',
'Add logging before fixing',
'Write regression test',
"Verify fix doesn't break other tests",
],
};
/**
* ReasoningBank - Vector-based pattern storage and retrieval
*
* Uses AgentDB adapter for HNSW-indexed pattern storage.
* Provides guidance generation from learned patterns.
*/
export class ReasoningBank extends EventEmitter {
config;
agentDB = null;
hnswIndex = null;
embeddingService;
initialized = false;
useRealBackend = false;
// In-memory caches for fast access
shortTermPatterns = new Map();
longTermPatterns = new Map();
// Metrics
metrics = {
patternsStored: 0,
patternsRetrieved: 0,
searchCount: 0,
totalSearchTime: 0,
promotions: 0,
hnswSearchTime: 0,
bruteForceSearchTime: 0,
};
constructor(config = {}) {
super();
this.config = { ...DEFAULT_CONFIG, ...config };
this.embeddingService = new FallbackEmbeddingService(this.config.dimensions);
}
/**
* Initialize ReasoningBank with AgentDB backend and real HNSW
*/
async initialize() {
if (this.initialized)
return;
try {
// Try to load real implementations
await this.loadDependencies();
if (AgentDBAdapter && HNSWIndex) {
// Initialize real HNSW index
this.hnswIndex = new HNSWIndex({
dimensions: this.config.dimensions,
M: this.config.hnswM,
efConstruction: this.config.hnswEfConstruction,
maxElements: this.config.maxShortTerm + this.config.maxLongTerm,
metric: 'cosine',
});
// Initialize AgentDB adapter
this.agentDB = new AgentDBAdapter({
dimensions: this.config.dimensions,
hnswM: this.config.hnswM,
hnswEfConstruction: this.config.hnswEfConstruction,
maxEntries: this.config.maxShortTerm + this.config.maxLongTerm,
persistenceEnabled: true,
persistencePath: this.config.dbPath,
embeddingGenerator: (text) => this.embeddingService.embed(text),
});
await this.agentDB.initialize();
this.useRealBackend = true;
// Try to use real embedding service
if (EmbeddingServiceImpl && !this.config.useMockEmbeddings) {
try {
this.embeddingService = new RealEmbeddingService(this.config.dimensions);
await this.embeddingService.initialize();
}
catch (e) {
console.warn('[ReasoningBank] Real embeddings unavailable, using hash-based fallback');
}
}
await this.loadPatterns();
console.log(`[ReasoningBank] Initialized with AgentDB + HNSW (M=${this.config.hnswM}, efConstruction=${this.config.hnswEfConstruction})`);
}
else {
throw new Error('Dependencies not available');
}
this.initialized = true;
this.emit('initialized', {
shortTermCount: this.shortTermPatterns.size,
longTermCount: this.longTermPatterns.size,
useRealBackend: this.useRealBackend,
});
}
catch (error) {
// Fallback to in-memory only mode
console.warn('[ReasoningBank] AgentDB not available, using in-memory mode');
this.useRealBackend = false;
this.initialized = true;
}
}
/**
* Load optional dependencies
*/
async loadDependencies() {
// Try to load optional peer dependencies at runtime
const dynamicImport = async (moduleName) => {
try {
return await import(/* webpackIgnore: true */ moduleName);
}
catch {
return null;
}
};
const memoryModule = await dynamicImport('@claude-flow/memory');
if (memoryModule) {
AgentDBAdapter = memoryModule.AgentDBAdapter;
HNSWIndex = memoryModule.HNSWIndex;
}
const embeddingsModule = await dynamicImport('@claude-flow/embeddings');
if (embeddingsModule) {
EmbeddingServiceImpl = embeddingsModule.createEmbeddingService;
}
}
/**
* Store a new pattern from hook execution
*/
async storePattern(strategy, domain, metadata = {}) {
await this.ensureInitialized();
const embedding = await this.embeddingService.embed(strategy);
// Check for duplicates using vector similarity
const similar = await this.searchPatterns(embedding, 1);
if (similar.length > 0 && similar[0].similarity > this.config.dedupThreshold) {
// Update existing pattern
const existing = similar[0].pattern;
existing.usageCount++;
existing.updatedAt = Date.now();
existing.quality = this.calculateQuality(existing);
await this.updateInStorage(existing);
this.checkPromotion(existing);
return { id: existing.id, action: 'updated' };
}
// Create new pattern
const pattern = {
id: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
strategy,
domain,
embedding,
quality: 0.5,
usageCount: 1,
successCount: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
metadata,
};
this.shortTermPatterns.set(pattern.id, pattern);
// Add to HNSW index if available
if (this.hnswIndex) {
await this.hnswIndex.addPoint(pattern.id, embedding);
}
await this.storeInAgentDB(pattern, 'short_term');
this.metrics.patternsStored++;
this.emit('pattern:stored', { id: pattern.id, domain });
return { id: pattern.id, action: 'created' };
}
/**
* Search for similar patterns using HNSW (if available) or brute-force
*/
async searchPatterns(query, k = 5) {
await this.ensureInitialized();
const startTime = performance.now();
const embedding = typeof query === 'string'
? await this.embeddingService.embed(query)
: query;
let results = [];
// Try HNSW search first (150x+ faster)
if (this.hnswIndex && this.useRealBackend) {
const hnswStart = performance.now();
try {
const hnswResults = await this.hnswIndex.search(embedding, k, this.config.hnswEfSearch);
this.metrics.hnswSearchTime += performance.now() - hnswStart;
for (const { id, distance } of hnswResults) {
const pattern = this.shortTermPatterns.get(id) || this.longTermPatterns.get(id);
if (pattern) {
// Convert distance to similarity (cosine distance -> similarity)
const similarity = 1 - distance;
results.push({ pattern, similarity });
}
}
}
catch (e) {
console.warn('[ReasoningBank] HNSW search failed, falling back to brute-force');
results = this.bruteForceSearch(embedding, k);
}
}
else {
// Brute-force search
results = this.bruteForceSearch(embedding, k);
}
const searchTime = performance.now() - startTime;
this.metrics.searchCount++;
this.metrics.totalSearchTime += searchTime;
this.metrics.patternsRetrieved += results.length;
return results;
}
/**
* Brute-force search (fallback)
*/
bruteForceSearch(embedding, k) {
const startTime = performance.now();
const results = [];
// Search long-term first (higher quality)
for (const pattern of this.longTermPatterns.values()) {
const similarity = this.cosineSimilarity(embedding, pattern.embedding);
results.push({ pattern, similarity });
}
// Search short-term
for (const pattern of this.shortTermPatterns.values()) {
const similarity = this.cosineSimilarity(embedding, pattern.embedding);
results.push({ pattern, similarity });
}
// Sort by similarity and take top k
results.sort((a, b) => b.similarity - a.similarity);
this.metrics.bruteForceSearchTime += performance.now() - startTime;
return results.slice(0, k);
}
/**
* Generate guidance for a given context
*/
async generateGuidance(context) {
await this.ensureInitialized();
const startTime = performance.now();
const query = this.buildQueryFromContext(context);
const patterns = await this.searchPatterns(query, 5);
// Detect domains from context
const domains = this.detectDomains(query);
// Build recommendations from domain templates
const recommendations = [];
for (const domain of domains) {
if (DOMAIN_GUIDANCE[domain]) {
recommendations.push(...DOMAIN_GUIDANCE[domain]);
}
}
// Generate context string
const contextParts = [];
if (domains.length > 0) {
contextParts.push(`**Detected Domains**: ${domains.join(', ')}`);
}
if (patterns.length > 0) {
contextParts.push('**Relevant Patterns**:');
for (const { pattern, similarity } of patterns.slice(0, 3)) {
contextParts.push(`- ${pattern.strategy} (${(similarity * 100).toFixed(0)}% match)`);
}
}
// Agent suggestion
const agentSuggestion = this.suggestAgent(query);
return {
patterns,
context: contextParts.join('\n'),
recommendations: recommendations.slice(0, 5),
agentSuggestion,
searchTimeMs: performance.now() - startTime,
};
}
/**
* Route task to optimal agent based on learned patterns
*/
async routeTask(task) {
await this.ensureInitialized();
const suggestion = this.suggestAgent(task);
// Get historical performance from patterns
const taskPatterns = await this.searchPatterns(task, 10);
const agentPerformance = new Map();
for (const { pattern } of taskPatterns) {
const agent = pattern.metadata.agent || 'coder';
const perf = agentPerformance.get(agent) || { success: 0, total: 0, quality: 0 };
perf.total++;
perf.success += pattern.successCount / Math.max(pattern.usageCount, 1);
perf.quality += pattern.quality;
agentPerformance.set(agent, perf);
}
// Calculate historical performance for suggested agent
const historicalPerf = agentPerformance.get(suggestion.agent);
const historicalPerformance = historicalPerf
? {
successRate: historicalPerf.success / historicalPerf.total,
avgQuality: historicalPerf.quality / historicalPerf.total,
taskCount: historicalPerf.total,
}
: undefined;
// Build alternatives
const alternatives = Object.entries(AGENT_PATTERNS)
.filter(([agent]) => agent !== suggestion.agent)
.map(([agent, pattern]) => ({
agent,
confidence: pattern.test(task) ? 85 : 60,
}))
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 3);
return {
agent: suggestion.agent,
confidence: suggestion.confidence,
alternatives,
reasoning: suggestion.reasoning,
historicalPerformance,
};
}
/**
* Record pattern usage outcome
*/
async recordOutcome(patternId, success) {
const pattern = this.shortTermPatterns.get(patternId) ||
this.longTermPatterns.get(patternId);
if (!pattern)
return;
pattern.usageCount++;
if (success)
pattern.successCount++;
pattern.quality = this.calculateQuality(pattern);
pattern.updatedAt = Date.now();
await this.updateInStorage(pattern);
this.checkPromotion(pattern);
this.emit('outcome:recorded', { patternId, success });
}
/**
* Consolidate patterns (dedup, prune, promote)
* Called by HooksLearningDaemon
*/
async consolidate() {
await this.ensureInitialized();
let duplicatesRemoved = 0;
let patternsPruned = 0;
let patternsPromoted = 0;
// Check promotions
for (const pattern of this.shortTermPatterns.values()) {
if (this.shouldPromote(pattern)) {
await this.promotePattern(pattern);
patternsPromoted++;
}
}
// Prune old low-quality short-term patterns
const now = Date.now();
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
for (const [id, pattern] of this.shortTermPatterns) {
if (now - pattern.createdAt > maxAge && pattern.usageCount < 2) {
this.shortTermPatterns.delete(id);
await this.deleteFromStorage(id);
patternsPruned++;
}
}
// Deduplicate similar patterns
const patterns = Array.from(this.shortTermPatterns.values());
for (let i = 0; i < patterns.length; i++) {
for (let j = i + 1; j < patterns.length; j++) {
const similarity = this.cosineSimilarity(patterns[i].embedding, patterns[j].embedding);
if (similarity > this.config.dedupThreshold) {
// Keep the one with higher quality
const toRemove = patterns[i].quality >= patterns[j].quality ? patterns[j] : patterns[i];
this.shortTermPatterns.delete(toRemove.id);
await this.deleteFromStorage(toRemove.id);
duplicatesRemoved++;
}
}
}
this.emit('consolidated', { duplicatesRemoved, patternsPruned, patternsPromoted });
return { duplicatesRemoved, patternsPruned, patternsPromoted };
}
/**
* Get statistics
*/
getStats() {
const avgHnsw = this.metrics.searchCount > 0 ? this.metrics.hnswSearchTime / this.metrics.searchCount : 0;
const avgBrute = this.metrics.searchCount > 0 ? this.metrics.bruteForceSearchTime / this.metrics.searchCount : 1;
return {
shortTermCount: this.shortTermPatterns.size,
longTermCount: this.longTermPatterns.size,
metrics: { ...this.metrics },
avgSearchTime: this.metrics.searchCount > 0
? this.metrics.totalSearchTime / this.metrics.searchCount
: 0,
useRealBackend: this.useRealBackend,
hnswSpeedup: avgBrute > 0 && avgHnsw > 0 ? avgBrute / avgHnsw : 1,
};
}
/**
* Export patterns for backup/transfer
*/
async exportPatterns() {
return {
shortTerm: Array.from(this.shortTermPatterns.values()),
longTerm: Array.from(this.longTermPatterns.values()),
};
}
/**
* Import patterns from backup
*/
async importPatterns(data) {
await this.ensureInitialized();
let imported = 0;
for (const pattern of data.shortTerm) {
if (!this.shortTermPatterns.has(pattern.id)) {
this.shortTermPatterns.set(pattern.id, pattern);
if (this.hnswIndex) {
await this.hnswIndex.addPoint(pattern.id, pattern.embedding);
}
await this.storeInAgentDB(pattern, 'short_term');
imported++;
}
}
for (const pattern of data.longTerm) {
if (!this.longTermPatterns.has(pattern.id)) {
this.longTermPatterns.set(pattern.id, pattern);
if (this.hnswIndex) {
await this.hnswIndex.addPoint(pattern.id, pattern.embedding);
}
await this.storeInAgentDB(pattern, 'long_term');
imported++;
}
}
return { imported };
}
// ===== Private Methods =====
async ensureInitialized() {
if (!this.initialized) {
await this.initialize();
}
}
async loadPatterns() {
if (!this.agentDB)
return;
try {
// Load from AgentDB namespaces
const shortTermEntries = await this.agentDB.query({
namespace: 'patterns:short_term',
limit: this.config.maxShortTerm,
});
for (const entry of shortTermEntries) {
const pattern = this.entryToPattern(entry);
this.shortTermPatterns.set(pattern.id, pattern);
if (this.hnswIndex) {
await this.hnswIndex.addPoint(pattern.id, pattern.embedding);
}
}
const longTermEntries = await this.agentDB.query({
namespace: 'patterns:long_term',
limit: this.config.maxLongTerm,
});
for (const entry of longTermEntries) {
const pattern = this.entryToPattern(entry);
this.longTermPatterns.set(pattern.id, pattern);
if (this.hnswIndex) {
await this.hnswIndex.addPoint(pattern.id, pattern.embedding);
}
}
}
catch (error) {
console.warn('[ReasoningBank] Failed to load patterns:', error);
}
}
async storeInAgentDB(pattern, type) {
if (!this.agentDB)
return;
try {
await this.agentDB.store({
key: pattern.id,
namespace: `patterns:${type}`,
content: pattern.strategy,
embedding: pattern.embedding,
tags: [pattern.domain, type],
metadata: {
quality: pattern.quality,
usageCount: pattern.usageCount,
successCount: pattern.successCount,
createdAt: pattern.createdAt,
updatedAt: pattern.updatedAt,
...pattern.metadata,
},
});
}
catch (error) {
console.warn('[ReasoningBank] Failed to store pattern:', error);
}
}
async updateInStorage(pattern) {
if (!this.agentDB)
return;
try {
await this.agentDB.update(pattern.id, {
metadata: {
quality: pattern.quality,
usageCount: pattern.usageCount,
successCount: pattern.successCount,
updatedAt: pattern.updatedAt,
},
});
}
catch (error) {
console.warn('[ReasoningBank] Failed to update pattern:', error);
}
}
async deleteFromStorage(id) {
if (!this.agentDB)
return;
try {
await this.agentDB.delete(id);
}
catch (error) {
console.warn('[ReasoningBank] Failed to delete pattern:', error);
}
}
entryToPattern(entry) {
return {
id: entry.id || entry.key,
strategy: entry.content,
domain: entry.tags?.[0] || 'general',
embedding: entry.embedding instanceof Float32Array
? entry.embedding
: new Float32Array(entry.embedding || []),
quality: entry.metadata?.quality || 0.5,
usageCount: entry.metadata?.usageCount || 1,
successCount: entry.metadata?.successCount || 0,
createdAt: entry.metadata?.createdAt || entry.createdAt || Date.now(),
updatedAt: entry.metadata?.updatedAt || entry.updatedAt || Date.now(),
metadata: entry.metadata || {},
};
}
buildQueryFromContext(context) {
const parts = [];
if (context.file?.path) {
parts.push(`file: ${context.file.path}`);
}
if (context.command?.raw) {
parts.push(`command: ${context.command.raw}`);
}
if (context.task?.description) {
parts.push(context.task.description);
}
if (context.routing?.task) {
parts.push(context.routing.task);
}
return parts.join(' ');
}
detectDomains(text) {
const domains = [];
const lowerText = text.toLowerCase();
if (/security|auth|password|token|secret|cve|vuln/i.test(lowerText)) {
domains.push('security');
}
if (/test|spec|mock|coverage|tdd|assert/i.test(lowerText)) {
domains.push('testing');
}
if (/perf|optim|fast|slow|memory|cache|speed/i.test(lowerText)) {
domains.push('performance');
}
if (/architect|design|ddd|domain|refactor|struct/i.test(lowerText)) {
domains.push('architecture');
}
if (/fix|bug|error|issue|broken|fail|debug/i.test(lowerText)) {
domains.push('debugging');
}
return domains;
}
suggestAgent(task) {
let bestAgent = 'coder';
let bestConfidence = 70;
let reasoning = 'Default agent for general tasks';
for (const [agent, pattern] of Object.entries(AGENT_PATTERNS)) {
if (pattern.test(task)) {
const matches = task.match(pattern);
const confidence = 85 + (matches ? Math.min(matches.length * 5, 13) : 0);
if (confidence > bestConfidence) {
bestAgent = agent;
bestConfidence = confidence;
reasoning = `Task matches ${agent} patterns`;
}
}
}
return { agent: bestAgent, confidence: bestConfidence, reasoning };
}
calculateQuality(pattern) {
if (pattern.usageCount === 0)
return 0.5;
const successRate = pattern.successCount / pattern.usageCount;
return 0.3 + successRate * 0.7; // Range: 0.3 to 1.0
}
shouldPromote(pattern) {
return (pattern.usageCount >= this.config.promotionThreshold &&
pattern.quality >= this.config.qualityThreshold);
}
checkPromotion(pattern) {
if (this.shortTermPatterns.has(pattern.id) && this.shouldPromote(pattern)) {
this.promotePattern(pattern);
}
}
async promotePattern(pattern) {
// Move from short-term to long-term
this.shortTermPatterns.delete(pattern.id);
this.longTermPatterns.set(pattern.id, pattern);
// Update storage
await this.deleteFromStorage(pattern.id);
await this.storeInAgentDB(pattern, 'long_term');
this.metrics.promotions++;
this.emit('pattern:promoted', { id: pattern.id });
}
cosineSimilarity(a, b) {
if (a.length !== b.length)
return 0;
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom > 0 ? dot / denom : 0;
}
}
/**
* Real embedding service using @claude-flow/embeddings
*/
class RealEmbeddingService {
service = null;
dimensions;
cache = new Map();
constructor(dimensions = 384) {
this.dimensions = dimensions;
}
async initialize() {
if (EmbeddingServiceImpl) {
this.service = await EmbeddingServiceImpl({
provider: 'transformers',
model: 'Xenova/all-MiniLM-L6-v2',
dimensions: this.dimensions,
cacheSize: 1000,
});
}
}
async embed(text) {
const cacheKey = text.slice(0, 200);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
if (this.service) {
const result = await this.service.embed(text);
const embedding = result.embedding;
this.cache.set(cacheKey, embedding);
return embedding;
}
throw new Error('Embedding service not initialized');
}
}
/**
* Fallback embedding service (hash-based)
*/
class FallbackEmbeddingService {
dimensions;
cache = new Map();
constructor(dimensions = 384) {
this.dimensions = dimensions;
}
async embed(text) {
const cacheKey = text.slice(0, 200);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// Try agentic-flow ONNX embeddings first
try {
const { execFileSync } = await import('child_process');
// Use execFileSync with shell: false to prevent command injection
// Pass text as argument array to avoid shell interpolation
const safeText = text.slice(0, 500).replace(/[\x00-\x1f]/g, ''); // Remove control chars
const result = execFileSync('npx', ['agentic-flow@alpha', 'embeddings', 'generate', safeText, '--format', 'json'], { encoding: 'utf-8', timeout: 10000, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
const parsed = JSON.parse(result);
const embedding = new Float32Array(parsed.embedding || parsed);
this.cache.set(cacheKey, embedding);
return embedding;
}
catch {
// Fallback to hash-based embedding
return this.hashEmbed(text);
}
}
hashEmbed(text) {
const embedding = new Float32Array(this.dimensions);
const normalized = text.toLowerCase().trim();
for (let i = 0; i < this.dimensions; i++) {
let hash = 0;
for (let j = 0; j < normalized.length; j++) {
hash = ((hash << 5) - hash + normalized.charCodeAt(j) * (i + 1)) | 0;
}
embedding[i] = (Math.sin(hash) + 1) / 2;
}
// Normalize
let norm = 0;
for (let i = 0; i < this.dimensions; i++) {
norm += embedding[i] * embedding[i];
}
norm = Math.sqrt(norm);
if (norm > 0) {
for (let i = 0; i < this.dimensions; i++) {
embedding[i] /= norm;
}
}
this.cache.set(text.slice(0, 200), embedding);
return embedding;
}
}
// Export singleton instance
export const reasoningBank = new ReasoningBank();
//# sourceMappingURL=index.js.map