Files
zclaw_openfang/docs/archive/v1-viking-dead-code/lib/viking-memory-adapter.ts
iven 1cf3f585d3 refactor(store): split gatewayStore into specialized domain stores
Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
2026-03-20 22:14:13 +08:00

409 lines
11 KiB
TypeScript

/**
* 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<VikingMemoryConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
// === Availability Check ===
private async isVikingAvailable(): Promise<boolean> {
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<MemoryEntry, 'id' | 'createdAt' | 'lastAccessedAt' | 'accessCount'>
): Promise<MemoryEntry> {
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<typeof getVikingAdapter>,
entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'lastAccessedAt' | 'accessCount'>
): Promise<MemoryEntry> {
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<MemoryEntry[]> {
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<typeof getVikingAdapter>,
query: string,
options?: MemorySearchOptions
): Promise<MemoryEntry[]> {
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<MemoryEntry[]> {
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<MemoryEntry | null> {
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<void> {
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<number> {
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<string> {
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<MemoryStats> {
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<void> {
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<VikingMemoryConfig>): void {
this.config = { ...this.config, ...config };
// Reset availability check when config changes
this.vikingAvailable = null;
}
getConfig(): Readonly<VikingMemoryConfig> {
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<VikingMemoryConfig>): VikingMemoryAdapter {
if (!_instance || config) {
_instance = new VikingMemoryAdapter(config);
}
return _instance;
}
export function resetVikingMemoryAdapter(): void {
_instance = null;
}