844 lines
26 KiB
JavaScript
844 lines
26 KiB
JavaScript
/**
|
|
* AgentDB Backend - Integration with agentdb@2.0.0-alpha.3.4
|
|
*
|
|
* Provides IMemoryBackend implementation using AgentDB with:
|
|
* - HNSW vector search (150x-12,500x faster than brute-force)
|
|
* - Native or WASM backend support with graceful fallback
|
|
* - Optional dependency handling (works without hnswlib-node)
|
|
* - Seamless integration with HybridBackend
|
|
*
|
|
* @module v3/memory/agentdb-backend
|
|
*/
|
|
import { EventEmitter } from 'node:events';
|
|
// ===== AgentDB Optional Import =====
|
|
let AgentDB;
|
|
let HNSWIndex;
|
|
let isHnswlibAvailable;
|
|
// Dynamically import agentdb (handled at runtime)
|
|
let agentdbImportPromise;
|
|
function ensureAgentDBImport() {
|
|
if (!agentdbImportPromise) {
|
|
agentdbImportPromise = (async () => {
|
|
try {
|
|
const agentdbModule = await import('agentdb');
|
|
AgentDB = agentdbModule.AgentDB || agentdbModule.default;
|
|
HNSWIndex = agentdbModule.HNSWIndex;
|
|
isHnswlibAvailable = agentdbModule.isHnswlibAvailable;
|
|
}
|
|
catch (error) {
|
|
// AgentDB not available - will use fallback
|
|
}
|
|
})();
|
|
}
|
|
return agentdbImportPromise;
|
|
}
|
|
/**
|
|
* Default configuration
|
|
*/
|
|
const DEFAULT_CONFIG = {
|
|
namespace: 'default',
|
|
forceWasm: false,
|
|
vectorBackend: 'auto',
|
|
vectorDimension: 1536,
|
|
hnswM: 16,
|
|
hnswEfConstruction: 200,
|
|
hnswEfSearch: 100,
|
|
cacheEnabled: true,
|
|
maxEntries: 1000000,
|
|
};
|
|
// ===== AgentDB Backend Implementation =====
|
|
/**
|
|
* AgentDB Backend
|
|
*
|
|
* Integrates AgentDB for vector search with the V3 memory system.
|
|
* Provides 150x-12,500x faster search compared to brute-force approaches.
|
|
*
|
|
* Features:
|
|
* - HNSW indexing for fast approximate nearest neighbor search
|
|
* - Automatic fallback: native hnswlib → ruvector → WASM
|
|
* - Graceful handling of optional native dependencies
|
|
* - Semantic search with filtering
|
|
* - Compatible with HybridBackend for combined SQLite+AgentDB queries
|
|
*/
|
|
export class AgentDBBackend extends EventEmitter {
|
|
config;
|
|
agentdb;
|
|
initialized = false;
|
|
available = false;
|
|
// In-memory storage for compatibility
|
|
entries = new Map();
|
|
namespaceIndex = new Map();
|
|
keyIndex = new Map();
|
|
// O(1) reverse lookup for numeric ID -> string ID (fixes O(n) linear scan)
|
|
numericToStringIdMap = new Map();
|
|
// Performance tracking
|
|
stats = {
|
|
queryCount: 0,
|
|
totalQueryTime: 0,
|
|
searchCount: 0,
|
|
totalSearchTime: 0,
|
|
};
|
|
constructor(config = {}) {
|
|
super();
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.available = false; // Will be set during initialization
|
|
}
|
|
/**
|
|
* Initialize AgentDB
|
|
*/
|
|
async initialize() {
|
|
if (this.initialized)
|
|
return;
|
|
// Try to import AgentDB
|
|
await ensureAgentDBImport();
|
|
this.available = AgentDB !== undefined;
|
|
if (!this.available) {
|
|
console.warn('AgentDB not available, using fallback in-memory storage');
|
|
this.initialized = true;
|
|
return;
|
|
}
|
|
try {
|
|
// Initialize AgentDB with config
|
|
this.agentdb = new AgentDB({
|
|
dbPath: this.config.dbPath || ':memory:',
|
|
namespace: this.config.namespace,
|
|
forceWasm: this.config.forceWasm,
|
|
vectorBackend: this.config.vectorBackend,
|
|
vectorDimension: this.config.vectorDimension,
|
|
});
|
|
// Suppress agentdb's noisy console.log during init
|
|
// (EmbeddingService, AgentDB core emit info-level logs we don't need)
|
|
const origLog = console.log;
|
|
console.log = (...args) => {
|
|
const msg = String(args[0] ?? '');
|
|
if (msg.includes('Transformers.js loaded') ||
|
|
msg.includes('Using better-sqlite3') ||
|
|
msg.includes('better-sqlite3 unavailable') ||
|
|
msg.includes('[AgentDB]'))
|
|
return;
|
|
origLog.apply(console, args);
|
|
};
|
|
try {
|
|
await this.agentdb.initialize();
|
|
}
|
|
finally {
|
|
console.log = origLog;
|
|
}
|
|
// Create memory_entries table if it doesn't exist
|
|
await this.createSchema();
|
|
this.initialized = true;
|
|
this.emit('initialized', {
|
|
backend: this.agentdb.vectorBackendName,
|
|
isWasm: this.agentdb.isWasm,
|
|
});
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to initialize AgentDB:', error);
|
|
this.available = false;
|
|
this.initialized = true;
|
|
this.emit('initialization:failed', { error });
|
|
}
|
|
}
|
|
/**
|
|
* Shutdown AgentDB
|
|
*/
|
|
async shutdown() {
|
|
if (!this.initialized)
|
|
return;
|
|
if (this.agentdb) {
|
|
await this.agentdb.close();
|
|
}
|
|
this.initialized = false;
|
|
this.emit('shutdown');
|
|
}
|
|
/**
|
|
* Store a memory entry
|
|
*/
|
|
async store(entry) {
|
|
// Generate embedding if needed
|
|
if (entry.content && !entry.embedding && this.config.embeddingGenerator) {
|
|
entry.embedding = await this.config.embeddingGenerator(entry.content);
|
|
}
|
|
// Store in-memory for quick access
|
|
this.entries.set(entry.id, entry);
|
|
// Register ID mapping for O(1) reverse lookup
|
|
this.registerIdMapping(entry.id);
|
|
// Update indexes
|
|
this.updateIndexes(entry);
|
|
// Store in AgentDB if available
|
|
if (this.agentdb) {
|
|
await this.storeInAgentDB(entry);
|
|
}
|
|
this.emit('entry:stored', { id: entry.id });
|
|
}
|
|
/**
|
|
* Get entry by ID
|
|
*/
|
|
async get(id) {
|
|
// Check in-memory first
|
|
const cached = this.entries.get(id);
|
|
if (cached)
|
|
return cached;
|
|
// Query AgentDB if available
|
|
if (this.agentdb) {
|
|
return this.getFromAgentDB(id);
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Get entry by key
|
|
*/
|
|
async getByKey(namespace, key) {
|
|
const keyIndexKey = `${namespace}:${key}`;
|
|
const id = this.keyIndex.get(keyIndexKey);
|
|
if (!id)
|
|
return null;
|
|
return this.get(id);
|
|
}
|
|
/**
|
|
* Update entry
|
|
*/
|
|
async update(id, update) {
|
|
const entry = this.entries.get(id);
|
|
if (!entry)
|
|
return null;
|
|
// Apply updates
|
|
if (update.content !== undefined) {
|
|
entry.content = update.content;
|
|
// Regenerate embedding if needed
|
|
if (this.config.embeddingGenerator) {
|
|
entry.embedding = await this.config.embeddingGenerator(entry.content);
|
|
}
|
|
}
|
|
if (update.tags !== undefined) {
|
|
entry.tags = update.tags;
|
|
}
|
|
if (update.metadata !== undefined) {
|
|
entry.metadata = { ...entry.metadata, ...update.metadata };
|
|
}
|
|
if (update.accessLevel !== undefined) {
|
|
entry.accessLevel = update.accessLevel;
|
|
}
|
|
if (update.expiresAt !== undefined) {
|
|
entry.expiresAt = update.expiresAt;
|
|
}
|
|
if (update.references !== undefined) {
|
|
entry.references = update.references;
|
|
}
|
|
entry.updatedAt = Date.now();
|
|
entry.version++;
|
|
// Update in AgentDB
|
|
if (this.agentdb) {
|
|
await this.updateInAgentDB(entry);
|
|
}
|
|
this.emit('entry:updated', { id });
|
|
return entry;
|
|
}
|
|
/**
|
|
* Delete entry
|
|
*/
|
|
async delete(id) {
|
|
const entry = this.entries.get(id);
|
|
if (!entry)
|
|
return false;
|
|
// Remove from indexes
|
|
this.entries.delete(id);
|
|
this.unregisterIdMapping(id); // Clean up reverse lookup map
|
|
this.namespaceIndex.get(entry.namespace)?.delete(id);
|
|
const keyIndexKey = `${entry.namespace}:${entry.key}`;
|
|
this.keyIndex.delete(keyIndexKey);
|
|
// Delete from AgentDB
|
|
if (this.agentdb) {
|
|
await this.deleteFromAgentDB(id);
|
|
}
|
|
this.emit('entry:deleted', { id });
|
|
return true;
|
|
}
|
|
/**
|
|
* Query entries
|
|
*/
|
|
async query(query) {
|
|
const startTime = performance.now();
|
|
let results = [];
|
|
if (query.type === 'semantic' && (query.embedding || query.content)) {
|
|
// Use semantic search
|
|
const searchResults = await this.semanticSearch(query);
|
|
results = searchResults.map((r) => r.entry);
|
|
}
|
|
else {
|
|
// Fallback to in-memory filtering
|
|
results = this.queryInMemory(query);
|
|
}
|
|
const duration = performance.now() - startTime;
|
|
this.stats.queryCount++;
|
|
this.stats.totalQueryTime += duration;
|
|
return results;
|
|
}
|
|
/**
|
|
* Semantic vector search
|
|
*/
|
|
async search(embedding, options) {
|
|
const startTime = performance.now();
|
|
if (!this.agentdb) {
|
|
// Fallback to brute-force search
|
|
return this.bruteForceSearch(embedding, options);
|
|
}
|
|
try {
|
|
// Use AgentDB HNSW search
|
|
const results = await this.searchWithAgentDB(embedding, options);
|
|
const duration = performance.now() - startTime;
|
|
this.stats.searchCount++;
|
|
this.stats.totalSearchTime += duration;
|
|
return results;
|
|
}
|
|
catch (error) {
|
|
console.error('AgentDB search failed, falling back to brute-force:', error);
|
|
return this.bruteForceSearch(embedding, options);
|
|
}
|
|
}
|
|
/**
|
|
* Bulk insert
|
|
*/
|
|
async bulkInsert(entries) {
|
|
for (const entry of entries) {
|
|
await this.store(entry);
|
|
}
|
|
}
|
|
/**
|
|
* Bulk delete
|
|
*/
|
|
async bulkDelete(ids) {
|
|
let deleted = 0;
|
|
for (const id of ids) {
|
|
if (await this.delete(id)) {
|
|
deleted++;
|
|
}
|
|
}
|
|
return deleted;
|
|
}
|
|
/**
|
|
* Count entries
|
|
*/
|
|
async count(namespace) {
|
|
if (namespace) {
|
|
return this.namespaceIndex.get(namespace)?.size || 0;
|
|
}
|
|
return this.entries.size;
|
|
}
|
|
/**
|
|
* List namespaces
|
|
*/
|
|
async listNamespaces() {
|
|
return Array.from(this.namespaceIndex.keys());
|
|
}
|
|
/**
|
|
* Clear namespace
|
|
*/
|
|
async clearNamespace(namespace) {
|
|
const ids = this.namespaceIndex.get(namespace);
|
|
if (!ids)
|
|
return 0;
|
|
let deleted = 0;
|
|
for (const id of ids) {
|
|
if (await this.delete(id)) {
|
|
deleted++;
|
|
}
|
|
}
|
|
return deleted;
|
|
}
|
|
/**
|
|
* Get statistics
|
|
*/
|
|
async getStats() {
|
|
const entriesByNamespace = {};
|
|
for (const [namespace, ids] of this.namespaceIndex) {
|
|
entriesByNamespace[namespace] = ids.size;
|
|
}
|
|
const entriesByType = {
|
|
episodic: 0,
|
|
semantic: 0,
|
|
procedural: 0,
|
|
working: 0,
|
|
cache: 0,
|
|
};
|
|
for (const entry of this.entries.values()) {
|
|
entriesByType[entry.type]++;
|
|
}
|
|
// Get HNSW stats if available
|
|
let hnswStats;
|
|
if (this.agentdb && HNSWIndex) {
|
|
try {
|
|
const hnsw = this.agentdb.getController('hnsw');
|
|
if (hnsw) {
|
|
const stats = hnsw.getStats();
|
|
hnswStats = {
|
|
vectorCount: stats.numElements || 0,
|
|
memoryUsage: 0,
|
|
avgSearchTime: stats.avgSearchTimeMs || 0,
|
|
buildTime: stats.lastBuildTime || 0,
|
|
compressionRatio: 1.0,
|
|
};
|
|
}
|
|
}
|
|
catch {
|
|
// HNSW not available
|
|
}
|
|
}
|
|
return {
|
|
totalEntries: this.entries.size,
|
|
entriesByNamespace,
|
|
entriesByType,
|
|
memoryUsage: this.estimateMemoryUsage(),
|
|
hnswStats,
|
|
avgQueryTime: this.stats.queryCount > 0
|
|
? this.stats.totalQueryTime / this.stats.queryCount
|
|
: 0,
|
|
avgSearchTime: this.stats.searchCount > 0
|
|
? this.stats.totalSearchTime / this.stats.searchCount
|
|
: 0,
|
|
};
|
|
}
|
|
/**
|
|
* Health check
|
|
*/
|
|
async healthCheck() {
|
|
const issues = [];
|
|
const recommendations = [];
|
|
// Check AgentDB availability
|
|
const storageHealth = this.agentdb
|
|
? { status: 'healthy', latency: 0 }
|
|
: {
|
|
status: 'degraded',
|
|
latency: 0,
|
|
message: 'AgentDB not available, using fallback',
|
|
};
|
|
// Check index health
|
|
const indexHealth = { status: 'healthy', latency: 0 };
|
|
if (!this.agentdb) {
|
|
indexHealth.status = 'degraded';
|
|
indexHealth.message = 'HNSW index not available';
|
|
recommendations.push('Install agentdb for 150x-12,500x faster vector search');
|
|
}
|
|
// Check cache health
|
|
const cacheHealth = { status: 'healthy', latency: 0 };
|
|
const status = storageHealth.status === 'unhealthy' || indexHealth.status === 'unhealthy'
|
|
? 'unhealthy'
|
|
: storageHealth.status === 'degraded' || indexHealth.status === 'degraded'
|
|
? 'degraded'
|
|
: 'healthy';
|
|
return {
|
|
status,
|
|
components: {
|
|
storage: storageHealth,
|
|
index: indexHealth,
|
|
cache: cacheHealth,
|
|
},
|
|
timestamp: Date.now(),
|
|
issues,
|
|
recommendations,
|
|
};
|
|
}
|
|
// ===== Private Methods =====
|
|
/**
|
|
* Create database schema
|
|
*/
|
|
async createSchema() {
|
|
if (!this.agentdb)
|
|
return;
|
|
const db = this.agentdb.database;
|
|
if (!db || typeof db.run !== 'function') {
|
|
// AgentDB doesn't expose raw database - using native API
|
|
return;
|
|
}
|
|
try {
|
|
// Create memory_entries table
|
|
await db.run(`
|
|
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
embedding BLOB,
|
|
type TEXT NOT NULL,
|
|
namespace TEXT NOT NULL,
|
|
tags TEXT,
|
|
metadata TEXT,
|
|
owner_id TEXT,
|
|
access_level TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
expires_at INTEGER,
|
|
version INTEGER NOT NULL,
|
|
references TEXT,
|
|
access_count INTEGER DEFAULT 0,
|
|
last_accessed_at INTEGER
|
|
)
|
|
`);
|
|
// Create indexes
|
|
await db.run('CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)');
|
|
await db.run('CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)');
|
|
await db.run('CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type)');
|
|
}
|
|
catch {
|
|
// Schema creation failed - using in-memory only
|
|
}
|
|
}
|
|
/**
|
|
* Store entry in AgentDB
|
|
*/
|
|
async storeInAgentDB(entry) {
|
|
if (!this.agentdb)
|
|
return;
|
|
// Try to use agentdb's native store method if available
|
|
try {
|
|
if (typeof this.agentdb.store === 'function') {
|
|
await this.agentdb.store(entry.id, {
|
|
key: entry.key,
|
|
content: entry.content,
|
|
embedding: entry.embedding,
|
|
type: entry.type,
|
|
namespace: entry.namespace,
|
|
tags: entry.tags,
|
|
metadata: entry.metadata,
|
|
});
|
|
return;
|
|
}
|
|
// Fallback: use database directly if available
|
|
const db = this.agentdb.database;
|
|
if (!db || typeof db.run !== 'function') {
|
|
// No compatible database interface - skip agentdb storage
|
|
// Entry is already stored in-memory
|
|
return;
|
|
}
|
|
await db.run(`
|
|
INSERT OR REPLACE INTO memory_entries
|
|
(id, key, content, embedding, type, namespace, tags, metadata, owner_id,
|
|
access_level, created_at, updated_at, expires_at, version, references,
|
|
access_count, last_accessed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
entry.id,
|
|
entry.key,
|
|
entry.content,
|
|
entry.embedding ? Buffer.from(entry.embedding.buffer) : null,
|
|
entry.type,
|
|
entry.namespace,
|
|
JSON.stringify(entry.tags),
|
|
JSON.stringify(entry.metadata),
|
|
entry.ownerId || null,
|
|
entry.accessLevel,
|
|
entry.createdAt,
|
|
entry.updatedAt,
|
|
entry.expiresAt || null,
|
|
entry.version,
|
|
JSON.stringify(entry.references),
|
|
entry.accessCount,
|
|
entry.lastAccessedAt,
|
|
]);
|
|
}
|
|
catch {
|
|
// AgentDB storage failed - entry is already in-memory
|
|
}
|
|
// Add to vector index if HNSW is available
|
|
if (entry.embedding && HNSWIndex) {
|
|
try {
|
|
const hnsw = this.agentdb.getController('hnsw');
|
|
if (hnsw) {
|
|
// Convert string ID to number for HNSW (use hash)
|
|
const numericId = this.stringIdToNumeric(entry.id);
|
|
hnsw.addVector(numericId, entry.embedding);
|
|
}
|
|
}
|
|
catch {
|
|
// HNSW not available
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get entry from AgentDB
|
|
*/
|
|
async getFromAgentDB(id) {
|
|
if (!this.agentdb)
|
|
return null;
|
|
try {
|
|
// Try native get method first
|
|
if (typeof this.agentdb.get === 'function') {
|
|
const data = await this.agentdb.get(id);
|
|
if (data)
|
|
return this.dataToEntry(id, data);
|
|
}
|
|
// Fallback to database
|
|
const db = this.agentdb.database;
|
|
if (!db || typeof db.get !== 'function')
|
|
return null;
|
|
const row = await db.get('SELECT * FROM memory_entries WHERE id = ?', [id]);
|
|
if (!row)
|
|
return null;
|
|
return this.rowToEntry(row);
|
|
}
|
|
catch {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Convert agentdb data to MemoryEntry
|
|
*/
|
|
dataToEntry(id, data) {
|
|
const now = Date.now();
|
|
return {
|
|
id,
|
|
key: data.key || id,
|
|
content: data.content || '',
|
|
embedding: data.embedding,
|
|
type: data.type || 'semantic',
|
|
namespace: data.namespace || this.config.namespace,
|
|
tags: data.tags || [],
|
|
metadata: data.metadata || {},
|
|
ownerId: data.ownerId,
|
|
accessLevel: data.accessLevel || 'private',
|
|
createdAt: data.createdAt || now,
|
|
updatedAt: data.updatedAt || now,
|
|
expiresAt: data.expiresAt,
|
|
version: data.version || 1,
|
|
references: data.references || [],
|
|
accessCount: data.accessCount || 0,
|
|
lastAccessedAt: data.lastAccessedAt || now,
|
|
};
|
|
}
|
|
/**
|
|
* Update entry in AgentDB
|
|
*/
|
|
async updateInAgentDB(entry) {
|
|
await this.storeInAgentDB(entry);
|
|
}
|
|
/**
|
|
* Delete entry from AgentDB
|
|
*/
|
|
async deleteFromAgentDB(id) {
|
|
if (!this.agentdb)
|
|
return;
|
|
try {
|
|
// Try native delete method first
|
|
if (typeof this.agentdb.delete === 'function') {
|
|
await this.agentdb.delete(id);
|
|
return;
|
|
}
|
|
// Fallback to database
|
|
const db = this.agentdb.database;
|
|
if (!db || typeof db.run !== 'function')
|
|
return;
|
|
await db.run('DELETE FROM memory_entries WHERE id = ?', [id]);
|
|
}
|
|
catch {
|
|
// Delete failed - entry removed from in-memory
|
|
}
|
|
}
|
|
/**
|
|
* Search with AgentDB HNSW
|
|
*/
|
|
async searchWithAgentDB(embedding, options) {
|
|
if (!this.agentdb || !HNSWIndex) {
|
|
return [];
|
|
}
|
|
try {
|
|
const hnsw = this.agentdb.getController('hnsw');
|
|
if (!hnsw) {
|
|
return this.bruteForceSearch(embedding, options);
|
|
}
|
|
const results = await hnsw.search(embedding, options.k, {
|
|
threshold: options.threshold,
|
|
});
|
|
const searchResults = [];
|
|
for (const result of results) {
|
|
const id = this.numericIdToString(result.id);
|
|
const entry = await this.get(id);
|
|
if (!entry)
|
|
continue;
|
|
searchResults.push({
|
|
entry,
|
|
score: result.similarity,
|
|
distance: result.distance,
|
|
});
|
|
}
|
|
return searchResults;
|
|
}
|
|
catch (error) {
|
|
console.error('HNSW search failed:', error);
|
|
return this.bruteForceSearch(embedding, options);
|
|
}
|
|
}
|
|
/**
|
|
* Brute-force vector search fallback
|
|
*/
|
|
bruteForceSearch(embedding, options) {
|
|
const results = [];
|
|
for (const entry of this.entries.values()) {
|
|
if (!entry.embedding)
|
|
continue;
|
|
const score = this.cosineSimilarity(embedding, entry.embedding);
|
|
const distance = 1 - score;
|
|
if (options.threshold && score < options.threshold)
|
|
continue;
|
|
results.push({ entry, score, distance });
|
|
}
|
|
// Sort by score descending
|
|
results.sort((a, b) => b.score - a.score);
|
|
return results.slice(0, options.k);
|
|
}
|
|
/**
|
|
* Semantic search helper
|
|
*/
|
|
async semanticSearch(query) {
|
|
let embedding = query.embedding;
|
|
if (!embedding && query.content && this.config.embeddingGenerator) {
|
|
embedding = await this.config.embeddingGenerator(query.content);
|
|
}
|
|
if (!embedding) {
|
|
return [];
|
|
}
|
|
return this.search(embedding, {
|
|
k: query.limit,
|
|
threshold: query.threshold,
|
|
filters: query,
|
|
});
|
|
}
|
|
/**
|
|
* In-memory query fallback
|
|
*/
|
|
queryInMemory(query) {
|
|
let results = Array.from(this.entries.values());
|
|
// Apply filters
|
|
if (query.namespace) {
|
|
results = results.filter((e) => e.namespace === query.namespace);
|
|
}
|
|
if (query.key) {
|
|
results = results.filter((e) => e.key === query.key);
|
|
}
|
|
if (query.keyPrefix) {
|
|
results = results.filter((e) => e.key.startsWith(query.keyPrefix));
|
|
}
|
|
if (query.tags && query.tags.length > 0) {
|
|
results = results.filter((e) => query.tags.every((tag) => e.tags.includes(tag)));
|
|
}
|
|
return results.slice(0, query.limit);
|
|
}
|
|
/**
|
|
* Update in-memory indexes
|
|
*/
|
|
updateIndexes(entry) {
|
|
const namespace = entry.namespace;
|
|
if (!this.namespaceIndex.has(namespace)) {
|
|
this.namespaceIndex.set(namespace, new Set());
|
|
}
|
|
this.namespaceIndex.get(namespace).add(entry.id);
|
|
const keyIndexKey = `${namespace}:${entry.key}`;
|
|
this.keyIndex.set(keyIndexKey, entry.id);
|
|
}
|
|
/**
|
|
* Convert DB row to MemoryEntry
|
|
*/
|
|
rowToEntry(row) {
|
|
return {
|
|
id: row.id,
|
|
key: row.key,
|
|
content: row.content,
|
|
embedding: row.embedding
|
|
? new Float32Array(new Uint8Array(row.embedding).buffer)
|
|
: undefined,
|
|
type: row.type,
|
|
namespace: row.namespace,
|
|
tags: JSON.parse(row.tags || '[]'),
|
|
metadata: JSON.parse(row.metadata || '{}'),
|
|
ownerId: row.owner_id,
|
|
accessLevel: row.access_level,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
expiresAt: row.expires_at,
|
|
version: row.version,
|
|
references: JSON.parse(row.references || '[]'),
|
|
accessCount: row.access_count || 0,
|
|
lastAccessedAt: row.last_accessed_at || row.created_at,
|
|
};
|
|
}
|
|
/**
|
|
* Convert string ID to numeric for HNSW
|
|
*/
|
|
stringIdToNumeric(id) {
|
|
let hash = 0;
|
|
for (let i = 0; i < id.length; i++) {
|
|
hash = (hash << 5) - hash + id.charCodeAt(i);
|
|
hash |= 0;
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
/**
|
|
* Convert numeric ID back to string using O(1) reverse lookup
|
|
* PERFORMANCE FIX: Uses pre-built reverse map instead of O(n) linear scan
|
|
*/
|
|
numericIdToString(numericId) {
|
|
// Use O(1) reverse lookup map
|
|
const stringId = this.numericToStringIdMap.get(numericId);
|
|
if (stringId) {
|
|
return stringId;
|
|
}
|
|
// Fallback for unmapped IDs
|
|
return String(numericId);
|
|
}
|
|
/**
|
|
* Register string ID in reverse lookup map
|
|
* Called when storing entries to maintain bidirectional mapping
|
|
*/
|
|
registerIdMapping(stringId) {
|
|
const numericId = this.stringIdToNumeric(stringId);
|
|
this.numericToStringIdMap.set(numericId, stringId);
|
|
}
|
|
/**
|
|
* Unregister string ID from reverse lookup map
|
|
* Called when deleting entries
|
|
*/
|
|
unregisterIdMapping(stringId) {
|
|
const numericId = this.stringIdToNumeric(stringId);
|
|
this.numericToStringIdMap.delete(numericId);
|
|
}
|
|
/**
|
|
* Cosine similarity (returns value in range [0, 1] where 1 = identical)
|
|
*/
|
|
cosineSimilarity(a, b) {
|
|
let dotProduct = 0;
|
|
let normA = 0;
|
|
let normB = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
dotProduct += a[i] * b[i];
|
|
normA += a[i] * a[i];
|
|
normB += b[i] * b[i];
|
|
}
|
|
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
}
|
|
/**
|
|
* Estimate memory usage
|
|
*/
|
|
estimateMemoryUsage() {
|
|
let total = 0;
|
|
for (const entry of this.entries.values()) {
|
|
total += entry.content.length * 2;
|
|
if (entry.embedding) {
|
|
total += entry.embedding.length * 4;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
/**
|
|
* Check if AgentDB is available
|
|
*/
|
|
isAvailable() {
|
|
return this.available;
|
|
}
|
|
/**
|
|
* Get underlying AgentDB instance
|
|
*/
|
|
getAgentDB() {
|
|
return this.agentdb;
|
|
}
|
|
}
|
|
export default AgentDBBackend;
|
|
//# sourceMappingURL=agentdb-backend.js.map
|