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
409 lines
11 KiB
TypeScript
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;
|
|
}
|