463 lines
18 KiB
JavaScript
463 lines
18 KiB
JavaScript
/**
|
|
* V3 ReasoningBank Adapter
|
|
*
|
|
* Provides agentic-flow@alpha compatible ReasoningBank interface:
|
|
* - 4-step pipeline: RETRIEVE, JUDGE, DISTILL, CONSOLIDATE
|
|
* - Trajectory tracking and verdict judgment
|
|
* - Memory distillation from successful trajectories
|
|
* - Pattern consolidation with deduplication and pruning
|
|
*
|
|
* Based on Algorithm 3 & 4 from ReasoningBank paper.
|
|
*
|
|
* Performance Targets:
|
|
* - Pattern retrieval: <5ms
|
|
* - Verdict judgment: <10ms
|
|
* - Memory distillation: <50ms
|
|
* - Consolidation: <100ms
|
|
*/
|
|
import { createSONAManager } from './sona-manager.js';
|
|
import { createPatternLearner } from './pattern-learner.js';
|
|
// ============================================================================
|
|
// ReasoningBank Adapter Implementation
|
|
// ============================================================================
|
|
export class ReasoningBankAdapter {
|
|
config;
|
|
sonaManager;
|
|
patternLearner;
|
|
patterns = new Map();
|
|
newPatternsSinceConsolidation = 0;
|
|
initialized = false;
|
|
constructor(config) {
|
|
this.config = {
|
|
dbPath: config?.dbPath || '.agentdb/reasoningbank.db',
|
|
enableLearning: config?.enableLearning ?? true,
|
|
enableReasoning: config?.enableReasoning ?? true,
|
|
sonaMode: config?.sonaMode || 'balanced',
|
|
duplicateThreshold: config?.duplicateThreshold ?? 0.95,
|
|
contradictionThreshold: config?.contradictionThreshold ?? 0.85,
|
|
pruneAgeDays: config?.pruneAgeDays ?? 30,
|
|
minConfidenceKeep: config?.minConfidenceKeep ?? 0.3,
|
|
consolidateTriggerThreshold: config?.consolidateTriggerThreshold ?? 100,
|
|
maxItemsSuccess: config?.maxItemsSuccess ?? 5,
|
|
maxItemsFailure: config?.maxItemsFailure ?? 3,
|
|
confidencePriorSuccess: config?.confidencePriorSuccess ?? 0.8,
|
|
confidencePriorFailure: config?.confidencePriorFailure ?? 0.5,
|
|
};
|
|
this.sonaManager = createSONAManager(this.config.sonaMode);
|
|
this.patternLearner = createPatternLearner({
|
|
maxPatterns: 5000,
|
|
matchThreshold: 0.7,
|
|
qualityThreshold: 0.5,
|
|
});
|
|
}
|
|
async initialize() {
|
|
if (this.initialized)
|
|
return;
|
|
await this.sonaManager.initialize();
|
|
this.initialized = true;
|
|
}
|
|
// ==========================================================================
|
|
// Step 1: RETRIEVE - Top-k memory injection with MMR diversity
|
|
// ==========================================================================
|
|
/**
|
|
* Retrieve relevant patterns for a query
|
|
*/
|
|
async retrieve(queryEmbedding, options) {
|
|
const k = options?.k ?? 5;
|
|
const domain = options?.domain;
|
|
const minConfidence = options?.minConfidence ?? 0;
|
|
const useMmr = options?.useMmr ?? true;
|
|
const mmrLambda = options?.mmrLambda ?? 0.7;
|
|
// Get all patterns, filter by domain and confidence
|
|
let candidates = Array.from(this.patterns.values());
|
|
if (domain) {
|
|
candidates = candidates.filter(p => p.domain === domain);
|
|
}
|
|
if (minConfidence > 0) {
|
|
candidates = candidates.filter(p => p.confidence >= minConfidence);
|
|
}
|
|
if (candidates.length === 0) {
|
|
return [];
|
|
}
|
|
// Compute similarities
|
|
const similarities = candidates.map(pattern => ({
|
|
pattern,
|
|
similarity: this.cosineSimilarity(queryEmbedding, pattern.embedding),
|
|
}));
|
|
if (!useMmr) {
|
|
// Simple top-k
|
|
return similarities
|
|
.sort((a, b) => b.similarity - a.similarity)
|
|
.slice(0, k)
|
|
.map(s => s.pattern);
|
|
}
|
|
// Maximal Marginal Relevance (MMR) for diversity
|
|
return this.mmrSelect(queryEmbedding, similarities, k, mmrLambda);
|
|
}
|
|
// ==========================================================================
|
|
// Step 2: JUDGE - LLM-as-judge trajectory evaluation
|
|
// ==========================================================================
|
|
/**
|
|
* Judge a trajectory's success
|
|
*/
|
|
async judge(trajectory) {
|
|
// Compute quality metrics
|
|
const qualityScore = trajectory.qualityScore;
|
|
const stepCount = trajectory.steps.length;
|
|
const avgReward = stepCount > 0
|
|
? trajectory.steps.reduce((sum, s) => sum + s.reward, 0) / stepCount
|
|
: 0;
|
|
// Determine verdict label
|
|
let label;
|
|
if (qualityScore >= 0.8 && avgReward >= 0.7) {
|
|
label = 'Success';
|
|
}
|
|
else if (qualityScore < 0.4 || avgReward < 0.3) {
|
|
label = 'Failure';
|
|
}
|
|
else {
|
|
label = 'Partial';
|
|
}
|
|
// Collect evidence
|
|
const evidence = [];
|
|
evidence.push(`Quality score: ${qualityScore.toFixed(2)}`);
|
|
evidence.push(`Average reward: ${avgReward.toFixed(2)}`);
|
|
evidence.push(`Step count: ${stepCount}`);
|
|
if (trajectory.steps.length > 0) {
|
|
const lastStep = trajectory.steps[trajectory.steps.length - 1];
|
|
evidence.push(`Final action: ${lastStep.action}`);
|
|
evidence.push(`Final reward: ${lastStep.reward.toFixed(2)}`);
|
|
}
|
|
// Generate reasoning
|
|
const reasoning = this.generateJudgmentReasoning(trajectory, label, evidence);
|
|
return {
|
|
label,
|
|
score: qualityScore,
|
|
evidence,
|
|
reasoning,
|
|
};
|
|
}
|
|
// ==========================================================================
|
|
// Step 3: DISTILL - Extract strategy memories from trajectories
|
|
// ==========================================================================
|
|
/**
|
|
* Distill memories from a judged trajectory
|
|
*/
|
|
async distill(trajectory, verdict, options) {
|
|
const maxItems = verdict.label === 'Success'
|
|
? this.config.maxItemsSuccess
|
|
: this.config.maxItemsFailure;
|
|
const confidencePrior = verdict.label === 'Success'
|
|
? this.config.confidencePriorSuccess
|
|
: this.config.confidencePriorFailure;
|
|
const memoryIds = [];
|
|
// Extract key patterns from trajectory
|
|
const patterns = this.extractPatternsFromTrajectory(trajectory, verdict);
|
|
for (let i = 0; i < Math.min(patterns.length, maxItems); i++) {
|
|
const pattern = patterns[i];
|
|
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
const rbPattern = {
|
|
id,
|
|
type: 'reasoning_memory',
|
|
domain: trajectory.domain,
|
|
patternData: {
|
|
title: pattern.title,
|
|
description: pattern.description,
|
|
content: pattern.content,
|
|
source: {
|
|
taskId: options?.taskId || trajectory.trajectoryId,
|
|
agentId: options?.agentId || 'unknown',
|
|
outcome: verdict.label,
|
|
evidence: verdict.evidence,
|
|
},
|
|
tags: pattern.tags,
|
|
domain: trajectory.domain,
|
|
createdAt: new Date().toISOString(),
|
|
confidence: confidencePrior,
|
|
nUses: 0,
|
|
},
|
|
confidence: confidencePrior,
|
|
usageCount: 0,
|
|
embedding: this.computePatternEmbedding(trajectory, i),
|
|
createdAt: Date.now(),
|
|
lastUsed: Date.now(),
|
|
};
|
|
this.patterns.set(id, rbPattern);
|
|
this.newPatternsSinceConsolidation++;
|
|
memoryIds.push(id);
|
|
}
|
|
// Check if consolidation should run
|
|
if (this.shouldConsolidate()) {
|
|
await this.consolidate();
|
|
}
|
|
return memoryIds;
|
|
}
|
|
// ==========================================================================
|
|
// Step 4: CONSOLIDATE - Dedup, detect contradictions, prune old patterns
|
|
// ==========================================================================
|
|
/**
|
|
* Run consolidation: deduplicate, detect contradictions, prune
|
|
*/
|
|
async consolidate() {
|
|
const startTime = Date.now();
|
|
const patterns = Array.from(this.patterns.values());
|
|
let duplicatesFound = 0;
|
|
let contradictionsFound = 0;
|
|
let itemsPruned = 0;
|
|
// Step 1: Deduplicate similar patterns
|
|
const toRemove = new Set();
|
|
for (let i = 0; i < patterns.length; i++) {
|
|
if (toRemove.has(patterns[i].id))
|
|
continue;
|
|
for (let j = i + 1; j < patterns.length; j++) {
|
|
if (toRemove.has(patterns[j].id))
|
|
continue;
|
|
const similarity = this.cosineSimilarity(patterns[i].embedding, patterns[j].embedding);
|
|
if (similarity >= this.config.duplicateThreshold) {
|
|
duplicatesFound++;
|
|
// Keep the one with higher usage/confidence
|
|
const score1 = patterns[i].usageCount * patterns[i].confidence;
|
|
const score2 = patterns[j].usageCount * patterns[j].confidence;
|
|
if (score1 >= score2) {
|
|
toRemove.add(patterns[j].id);
|
|
}
|
|
else {
|
|
toRemove.add(patterns[i].id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Step 2: Detect contradictions (similar embeddings, different outcomes)
|
|
for (let i = 0; i < patterns.length; i++) {
|
|
if (toRemove.has(patterns[i].id))
|
|
continue;
|
|
for (let j = i + 1; j < patterns.length; j++) {
|
|
if (toRemove.has(patterns[j].id))
|
|
continue;
|
|
const similarity = this.cosineSimilarity(patterns[i].embedding, patterns[j].embedding);
|
|
const outcome1 = patterns[i].patternData.source.outcome;
|
|
const outcome2 = patterns[j].patternData.source.outcome;
|
|
if (similarity >= this.config.contradictionThreshold &&
|
|
outcome1 !== outcome2) {
|
|
contradictionsFound++;
|
|
// Log contradiction for analysis (don't auto-remove)
|
|
console.warn(`Contradiction detected: ${patterns[i].id} vs ${patterns[j].id}`);
|
|
}
|
|
}
|
|
}
|
|
// Step 3: Prune old, low-confidence patterns
|
|
const now = Date.now();
|
|
const maxAge = this.config.pruneAgeDays * 24 * 60 * 60 * 1000;
|
|
for (const pattern of patterns) {
|
|
if (toRemove.has(pattern.id))
|
|
continue;
|
|
const age = now - pattern.createdAt;
|
|
if (age > maxAge &&
|
|
pattern.confidence < this.config.minConfidenceKeep &&
|
|
pattern.usageCount < 3) {
|
|
toRemove.add(pattern.id);
|
|
itemsPruned++;
|
|
}
|
|
}
|
|
// Remove marked patterns
|
|
for (const id of toRemove) {
|
|
this.patterns.delete(id);
|
|
}
|
|
this.newPatternsSinceConsolidation = 0;
|
|
const durationMs = Date.now() - startTime;
|
|
return {
|
|
itemsProcessed: patterns.length,
|
|
duplicatesFound,
|
|
contradictionsFound,
|
|
itemsPruned,
|
|
durationMs,
|
|
};
|
|
}
|
|
// ==========================================================================
|
|
// Pattern Management
|
|
// ==========================================================================
|
|
/**
|
|
* Insert a pattern directly
|
|
*/
|
|
insertPattern(pattern) {
|
|
const fullPattern = {
|
|
...pattern,
|
|
createdAt: Date.now(),
|
|
lastUsed: Date.now(),
|
|
};
|
|
this.patterns.set(pattern.id, fullPattern);
|
|
this.newPatternsSinceConsolidation++;
|
|
return pattern.id;
|
|
}
|
|
/**
|
|
* Get a pattern by ID
|
|
*/
|
|
getPattern(id) {
|
|
const pattern = this.patterns.get(id);
|
|
if (pattern) {
|
|
pattern.lastUsed = Date.now();
|
|
pattern.usageCount++;
|
|
}
|
|
return pattern;
|
|
}
|
|
/**
|
|
* Update pattern confidence
|
|
*/
|
|
updateConfidence(id, delta) {
|
|
const pattern = this.patterns.get(id);
|
|
if (pattern) {
|
|
pattern.confidence = Math.max(0, Math.min(1, pattern.confidence + delta));
|
|
pattern.patternData.confidence = pattern.confidence;
|
|
}
|
|
}
|
|
/**
|
|
* Get statistics
|
|
*/
|
|
getStats() {
|
|
const patterns = Array.from(this.patterns.values());
|
|
const byDomain = {};
|
|
const byOutcome = {};
|
|
for (const pattern of patterns) {
|
|
byDomain[pattern.domain] = (byDomain[pattern.domain] || 0) + 1;
|
|
byOutcome[pattern.patternData.source.outcome] =
|
|
(byOutcome[pattern.patternData.source.outcome] || 0) + 1;
|
|
}
|
|
const avgConfidence = patterns.length > 0
|
|
? patterns.reduce((sum, p) => sum + p.confidence, 0) / patterns.length
|
|
: 0;
|
|
return {
|
|
totalPatterns: patterns.length,
|
|
byDomain,
|
|
byOutcome,
|
|
avgConfidence,
|
|
};
|
|
}
|
|
// ==========================================================================
|
|
// Private Helper Methods
|
|
// ==========================================================================
|
|
cosineSimilarity(a, b) {
|
|
if (a.length !== b.length)
|
|
return 0;
|
|
let dot = 0;
|
|
let normA = 0;
|
|
let 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;
|
|
}
|
|
mmrSelect(query, candidates, k, lambda) {
|
|
const selected = [];
|
|
const remaining = [...candidates];
|
|
while (selected.length < k && remaining.length > 0) {
|
|
let bestScore = -Infinity;
|
|
let bestIdx = 0;
|
|
for (let i = 0; i < remaining.length; i++) {
|
|
const candidate = remaining[i];
|
|
const relevance = candidate.similarity;
|
|
// Calculate max similarity to already selected patterns
|
|
let maxSimilarity = 0;
|
|
for (const sel of selected) {
|
|
const sim = this.cosineSimilarity(candidate.pattern.embedding, sel.embedding);
|
|
maxSimilarity = Math.max(maxSimilarity, sim);
|
|
}
|
|
// MMR score: lambda * relevance - (1 - lambda) * redundancy
|
|
const score = lambda * relevance - (1 - lambda) * maxSimilarity;
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
selected.push(remaining[bestIdx].pattern);
|
|
remaining.splice(bestIdx, 1);
|
|
}
|
|
return selected;
|
|
}
|
|
shouldConsolidate() {
|
|
return this.newPatternsSinceConsolidation >= this.config.consolidateTriggerThreshold;
|
|
}
|
|
generateJudgmentReasoning(trajectory, label, evidence) {
|
|
const parts = [];
|
|
parts.push(`Trajectory ${trajectory.trajectoryId} judged as ${label}.`);
|
|
parts.push(`Domain: ${trajectory.domain}, Steps: ${trajectory.steps.length}.`);
|
|
if (label === 'Success') {
|
|
parts.push('The trajectory achieved high quality scores with positive rewards.');
|
|
}
|
|
else if (label === 'Failure') {
|
|
parts.push('The trajectory had low quality scores or negative rewards.');
|
|
}
|
|
else {
|
|
parts.push('The trajectory showed mixed results with room for improvement.');
|
|
}
|
|
return parts.join(' ');
|
|
}
|
|
extractPatternsFromTrajectory(trajectory, verdict) {
|
|
const patterns = [];
|
|
// Extract overall strategy pattern
|
|
const actions = trajectory.steps.map(s => s.action);
|
|
patterns.push({
|
|
title: `${verdict.label}: ${trajectory.context.slice(0, 50)}`,
|
|
description: `Strategy for ${trajectory.domain} task with ${verdict.label.toLowerCase()} outcome`,
|
|
content: `Actions: ${actions.slice(0, 5).join(' -> ')}${actions.length > 5 ? '...' : ''}`,
|
|
tags: [verdict.label.toLowerCase(), trajectory.domain, 'strategy'],
|
|
});
|
|
// Extract key step patterns for successful trajectories
|
|
if (verdict.label === 'Success' && trajectory.steps.length > 0) {
|
|
const highRewardSteps = trajectory.steps
|
|
.filter(s => s.reward > 0.7)
|
|
.slice(0, 3);
|
|
for (const step of highRewardSteps) {
|
|
patterns.push({
|
|
title: `High-reward action: ${step.action.slice(0, 30)}`,
|
|
description: `Effective action in ${trajectory.domain} context`,
|
|
content: `Action: ${step.action}, Reward: ${step.reward.toFixed(2)}`,
|
|
tags: ['high-reward', trajectory.domain, 'action'],
|
|
});
|
|
}
|
|
}
|
|
return patterns;
|
|
}
|
|
computePatternEmbedding(trajectory, index) {
|
|
if (trajectory.steps.length === 0) {
|
|
return new Float32Array(768);
|
|
}
|
|
// Use weighted average of step embeddings
|
|
const dim = trajectory.steps[0].stateAfter.length;
|
|
const embedding = new Float32Array(dim);
|
|
let totalWeight = 0;
|
|
for (let i = 0; i < trajectory.steps.length; i++) {
|
|
const weight = (i + 1 + index) / (trajectory.steps.length + index);
|
|
totalWeight += weight;
|
|
for (let j = 0; j < dim; j++) {
|
|
embedding[j] += trajectory.steps[i].stateAfter[j] * weight;
|
|
}
|
|
}
|
|
for (let j = 0; j < dim; j++) {
|
|
embedding[j] /= totalWeight;
|
|
}
|
|
return embedding;
|
|
}
|
|
}
|
|
// ============================================================================
|
|
// Factory Functions
|
|
// ============================================================================
|
|
/**
|
|
* Create ReasoningBank adapter
|
|
*/
|
|
export function createReasoningBankAdapter(config) {
|
|
return new ReasoningBankAdapter(config);
|
|
}
|
|
/**
|
|
* Create default ReasoningBank adapter
|
|
*/
|
|
export function createDefaultReasoningBankAdapter() {
|
|
return new ReasoningBankAdapter({
|
|
enableLearning: true,
|
|
enableReasoning: true,
|
|
sonaMode: 'balanced',
|
|
});
|
|
}
|
|
//# sourceMappingURL=reasoningbank-adapter.js.map
|