993 lines
37 KiB
JavaScript
993 lines
37 KiB
JavaScript
/**
|
|
* ReasoningBank Integration with AgentDB
|
|
*
|
|
* Implements the 4-step learning pipeline with real vector storage:
|
|
* 1. RETRIEVE - Top-k memory injection with MMR diversity (using AgentDB HNSW)
|
|
* 2. JUDGE - LLM-as-judge trajectory evaluation
|
|
* 3. DISTILL - Extract strategy memories from trajectories
|
|
* 4. CONSOLIDATE - Dedup, detect contradictions, prune old patterns
|
|
*
|
|
* Performance Targets:
|
|
* - Retrieval: <10ms with AgentDB HNSW (150x faster than brute-force)
|
|
* - Learning step: <10ms
|
|
* - Consolidation: <100ms
|
|
*
|
|
* @module reasoning-bank
|
|
*/
|
|
// ============================================================================
|
|
// AgentDB Integration
|
|
// ============================================================================
|
|
let AgentDB;
|
|
let agentdbImportPromise;
|
|
async function ensureAgentDBImport() {
|
|
if (!agentdbImportPromise) {
|
|
agentdbImportPromise = (async () => {
|
|
try {
|
|
const agentdbModule = await import('agentdb');
|
|
AgentDB = agentdbModule.AgentDB || agentdbModule.default;
|
|
}
|
|
catch {
|
|
// AgentDB not available - will use fallback
|
|
AgentDB = undefined;
|
|
}
|
|
})();
|
|
}
|
|
return agentdbImportPromise;
|
|
}
|
|
/**
|
|
* Default ReasoningBank configuration
|
|
*/
|
|
const DEFAULT_CONFIG = {
|
|
maxTrajectories: 5000,
|
|
distillationThreshold: 0.6,
|
|
retrievalK: 3,
|
|
mmrLambda: 0.7,
|
|
maxPatternAgeDays: 30,
|
|
dedupThreshold: 0.95,
|
|
enableContradictionDetection: true,
|
|
dbPath: undefined,
|
|
vectorDimension: 768,
|
|
namespace: 'reasoning-bank',
|
|
enableAgentDB: true,
|
|
};
|
|
// ============================================================================
|
|
// ReasoningBank Class
|
|
// ============================================================================
|
|
/**
|
|
* ReasoningBank - Trajectory storage and learning pipeline with AgentDB
|
|
*
|
|
* This class implements a complete learning pipeline for AI agents:
|
|
* - Store and retrieve trajectories using vector similarity
|
|
* - Judge trajectory quality using rule-based evaluation
|
|
* - Distill successful trajectories into reusable patterns
|
|
* - Consolidate patterns to remove duplicates and contradictions
|
|
*/
|
|
export class ReasoningBank {
|
|
config;
|
|
trajectories = new Map();
|
|
memories = new Map();
|
|
patterns = new Map();
|
|
eventListeners = new Set();
|
|
// AgentDB instance for vector storage
|
|
agentdb = null;
|
|
agentdbAvailable = false;
|
|
initialized = false;
|
|
// Performance tracking
|
|
retrievalCount = 0;
|
|
totalRetrievalTime = 0;
|
|
distillationCount = 0;
|
|
totalDistillationTime = 0;
|
|
judgeCount = 0;
|
|
totalJudgeTime = 0;
|
|
consolidationCount = 0;
|
|
totalConsolidationTime = 0;
|
|
constructor(config = {}) {
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
}
|
|
// ==========================================================================
|
|
// Initialization
|
|
// ==========================================================================
|
|
/**
|
|
* Initialize ReasoningBank with AgentDB
|
|
*/
|
|
async initialize() {
|
|
if (this.initialized)
|
|
return;
|
|
if (this.config.enableAgentDB) {
|
|
await ensureAgentDBImport();
|
|
this.agentdbAvailable = AgentDB !== undefined;
|
|
if (this.agentdbAvailable) {
|
|
try {
|
|
this.agentdb = new AgentDB({
|
|
dbPath: this.config.dbPath || ':memory:',
|
|
namespace: this.config.namespace,
|
|
vectorDimension: this.config.vectorDimension,
|
|
vectorBackend: 'auto',
|
|
});
|
|
await this.agentdb.initialize();
|
|
this.emitEvent({ type: 'memory_consolidated', memoriesCount: 0 });
|
|
}
|
|
catch (error) {
|
|
console.warn('AgentDB initialization failed, using fallback:', error);
|
|
this.agentdbAvailable = false;
|
|
}
|
|
}
|
|
}
|
|
this.initialized = true;
|
|
}
|
|
/**
|
|
* Shutdown and cleanup resources
|
|
*/
|
|
async shutdown() {
|
|
if (this.agentdb) {
|
|
await this.agentdb.close?.();
|
|
}
|
|
this.initialized = false;
|
|
}
|
|
// ==========================================================================
|
|
// STEP 1: RETRIEVE - Top-k memory injection with MMR diversity
|
|
// ==========================================================================
|
|
/**
|
|
* Retrieve relevant memories using Maximal Marginal Relevance (MMR)
|
|
*
|
|
* Uses AgentDB HNSW index for 150x faster retrieval when available.
|
|
*
|
|
* @param queryEmbedding - Query vector for similarity search
|
|
* @param k - Number of results to return (default: config.retrievalK)
|
|
* @returns Retrieval results with relevance and diversity scores
|
|
*/
|
|
async retrieve(queryEmbedding, k) {
|
|
const startTime = performance.now();
|
|
const retrieveK = k ?? this.config.retrievalK;
|
|
if (this.memories.size === 0) {
|
|
return [];
|
|
}
|
|
let candidates = [];
|
|
// Try AgentDB HNSW search first
|
|
if (this.agentdb && this.agentdbAvailable) {
|
|
try {
|
|
const results = await this.searchWithAgentDB(queryEmbedding, retrieveK * 3);
|
|
candidates = results
|
|
.map(r => {
|
|
const entry = this.memories.get(r.id);
|
|
return entry ? { entry, relevance: r.similarity } : null;
|
|
})
|
|
.filter((c) => c !== null);
|
|
}
|
|
catch {
|
|
// Fall through to brute-force
|
|
}
|
|
}
|
|
// Fallback: brute-force search
|
|
if (candidates.length === 0) {
|
|
for (const entry of this.memories.values()) {
|
|
const relevance = this.cosineSimilarity(queryEmbedding, entry.memory.embedding);
|
|
candidates.push({ entry, relevance });
|
|
}
|
|
candidates.sort((a, b) => b.relevance - a.relevance);
|
|
}
|
|
// Apply MMR for diversity
|
|
const results = [];
|
|
const selected = [];
|
|
while (results.length < retrieveK && candidates.length > 0) {
|
|
let bestIdx = 0;
|
|
let bestScore = -Infinity;
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
const candidate = candidates[i];
|
|
// Compute MMR score: lambda * relevance - (1 - lambda) * max_similarity_to_selected
|
|
const relevance = candidate.relevance;
|
|
let maxSimilarity = 0;
|
|
for (const sel of selected) {
|
|
const sim = this.cosineSimilarity(candidate.entry.memory.embedding, sel.memory.embedding);
|
|
maxSimilarity = Math.max(maxSimilarity, sim);
|
|
}
|
|
const diversityScore = 1 - maxSimilarity;
|
|
const mmrScore = this.config.mmrLambda * relevance +
|
|
(1 - this.config.mmrLambda) * diversityScore;
|
|
if (mmrScore > bestScore) {
|
|
bestScore = mmrScore;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
// Add best candidate
|
|
const best = candidates[bestIdx];
|
|
selected.push(best.entry);
|
|
results.push({
|
|
memory: best.entry.memory,
|
|
relevanceScore: best.relevance,
|
|
diversityScore: 1 - this.computeMaxSimilarity(best.entry, selected.slice(0, -1)),
|
|
combinedScore: bestScore,
|
|
});
|
|
// Remove from candidates
|
|
candidates.splice(bestIdx, 1);
|
|
}
|
|
// Update stats
|
|
this.retrievalCount++;
|
|
this.totalRetrievalTime += performance.now() - startTime;
|
|
return results;
|
|
}
|
|
/**
|
|
* Search for similar memories by content string
|
|
*
|
|
* @param content - Text content to search for
|
|
* @param k - Number of results
|
|
* @returns Retrieval results
|
|
*/
|
|
async retrieveByContent(content, k) {
|
|
// Simple content-based retrieval using memory strategies
|
|
const retrieveK = k ?? this.config.retrievalK;
|
|
const results = [];
|
|
const contentLower = content.toLowerCase();
|
|
const entries = Array.from(this.memories.values());
|
|
// Score by content similarity
|
|
const scored = entries.map(entry => ({
|
|
entry,
|
|
score: this.computeContentSimilarity(contentLower, entry.memory.strategy),
|
|
}));
|
|
scored.sort((a, b) => b.score - a.score);
|
|
for (let i = 0; i < Math.min(retrieveK, scored.length); i++) {
|
|
const { entry, score } = scored[i];
|
|
if (score > 0) {
|
|
results.push({
|
|
memory: entry.memory,
|
|
relevanceScore: score,
|
|
diversityScore: 1,
|
|
combinedScore: score,
|
|
});
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
// ==========================================================================
|
|
// STEP 2: JUDGE - LLM-as-judge trajectory evaluation
|
|
// ==========================================================================
|
|
/**
|
|
* Judge a trajectory and produce a verdict
|
|
*
|
|
* Uses rule-based evaluation to assess trajectory quality.
|
|
* In production, this could be enhanced with LLM-as-judge.
|
|
*
|
|
* @param trajectory - Completed trajectory to judge
|
|
* @returns Verdict with success status and analysis
|
|
*/
|
|
async judge(trajectory) {
|
|
const startTime = performance.now();
|
|
if (!trajectory.isComplete) {
|
|
throw new Error('Cannot judge incomplete trajectory');
|
|
}
|
|
// Analyze trajectory steps
|
|
const stepAnalysis = this.analyzeSteps(trajectory.steps);
|
|
// Compute success based on quality and step analysis
|
|
const success = trajectory.qualityScore >= this.config.distillationThreshold &&
|
|
stepAnalysis.positiveRatio > 0.6;
|
|
// Identify strengths and weaknesses
|
|
const strengths = this.identifyStrengths(trajectory, stepAnalysis);
|
|
const weaknesses = this.identifyWeaknesses(trajectory, stepAnalysis);
|
|
// Generate improvement suggestions
|
|
const improvements = this.generateImprovements(weaknesses);
|
|
// Compute relevance for similar future tasks
|
|
const relevanceScore = this.computeRelevanceScore(trajectory);
|
|
const verdict = {
|
|
success,
|
|
confidence: this.computeConfidence(trajectory, stepAnalysis),
|
|
strengths,
|
|
weaknesses,
|
|
improvements,
|
|
relevanceScore,
|
|
};
|
|
// Store verdict with trajectory
|
|
trajectory.verdict = verdict;
|
|
// Update stats
|
|
this.judgeCount++;
|
|
this.totalJudgeTime += performance.now() - startTime;
|
|
return verdict;
|
|
}
|
|
// ==========================================================================
|
|
// STEP 3: DISTILL - Extract strategy memories from trajectories
|
|
// ==========================================================================
|
|
/**
|
|
* Distill a trajectory into a reusable memory
|
|
*
|
|
* @param trajectory - Trajectory to distill
|
|
* @returns Distilled memory or null if quality too low
|
|
*/
|
|
async distill(trajectory) {
|
|
const startTime = performance.now();
|
|
// Must be judged first
|
|
if (!trajectory.verdict) {
|
|
await this.judge(trajectory);
|
|
}
|
|
// Only distill successful trajectories
|
|
if (!trajectory.verdict.success ||
|
|
trajectory.qualityScore < this.config.distillationThreshold) {
|
|
return null;
|
|
}
|
|
// Extract strategy from trajectory
|
|
const strategy = this.extractStrategy(trajectory);
|
|
// Extract key learnings
|
|
const keyLearnings = this.extractKeyLearnings(trajectory);
|
|
// Compute aggregated embedding
|
|
const embedding = this.computeAggregateEmbedding(trajectory);
|
|
const memory = {
|
|
memoryId: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
trajectoryId: trajectory.trajectoryId,
|
|
strategy,
|
|
keyLearnings,
|
|
embedding,
|
|
quality: trajectory.qualityScore,
|
|
usageCount: 0,
|
|
lastUsed: Date.now(),
|
|
};
|
|
// Store the memory
|
|
const entry = {
|
|
memory,
|
|
trajectory,
|
|
verdict: trajectory.verdict,
|
|
consolidated: false,
|
|
};
|
|
this.memories.set(memory.memoryId, entry);
|
|
// Store in AgentDB for vector search
|
|
if (this.agentdb && this.agentdbAvailable) {
|
|
await this.storeInAgentDB(memory);
|
|
}
|
|
// Also store trajectory reference
|
|
trajectory.distilledMemory = memory;
|
|
// Update stats
|
|
this.distillationCount++;
|
|
this.totalDistillationTime += performance.now() - startTime;
|
|
this.emitEvent({
|
|
type: 'trajectory_completed',
|
|
trajectoryId: trajectory.trajectoryId,
|
|
qualityScore: trajectory.qualityScore,
|
|
});
|
|
return memory;
|
|
}
|
|
/**
|
|
* Batch distill multiple trajectories
|
|
*
|
|
* @param trajectories - Array of trajectories to distill
|
|
* @returns Array of distilled memories (excludes nulls)
|
|
*/
|
|
async distillBatch(trajectories) {
|
|
const memories = [];
|
|
for (const trajectory of trajectories) {
|
|
const memory = await this.distill(trajectory);
|
|
if (memory) {
|
|
memories.push(memory);
|
|
}
|
|
}
|
|
return memories;
|
|
}
|
|
// ==========================================================================
|
|
// STEP 4: CONSOLIDATE - Dedup, detect contradictions, prune old patterns
|
|
// ==========================================================================
|
|
/**
|
|
* Consolidate memories: deduplicate, detect contradictions, prune old
|
|
*
|
|
* @returns Consolidation statistics
|
|
*/
|
|
async consolidate() {
|
|
const startTime = performance.now();
|
|
const result = {
|
|
removedDuplicates: 0,
|
|
contradictionsDetected: 0,
|
|
prunedPatterns: 0,
|
|
mergedPatterns: 0,
|
|
};
|
|
// 1. Deduplicate similar memories
|
|
result.removedDuplicates = await this.deduplicateMemories();
|
|
// 2. Detect contradictions
|
|
if (this.config.enableContradictionDetection) {
|
|
result.contradictionsDetected = await this.detectContradictions();
|
|
}
|
|
// 3. Prune old patterns
|
|
result.prunedPatterns = await this.pruneOldPatterns();
|
|
// 4. Merge similar patterns
|
|
result.mergedPatterns = await this.mergePatterns();
|
|
// Update stats
|
|
this.consolidationCount++;
|
|
this.totalConsolidationTime += performance.now() - startTime;
|
|
// Emit consolidation event
|
|
this.emitEvent({
|
|
type: 'memory_consolidated',
|
|
memoriesCount: this.memories.size,
|
|
});
|
|
return result;
|
|
}
|
|
// ==========================================================================
|
|
// Pattern Management
|
|
// ==========================================================================
|
|
/**
|
|
* Convert a distilled memory to a pattern
|
|
*/
|
|
memoryToPattern(memory) {
|
|
const pattern = {
|
|
patternId: `pat_${memory.memoryId}`,
|
|
name: this.generatePatternName(memory),
|
|
domain: this.inferDomain(memory),
|
|
embedding: memory.embedding,
|
|
strategy: memory.strategy,
|
|
successRate: memory.quality,
|
|
usageCount: memory.usageCount,
|
|
qualityHistory: [memory.quality],
|
|
evolutionHistory: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
};
|
|
this.patterns.set(pattern.patternId, pattern);
|
|
return pattern;
|
|
}
|
|
/**
|
|
* Evolve a pattern based on new experience
|
|
*/
|
|
evolvePattern(patternId, newExperience) {
|
|
const pattern = this.patterns.get(patternId);
|
|
if (!pattern)
|
|
return;
|
|
const previousQuality = pattern.successRate;
|
|
// Update quality history
|
|
pattern.qualityHistory.push(newExperience.qualityScore);
|
|
if (pattern.qualityHistory.length > 100) {
|
|
pattern.qualityHistory = pattern.qualityHistory.slice(-100);
|
|
}
|
|
// Update success rate
|
|
pattern.successRate = pattern.qualityHistory.reduce((a, b) => a + b, 0) /
|
|
pattern.qualityHistory.length;
|
|
pattern.usageCount++;
|
|
pattern.updatedAt = Date.now();
|
|
// Record evolution
|
|
const evolutionType = this.determineEvolutionType(previousQuality, pattern.successRate);
|
|
pattern.evolutionHistory.push({
|
|
timestamp: Date.now(),
|
|
type: evolutionType,
|
|
previousQuality,
|
|
newQuality: pattern.successRate,
|
|
description: `Updated based on trajectory ${newExperience.trajectoryId}`,
|
|
});
|
|
// Emit event
|
|
this.emitEvent({
|
|
type: 'pattern_evolved',
|
|
patternId,
|
|
evolutionType,
|
|
});
|
|
}
|
|
/**
|
|
* Get all patterns
|
|
*/
|
|
getPatterns() {
|
|
return Array.from(this.patterns.values());
|
|
}
|
|
/**
|
|
* Find patterns matching a query
|
|
*/
|
|
async findPatterns(queryEmbedding, k = 5) {
|
|
const results = [];
|
|
for (const pattern of this.patterns.values()) {
|
|
const score = this.cosineSimilarity(queryEmbedding, pattern.embedding);
|
|
results.push({ pattern, score });
|
|
}
|
|
results.sort((a, b) => b.score - a.score);
|
|
return results.slice(0, k).map(r => r.pattern);
|
|
}
|
|
// ==========================================================================
|
|
// Trajectory Management
|
|
// ==========================================================================
|
|
/**
|
|
* Store a trajectory
|
|
*/
|
|
storeTrajectory(trajectory) {
|
|
this.trajectories.set(trajectory.trajectoryId, trajectory);
|
|
// Prune if over capacity
|
|
if (this.trajectories.size > this.config.maxTrajectories) {
|
|
this.pruneTrajectories();
|
|
}
|
|
}
|
|
/**
|
|
* Get trajectory by ID
|
|
*/
|
|
getTrajectory(trajectoryId) {
|
|
return this.trajectories.get(trajectoryId);
|
|
}
|
|
/**
|
|
* Get all trajectories
|
|
*/
|
|
getTrajectories() {
|
|
return Array.from(this.trajectories.values());
|
|
}
|
|
/**
|
|
* Get successful trajectories
|
|
*/
|
|
getSuccessfulTrajectories() {
|
|
return Array.from(this.trajectories.values())
|
|
.filter(t => t.verdict?.success);
|
|
}
|
|
/**
|
|
* Get failed trajectories
|
|
*/
|
|
getFailedTrajectories() {
|
|
return Array.from(this.trajectories.values())
|
|
.filter(t => t.isComplete && !t.verdict?.success);
|
|
}
|
|
// ==========================================================================
|
|
// Statistics
|
|
// ==========================================================================
|
|
/**
|
|
* Get ReasoningBank statistics
|
|
*/
|
|
getStats() {
|
|
return {
|
|
trajectoryCount: this.trajectories.size,
|
|
memoryCount: this.memories.size,
|
|
patternCount: this.patterns.size,
|
|
avgRetrievalTimeMs: this.retrievalCount > 0
|
|
? this.totalRetrievalTime / this.retrievalCount
|
|
: 0,
|
|
avgDistillationTimeMs: this.distillationCount > 0
|
|
? this.totalDistillationTime / this.distillationCount
|
|
: 0,
|
|
avgJudgeTimeMs: this.judgeCount > 0
|
|
? this.totalJudgeTime / this.judgeCount
|
|
: 0,
|
|
avgConsolidationTimeMs: this.consolidationCount > 0
|
|
? this.totalConsolidationTime / this.consolidationCount
|
|
: 0,
|
|
consolidatedMemories: Array.from(this.memories.values())
|
|
.filter(e => e.consolidated).length,
|
|
successfulTrajectories: this.getSuccessfulTrajectories().length,
|
|
failedTrajectories: this.getFailedTrajectories().length,
|
|
agentdbEnabled: this.agentdbAvailable ? 1 : 0,
|
|
retrievalCount: this.retrievalCount,
|
|
distillationCount: this.distillationCount,
|
|
judgeCount: this.judgeCount,
|
|
consolidationCount: this.consolidationCount,
|
|
};
|
|
}
|
|
/**
|
|
* Get detailed metrics for hooks
|
|
*/
|
|
getDetailedMetrics() {
|
|
const successfulTrajectories = this.getSuccessfulTrajectories();
|
|
const allTrajectories = this.getTrajectories();
|
|
const successRate = allTrajectories.length > 0
|
|
? successfulTrajectories.length / allTrajectories.length
|
|
: 0;
|
|
// Extract domain-based routing stats
|
|
const domainStats = new Map();
|
|
for (const t of allTrajectories) {
|
|
const domain = t.domain || 'general';
|
|
const stats = domainStats.get(domain) || { count: 0, successes: 0 };
|
|
stats.count++;
|
|
if (t.verdict?.success)
|
|
stats.successes++;
|
|
domainStats.set(domain, stats);
|
|
}
|
|
const topAgents = Array.from(domainStats.entries())
|
|
.map(([agent, stats]) => ({
|
|
agent,
|
|
count: stats.count,
|
|
successRate: stats.count > 0 ? stats.successes / stats.count : 0,
|
|
}))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 5);
|
|
// Extract common patterns
|
|
const patternStrategies = Array.from(this.patterns.values())
|
|
.sort((a, b) => b.usageCount - a.usageCount)
|
|
.slice(0, 5)
|
|
.map(p => p.strategy);
|
|
return {
|
|
routing: {
|
|
totalRoutes: allTrajectories.length,
|
|
avgConfidence: successfulTrajectories.length > 0
|
|
? successfulTrajectories.reduce((sum, t) => sum + (t.verdict?.confidence || 0), 0) / successfulTrajectories.length
|
|
: 0,
|
|
topAgents,
|
|
},
|
|
edits: {
|
|
totalEdits: this.memories.size,
|
|
successRate,
|
|
commonPatterns: patternStrategies.slice(0, 4),
|
|
},
|
|
commands: {
|
|
totalCommands: this.distillationCount,
|
|
successRate,
|
|
avgExecutionTime: this.totalDistillationTime / Math.max(this.distillationCount, 1),
|
|
commonCommands: patternStrategies.slice(0, 4),
|
|
},
|
|
};
|
|
}
|
|
// ==========================================================================
|
|
// Event System
|
|
// ==========================================================================
|
|
addEventListener(listener) {
|
|
this.eventListeners.add(listener);
|
|
}
|
|
removeEventListener(listener) {
|
|
this.eventListeners.delete(listener);
|
|
}
|
|
emitEvent(event) {
|
|
for (const listener of this.eventListeners) {
|
|
try {
|
|
listener(event);
|
|
}
|
|
catch (error) {
|
|
console.error('Error in ReasoningBank event listener:', error);
|
|
}
|
|
}
|
|
}
|
|
// ==========================================================================
|
|
// AgentDB Integration Helpers
|
|
// ==========================================================================
|
|
/**
|
|
* Store memory in AgentDB for vector search
|
|
*/
|
|
async storeInAgentDB(memory) {
|
|
if (!this.agentdb)
|
|
return;
|
|
try {
|
|
if (typeof this.agentdb.store === 'function') {
|
|
await this.agentdb.store(memory.memoryId, {
|
|
content: memory.strategy,
|
|
embedding: memory.embedding,
|
|
metadata: {
|
|
trajectoryId: memory.trajectoryId,
|
|
quality: memory.quality,
|
|
keyLearnings: memory.keyLearnings,
|
|
usageCount: memory.usageCount,
|
|
lastUsed: memory.lastUsed,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.warn('Failed to store in AgentDB:', error);
|
|
}
|
|
}
|
|
/**
|
|
* Search using AgentDB HNSW index
|
|
*/
|
|
async searchWithAgentDB(queryEmbedding, k) {
|
|
if (!this.agentdb)
|
|
return [];
|
|
try {
|
|
if (typeof this.agentdb.search === 'function') {
|
|
return await this.agentdb.search(queryEmbedding, k);
|
|
}
|
|
// Try HNSW controller if available
|
|
const hnsw = this.agentdb.getController?.('hnsw');
|
|
if (hnsw) {
|
|
const results = await hnsw.search(queryEmbedding, k);
|
|
return results.map((r) => ({
|
|
id: String(r.id),
|
|
similarity: r.similarity || 1 - r.distance,
|
|
}));
|
|
}
|
|
}
|
|
catch {
|
|
// Fall through to return empty
|
|
}
|
|
return [];
|
|
}
|
|
/**
|
|
* Delete from AgentDB
|
|
*/
|
|
async deleteFromAgentDB(memoryId) {
|
|
if (!this.agentdb)
|
|
return;
|
|
try {
|
|
if (typeof this.agentdb.delete === 'function') {
|
|
await this.agentdb.delete(memoryId);
|
|
}
|
|
}
|
|
catch {
|
|
// Ignore deletion errors
|
|
}
|
|
}
|
|
// ==========================================================================
|
|
// Private Helper Methods
|
|
// ==========================================================================
|
|
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);
|
|
// Clamp to [0, 1] to handle floating point precision issues
|
|
const similarity = denom > 0 ? dot / denom : 0;
|
|
return Math.max(0, Math.min(1, similarity));
|
|
}
|
|
computeContentSimilarity(query, content) {
|
|
const queryWords = new Set(query.toLowerCase().split(/\s+/));
|
|
const contentWords = content.toLowerCase().split(/\s+/);
|
|
let matches = 0;
|
|
for (const word of contentWords) {
|
|
if (queryWords.has(word))
|
|
matches++;
|
|
}
|
|
return contentWords.length > 0 ? matches / contentWords.length : 0;
|
|
}
|
|
computeMaxSimilarity(entry, selected) {
|
|
let maxSim = 0;
|
|
for (const sel of selected) {
|
|
const sim = this.cosineSimilarity(entry.memory.embedding, sel.memory.embedding);
|
|
maxSim = Math.max(maxSim, sim);
|
|
}
|
|
return maxSim;
|
|
}
|
|
analyzeSteps(steps) {
|
|
const rewardSum = steps.reduce((s, step) => s + step.reward, 0);
|
|
const positiveSteps = steps.filter(s => s.reward > 0.5).length;
|
|
return {
|
|
totalSteps: steps.length,
|
|
avgReward: steps.length > 0 ? rewardSum / steps.length : 0,
|
|
positiveRatio: steps.length > 0 ? positiveSteps / steps.length : 0,
|
|
trajectory: steps.length > 1
|
|
? (steps[steps.length - 1].reward - steps[0].reward)
|
|
: 0,
|
|
};
|
|
}
|
|
identifyStrengths(trajectory, analysis) {
|
|
const strengths = [];
|
|
if (analysis.avgReward > 0.7) {
|
|
strengths.push('High average reward across steps');
|
|
}
|
|
if (analysis.trajectory > 0.2) {
|
|
strengths.push('Positive reward trajectory');
|
|
}
|
|
if (trajectory.qualityScore > 0.8) {
|
|
strengths.push('High overall quality');
|
|
}
|
|
if (analysis.totalSteps < 5 && trajectory.qualityScore > 0.6) {
|
|
strengths.push('Efficient solution (few steps)');
|
|
}
|
|
return strengths;
|
|
}
|
|
identifyWeaknesses(trajectory, analysis) {
|
|
const weaknesses = [];
|
|
if (analysis.avgReward < 0.4) {
|
|
weaknesses.push('Low average reward');
|
|
}
|
|
if (analysis.trajectory < -0.1) {
|
|
weaknesses.push('Declining reward trajectory');
|
|
}
|
|
if (analysis.positiveRatio < 0.5) {
|
|
weaknesses.push('Many negative/neutral steps');
|
|
}
|
|
if (analysis.totalSteps > 10 && trajectory.qualityScore < 0.7) {
|
|
weaknesses.push('Long trajectory with mediocre outcome');
|
|
}
|
|
return weaknesses;
|
|
}
|
|
generateImprovements(weaknesses) {
|
|
const improvements = [];
|
|
for (const weakness of weaknesses) {
|
|
if (weakness.includes('Low average reward')) {
|
|
improvements.push('Consider alternative strategies for each step');
|
|
}
|
|
if (weakness.includes('Declining')) {
|
|
improvements.push('Re-evaluate approach when reward decreases');
|
|
}
|
|
if (weakness.includes('negative/neutral')) {
|
|
improvements.push('Focus on steps with clearer positive signals');
|
|
}
|
|
if (weakness.includes('Long trajectory')) {
|
|
improvements.push('Look for shortcuts or more direct approaches');
|
|
}
|
|
}
|
|
return improvements;
|
|
}
|
|
computeRelevanceScore(trajectory) {
|
|
// Base relevance on quality and recency
|
|
const qualityFactor = trajectory.qualityScore;
|
|
const ageDays = (Date.now() - trajectory.startTime) / (1000 * 60 * 60 * 24);
|
|
const recencyFactor = Math.exp(-ageDays / 30); // Decay over 30 days
|
|
return qualityFactor * 0.7 + recencyFactor * 0.3;
|
|
}
|
|
computeConfidence(trajectory, analysis) {
|
|
// More steps = more confidence
|
|
const stepFactor = Math.min(analysis.totalSteps / 10, 1);
|
|
// Consistent rewards = more confidence
|
|
const consistencyFactor = analysis.positiveRatio;
|
|
// Clear outcome = more confidence
|
|
const outcomeFactor = Math.abs(trajectory.qualityScore - 0.5) * 2;
|
|
return (stepFactor * 0.3 + consistencyFactor * 0.4 + outcomeFactor * 0.3);
|
|
}
|
|
extractStrategy(trajectory) {
|
|
const actions = trajectory.steps.map(s => s.action);
|
|
const uniqueActions = [...new Set(actions)];
|
|
if (uniqueActions.length <= 3) {
|
|
return `Apply ${uniqueActions.join(' -> ')}`;
|
|
}
|
|
return `Multi-step approach: ${uniqueActions.slice(0, 3).join(', ')}...`;
|
|
}
|
|
extractKeyLearnings(trajectory) {
|
|
const learnings = [];
|
|
const verdict = trajectory.verdict;
|
|
// Add key success factors
|
|
if (verdict.success) {
|
|
learnings.push(`Successful approach for ${trajectory.domain} domain`);
|
|
for (const strength of verdict.strengths.slice(0, 2)) {
|
|
learnings.push(`Strength: ${strength}`);
|
|
}
|
|
}
|
|
else {
|
|
learnings.push(`Approach needs refinement`);
|
|
for (const improvement of verdict.improvements.slice(0, 2)) {
|
|
learnings.push(`Improvement: ${improvement}`);
|
|
}
|
|
}
|
|
return learnings;
|
|
}
|
|
computeAggregateEmbedding(trajectory) {
|
|
if (trajectory.steps.length === 0) {
|
|
return new Float32Array(this.config.vectorDimension);
|
|
}
|
|
const dim = trajectory.steps[0].stateAfter.length;
|
|
const aggregate = new Float32Array(dim);
|
|
// Weighted average of step embeddings (higher weight for later steps)
|
|
let totalWeight = 0;
|
|
for (let i = 0; i < trajectory.steps.length; i++) {
|
|
const weight = (i + 1) / trajectory.steps.length;
|
|
totalWeight += weight;
|
|
const step = trajectory.steps[i];
|
|
for (let j = 0; j < dim; j++) {
|
|
aggregate[j] += step.stateAfter[j] * weight;
|
|
}
|
|
}
|
|
// Normalize
|
|
for (let j = 0; j < dim; j++) {
|
|
aggregate[j] /= totalWeight;
|
|
}
|
|
return aggregate;
|
|
}
|
|
async deduplicateMemories() {
|
|
let removed = 0;
|
|
const entries = Array.from(this.memories.entries());
|
|
for (let i = 0; i < entries.length; i++) {
|
|
for (let j = i + 1; j < entries.length; j++) {
|
|
const sim = this.cosineSimilarity(entries[i][1].memory.embedding, entries[j][1].memory.embedding);
|
|
if (sim > this.config.dedupThreshold) {
|
|
// Keep the higher quality one
|
|
if (entries[i][1].memory.quality >= entries[j][1].memory.quality) {
|
|
this.memories.delete(entries[j][0]);
|
|
await this.deleteFromAgentDB(entries[j][0]);
|
|
}
|
|
else {
|
|
this.memories.delete(entries[i][0]);
|
|
await this.deleteFromAgentDB(entries[i][0]);
|
|
}
|
|
removed++;
|
|
}
|
|
}
|
|
}
|
|
return removed;
|
|
}
|
|
async detectContradictions() {
|
|
let contradictions = 0;
|
|
const entries = Array.from(this.memories.values());
|
|
for (let i = 0; i < entries.length; i++) {
|
|
for (let j = i + 1; j < entries.length; j++) {
|
|
// Similar context but opposite outcomes
|
|
const sim = this.cosineSimilarity(entries[i].memory.embedding, entries[j].memory.embedding);
|
|
if (sim > 0.8) {
|
|
const qualityDiff = Math.abs(entries[i].memory.quality - entries[j].memory.quality);
|
|
if (qualityDiff > 0.4) {
|
|
contradictions++;
|
|
// Mark lower quality as consolidated (to exclude from retrieval)
|
|
if (entries[i].memory.quality < entries[j].memory.quality) {
|
|
entries[i].consolidated = true;
|
|
}
|
|
else {
|
|
entries[j].consolidated = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return contradictions;
|
|
}
|
|
async pruneOldPatterns() {
|
|
const now = Date.now();
|
|
const maxAge = this.config.maxPatternAgeDays * 24 * 60 * 60 * 1000;
|
|
let pruned = 0;
|
|
for (const [id, pattern] of this.patterns) {
|
|
const age = now - pattern.updatedAt;
|
|
if (age > maxAge && pattern.usageCount < 5) {
|
|
this.patterns.delete(id);
|
|
pruned++;
|
|
}
|
|
}
|
|
return pruned;
|
|
}
|
|
async mergePatterns() {
|
|
let merged = 0;
|
|
const patterns = Array.from(this.patterns.entries());
|
|
for (let i = 0; i < patterns.length; i++) {
|
|
for (let j = i + 1; j < patterns.length; j++) {
|
|
const sim = this.cosineSimilarity(patterns[i][1].embedding, patterns[j][1].embedding);
|
|
if (sim > 0.9 && patterns[i][1].domain === patterns[j][1].domain) {
|
|
// Merge into higher quality pattern
|
|
const [keepId, keep] = patterns[i][1].successRate >= patterns[j][1].successRate
|
|
? patterns[i]
|
|
: patterns[j];
|
|
const [removeId, remove] = patterns[i][1].successRate < patterns[j][1].successRate
|
|
? patterns[i]
|
|
: patterns[j];
|
|
// Combine statistics
|
|
keep.usageCount += remove.usageCount;
|
|
keep.qualityHistory.push(...remove.qualityHistory);
|
|
keep.evolutionHistory.push({
|
|
timestamp: Date.now(),
|
|
type: 'merge',
|
|
previousQuality: keep.successRate,
|
|
newQuality: (keep.successRate + remove.successRate) / 2,
|
|
description: `Merged with pattern ${removeId}`,
|
|
});
|
|
this.patterns.delete(removeId);
|
|
merged++;
|
|
}
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
pruneTrajectories() {
|
|
const entries = Array.from(this.trajectories.entries())
|
|
.sort((a, b) => a[1].qualityScore - b[1].qualityScore);
|
|
const toRemove = entries.length - Math.floor(this.config.maxTrajectories * 0.8);
|
|
for (let i = 0; i < toRemove && i < entries.length; i++) {
|
|
this.trajectories.delete(entries[i][0]);
|
|
}
|
|
}
|
|
generatePatternName(memory) {
|
|
const words = memory.strategy.split(' ').slice(0, 4);
|
|
return words.join('_').toLowerCase().replace(/[^a-z0-9_]/g, '');
|
|
}
|
|
inferDomain(memory) {
|
|
// First check if we have the trajectory directly in our store
|
|
const trajectory = this.trajectories.get(memory.trajectoryId);
|
|
if (trajectory?.domain) {
|
|
return trajectory.domain;
|
|
}
|
|
// Check if the memory entry has the trajectory with domain info
|
|
const memoryEntry = this.memories.get(memory.memoryId);
|
|
if (memoryEntry?.trajectory?.domain) {
|
|
return memoryEntry.trajectory.domain;
|
|
}
|
|
return 'general';
|
|
}
|
|
determineEvolutionType(prev, curr) {
|
|
const delta = curr - prev;
|
|
if (delta > 0.05)
|
|
return 'improvement';
|
|
if (delta < -0.1)
|
|
return 'prune';
|
|
return 'improvement';
|
|
}
|
|
/**
|
|
* Check if AgentDB is available and initialized
|
|
*/
|
|
isAgentDBAvailable() {
|
|
return this.agentdbAvailable;
|
|
}
|
|
}
|
|
// ============================================================================
|
|
// Factory Function
|
|
// ============================================================================
|
|
/**
|
|
* Factory function for creating ReasoningBank
|
|
*/
|
|
export function createReasoningBank(config) {
|
|
return new ReasoningBank(config);
|
|
}
|
|
/**
|
|
* Create and initialize a ReasoningBank instance
|
|
*/
|
|
export async function createInitializedReasoningBank(config) {
|
|
const bank = new ReasoningBank(config);
|
|
await bank.initialize();
|
|
return bank;
|
|
}
|
|
//# sourceMappingURL=reasoning-bank.js.map
|