/** * Intelligence Layer Unified Client * * Provides a unified API for intelligence operations that: * - Uses Rust backend (via Tauri commands) when running in Tauri environment * - Falls back to localStorage-based implementation in browser/dev environment * * Degradation strategy: * - In Tauri mode: if a Tauri invoke fails, the error is logged and re-thrown. * The caller is responsible for handling the error. We do NOT silently fall * back to localStorage, because that would give users degraded functionality * (localStorage instead of SQLite, rule-based instead of LLM-based, no-op * instead of real execution) without any indication that something is wrong. * - In browser/dev mode: localStorage fallback is the intended behavior for * development and testing without a Tauri backend. * * This replaces direct usage of: * - agent-memory.ts * - heartbeat-engine.ts * - context-compactor.ts * - reflection-engine.ts * - agent-identity.ts * * Usage: * ```typescript * import { intelligenceClient, toFrontendMemory, toBackendMemoryInput } from './intelligence-client'; * * // Store memory * const id = await intelligenceClient.memory.store({ * agent_id: 'agent-1', * memory_type: 'fact', * content: 'User prefers concise responses', * importance: 7, * }); * * // Search memories * const memories = await intelligenceClient.memory.search({ * agent_id: 'agent-1', * query: 'user preference', * limit: 10, * }); * * // Convert to frontend format if needed * const frontendMemories = memories.map(toFrontendMemory); * ``` */ import { invoke } from '@tauri-apps/api/core'; import { isTauriRuntime } from '../tauri-gateway'; import { intelligence } from './type-conversions'; import type { PersistentMemory } from '../intelligence-backend'; import type { HeartbeatConfig, HeartbeatResult, CompactableMessage, CompactionResult, CompactionCheck, CompactionConfig, ReflectionConfig, ReflectionResult, ReflectionState, MemoryEntryForAnalysis, IdentityFiles, IdentityChangeProposal, IdentitySnapshot, } from '../intelligence-backend'; import type { MemoryEntry, MemorySearchOptions, MemoryStats } from './types'; import { toFrontendMemory, toBackendSearchOptions, toFrontendStats } from './type-conversions'; import { fallbackMemory } from './fallback-memory'; import { fallbackCompactor } from './fallback-compactor'; import { fallbackReflection } from './fallback-reflection'; import { fallbackIdentity } from './fallback-identity'; import { fallbackHeartbeat } from './fallback-heartbeat'; /** * Helper: wrap a Tauri invoke call so that failures are logged and re-thrown * instead of silently falling back to localStorage implementations. */ function tauriInvoke(label: string, fn: () => Promise): Promise { return fn().catch((e: unknown) => { console.warn(`[IntelligenceClient] Tauri invoke failed (${label}):`, e); throw e; }); } /** * Unified intelligence client that automatically selects backend or fallback. * * - In Tauri mode: calls Rust backend via invoke(). On failure, logs a warning * and re-throws -- does NOT fall back to localStorage. * - In browser/dev mode: uses localStorage-based fallback implementations. */ export const intelligenceClient = { memory: { init: async (): Promise => { if (isTauriRuntime()) { await tauriInvoke('memory.init', () => intelligence.memory.init()); } else { await fallbackMemory.init(); } }, store: async (entry: import('../intelligence-backend').MemoryEntryInput): Promise => { if (isTauriRuntime()) { return tauriInvoke('memory.store', () => intelligence.memory.store(entry)); } return fallbackMemory.store(entry); }, get: async (id: string): Promise => { if (isTauriRuntime()) { const result = await tauriInvoke('memory.get', () => intelligence.memory.get(id)); return result ? toFrontendMemory(result) : null; } return fallbackMemory.get(id); }, search: async (options: MemorySearchOptions): Promise => { if (isTauriRuntime()) { const results = await tauriInvoke('memory.search', () => intelligence.memory.search(toBackendSearchOptions(options)) ); return results.map(toFrontendMemory); } return fallbackMemory.search(options); }, delete: async (id: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('memory.delete', () => intelligence.memory.delete(id)); } else { await fallbackMemory.delete(id); } }, deleteAll: async (agentId: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('memory.deleteAll', () => intelligence.memory.deleteAll(agentId)); } return fallbackMemory.deleteAll(agentId); }, stats: async (): Promise => { if (isTauriRuntime()) { const stats = await tauriInvoke('memory.stats', () => intelligence.memory.stats()); return toFrontendStats(stats); } return fallbackMemory.stats(); }, export: async (): Promise => { if (isTauriRuntime()) { const results = await tauriInvoke('memory.export', () => intelligence.memory.export()); return results.map(toFrontendMemory); } return fallbackMemory.export(); }, import: async (memories: MemoryEntry[]): Promise => { if (isTauriRuntime()) { const backendMemories = memories.map(m => ({ ...m, agent_id: m.agentId, memory_type: m.type, last_accessed_at: m.lastAccessedAt, created_at: m.createdAt, access_count: m.accessCount, conversation_id: m.conversationId ?? null, tags: JSON.stringify(m.tags), embedding: null, })); return tauriInvoke('memory.import', () => intelligence.memory.import(backendMemories as PersistentMemory[]) ); } return fallbackMemory.import(memories); }, dbPath: async (): Promise => { if (isTauriRuntime()) { return tauriInvoke('memory.dbPath', () => intelligence.memory.dbPath()); } return fallbackMemory.dbPath(); }, buildContext: async ( agentId: string, query: string, maxTokens?: number, ): Promise<{ systemPromptAddition: string; totalTokens: number; memoriesUsed: number }> => { if (isTauriRuntime()) { return tauriInvoke('memory.buildContext', () => intelligence.memory.buildContext(agentId, query, maxTokens ?? null) ); } // Browser/dev fallback: use basic search const memories = await fallbackMemory.search({ agentId, query, limit: 8, minImportance: 3, }); const addition = memories.length > 0 ? `## 相关记忆\n${memories.map(m => `- [${m.type}] ${m.content}`).join('\n')}` : ''; return { systemPromptAddition: addition, totalTokens: 0, memoriesUsed: memories.length }; }, }, heartbeat: { init: async (agentId: string, config?: HeartbeatConfig): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.init', () => intelligence.heartbeat.init(agentId, config)); } else { await fallbackHeartbeat.init(agentId, config); } }, start: async (agentId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.start', () => intelligence.heartbeat.start(agentId)); } else { await fallbackHeartbeat.start(agentId); } }, stop: async (agentId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.stop', () => intelligence.heartbeat.stop(agentId)); } else { await fallbackHeartbeat.stop(agentId); } }, tick: async (agentId: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('heartbeat.tick', () => intelligence.heartbeat.tick(agentId)); } return fallbackHeartbeat.tick(agentId); }, getConfig: async (agentId: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('heartbeat.getConfig', () => intelligence.heartbeat.getConfig(agentId)); } return fallbackHeartbeat.getConfig(agentId); }, updateConfig: async (agentId: string, config: HeartbeatConfig): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.updateConfig', () => intelligence.heartbeat.updateConfig(agentId, config) ); } else { await fallbackHeartbeat.updateConfig(agentId, config); } }, getHistory: async (agentId: string, limit?: number): Promise => { if (isTauriRuntime()) { return tauriInvoke('heartbeat.getHistory', () => intelligence.heartbeat.getHistory(agentId, limit) ); } return fallbackHeartbeat.getHistory(agentId, limit); }, updateMemoryStats: async ( agentId: string, taskCount: number, totalEntries: number, storageSizeBytes: number ): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.updateMemoryStats', () => invoke('heartbeat_update_memory_stats', { agent_id: agentId, task_count: taskCount, total_entries: totalEntries, storage_size_bytes: storageSizeBytes, }) ); } else { // Browser/dev fallback only const cache = { taskCount, totalEntries, storageSizeBytes, lastUpdated: new Date().toISOString(), }; localStorage.setItem(`zclaw-memory-stats-${agentId}`, JSON.stringify(cache)); } }, recordCorrection: async (agentId: string, correctionType: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.recordCorrection', () => invoke('heartbeat_record_correction', { agent_id: agentId, correction_type: correctionType, }) ); } else { // Browser/dev fallback only const key = `zclaw-corrections-${agentId}`; const stored = localStorage.getItem(key); const counters = stored ? JSON.parse(stored) : {}; counters[correctionType] = (counters[correctionType] || 0) + 1; localStorage.setItem(key, JSON.stringify(counters)); } }, recordInteraction: async (agentId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('heartbeat.recordInteraction', () => invoke('heartbeat_record_interaction', { agent_id: agentId, }) ); } else { // Browser/dev fallback only localStorage.setItem(`zclaw-last-interaction-${agentId}`, new Date().toISOString()); } }, }, compactor: { estimateTokens: async (text: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('compactor.estimateTokens', () => intelligence.compactor.estimateTokens(text) ); } return fallbackCompactor.estimateTokens(text); }, estimateMessagesTokens: async (messages: CompactableMessage[]): Promise => { if (isTauriRuntime()) { return tauriInvoke('compactor.estimateMessagesTokens', () => intelligence.compactor.estimateMessagesTokens(messages) ); } return fallbackCompactor.estimateMessagesTokens(messages); }, checkThreshold: async ( messages: CompactableMessage[], config?: CompactionConfig ): Promise => { if (isTauriRuntime()) { return tauriInvoke('compactor.checkThreshold', () => intelligence.compactor.checkThreshold(messages, config) ); } return fallbackCompactor.checkThreshold(messages, config); }, compact: async ( messages: CompactableMessage[], agentId: string, conversationId?: string, config?: CompactionConfig ): Promise => { if (isTauriRuntime()) { return tauriInvoke('compactor.compact', () => intelligence.compactor.compact(messages, agentId, conversationId, config) ); } return fallbackCompactor.compact(messages, agentId, conversationId, config); }, }, reflection: { init: async (config?: ReflectionConfig): Promise => { if (isTauriRuntime()) { await tauriInvoke('reflection.init', () => intelligence.reflection.init(config)); } else { await fallbackReflection.init(config); } }, recordConversation: async (): Promise => { if (isTauriRuntime()) { await tauriInvoke('reflection.recordConversation', () => intelligence.reflection.recordConversation() ); } else { await fallbackReflection.recordConversation(); } }, shouldReflect: async (): Promise => { if (isTauriRuntime()) { return tauriInvoke('reflection.shouldReflect', () => intelligence.reflection.shouldReflect() ); } return fallbackReflection.shouldReflect(); }, reflect: async (agentId: string, memories: MemoryEntryForAnalysis[]): Promise => { if (isTauriRuntime()) { return tauriInvoke('reflection.reflect', () => intelligence.reflection.reflect(agentId, memories) ); } return fallbackReflection.reflect(agentId, memories); }, getHistory: async (limit?: number, agentId?: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('reflection.getHistory', () => intelligence.reflection.getHistory(limit, agentId) ); } return fallbackReflection.getHistory(limit, agentId); }, getState: async (): Promise => { if (isTauriRuntime()) { return tauriInvoke('reflection.getState', () => intelligence.reflection.getState()); } return fallbackReflection.getState(); }, }, identity: { get: async (agentId: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.get', () => intelligence.identity.get(agentId)); } return fallbackIdentity.get(agentId); }, getFile: async (agentId: string, file: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.getFile', () => intelligence.identity.getFile(agentId, file)); } return fallbackIdentity.getFile(agentId, file); }, buildPrompt: async (agentId: string, memoryContext?: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.buildPrompt', () => intelligence.identity.buildPrompt(agentId, memoryContext) ); } return fallbackIdentity.buildPrompt(agentId, memoryContext); }, updateUserProfile: async (agentId: string, content: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.updateUserProfile', () => intelligence.identity.updateUserProfile(agentId, content) ); } else { await fallbackIdentity.updateUserProfile(agentId, content); } }, appendUserProfile: async (agentId: string, addition: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.appendUserProfile', () => intelligence.identity.appendUserProfile(agentId, addition) ); } else { await fallbackIdentity.appendUserProfile(agentId, addition); } }, proposeChange: async ( agentId: string, file: 'soul' | 'instructions', suggestedContent: string, reason: string ): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.proposeChange', () => intelligence.identity.proposeChange(agentId, file, suggestedContent, reason) ); } return fallbackIdentity.proposeChange(agentId, file, suggestedContent, reason); }, approveProposal: async (proposalId: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.approveProposal', () => intelligence.identity.approveProposal(proposalId) ); } return fallbackIdentity.approveProposal(proposalId); }, rejectProposal: async (proposalId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.rejectProposal', () => intelligence.identity.rejectProposal(proposalId) ); } else { await fallbackIdentity.rejectProposal(proposalId); } }, getPendingProposals: async (agentId?: string): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.getPendingProposals', () => intelligence.identity.getPendingProposals(agentId) ); } return fallbackIdentity.getPendingProposals(agentId); }, updateFile: async (agentId: string, file: string, content: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.updateFile', () => intelligence.identity.updateFile(agentId, file, content) ); } else { await fallbackIdentity.updateFile(agentId, file, content); } }, getSnapshots: async (agentId: string, limit?: number): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.getSnapshots', () => intelligence.identity.getSnapshots(agentId, limit) ); } return fallbackIdentity.getSnapshots(agentId, limit); }, restoreSnapshot: async (agentId: string, snapshotId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.restoreSnapshot', () => intelligence.identity.restoreSnapshot(agentId, snapshotId) ); } else { await fallbackIdentity.restoreSnapshot(agentId, snapshotId); } }, listAgents: async (): Promise => { if (isTauriRuntime()) { return tauriInvoke('identity.listAgents', () => intelligence.identity.listAgents()); } return fallbackIdentity.listAgents(); }, deleteAgent: async (agentId: string): Promise => { if (isTauriRuntime()) { await tauriInvoke('identity.deleteAgent', () => intelligence.identity.deleteAgent(agentId)); } else { await fallbackIdentity.deleteAgent(agentId); } }, }, }; export default intelligenceClient;