335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
/**
|
|
* LearningBridge - Connects AutoMemoryBridge to NeuralLearningSystem
|
|
*
|
|
* When insights are recorded via AutoMemoryBridge, this module triggers
|
|
* neural learning trajectories so the system continuously improves from
|
|
* its own discoveries. The NeuralLearningSystem dependency is optional:
|
|
* when unavailable, all operations degrade gracefully to no-ops.
|
|
*
|
|
* @module @claude-flow/memory/learning-bridge
|
|
*/
|
|
import { EventEmitter } from 'node:events';
|
|
const DEFAULT_CONFIG = {
|
|
sonaMode: 'balanced',
|
|
confidenceDecayRate: 0.005,
|
|
accessBoostAmount: 0.03,
|
|
maxConfidence: 1.0,
|
|
minConfidence: 0.1,
|
|
ewcLambda: 2000,
|
|
consolidationThreshold: 10,
|
|
enabled: true,
|
|
};
|
|
const MS_PER_HOUR = 3_600_000;
|
|
// ===== LearningBridge =====
|
|
/**
|
|
* Connects AutoMemoryBridge insights to the NeuralLearningSystem.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const bridge = new LearningBridge(memoryBackend);
|
|
* await bridge.onInsightRecorded(insight, entryId);
|
|
* await bridge.onInsightAccessed(entryId);
|
|
* const result = await bridge.consolidate();
|
|
* ```
|
|
*/
|
|
export class LearningBridge extends EventEmitter {
|
|
neural = null;
|
|
backend;
|
|
config;
|
|
activeTrajectories = new Map();
|
|
stats = {
|
|
totalTrajectories: 0,
|
|
completedTrajectories: 0,
|
|
totalConsolidations: 0,
|
|
totalDecays: 0,
|
|
confidenceBoosts: 0,
|
|
totalBoostAmount: 0,
|
|
};
|
|
destroyed = false;
|
|
neuralInitPromise = null;
|
|
constructor(backend, config) {
|
|
super();
|
|
this.backend = backend;
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
}
|
|
// ===== Public API =====
|
|
/**
|
|
* Notify the bridge that an insight has been recorded in AgentDB.
|
|
* Creates a learning trajectory so the neural system can track the
|
|
* insight's lifecycle.
|
|
*/
|
|
async onInsightRecorded(insight, entryId) {
|
|
if (!this.config.enabled || this.destroyed)
|
|
return;
|
|
await this.initNeural();
|
|
if (this.neural) {
|
|
try {
|
|
const trajectoryId = this.neural.beginTask(insight.summary, 'general');
|
|
this.activeTrajectories.set(entryId, trajectoryId);
|
|
this.stats.totalTrajectories++;
|
|
const embedding = this.createHashEmbedding(insight.summary);
|
|
this.neural.recordStep(trajectoryId, {
|
|
action: `record:${insight.category}`,
|
|
reward: insight.confidence,
|
|
stateEmbedding: embedding,
|
|
});
|
|
}
|
|
catch {
|
|
// Neural system failure is non-fatal
|
|
}
|
|
}
|
|
this.emit('insight:learning-started', { entryId, category: insight.category });
|
|
}
|
|
/**
|
|
* Notify the bridge that an insight entry was accessed.
|
|
* Boosts confidence in the backend and records a step in the
|
|
* trajectory if one exists.
|
|
*/
|
|
async onInsightAccessed(entryId) {
|
|
if (!this.config.enabled || this.destroyed)
|
|
return;
|
|
const entry = await this.backend.get(entryId);
|
|
if (!entry)
|
|
return;
|
|
const currentConf = entry.metadata?.confidence ?? 0.5;
|
|
const newConf = Math.min(this.config.maxConfidence, currentConf + this.config.accessBoostAmount);
|
|
await this.backend.update(entryId, {
|
|
metadata: { ...entry.metadata, confidence: newConf },
|
|
});
|
|
this.stats.confidenceBoosts++;
|
|
this.stats.totalBoostAmount += this.config.accessBoostAmount;
|
|
if (this.neural && this.activeTrajectories.has(entryId)) {
|
|
try {
|
|
const trajectoryId = this.activeTrajectories.get(entryId);
|
|
this.neural.recordStep(trajectoryId, {
|
|
action: 'access',
|
|
reward: this.config.accessBoostAmount,
|
|
});
|
|
}
|
|
catch {
|
|
// Non-fatal
|
|
}
|
|
}
|
|
this.emit('insight:accessed', { entryId, newConfidence: newConf });
|
|
}
|
|
/**
|
|
* Consolidate active trajectories by completing them in the neural system.
|
|
* Only runs when there are enough active trajectories to justify the cost.
|
|
*/
|
|
async consolidate() {
|
|
const startTime = Date.now();
|
|
const earlyResult = {
|
|
trajectoriesCompleted: 0,
|
|
patternsLearned: 0,
|
|
entriesUpdated: 0,
|
|
durationMs: 0,
|
|
};
|
|
if (!this.config.enabled || this.destroyed) {
|
|
return earlyResult;
|
|
}
|
|
if (!this.neural || this.activeTrajectories.size < this.config.consolidationThreshold) {
|
|
earlyResult.durationMs = Date.now() - startTime;
|
|
return earlyResult;
|
|
}
|
|
let completed = 0;
|
|
let patternsLearned = 0;
|
|
const toRemove = [];
|
|
const entries = Array.from(this.activeTrajectories.entries());
|
|
for (const [entryId, trajectoryId] of entries) {
|
|
try {
|
|
await this.neural.completeTask(trajectoryId, 1.0);
|
|
completed++;
|
|
patternsLearned++;
|
|
toRemove.push(entryId);
|
|
}
|
|
catch {
|
|
// Skip failed completions
|
|
}
|
|
}
|
|
for (const key of toRemove) {
|
|
this.activeTrajectories.delete(key);
|
|
}
|
|
this.stats.completedTrajectories += completed;
|
|
this.stats.totalConsolidations++;
|
|
const result = {
|
|
trajectoriesCompleted: completed,
|
|
patternsLearned,
|
|
entriesUpdated: completed,
|
|
durationMs: Date.now() - startTime,
|
|
};
|
|
this.emit('consolidation:completed', result);
|
|
return result;
|
|
}
|
|
/**
|
|
* Apply time-based confidence decay to entries in the given namespace.
|
|
* Entries not accessed for more than one hour see their confidence reduced
|
|
* proportionally to the hours elapsed, down to minConfidence.
|
|
*
|
|
* @returns number of entries whose confidence was lowered
|
|
*/
|
|
async decayConfidences(namespace) {
|
|
if (!this.config.enabled || this.destroyed)
|
|
return 0;
|
|
let entries;
|
|
try {
|
|
entries = await this.backend.query({
|
|
type: 'hybrid',
|
|
namespace,
|
|
limit: 1000,
|
|
});
|
|
}
|
|
catch {
|
|
return 0;
|
|
}
|
|
const now = Date.now();
|
|
let decayed = 0;
|
|
for (const entry of entries) {
|
|
const hoursSinceUpdate = (now - entry.updatedAt) / MS_PER_HOUR;
|
|
if (hoursSinceUpdate < 1)
|
|
continue;
|
|
const currentConf = entry.metadata?.confidence ?? 0.5;
|
|
const newConf = Math.max(this.config.minConfidence, currentConf - this.config.confidenceDecayRate * hoursSinceUpdate);
|
|
if (newConf < currentConf) {
|
|
try {
|
|
await this.backend.update(entry.id, {
|
|
metadata: { ...entry.metadata, confidence: newConf },
|
|
});
|
|
decayed++;
|
|
}
|
|
catch {
|
|
// Skip failed updates
|
|
}
|
|
}
|
|
}
|
|
this.stats.totalDecays += decayed;
|
|
return decayed;
|
|
}
|
|
/**
|
|
* Find patterns similar to the given content using the neural system.
|
|
* Returns an empty array when the neural system is unavailable.
|
|
*/
|
|
async findSimilarPatterns(content, k = 5) {
|
|
if (!this.config.enabled || this.destroyed)
|
|
return [];
|
|
await this.initNeural();
|
|
if (!this.neural)
|
|
return [];
|
|
try {
|
|
const embedding = this.createHashEmbedding(content);
|
|
const results = await this.neural.findPatterns(embedding, k);
|
|
if (!Array.isArray(results))
|
|
return [];
|
|
return results.map((r) => ({
|
|
content: r.content ?? r.data ?? '',
|
|
similarity: r.similarity ?? r.score ?? 0,
|
|
category: r.category ?? 'unknown',
|
|
confidence: r.confidence ?? r.reward ?? 0,
|
|
}));
|
|
}
|
|
catch {
|
|
return [];
|
|
}
|
|
}
|
|
/** Return aggregated learning statistics */
|
|
getStats() {
|
|
const avgBoost = this.stats.confidenceBoosts > 0
|
|
? this.stats.totalBoostAmount / this.stats.confidenceBoosts
|
|
: 0;
|
|
return {
|
|
totalTrajectories: this.stats.totalTrajectories,
|
|
completedTrajectories: this.stats.completedTrajectories,
|
|
activeTrajectories: this.activeTrajectories.size,
|
|
totalConsolidations: this.stats.totalConsolidations,
|
|
totalDecays: this.stats.totalDecays,
|
|
avgConfidenceBoost: avgBoost,
|
|
neuralAvailable: this.neural !== null,
|
|
};
|
|
}
|
|
/** Tear down the bridge. Subsequent method calls become no-ops. */
|
|
destroy() {
|
|
this.destroyed = true;
|
|
this.activeTrajectories.clear();
|
|
if (this.neural && typeof this.neural.cleanup === 'function') {
|
|
try {
|
|
this.neural.cleanup();
|
|
}
|
|
catch {
|
|
// Best-effort cleanup
|
|
}
|
|
}
|
|
this.neural = null;
|
|
this.neuralInitPromise = null;
|
|
this.removeAllListeners();
|
|
}
|
|
// ===== Private =====
|
|
/**
|
|
* Lazily attempt to load and initialize the NeuralLearningSystem.
|
|
* The promise is cached so that repeated calls do not re-attempt
|
|
* after a failure.
|
|
*/
|
|
async initNeural() {
|
|
if (this.neural)
|
|
return;
|
|
if (this.neuralInitPromise) {
|
|
await this.neuralInitPromise;
|
|
return;
|
|
}
|
|
this.neuralInitPromise = this.loadNeural();
|
|
await this.neuralInitPromise;
|
|
}
|
|
async loadNeural() {
|
|
try {
|
|
if (this.config.neuralLoader) {
|
|
// Use injected loader (test / custom integrations)
|
|
this.neural = await this.config.neuralLoader();
|
|
return;
|
|
}
|
|
const mod = await import('@claude-flow/neural');
|
|
const NeuralLearningSystem = mod.NeuralLearningSystem ?? mod.default;
|
|
if (!NeuralLearningSystem)
|
|
return;
|
|
const instance = new NeuralLearningSystem({
|
|
mode: this.config.sonaMode,
|
|
ewcLambda: this.config.ewcLambda,
|
|
});
|
|
if (typeof instance.initialize === 'function') {
|
|
await instance.initialize();
|
|
}
|
|
this.neural = instance;
|
|
}
|
|
catch {
|
|
// @claude-flow/neural not installed or failed to initialize.
|
|
// This is expected in many environments; degrade silently.
|
|
this.neural = null;
|
|
}
|
|
}
|
|
/**
|
|
* Create a deterministic hash-based embedding for content.
|
|
* This is a lightweight stand-in for a real embedding model,
|
|
* suitable for pattern matching within the neural trajectory system.
|
|
*/
|
|
createHashEmbedding(text, dimensions = 768) {
|
|
const embedding = new Float32Array(dimensions);
|
|
const normalized = text.toLowerCase().trim();
|
|
for (let i = 0; i < 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;
|
|
}
|
|
let norm = 0;
|
|
for (let i = 0; i < dimensions; i++) {
|
|
norm += embedding[i] * embedding[i];
|
|
}
|
|
norm = Math.sqrt(norm);
|
|
if (norm > 0) {
|
|
for (let i = 0; i < dimensions; i++) {
|
|
embedding[i] /= norm;
|
|
}
|
|
}
|
|
return embedding;
|
|
}
|
|
}
|
|
export default LearningBridge;
|
|
//# sourceMappingURL=learning-bridge.js.map
|