/** * VikingMemoryAdapter - Bridges VikingAdapter to MemoryManager Interface * * This adapter allows the existing MemoryPanel to use OpenViking as a backend * while maintaining compatibility with the existing MemoryManager interface. * * Features: * - Implements MemoryManager interface * - Falls back to local MemoryManager when OpenViking unavailable * - Supports both sidecar and remote modes */ import { getMemoryManager, type MemoryEntry, type MemoryType, type MemorySource, type MemorySearchOptions, type MemoryStats, } from './agent-memory'; import { getVikingAdapter, type MemoryResult, type VikingMode, } from './viking-adapter'; // === Types === export interface VikingMemoryConfig { enabled: boolean; mode: VikingMode | 'auto'; fallbackToLocal: boolean; } const DEFAULT_CONFIG: VikingMemoryConfig = { enabled: true, mode: 'auto', fallbackToLocal: true, }; // === VikingMemoryAdapter Implementation === /** * VikingMemoryAdapter implements the MemoryManager interface * using OpenViking as the backend with optional fallback to localStorage. */ export class VikingMemoryAdapter { private config: VikingMemoryConfig; private vikingAvailable: boolean | null = null; private lastCheckTime: number = 0; private static CHECK_INTERVAL = 30000; // 30 seconds constructor(config?: Partial) { this.config = { ...DEFAULT_CONFIG, ...config }; } // === Availability Check === private async isVikingAvailable(): Promise { const now = Date.now(); if (this.vikingAvailable !== null && now - this.lastCheckTime < VikingMemoryAdapter.CHECK_INTERVAL) { return this.vikingAvailable; } try { const viking = getVikingAdapter(); const connected = await viking.isConnected(); this.vikingAvailable = connected; this.lastCheckTime = now; return connected; } catch { this.vikingAvailable = false; this.lastCheckTime = now; return false; } } private async getBackend(): Promise<'viking' | 'local'> { if (!this.config.enabled) { return 'local'; } const available = await this.isVikingAvailable(); if (available) { return 'viking'; } if (this.config.fallbackToLocal) { console.log('[VikingMemoryAdapter] OpenViking unavailable, using local fallback'); return 'local'; } throw new Error('OpenViking unavailable and fallback disabled'); } // === MemoryManager Interface Implementation === async save( entry: Omit ): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); const result = await this.saveToViking(viking, entry); return result; } return getMemoryManager().save(entry); } private async saveToViking( viking: ReturnType, entry: Omit ): Promise { const now = new Date().toISOString(); let result; const tags = entry.tags.join(','); switch (entry.type) { case 'fact': result = await viking.saveUserFact('general', entry.content, entry.tags); break; case 'preference': result = await viking.saveUserPreference(tags || 'preference', entry.content); break; case 'lesson': result = await viking.saveAgentLesson(entry.agentId, entry.content, entry.tags); break; case 'context': result = await viking.saveAgentPattern(entry.agentId, `[Context] ${entry.content}`, entry.tags); break; case 'task': result = await viking.saveAgentPattern(entry.agentId, `[Task] ${entry.content}`, entry.tags); break; default: result = await viking.saveUserFact('general', entry.content, entry.tags); } return { id: result.uri, agentId: entry.agentId, content: entry.content, type: entry.type, importance: entry.importance, source: entry.source, tags: entry.tags, createdAt: now, lastAccessedAt: now, accessCount: 0, }; } async search(query: string, options?: MemorySearchOptions): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); return this.searchViking(viking, query, options); } return getMemoryManager().search(query, options); } private async searchViking( viking: ReturnType, query: string, options?: MemorySearchOptions ): Promise { const results: MemoryEntry[] = []; const agentId = options?.agentId || 'zclaw-main'; // Search user memories const userResults = await viking.searchUserMemories(query, options?.limit || 10); for (const r of userResults) { results.push(this.memoryResultToEntry(r, agentId)); } // Search agent memories const agentResults = await viking.searchAgentMemories(agentId, query, options?.limit || 10); for (const r of agentResults) { results.push(this.memoryResultToEntry(r, agentId)); } // Filter by type if specified if (options?.type) { return results.filter(r => r.type === options.type); } // Sort by score (desc) and limit return results.slice(0, options?.limit || 10); } private memoryResultToEntry(result: MemoryResult, agentId: string): MemoryEntry { const type = this.mapCategoryToType(result.category); return { id: result.uri, agentId, content: result.content, type, importance: Math.round(result.score * 10), source: 'auto' as MemorySource, tags: result.tags || [], createdAt: new Date().toISOString(), lastAccessedAt: new Date().toISOString(), accessCount: 0, }; } private mapCategoryToType(category: string): MemoryType { const categoryLower = category.toLowerCase(); if (categoryLower.includes('prefer') || categoryLower.includes('偏好')) { return 'preference'; } if (categoryLower.includes('fact') || categoryLower.includes('事实')) { return 'fact'; } if (categoryLower.includes('lesson') || categoryLower.includes('经验')) { return 'lesson'; } if (categoryLower.includes('context') || categoryLower.includes('上下文')) { return 'context'; } if (categoryLower.includes('task') || categoryLower.includes('任务')) { return 'task'; } return 'fact'; } async getAll(agentId: string, options?: { type?: MemoryType; limit?: number }): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); const entries = await viking.browseMemories(`viking://agent/${agentId}/memories`); return entries .filter(_e => !options?.type || true) // TODO: filter by type .slice(0, options?.limit || 50) .map(e => ({ id: e.uri, agentId, content: e.name, // Placeholder - would need to fetch full content type: 'fact' as MemoryType, importance: 5, source: 'auto' as MemorySource, tags: [], createdAt: e.modifiedAt || new Date().toISOString(), lastAccessedAt: new Date().toISOString(), accessCount: 0, })); } return getMemoryManager().getAll(agentId, options); } async get(id: string): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); try { const content = await viking.getIdentityFromViking('zclaw-main', id); return { id, agentId: 'zclaw-main', content, type: 'fact', importance: 5, source: 'auto', tags: [], createdAt: new Date().toISOString(), lastAccessedAt: new Date().toISOString(), accessCount: 0, }; } catch { return null; } } return getMemoryManager().get(id); } async forget(id: string): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); await viking.deleteMemory(id); return; } return getMemoryManager().forget(id); } async prune(options: { maxAgeDays?: number; minImportance?: number; agentId?: string; }): Promise { const backend = await this.getBackend(); if (backend === 'viking') { // OpenViking handles pruning internally // For now, return 0 (no items pruned) console.log('[VikingMemoryAdapter] Pruning delegated to OpenViking'); return 0; } return getMemoryManager().prune(options); } async exportToMarkdown(agentId: string): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const entries = await this.getAll(agentId, { limit: 100 }); // Generate markdown from entries const lines = [ `# Agent Memory Export (OpenViking)`, '', `> Agent: ${agentId}`, `> Exported: ${new Date().toISOString()}`, `> Total entries: ${entries.length}`, '', ]; for (const entry of entries) { lines.push(`- [${entry.type}] ${entry.content}`); } return lines.join('\n'); } return getMemoryManager().exportToMarkdown(agentId); } async stats(agentId?: string): Promise { const backend = await this.getBackend(); if (backend === 'viking') { const viking = getVikingAdapter(); try { const vikingStats = await viking.getMemoryStats(agentId || 'zclaw-main'); return { totalEntries: vikingStats.totalEntries, byType: vikingStats.categories, byAgent: { [agentId || 'zclaw-main']: vikingStats.agentMemories }, oldestEntry: null, newestEntry: null, }; } catch { // Fall back to local stats return getMemoryManager().stats(agentId); } } return getMemoryManager().stats(agentId); } async updateImportance(id: string, importance: number): Promise { const backend = await this.getBackend(); if (backend === 'viking') { // OpenViking handles importance internally via access patterns console.log(`[VikingMemoryAdapter] Importance update for ${id}: ${importance}`); return; } return getMemoryManager().updateImportance(id, importance); } // === Configuration === updateConfig(config: Partial): void { this.config = { ...this.config, ...config }; // Reset availability check when config changes this.vikingAvailable = null; } getConfig(): Readonly { return { ...this.config }; } getMode(): 'viking' | 'local' | 'unavailable' { if (!this.config.enabled) return 'local'; if (this.vikingAvailable === true) return 'viking'; if (this.vikingAvailable === false && this.config.fallbackToLocal) return 'local'; return 'unavailable'; } } // === Singleton === let _instance: VikingMemoryAdapter | null = null; export function getVikingMemoryAdapter(config?: Partial): VikingMemoryAdapter { if (!_instance || config) { _instance = new VikingMemoryAdapter(config); } return _instance; } export function resetVikingMemoryAdapter(): void { _instance = null; }