diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 7020933..c46bae3 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -18,6 +18,7 @@ import { useConnectionStore } from './store/connectionStore'; import { useHandStore, type HandRun } from './store/handStore'; import { useTeamStore } from './store/teamStore'; import { useChatStore } from './store/chatStore'; +import { initializeStores } from './store'; import { getStoredGatewayToken } from './lib/gateway-client'; import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations'; import { Users, Loader2, Settings } from 'lucide-react'; @@ -156,7 +157,10 @@ function App() { setShowOnboarding(true); } - // Step 4: Bootstrap complete + // Step 4: Initialize stores with gateway client + initializeStores(); + + // Step 5: Bootstrap complete setBootstrapping(false); } catch (err) { console.error('[App] Bootstrap failed:', err); diff --git a/desktop/src/components/Automation/AutomationPanel.tsx b/desktop/src/components/Automation/AutomationPanel.tsx index d5896c3..ea406ef 100644 --- a/desktop/src/components/Automation/AutomationPanel.tsx +++ b/desktop/src/components/Automation/AutomationPanel.tsx @@ -9,7 +9,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useHandStore } from '../../store/handStore'; -import { useWorkflowStore, type Workflow } from '../../store/workflowStore'; +import { useWorkflowStore } from '../../store/workflowStore'; import { type AutomationItem, type CategoryType, @@ -54,7 +54,9 @@ export function AutomationPanel({ // Store state - use domain stores const hands = useHandStore((s) => s.hands); const workflows = useWorkflowStore((s) => s.workflows); - const isLoading = useHandStore((s) => s.isLoading) || useWorkflowStore((s) => s.isLoading); + const handLoading = useHandStore((s) => s.isLoading); + const workflowLoading = useWorkflowStore((s) => s.isLoading); + const isLoading = handLoading || workflowLoading; const loadHands = useHandStore((s) => s.loadHands); const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows); const triggerHand = useHandStore((s) => s.triggerHand); diff --git a/desktop/src/components/ErrorNotification.tsx b/desktop/src/components/ErrorNotification.tsx index a1c40cd..d06a197 100644 --- a/desktop/src/components/ErrorNotification.tsx +++ b/desktop/src/components/ErrorNotification.tsx @@ -11,7 +11,6 @@ import { X, AlertCircle, AlertTriangle, - Info, Bug, WifiOff, ShieldAlert, @@ -44,14 +43,14 @@ interface ErrorNotificationProps { const categoryIcons: Record = { network: WifiOff, - authentication: ShieldAlert, - authorization: ShieldAlert, + auth: ShieldAlert, + permission: ShieldAlert, validation: AlertTriangle, - configuration: AlertTriangle, - internal: Bug, - external: AlertCircle, + config: AlertTriangle, + server: Bug, + client: AlertCircle, timeout: Clock, - unknown: AlertCircle, + system: Bug, }; const severityColors: Record { setIsTesting(true); try { - const engine = new HeartbeatEngine('zclaw-main', config); - const result = await engine.tick(); + await intelligenceClient.heartbeat.init('zclaw-main', config); + const result = await intelligenceClient.heartbeat.tick('zclaw-main'); setLastResult(result); } catch (error) { console.error('[HeartbeatConfig] Test failed:', error); @@ -408,12 +420,12 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon min="5" max="120" step="5" - value={config.intervalMinutes} - onChange={(e) => updateConfig({ intervalMinutes: parseInt(e.target.value) })} + value={config.interval_minutes} + onChange={(e) => updateConfig({ interval_minutes: parseInt(e.target.value) })} className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-pink-500" /> - {config.intervalMinutes} 分钟 + {config.interval_minutes} 分钟 @@ -428,8 +440,8 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
updateConfig({ proactivityLevel: level })} + value={config.proactivity_level} + onChange={(level) => updateConfig({ proactivity_level: level })} />
@@ -437,15 +449,15 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon {/* Quiet Hours */}
updateConfig({ quietHoursStart: time })} - onEndChange={(time) => updateConfig({ quietHoursEnd: time })} + start={config.quiet_hours_start ?? undefined} + end={config.quiet_hours_end ?? undefined} + enabled={!!config.quiet_hours_start} + onStartChange={(time) => updateConfig({ quiet_hours_start: time })} + onEndChange={(time) => updateConfig({ quiet_hours_end: time })} onToggle={(enabled) => updateConfig({ - quietHoursStart: enabled ? '22:00' : undefined, - quietHoursEnd: enabled ? '08:00' : undefined, + quiet_hours_start: enabled ? '22:00' : null, + quiet_hours_end: enabled ? '08:00' : null, }) } /> @@ -484,12 +496,12 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
- 检查了 {lastResult.checkedItems} 项 + 检查了 {lastResult.checked_items} 项 {lastResult.alerts.length > 0 && ` · ${lastResult.alerts.length} 个提醒`}
{lastResult.alerts.length > 0 && (
- {lastResult.alerts.map((alert, i) => ( + {lastResult.alerts.map((alert: HeartbeatAlert, i: number) => (
= { @@ -34,22 +34,26 @@ export function MemoryPanel() { const [isExporting, setIsExporting] = useState(false); const loadMemories = useCallback(async () => { - const mgr = getMemoryManager(); const typeFilter = filterType !== 'all' ? { type: filterType as MemoryType } : {}; if (searchQuery.trim()) { - const results = await mgr.search(searchQuery, { + const results = await intelligenceClient.memory.search({ agentId, + query: searchQuery, limit: 50, ...typeFilter, }); setMemories(results); } else { - const all = await mgr.getAll(agentId, { ...typeFilter, limit: 50 }); - setMemories(all); + const results = await intelligenceClient.memory.search({ + agentId, + limit: 50, + ...typeFilter, + }); + setMemories(results); } - const s = await mgr.stats(agentId); + const s = await intelligenceClient.memory.stats(); setStats(s); }, [agentId, searchQuery, filterType]); @@ -58,15 +62,22 @@ export function MemoryPanel() { }, [loadMemories]); const handleDelete = async (id: string) => { - await getMemoryManager().forget(id); + await intelligenceClient.memory.delete(id); loadMemories(); }; const handleExport = async () => { setIsExporting(true); try { - const md = await getMemoryManager().exportToMarkdown(agentId); - const blob = new Blob([md], { type: 'text/markdown' }); + const memories = await intelligenceClient.memory.export(); + const filtered = memories.filter(m => m.agentId === agentId); + const md = filtered.map(m => + `## [${m.type}] ${m.content}\n` + + `- 重要度: ${m.importance}\n` + + `- 标签: ${m.tags.join(', ') || '无'}\n` + + `- 创建时间: ${m.createdAt}\n` + ).join('\n---\n\n'); + const blob = new Blob([md || '# 无记忆数据'], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -79,12 +90,20 @@ export function MemoryPanel() { }; const handlePrune = async () => { - const pruned = await getMemoryManager().prune({ + // Find old, low-importance memories and delete them + const memories = await intelligenceClient.memory.search({ agentId, - maxAgeDays: 30, - minImportance: 3, + minImportance: 0, + limit: 1000, }); - if (pruned > 0) { + const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000; + const toDelete = memories.filter(m => + new Date(m.createdAt).getTime() < thirtyDaysAgo && m.importance < 3 + ); + for (const m of toDelete) { + await intelligenceClient.memory.delete(m.id); + } + if (toDelete.length > 0) { loadMemories(); } }; diff --git a/desktop/src/components/ReflectionLog.tsx b/desktop/src/components/ReflectionLog.tsx index 8f5cb4a..f6a5f36 100644 --- a/desktop/src/components/ReflectionLog.tsx +++ b/desktop/src/components/ReflectionLog.tsx @@ -29,14 +29,13 @@ import { Settings, } from 'lucide-react'; import { - ReflectionEngine, + intelligenceClient, type ReflectionResult, + type IdentityChangeProposal, + type ReflectionConfig, type PatternObservation, type ImprovementSuggestion, - type ReflectionConfig, - DEFAULT_REFLECTION_CONFIG, -} from '../lib/reflection-engine'; -import { getAgentIdentityManager, type IdentityChangeProposal } from '../lib/agent-identity'; +} from '../lib/intelligence-client'; // === Types === @@ -231,8 +230,8 @@ function ProposalCard({ 当前内容
-                  {proposal.currentContent.slice(0, 500)}
-                  {proposal.currentContent.length > 500 && '...'}
+                  {proposal.current_content.slice(0, 500)}
+                  {proposal.current_content.length > 500 && '...'}
                 
@@ -240,8 +239,8 @@ function ProposalCard({ 建议内容
-                  {proposal.suggestedContent.slice(0, 500)}
-                  {proposal.suggestedContent.length > 500 && '...'}
+                  {proposal.suggested_content.slice(0, 500)}
+                  {proposal.suggested_content.length > 500 && '...'}
                 
@@ -309,9 +308,9 @@ function ReflectionEntry({ {result.improvements.length} 建议 - {result.identityProposals.length > 0 && ( + {result.identity_proposals.length > 0 && ( - {result.identityProposals.length} 变更 + {result.identity_proposals.length} 变更 )} @@ -362,8 +361,8 @@ function ReflectionEntry({ {/* Meta */}
- 新增记忆: {result.newMemories} - 身份变更提议: {result.identityProposals.length} + 新增记忆: {result.new_memories} + 身份变更提议: {result.identity_proposals.length}
@@ -381,56 +380,63 @@ export function ReflectionLog({ onProposalApprove, onProposalReject, }: ReflectionLogProps) { - const [engine] = useState(() => new ReflectionEngine()); const [history, setHistory] = useState([]); const [pendingProposals, setPendingProposals] = useState([]); const [expandedId, setExpandedId] = useState(null); const [isReflecting, setIsReflecting] = useState(false); - const [config, setConfig] = useState(DEFAULT_REFLECTION_CONFIG); const [showConfig, setShowConfig] = useState(false); + const [config, setConfig] = useState({ + trigger_after_conversations: 5, + allow_soul_modification: true, + require_approval: true, + }); // Load history and pending proposals useEffect(() => { - const loadedHistory = engine.getHistory(); - setHistory([...loadedHistory].reverse()); // Most recent first + const loadData = async () => { + try { + const loadedHistory = await intelligenceClient.reflection.getHistory(); + setHistory([...loadedHistory].reverse()); // Most recent first - const identityManager = getAgentIdentityManager(); - const proposals = identityManager.getPendingProposals(agentId); - setPendingProposals(proposals); - }, [engine, agentId]); + const proposals = await intelligenceClient.identity.getPendingProposals(agentId); + setPendingProposals(proposals); + } catch (error) { + console.error('[ReflectionLog] Failed to load data:', error); + } + }; + loadData(); + }, [agentId]); const handleReflect = useCallback(async () => { setIsReflecting(true); try { - const result = await engine.reflect(agentId); + const result = await intelligenceClient.reflection.reflect(agentId, []); setHistory((prev) => [result, ...prev]); // Update pending proposals - if (result.identityProposals.length > 0) { - setPendingProposals((prev) => [...prev, ...result.identityProposals]); + if (result.identity_proposals.length > 0) { + setPendingProposals((prev) => [...prev, ...result.identity_proposals]); } } catch (error) { console.error('[ReflectionLog] Reflection failed:', error); } finally { setIsReflecting(false); } - }, [engine, agentId]); + }, [agentId]); const handleApproveProposal = useCallback( - (proposal: IdentityChangeProposal) => { - const identityManager = getAgentIdentityManager(); - identityManager.approveProposal(proposal.id); - setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id)); + async (proposal: IdentityChangeProposal) => { + await intelligenceClient.identity.approveProposal(proposal.id); + setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id)); onProposalApprove?.(proposal); }, [onProposalApprove] ); const handleRejectProposal = useCallback( - (proposal: IdentityChangeProposal) => { - const identityManager = getAgentIdentityManager(); - identityManager.rejectProposal(proposal.id); - setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id)); + async (proposal: IdentityChangeProposal) => { + await intelligenceClient.identity.rejectProposal(proposal.id); + setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id)); onProposalReject?.(proposal); }, [onProposalReject] @@ -438,9 +444,9 @@ export function ReflectionLog({ const stats = useMemo(() => { const totalReflections = history.length; - const totalPatterns = history.reduce((sum, r) => sum + r.patterns.length, 0); - const totalImprovements = history.reduce((sum, r) => sum + r.improvements.length, 0); - const totalIdentityChanges = history.reduce((sum, r) => sum + r.identityProposals.length, 0); + const totalPatterns = history.reduce((sum: number, r: ReflectionResult) => sum + r.patterns.length, 0); + const totalImprovements = history.reduce((sum: number, r: ReflectionResult) => sum + r.improvements.length, 0); + const totalIdentityChanges = history.reduce((sum: number, r: ReflectionResult) => sum + r.identity_proposals.length, 0); return { totalReflections, totalPatterns, totalImprovements, totalIdentityChanges }; }, [history]); @@ -507,9 +513,9 @@ export function ReflectionLog({ type="number" min="1" max="20" - value={config.triggerAfterConversations} + value={config.trigger_after_conversations || 5} onChange={(e) => - setConfig((prev) => ({ ...prev, triggerAfterConversations: parseInt(e.target.value) || 5 })) + setConfig((prev) => ({ ...prev, trigger_after_conversations: parseInt(e.target.value) || 5 })) } className="w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" /> @@ -517,13 +523,13 @@ export function ReflectionLog({
允许修改 SOUL.md @@ -531,13 +537,13 @@ export function ReflectionLog({
变更需审批 diff --git a/desktop/src/components/SkillMarket.tsx b/desktop/src/components/SkillMarket.tsx index fa83717..68756df 100644 --- a/desktop/src/components/SkillMarket.tsx +++ b/desktop/src/components/SkillMarket.tsx @@ -11,6 +11,7 @@ */ import { useState, useEffect, useMemo, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { Search, Package, @@ -23,7 +24,7 @@ import { ChevronRight, RefreshCw, } from 'lucide-react'; -import { useConfigStore, type SkillInfo } from '../store/configStore'; +import { useConfigStore } from '../store/configStore'; import { adaptSkillsCatalog, type SkillDisplay, diff --git a/desktop/src/lib/__tests__/llm-integration.test.ts b/desktop/src/lib/__tests__/llm-integration.test.ts deleted file mode 100644 index 3a3b76d..0000000 --- a/desktop/src/lib/__tests__/llm-integration.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * LLM Integration Tests - Phase 2 Engine Upgrades - * - * Tests for LLM-powered features: - * - ReflectionEngine with LLM semantic analysis - * - ContextCompactor with LLM summarization - * - MemoryExtractor with LLM importance scoring - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - ReflectionEngine, -} from '../reflection-engine'; -import { - ContextCompactor, -} from '../context-compactor'; -import { - MemoryExtractor, -} from '../memory-extractor'; -import { - type LLMProvider, -} from '../llm-service'; - -// === Mock LLM Adapter === - -const mockLLMAdapter = { - complete: vi.fn(), - isAvailable: vi.fn(() => true), - getProvider: vi.fn(() => 'mock' as LLMProvider), -}; - -vi.mock('../llm-service', () => ({ - getLLMAdapter: vi.fn(() => mockLLMAdapter), - resetLLMAdapter: vi.fn(), - llmReflect: vi.fn(async () => JSON.stringify({ - patterns: [ - { - observation: '用户经常询问代码优化问题', - frequency: 5, - sentiment: 'positive', - evidence: ['多次讨论性能优化'], - }, - ], - improvements: [ - { - area: '代码解释', - suggestion: '可以提供更详细的代码注释', - priority: 'medium', - }, - ], - identityProposals: [], - })), - llmCompact: vi.fn(async () => '[LLM摘要]\n讨论主题: 代码优化\n关键决策: 使用缓存策略\n待办事项: 完成性能测试'), - llmExtract: vi.fn(async () => JSON.stringify([ - { content: '用户偏好简洁的回答', type: 'preference', importance: 7, tags: ['style'] }, - { content: '项目使用 TypeScript', type: 'fact', importance: 6, tags: ['tech'] }, - ])), -})); - -// === ReflectionEngine Tests === - -describe('ReflectionEngine with LLM', () => { - let engine: ReflectionEngine; - - beforeEach(() => { - vi.clearAllMocks(); - engine = new ReflectionEngine({ useLLM: true }); - }); - - afterEach(() => { - engine?.updateConfig({ useLLM: false }); - }); - - it('should initialize with LLM config', () => { - const config = engine.getConfig(); - expect(config.useLLM).toBe(true); - }); - - it('should have llmFallbackToRules enabled by default', () => { - const config = engine.getConfig(); - expect(config.llmFallbackToRules).toBe(true); - }); - - it('should track conversations for reflection trigger', () => { - engine.recordConversation(); - engine.recordConversation(); - expect(engine.shouldReflect()).toBe(false); - - // After 5 conversations (default trigger) - for (let i = 0; i < 4; i++) { - engine.recordConversation(); - } - expect(engine.shouldReflect()).toBe(true); - }); - - it('should use LLM when enabled and available', async () => { - mockLLMAdapter.isAvailable.mockReturnValue(true); - - const result = await engine.reflect('test-agent', { forceLLM: true }); - - expect(result.patterns.length).toBeGreaterThan(0); - expect(result.timestamp).toBeDefined(); - }); - - it('should fallback to rules when LLM fails', async () => { - mockLLMAdapter.isAvailable.mockReturnValue(false); - - const result = await engine.reflect('test-agent'); - - // Should still work with rule-based approach - expect(result).toBeDefined(); - expect(result.timestamp).toBeDefined(); - }); -}); - -// === ContextCompactor Tests === - -describe('ContextCompactor with LLM', () => { - let compactor: ContextCompactor; - - beforeEach(() => { - vi.clearAllMocks(); - compactor = new ContextCompactor({ useLLM: true }); - }); - - it('should initialize with LLM config', () => { - const config = compactor.getConfig(); - expect(config.useLLM).toBe(true); - }); - - it('should have llmFallbackToRules enabled by default', () => { - const config = compactor.getConfig(); - expect(config.llmFallbackToRules).toBe(true); - }); - - it('should check threshold correctly', () => { - const messages = [ - { role: 'user', content: 'Hello'.repeat(1000) }, - { role: 'assistant', content: 'Response'.repeat(1000) }, - ]; - - const check = compactor.checkThreshold(messages); - expect(check.shouldCompact).toBe(false); - expect(check.urgency).toBe('none'); - }); - - it('should trigger soft threshold', () => { - // Create enough messages to exceed 15000 soft threshold but not 20000 hard threshold - // estimateTokens: CJK chars ~1.5 tokens each - // 20 messages × 600 CJK chars × 1.5 = ~18000 tokens (between soft and hard) - const messages = Array(20).fill(null).map((_, i) => ({ - role: i % 2 === 0 ? 'user' : 'assistant', - content: '测试内容'.repeat(150), // 600 CJK chars ≈ 900 tokens each - })); - - const check = compactor.checkThreshold(messages); - expect(check.shouldCompact).toBe(true); - expect(check.urgency).toBe('soft'); - }); -}); - -// === MemoryExtractor Tests === - -describe('MemoryExtractor with LLM', () => { - let extractor: MemoryExtractor; - - beforeEach(() => { - vi.clearAllMocks(); - extractor = new MemoryExtractor({ useLLM: true }); - }); - - it('should initialize with LLM config', () => { - // MemoryExtractor doesn't expose config directly, but we can test behavior - expect(extractor).toBeDefined(); - }); - - it('should skip extraction with too few messages', async () => { - const messages = [ - { role: 'user', content: 'Hi' }, - { role: 'assistant', content: 'Hello!' }, - ]; - - const result = await extractor.extractFromConversation(messages, 'test-agent'); - expect(result.saved).toBe(0); - }); - - it('should extract with enough messages', async () => { - const messages = [ - { role: 'user', content: '我喜欢简洁的回答' }, - { role: 'assistant', content: '好的,我会简洁一些' }, - { role: 'user', content: '我的项目使用 TypeScript' }, - { role: 'assistant', content: 'TypeScript 是个好选择' }, - { role: 'user', content: '继续' }, - { role: 'assistant', content: '继续...' }, - ]; - - const result = await extractor.extractFromConversation(messages, 'test-agent'); - expect(result.items.length).toBeGreaterThanOrEqual(0); - }); -}); - -// === Integration Test === - -describe('LLM Integration Full Flow', () => { - it('should work end-to-end with all engines', async () => { - // Setup all engines with LLM - const engine = new ReflectionEngine({ useLLM: true, llmFallbackToRules: true }); - const compactor = new ContextCompactor({ useLLM: true, llmFallbackToRules: true }); - const extractor = new MemoryExtractor({ useLLM: true, llmFallbackToRules: true }); - - // Verify they all have LLM support - expect(engine.getConfig().useLLM).toBe(true); - expect(compactor.getConfig().useLLM).toBe(true); - - // All should work without throwing - await expect(engine.reflect('test-agent')).resolves; - await expect(compactor.compact([], 'test-agent')).resolves; - await expect(extractor.extractFromConversation([], 'test-agent')).resolves; - }); -}); diff --git a/desktop/src/lib/agent-identity.ts b/desktop/src/lib/agent-identity.ts deleted file mode 100644 index eade72b..0000000 --- a/desktop/src/lib/agent-identity.ts +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Agent Identity Manager - Per-agent dynamic identity files - * - * Manages SOUL.md, AGENTS.md, USER.md per agent with: - * - Per-agent isolated identity directories - * - USER.md auto-update by agent (stores learned preferences) - * - SOUL.md/AGENTS.md change proposals (require user approval) - * - Snapshot history for rollback - * - * Phase 1: localStorage-based storage (same as agent-memory.ts) - * Upgrade path: Tauri filesystem API for real .md files - * - * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.3 - */ - -import { canAutoExecute } from './autonomy-manager'; - -// === Types === - -export interface IdentityFiles { - soul: string; - instructions: string; - userProfile: string; - heartbeat?: string; -} - -export interface IdentityChangeProposal { - id: string; - agentId: string; - file: 'soul' | 'instructions'; - reason: string; - currentContent: string; - suggestedContent: string; - status: 'pending' | 'approved' | 'rejected'; - createdAt: string; -} - -export interface IdentitySnapshot { - id: string; - agentId: string; - files: IdentityFiles; - timestamp: string; - reason: string; -} - -// === Storage Keys === - -const IDENTITY_STORAGE_KEY = 'zclaw-agent-identities'; -const PROPOSALS_STORAGE_KEY = 'zclaw-identity-proposals'; -const SNAPSHOTS_STORAGE_KEY = 'zclaw-identity-snapshots'; - -// === Default Identity Content === - -const DEFAULT_SOUL = `# ZCLAW 人格 - -你是 ZCLAW(小龙虾),一个基于 OpenClaw 定制的中文 AI 助手。 - -## 核心特质 - -- **高效执行**: 你不只是出主意,你会真正动手完成任务 -- **中文优先**: 默认使用中文交流,必要时切换英文 -- **专业可靠**: 对技术问题给出精确答案,不确定时坦诚说明 -- **持续成长**: 你会记住与用户的交互,不断改进自己的服务方式 - -## 语气 - -简洁、专业、友好。避免过度客套,直接给出有用信息。`; - -const DEFAULT_INSTRUCTIONS = `# Agent 指令 - -## 操作规范 - -1. 执行文件操作前,先确认目标路径 -2. 执行 Shell 命令前,评估安全风险 -3. 长时间任务需定期汇报进度 -4. 优先使用中文回复 - -## 记忆管理 - -- 重要的用户偏好自动记录 -- 项目上下文保存到工作区 -- 对话结束时总结关键信息`; - -const DEFAULT_USER_PROFILE = `# 用户画像 - -_尚未收集到用户偏好信息。随着交互积累,此文件将自动更新。_`; - -// === AgentIdentityManager Implementation === - -export class AgentIdentityManager { - private identities: Map = new Map(); - private proposals: IdentityChangeProposal[] = []; - private snapshots: IdentitySnapshot[] = []; - - constructor() { - this.load(); - } - - // === Persistence === - - private load(): void { - try { - const rawIdentities = localStorage.getItem(IDENTITY_STORAGE_KEY); - if (rawIdentities) { - const parsed = JSON.parse(rawIdentities) as Record; - this.identities = new Map(Object.entries(parsed)); - } - - const rawProposals = localStorage.getItem(PROPOSALS_STORAGE_KEY); - if (rawProposals) { - this.proposals = JSON.parse(rawProposals); - } - - const rawSnapshots = localStorage.getItem(SNAPSHOTS_STORAGE_KEY); - if (rawSnapshots) { - this.snapshots = JSON.parse(rawSnapshots); - } - } catch (err) { - console.warn('[AgentIdentity] Failed to load:', err); - } - } - - private persist(): void { - try { - const obj: Record = {}; - for (const [key, value] of this.identities) { - obj[key] = value; - } - localStorage.setItem(IDENTITY_STORAGE_KEY, JSON.stringify(obj)); - localStorage.setItem(PROPOSALS_STORAGE_KEY, JSON.stringify(this.proposals)); - localStorage.setItem(SNAPSHOTS_STORAGE_KEY, JSON.stringify(this.snapshots.slice(-50))); // Keep last 50 snapshots - } catch (err) { - console.warn('[AgentIdentity] Failed to persist:', err); - } - } - - // === Read Identity === - - getIdentity(agentId: string): IdentityFiles { - const existing = this.identities.get(agentId); - if (existing) return { ...existing }; - - // Initialize with defaults - const defaults: IdentityFiles = { - soul: DEFAULT_SOUL, - instructions: DEFAULT_INSTRUCTIONS, - userProfile: DEFAULT_USER_PROFILE, - }; - this.identities.set(agentId, defaults); - this.persist(); - return { ...defaults }; - } - - getFile(agentId: string, file: keyof IdentityFiles): string { - const identity = this.getIdentity(agentId); - return identity[file] || ''; - } - - // === Build System Prompt === - - buildSystemPrompt(agentId: string, memoryContext?: string): string { - const identity = this.getIdentity(agentId); - const sections: string[] = []; - - if (identity.soul) sections.push(identity.soul); - if (identity.instructions) sections.push(identity.instructions); - if (identity.userProfile && identity.userProfile !== DEFAULT_USER_PROFILE) { - sections.push(`## 用户画像\n${identity.userProfile}`); - } - if (memoryContext) { - sections.push(memoryContext); - } - - return sections.join('\n\n'); - } - - // === Update USER.md (auto, no approval needed) === - - updateUserProfile(agentId: string, newContent: string): void { - const identity = this.getIdentity(agentId); - const oldContent = identity.userProfile; - - // Create snapshot before update - this.createSnapshot(agentId, 'Auto-update USER.md'); - - identity.userProfile = newContent; - this.identities.set(agentId, identity); - this.persist(); - - console.log(`[AgentIdentity] Updated USER.md for ${agentId} (${oldContent.length} → ${newContent.length} chars)`); - } - - appendToUserProfile(agentId: string, addition: string): void { - const identity = this.getIdentity(agentId); - const updated = identity.userProfile.trimEnd() + '\n\n' + addition; - this.updateUserProfile(agentId, updated); - } - - // === Update SOUL.md / AGENTS.md (requires approval) === - - proposeChange( - agentId: string, - file: 'soul' | 'instructions', - suggestedContent: string, - reason: string, - options?: { skipAutonomyCheck?: boolean } - ): IdentityChangeProposal | null { - // Autonomy check - identity updates are high-risk, always require approval - if (!options?.skipAutonomyCheck) { - const { decision } = canAutoExecute('identity_update', 8); - console.log(`[AgentIdentity] Autonomy check for identity update: ${decision.reason}`); - // Identity updates always require approval regardless of autonomy level - // But we log the decision for audit purposes - } - - const identity = this.getIdentity(agentId); - const currentContent = file === 'soul' ? identity.soul : identity.instructions; - - const proposal: IdentityChangeProposal = { - id: `prop_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, - agentId, - file, - reason, - currentContent, - suggestedContent, - status: 'pending', - createdAt: new Date().toISOString(), - }; - - this.proposals.push(proposal); - this.persist(); - return proposal; - } - - approveProposal(proposalId: string): boolean { - const proposal = this.proposals.find(p => p.id === proposalId); - if (!proposal || proposal.status !== 'pending') return false; - - const identity = this.getIdentity(proposal.agentId); - this.createSnapshot(proposal.agentId, `Approved proposal: ${proposal.reason}`); - - if (proposal.file === 'soul') { - identity.soul = proposal.suggestedContent; - } else { - identity.instructions = proposal.suggestedContent; - } - - this.identities.set(proposal.agentId, identity); - proposal.status = 'approved'; - this.persist(); - return true; - } - - rejectProposal(proposalId: string): boolean { - const proposal = this.proposals.find(p => p.id === proposalId); - if (!proposal || proposal.status !== 'pending') return false; - - proposal.status = 'rejected'; - this.persist(); - return true; - } - - getPendingProposals(agentId?: string): IdentityChangeProposal[] { - return this.proposals.filter(p => - p.status === 'pending' && (!agentId || p.agentId === agentId) - ); - } - - // === Direct Edit (user explicitly edits in UI) === - - updateFile(agentId: string, file: keyof IdentityFiles, content: string): void { - const identity = this.getIdentity(agentId); - this.createSnapshot(agentId, `Manual edit: ${file}`); - - identity[file] = content; - this.identities.set(agentId, identity); - this.persist(); - } - - // === Snapshots === - - private snapshotCounter = 0; - - private createSnapshot(agentId: string, reason: string): void { - const identity = this.getIdentity(agentId); - this.snapshotCounter++; - this.snapshots.push({ - id: `snap_${Date.now()}_${this.snapshotCounter}_${Math.random().toString(36).slice(2, 6)}`, - agentId, - files: { ...identity }, - timestamp: new Date().toISOString(), - reason, - }); - } - - getSnapshots(agentId: string, limit: number = 10): IdentitySnapshot[] { - // Return newest first; use array index as tiebreaker for same-millisecond snapshots - const filtered = this.snapshots - .map((s, idx) => ({ s, idx })) - .filter(({ s }) => s.agentId === agentId) - .sort((a, b) => { - const timeDiff = new Date(b.s.timestamp).getTime() - new Date(a.s.timestamp).getTime(); - return timeDiff !== 0 ? timeDiff : b.idx - a.idx; - }) - .map(({ s }) => s); - return filtered.slice(0, limit); - } - - restoreSnapshot(agentId: string, snapshotId: string): boolean { - const snapshot = this.snapshots.find(s => - s.agentId === agentId && s.id === snapshotId - ); - if (!snapshot) return false; - - this.createSnapshot(agentId, `Rollback to ${snapshot.timestamp}`); - this.identities.set(agentId, { ...snapshot.files }); - this.persist(); - return true; - } - - // === List agents === - - listAgents(): string[] { - return [...this.identities.keys()]; - } - - // === Delete agent identity === - - deleteAgent(agentId: string): void { - this.identities.delete(agentId); - this.proposals = this.proposals.filter(p => p.agentId !== agentId); - this.snapshots = this.snapshots.filter(s => s.agentId !== agentId); - this.persist(); - } -} - -// === Singleton === - -let _instance: AgentIdentityManager | null = null; - -export function getAgentIdentityManager(): AgentIdentityManager { - if (!_instance) { - _instance = new AgentIdentityManager(); - } - return _instance; -} - -export function resetAgentIdentityManager(): void { - _instance = null; -} diff --git a/desktop/src/lib/agent-memory.ts b/desktop/src/lib/agent-memory.ts deleted file mode 100644 index 9ef8dee..0000000 --- a/desktop/src/lib/agent-memory.ts +++ /dev/null @@ -1,486 +0,0 @@ -/** - * Agent Memory System - Persistent cross-session memory for ZCLAW agents - * - * Phase 1 implementation: zustand persist (localStorage) with keyword search. - * Optimized with inverted index for sub-20ms retrieval on 1000+ memories. - * Designed for easy upgrade to SQLite + FTS5 + vector search in Phase 2. - * - * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.1 - */ - -import { MemoryIndex, getMemoryIndex, tokenize } from './memory-index'; - -// === Types === - -export type MemoryType = 'fact' | 'preference' | 'lesson' | 'context' | 'task'; -export type MemorySource = 'auto' | 'user' | 'reflection' | 'llm-reflection'; - -export interface MemoryEntry { - id: string; - agentId: string; - content: string; - type: MemoryType; - importance: number; // 0-10 - source: MemorySource; - tags: string[]; - createdAt: string; // ISO timestamp - lastAccessedAt: string; - accessCount: number; - conversationId?: string; -} - -export interface MemorySearchOptions { - agentId?: string; - type?: MemoryType; - types?: MemoryType[]; - tags?: string[]; - limit?: number; - minImportance?: number; - [key: string]: unknown; -} - -export interface MemoryStats { - totalEntries: number; - byType: Record; - byAgent: Record; - oldestEntry: string | null; - newestEntry: string | null; - indexStats?: { - cacheHitRate: number; - avgQueryTime: number; - }; -} - -// === Memory ID Generator === - -function generateMemoryId(): string { - return `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; -} - -// === Keyword Search Scoring === - -function searchScore( - entry: MemoryEntry, - queryTokens: string[], - cachedTokens?: string[] -): number { - // Use cached tokens if available, otherwise tokenize - const contentTokens = cachedTokens ?? tokenize(entry.content); - const tagTokens = entry.tags.flatMap(t => tokenize(t)); - const allTokens = [...contentTokens, ...tagTokens]; - - let matched = 0; - for (const qt of queryTokens) { - if (allTokens.some(t => t.includes(qt) || qt.includes(t))) { - matched++; - } - } - - if (matched === 0) return 0; - - const relevance = matched / queryTokens.length; - const importanceBoost = entry.importance / 10; - const recencyBoost = Math.max(0, 1 - (Date.now() - new Date(entry.lastAccessedAt).getTime()) / (30 * 24 * 60 * 60 * 1000)); // decay over 30 days - - return relevance * 0.6 + importanceBoost * 0.25 + recencyBoost * 0.15; -} - -// === MemoryManager Implementation === - -const STORAGE_KEY = 'zclaw-agent-memories'; - -export class MemoryManager { - private entries: MemoryEntry[] = []; - private entryIndex: Map = new Map(); // id -> array index for O(1) lookup - private memoryIndex: MemoryIndex; - private indexInitialized = false; - - constructor() { - this.load(); - this.memoryIndex = getMemoryIndex(); - } - - // === Persistence === - - private load(): void { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw) { - this.entries = JSON.parse(raw); - // Build entry index for O(1) lookups - this.entries.forEach((entry, index) => { - this.entryIndex.set(entry.id, index); - }); - } - } catch (err) { - console.warn('[MemoryManager] Failed to load memories:', err); - this.entries = []; - } - } - - private persist(): void { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(this.entries)); - } catch (err) { - console.warn('[MemoryManager] Failed to persist memories:', err); - } - } - - // === Index Management === - - private ensureIndexInitialized(): void { - if (!this.indexInitialized) { - this.memoryIndex.rebuild(this.entries); - this.indexInitialized = true; - } - } - - private indexEntry(entry: MemoryEntry): void { - this.ensureIndexInitialized(); - this.memoryIndex.index(entry); - } - - private removeEntryFromIndex(id: string): void { - if (this.indexInitialized) { - this.memoryIndex.remove(id); - } - } - - // === Write === - - async save( - entry: Omit - ): Promise { - const now = new Date().toISOString(); - const newEntry: MemoryEntry = { - ...entry, - id: generateMemoryId(), - createdAt: now, - lastAccessedAt: now, - accessCount: 0, - }; - - // Deduplicate: check if very similar content already exists for this agent - const duplicate = this.entries.find(e => - e.agentId === entry.agentId && - e.type === entry.type && - this.contentSimilarity(e.content, entry.content) >= 0.8 - ); - - if (duplicate) { - // Update existing entry instead of creating duplicate - duplicate.content = entry.content; - duplicate.importance = Math.max(duplicate.importance, entry.importance); - duplicate.lastAccessedAt = now; - duplicate.accessCount++; - duplicate.tags = [...new Set([...duplicate.tags, ...entry.tags])]; - // Re-index the updated entry - this.indexEntry(duplicate); - this.persist(); - return duplicate; - } - - this.entries.push(newEntry); - this.entryIndex.set(newEntry.id, this.entries.length - 1); - this.indexEntry(newEntry); - this.persist(); - return newEntry; - } - - // === Search (Optimized with Index) === - - async search(query: string, options?: MemorySearchOptions): Promise { - const startTime = performance.now(); - const queryTokens = tokenize(query); - if (queryTokens.length === 0) return []; - - this.ensureIndexInitialized(); - - // Check query cache first - const cached = this.memoryIndex.getCached(query, options); - if (cached) { - // Retrieve entries by IDs - const results = cached - .map(id => this.entries[this.entryIndex.get(id) ?? -1]) - .filter((e): e is MemoryEntry => e !== undefined); - - this.memoryIndex.recordQueryTime(performance.now() - startTime); - return results; - } - - // Get candidate IDs using index (O(1) lookups) - const candidateIds = this.memoryIndex.getCandidates(options || {}); - - // If no candidates from index, return empty - if (candidateIds && candidateIds.size === 0) { - this.memoryIndex.setCached(query, options, []); - this.memoryIndex.recordQueryTime(performance.now() - startTime); - return []; - } - - // Build candidates list - let candidates: MemoryEntry[]; - if (candidateIds) { - // Use indexed candidates - candidates = []; - for (const id of candidateIds) { - const idx = this.entryIndex.get(id); - if (idx !== undefined) { - const entry = this.entries[idx]; - // Additional filter for minImportance (not handled by index) - if (options?.minImportance !== undefined && entry.importance < options.minImportance) { - continue; - } - candidates.push(entry); - } - } - } else { - // Fallback: no index-based candidates, use all entries - candidates = [...this.entries]; - // Apply minImportance filter - if (options?.minImportance !== undefined) { - candidates = candidates.filter(e => e.importance >= options.minImportance!); - } - } - - // Score and rank using cached tokens - const scored = candidates - .map(entry => { - const cachedTokens = this.memoryIndex.getTokens(entry.id); - return { entry, score: searchScore(entry, queryTokens, cachedTokens) }; - }) - .filter(item => item.score > 0) - .sort((a, b) => b.score - a.score); - - const limit = options?.limit ?? 10; - const results = scored.slice(0, limit).map(item => item.entry); - - // Cache the results - this.memoryIndex.setCached(query, options, results.map(r => r.id)); - - // Update access metadata - const now = new Date().toISOString(); - for (const entry of results) { - entry.lastAccessedAt = now; - entry.accessCount++; - } - if (results.length > 0) { - this.persist(); - } - - this.memoryIndex.recordQueryTime(performance.now() - startTime); - return results; - } - - // === Get All (for an agent) - Optimized with Index === - - async getAll(agentId: string, options?: { type?: MemoryType; limit?: number }): Promise { - this.ensureIndexInitialized(); - - // Use index to get candidates for this agent - const candidateIds = this.memoryIndex.getCandidates({ - agentId, - type: options?.type, - }); - - let results: MemoryEntry[]; - if (candidateIds) { - results = []; - for (const id of candidateIds) { - const idx = this.entryIndex.get(id); - if (idx !== undefined) { - results.push(this.entries[idx]); - } - } - } else { - // Fallback to linear scan - results = this.entries.filter(e => e.agentId === agentId); - if (options?.type) { - results = results.filter(e => e.type === options.type); - } - } - - results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - - if (options?.limit) { - results = results.slice(0, options.limit); - } - - return results; - } - - // === Get by ID (O(1) with index) === - - async get(id: string): Promise { - const idx = this.entryIndex.get(id); - return idx !== undefined ? this.entries[idx] ?? null : null; - } - - // === Forget === - - async forget(id: string): Promise { - const idx = this.entryIndex.get(id); - if (idx !== undefined) { - this.removeEntryFromIndex(id); - this.entries.splice(idx, 1); - // Rebuild entry index since positions changed - this.entryIndex.clear(); - this.entries.forEach((entry, i) => { - this.entryIndex.set(entry.id, i); - }); - this.persist(); - } - } - - // === Prune (bulk cleanup) === - - async prune(options: { - maxAgeDays?: number; - minImportance?: number; - agentId?: string; - }): Promise { - const before = this.entries.length; - const now = Date.now(); - - const toRemove: string[] = []; - - this.entries = this.entries.filter(entry => { - if (options.agentId && entry.agentId !== options.agentId) return true; // keep other agents - - const ageDays = (now - new Date(entry.lastAccessedAt).getTime()) / (24 * 60 * 60 * 1000); - const tooOld = options.maxAgeDays !== undefined && ageDays > options.maxAgeDays; - const tooLow = options.minImportance !== undefined && entry.importance < options.minImportance; - - // Only prune if both conditions met (old AND low importance) - if (tooOld && tooLow) { - toRemove.push(entry.id); - return false; - } - return true; - }); - - // Remove from index - for (const id of toRemove) { - this.removeEntryFromIndex(id); - } - - // Rebuild entry index - this.entryIndex.clear(); - this.entries.forEach((entry, i) => { - this.entryIndex.set(entry.id, i); - }); - - const pruned = before - this.entries.length; - if (pruned > 0) { - this.persist(); - } - return pruned; - } - - // === Export to Markdown === - - async exportToMarkdown(agentId: string): Promise { - const agentEntries = this.entries - .filter(e => e.agentId === agentId) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - - if (agentEntries.length === 0) { - return `# Agent Memory Export\n\n_No memories recorded._\n`; - } - - const sections: string[] = [`# Agent Memory Export\n\n> Agent: ${agentId}\n> Exported: ${new Date().toISOString()}\n> Total entries: ${agentEntries.length}\n`]; - - const byType = new Map(); - for (const entry of agentEntries) { - const list = byType.get(entry.type) || []; - list.push(entry); - byType.set(entry.type, list); - } - - const typeLabels: Record = { - fact: '📋 事实', - preference: '⭐ 偏好', - lesson: '💡 经验教训', - context: '📌 上下文', - task: '📝 任务', - }; - - for (const [type, entries] of byType) { - sections.push(`\n## ${typeLabels[type] || type}\n`); - for (const entry of entries) { - const tags = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : ''; - sections.push(`- **[重要性:${entry.importance}]** ${entry.content}${tags}`); - sections.push(` _创建: ${entry.createdAt} | 访问: ${entry.accessCount}次_\n`); - } - } - - return sections.join('\n'); - } - - // === Stats === - - async stats(agentId?: string): Promise { - const entries = agentId - ? this.entries.filter(e => e.agentId === agentId) - : this.entries; - - const byType: Record = {}; - const byAgent: Record = {}; - - for (const entry of entries) { - byType[entry.type] = (byType[entry.type] || 0) + 1; - byAgent[entry.agentId] = (byAgent[entry.agentId] || 0) + 1; - } - - const sorted = [...entries].sort((a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() - ); - - return { - totalEntries: entries.length, - byType, - byAgent, - oldestEntry: sorted[0]?.createdAt ?? null, - newestEntry: sorted[sorted.length - 1]?.createdAt ?? null, - }; - } - - // === Update importance === - - async updateImportance(id: string, importance: number): Promise { - const entry = this.entries.find(e => e.id === id); - if (entry) { - entry.importance = Math.max(0, Math.min(10, importance)); - this.persist(); - } - } - - // === Helpers === - - private contentSimilarity(a: string, b: string): number { - const tokensA = new Set(tokenize(a)); - const tokensB = new Set(tokenize(b)); - if (tokensA.size === 0 || tokensB.size === 0) return 0; - - let intersection = 0; - for (const t of tokensA) { - if (tokensB.has(t)) intersection++; - } - return (2 * intersection) / (tokensA.size + tokensB.size); - } -} - -// === Singleton === - -let _instance: MemoryManager | null = null; - -export function getMemoryManager(): MemoryManager { - if (!_instance) { - _instance = new MemoryManager(); - } - return _instance; -} - -export function resetMemoryManager(): void { - _instance = null; -} diff --git a/desktop/src/lib/agent-swarm.ts b/desktop/src/lib/agent-swarm.ts index 1171ec7..523f59c 100644 --- a/desktop/src/lib/agent-swarm.ts +++ b/desktop/src/lib/agent-swarm.ts @@ -11,7 +11,7 @@ * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.5.1 */ -import { getMemoryManager } from './agent-memory'; +import { intelligenceClient } from './intelligence-client'; // === Types === @@ -199,10 +199,10 @@ export class AgentSwarm { // Save task result as memory try { - await getMemoryManager().save({ - agentId: this.config.coordinator, + await intelligenceClient.memory.store({ + agent_id: this.config.coordinator, + memory_type: 'lesson', content: `协作任务完成: "${task.description}" — ${task.subtasks.length}个子任务, 模式: ${task.communicationStyle}, 结果: ${(task.finalResult || '').slice(0, 200)}`, - type: 'lesson', importance: 6, source: 'auto', tags: ['swarm', task.communicationStyle], diff --git a/desktop/src/lib/context-compactor.ts b/desktop/src/lib/context-compactor.ts deleted file mode 100644 index 61ce404..0000000 --- a/desktop/src/lib/context-compactor.ts +++ /dev/null @@ -1,442 +0,0 @@ -/** - * Context Compactor - Manages infinite-length conversations without losing key info - * - * Flow: - * 1. Monitor token count against soft threshold - * 2. When threshold approached: flush memories from old messages - * 3. Summarize old messages into a compact system message - * 4. Replace old messages with summary — user sees no interruption - * - * Phase 2 implementation: heuristic token estimation + rule-based summarization. - * Phase 4 upgrade: LLM-powered summarization + semantic importance scoring. - * - * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.3.1 - */ - -import { getMemoryExtractor, type ConversationMessage } from './memory-extractor'; -import { - getLLMAdapter, - llmCompact, - type LLMServiceAdapter, - type LLMProvider, -} from './llm-service'; -import { canAutoExecute } from './autonomy-manager'; - -// === Types === - -export interface CompactionConfig { - softThresholdTokens: number; // Trigger compaction when approaching this (default 15000) - hardThresholdTokens: number; // Force compaction at this limit (default 20000) - reserveTokens: number; // Reserve for new messages (default 4000) - memoryFlushEnabled: boolean; // Extract memories before compacting (default true) - keepRecentMessages: number; // Always keep this many recent messages (default 6) - summaryMaxTokens: number; // Max tokens for the compaction summary (default 800) - useLLM: boolean; // Use LLM for high-quality summarization (Phase 4) - llmProvider?: LLMProvider; // Preferred LLM provider - llmFallbackToRules: boolean; // Fall back to rules if LLM fails -} - -export interface CompactableMessage { - role: string; - content: string; - id?: string; - timestamp?: Date; -} - -export interface CompactionResult { - compactedMessages: CompactableMessage[]; - summary: string; - originalCount: number; - retainedCount: number; - flushedMemories: number; - tokensBeforeCompaction: number; - tokensAfterCompaction: number; -} - -export interface CompactionCheck { - shouldCompact: boolean; - currentTokens: number; - threshold: number; - urgency: 'none' | 'soft' | 'hard'; -} - -// === Default Config === - -export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = { - softThresholdTokens: 15000, - hardThresholdTokens: 20000, - reserveTokens: 4000, - memoryFlushEnabled: true, - keepRecentMessages: 6, - summaryMaxTokens: 800, - useLLM: false, - llmFallbackToRules: true, -}; - -// === Token Estimation === - -/** - * Heuristic token count estimation. - * CJK characters ≈ 1.5 tokens each, English words ≈ 1.3 tokens each. - * This is intentionally conservative (overestimates) to avoid hitting real limits. - */ -export function estimateTokens(text: string): number { - if (!text) return 0; - - let tokens = 0; - for (const char of text) { - const code = char.codePointAt(0) || 0; - if (code >= 0x4E00 && code <= 0x9FFF) { - tokens += 1.5; // CJK ideographs - } else if (code >= 0x3400 && code <= 0x4DBF) { - tokens += 1.5; // CJK Extension A - } else if (code >= 0x3000 && code <= 0x303F) { - tokens += 1; // CJK punctuation - } else if (char === ' ' || char === '\n' || char === '\t') { - tokens += 0.25; // whitespace - } else { - tokens += 0.3; // ASCII chars (roughly 4 chars per token for English) - } - } - - return Math.ceil(tokens); -} - -export function estimateMessagesTokens(messages: CompactableMessage[]): number { - let total = 0; - for (const msg of messages) { - total += estimateTokens(msg.content); - total += 4; // message framing overhead (role, separators) - } - return total; -} - -// === Context Compactor === - -export class ContextCompactor { - private config: CompactionConfig; - private llmAdapter: LLMServiceAdapter | null = null; - - constructor(config?: Partial) { - this.config = { ...DEFAULT_COMPACTION_CONFIG, ...config }; - - // Initialize LLM adapter if configured - if (this.config.useLLM) { - try { - this.llmAdapter = getLLMAdapter(); - } catch (error) { - console.warn('[ContextCompactor] Failed to initialize LLM adapter:', error); - } - } - } - - /** - * Check if compaction is needed based on current message token count. - */ - checkThreshold(messages: CompactableMessage[]): CompactionCheck { - const currentTokens = estimateMessagesTokens(messages); - - if (currentTokens >= this.config.hardThresholdTokens) { - return { shouldCompact: true, currentTokens, threshold: this.config.hardThresholdTokens, urgency: 'hard' }; - } - - if (currentTokens >= this.config.softThresholdTokens) { - return { shouldCompact: true, currentTokens, threshold: this.config.softThresholdTokens, urgency: 'soft' }; - } - - return { shouldCompact: false, currentTokens, threshold: this.config.softThresholdTokens, urgency: 'none' }; - } - - /** - * Execute memory flush: extract memories from messages about to be compacted. - */ - async memoryFlush( - messagesToCompact: CompactableMessage[], - agentId: string, - conversationId?: string - ): Promise { - if (!this.config.memoryFlushEnabled) return 0; - - try { - const extractor = getMemoryExtractor(); - const convMessages: ConversationMessage[] = messagesToCompact.map(m => ({ - role: m.role, - content: m.content, - })); - - const result = await extractor.extractFromConversation(convMessages, agentId, conversationId); - return result.saved; - } catch (err) { - console.warn('[ContextCompactor] Memory flush failed:', err); - return 0; - } - } - - /** - * Execute compaction: summarize old messages, keep recent ones. - * - * Phase 2: Rule-based summarization (extract key points heuristically). - * Phase 4: LLM-powered summarization for higher quality summaries. - */ - async compact( - messages: CompactableMessage[], - agentId: string, - conversationId?: string, - options?: { forceLLM?: boolean; skipAutonomyCheck?: boolean } - ): Promise { - // Autonomy check - verify if compaction is allowed - if (!options?.skipAutonomyCheck) { - const { canProceed, decision } = canAutoExecute('compaction_run', 5); - if (!canProceed) { - console.log(`[ContextCompactor] Autonomy check failed: ${decision.reason}`); - // Return result without compaction - return { - compactedMessages: messages, - summary: '', - originalCount: messages.length, - retainedCount: messages.length, - flushedMemories: 0, - tokensBeforeCompaction: estimateMessagesTokens(messages), - tokensAfterCompaction: estimateMessagesTokens(messages), - }; - } - console.log(`[ContextCompactor] Autonomy check passed: ${decision.reason}`); - } - - const tokensBeforeCompaction = estimateMessagesTokens(messages); - const keepCount = Math.min(this.config.keepRecentMessages, messages.length); - - // Split: old messages to compact vs recent to keep - const splitIndex = messages.length - keepCount; - const oldMessages = messages.slice(0, splitIndex); - const recentMessages = messages.slice(splitIndex); - - // Step 1: Memory flush from old messages - let flushedMemories = 0; - if (oldMessages.length > 0) { - flushedMemories = await this.memoryFlush(oldMessages, agentId, conversationId); - } - - // Step 2: Generate summary of old messages - let summary: string; - if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) { - try { - console.log('[ContextCompactor] Using LLM-powered summarization'); - summary = await this.llmGenerateSummary(oldMessages); - } catch (error) { - console.error('[ContextCompactor] LLM summarization failed:', error); - if (!this.config.llmFallbackToRules) { - throw error; - } - console.log('[ContextCompactor] Falling back to rule-based summarization'); - summary = this.generateSummary(oldMessages); - } - } else { - summary = this.generateSummary(oldMessages); - } - - // Step 3: Build compacted message list - const summaryMessage: CompactableMessage = { - role: 'system', - content: summary, - id: `compaction_${Date.now()}`, - timestamp: new Date(), - }; - - const compactedMessages = [summaryMessage, ...recentMessages]; - const tokensAfterCompaction = estimateMessagesTokens(compactedMessages); - - console.log( - `[ContextCompactor] Compacted: ${messages.length} → ${compactedMessages.length} messages, ` + - `${tokensBeforeCompaction} → ${tokensAfterCompaction} tokens, ` + - `${flushedMemories} memories flushed` - ); - - return { - compactedMessages, - summary, - originalCount: messages.length, - retainedCount: compactedMessages.length, - flushedMemories, - tokensBeforeCompaction, - tokensAfterCompaction, - }; - } - - /** - * LLM-powered summary generation for high-quality compaction. - */ - private async llmGenerateSummary(messages: CompactableMessage[]): Promise { - if (messages.length === 0) return '[对话开始]'; - - // Build conversation text for LLM - const conversationText = messages - .filter(m => m.role === 'user' || m.role === 'assistant') - .map(m => `[${m.role === 'user' ? '用户' : '助手'}]: ${m.content}`) - .join('\n\n'); - - // Use llmCompact helper from llm-service - const llmSummary = await llmCompact(conversationText, this.llmAdapter!); - - // Enforce token limit - const summaryTokens = estimateTokens(llmSummary); - if (summaryTokens > this.config.summaryMaxTokens) { - return llmSummary.slice(0, this.config.summaryMaxTokens * 2) + '\n...(摘要已截断)'; - } - - return `[LLM摘要]\n${llmSummary}`; - } - - /** - * Phase 2: Rule-based summary generation. - * Extracts key topics, decisions, and action items from old messages. - */ - private generateSummary(messages: CompactableMessage[]): string { - if (messages.length === 0) return '[对话开始]'; - - const sections: string[] = ['[以下是之前对话的摘要]']; - - // Extract user questions/topics - const userMessages = messages.filter(m => m.role === 'user'); - const assistantMessages = messages.filter(m => m.role === 'assistant'); - - // Summarize topics discussed - if (userMessages.length > 0) { - const topics = userMessages - .map(m => this.extractTopic(m.content)) - .filter(Boolean); - - if (topics.length > 0) { - sections.push(`讨论主题: ${topics.join('; ')}`); - } - } - - // Extract key decisions/conclusions from assistant - if (assistantMessages.length > 0) { - const conclusions = assistantMessages - .flatMap(m => this.extractConclusions(m.content)) - .slice(0, 5); - - if (conclusions.length > 0) { - sections.push(`关键结论:\n${conclusions.map(c => `- ${c}`).join('\n')}`); - } - } - - // Extract any code/technical context - const technicalContext = messages - .filter(m => m.content.includes('```') || m.content.includes('function ') || m.content.includes('class ')) - .map(m => { - const codeMatch = m.content.match(/```(\w+)?[\s\S]*?```/); - return codeMatch ? `代码片段 (${codeMatch[1] || 'code'})` : null; - }) - .filter(Boolean); - - if (technicalContext.length > 0) { - sections.push(`技术上下文: ${technicalContext.join(', ')}`); - } - - // Message count summary - sections.push(`(已压缩 ${messages.length} 条消息,其中用户 ${userMessages.length} 条,助手 ${assistantMessages.length} 条)`); - - const summary = sections.join('\n'); - - // Enforce token limit - const summaryTokens = estimateTokens(summary); - if (summaryTokens > this.config.summaryMaxTokens) { - return summary.slice(0, this.config.summaryMaxTokens * 2) + '\n...(摘要已截断)'; - } - - return summary; - } - - /** - * Extract the main topic from a user message (first 50 chars or first sentence). - */ - private extractTopic(content: string): string { - const trimmed = content.trim(); - // First sentence or first 50 chars - const sentenceEnd = trimmed.search(/[。!?\n]/); - if (sentenceEnd > 0 && sentenceEnd <= 80) { - return trimmed.slice(0, sentenceEnd + 1); - } - if (trimmed.length <= 50) return trimmed; - return trimmed.slice(0, 50) + '...'; - } - - /** - * Extract key conclusions/decisions from assistant messages. - */ - private extractConclusions(content: string): string[] { - const conclusions: string[] = []; - const patterns = [ - /(?:总结|结论|关键点|建议|方案)[::]\s*(.{10,100})/g, - /(?:\d+\.\s+)(.{10,80})/g, - /(?:需要|应该|可以|建议)(.{5,60})/g, - ]; - - for (const pattern of patterns) { - const matches = content.matchAll(pattern); - for (const match of matches) { - const text = match[1]?.trim() || match[0].trim(); - if (text.length > 10 && text.length < 100) { - conclusions.push(text); - } - } - } - - return conclusions.slice(0, 3); - } - - /** - * Build the LLM compaction prompt for Phase 3. - * Returns the prompt to send to LLM for generating a high-quality summary. - */ - buildCompactionPrompt(messages: CompactableMessage[]): string { - const conversationText = messages - .filter(m => m.role === 'user' || m.role === 'assistant') - .map(m => `[${m.role === 'user' ? '用户' : '助手'}]: ${m.content}`) - .join('\n\n'); - - return `请将以下对话压缩为简洁摘要,保留: -1. 用户提出的所有问题和需求 -2. 达成的关键决策和结论 -3. 重要的技术上下文(文件路径、配置、代码片段名称) -4. 未完成的任务或待办事项 - -输出格式: -- 讨论主题: ... -- 关键决策: ... -- 技术上下文: ... -- 待办事项: ... - -请用中文输出,控制在 300 字以内。 - -对话内容: -${conversationText}`; - } - - // === Config Management === - - getConfig(): CompactionConfig { - return { ...this.config }; - } - - updateConfig(updates: Partial): void { - this.config = { ...this.config, ...updates }; - } -} - -// === Singleton === - -let _instance: ContextCompactor | null = null; - -export function getContextCompactor(config?: Partial): ContextCompactor { - if (!_instance) { - _instance = new ContextCompactor(config); - } - return _instance; -} - -export function resetContextCompactor(): void { - _instance = null; -} diff --git a/desktop/src/lib/error-handling.ts b/desktop/src/lib/error-handling.ts index c23fb38..444b1d3 100644 --- a/desktop/src/lib/error-handling.ts +++ b/desktop/src/lib/error-handling.ts @@ -17,6 +17,8 @@ import { export interface StoredError extends AppError { dismissed: boolean; reported: boolean; + stack?: string; + context?: Record; } // === Error Store === @@ -303,6 +305,13 @@ export function dismissAllErrors(): void { errorStore.dismissAll(); } +/** + * Dismiss all active errors (alias for dismissAllErrors). + */ +export function dismissAll(): void { + errorStore.dismissAll(); +} + /** * Mark an error as reported. */ @@ -317,6 +326,13 @@ export function getActiveErrors(): StoredError[] { return errorStore.getUndismissedErrors(); } +/** + * Get all undismissed errors (alias for getActiveErrors). + */ +export function getUndismissedErrors(): StoredError[] { + return errorStore.getUndismissedErrors(); +} + /** * Get the count of active errors. */ diff --git a/desktop/src/lib/heartbeat-engine.ts b/desktop/src/lib/heartbeat-engine.ts deleted file mode 100644 index 0d63607..0000000 --- a/desktop/src/lib/heartbeat-engine.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * Heartbeat Engine - Periodic proactive checks for ZCLAW agents - * - * Runs on a configurable interval, executing a checklist of items. - * Each check can produce alerts that surface via desktop notification or UI. - * Supports quiet hours (no notifications during sleep time). - * - * Phase 3 implementation: rule-based checks with configurable checklist. - * Phase 4 upgrade: LLM-powered interpretation of HEARTBEAT.md checklists. - * - * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.1 - */ - -import { getMemoryManager } from './agent-memory'; - -// === Types === - -export interface HeartbeatConfig { - enabled: boolean; - intervalMinutes: number; - quietHoursStart?: string; // "22:00" format - quietHoursEnd?: string; // "08:00" format - notifyChannel: 'desktop' | 'ui' | 'all'; - proactivityLevel: 'silent' | 'light' | 'standard' | 'autonomous'; - maxAlertsPerTick: number; -} - -export interface HeartbeatAlert { - title: string; - content: string; - urgency: 'low' | 'medium' | 'high'; - source: string; - timestamp: string; -} - -export interface HeartbeatResult { - status: 'ok' | 'alert'; - alerts: HeartbeatAlert[]; - checkedItems: number; - timestamp: string; -} - -export type HeartbeatCheckFn = (agentId: string) => Promise; - -// === Default Config === - -export const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfig = { - enabled: false, - intervalMinutes: 30, - quietHoursStart: '22:00', - quietHoursEnd: '08:00', - notifyChannel: 'ui', - proactivityLevel: 'light', - maxAlertsPerTick: 5, -}; - -// === Built-in Checks === - -/** Check if agent has unresolved task memories */ -async function checkPendingTasks(agentId: string): Promise { - const mgr = getMemoryManager(); - const tasks = await mgr.getAll(agentId, { type: 'task', limit: 10 }); - const pending = tasks.filter(t => t.importance >= 6); - - if (pending.length > 0) { - return { - title: '待办任务提醒', - content: `有 ${pending.length} 个待处理任务:${pending.slice(0, 3).map(t => t.content).join(';')}`, - urgency: pending.some(t => t.importance >= 8) ? 'high' : 'medium', - source: 'pending-tasks', - timestamp: new Date().toISOString(), - }; - } - return null; -} - -/** Check if memory storage is getting large and might need pruning */ -async function checkMemoryHealth(agentId: string): Promise { - const mgr = getMemoryManager(); - const stats = await mgr.stats(agentId); - - if (stats.totalEntries > 500) { - return { - title: '记忆存储提醒', - content: `已积累 ${stats.totalEntries} 条记忆,建议清理低重要性的旧记忆以保持检索效率。`, - urgency: stats.totalEntries > 1000 ? 'high' : 'low', - source: 'memory-health', - timestamp: new Date().toISOString(), - }; - } - return null; -} - -/** Check if it's been a while since last interaction (greeting opportunity) */ -async function checkIdleGreeting(_agentId: string): Promise { - // Check localStorage for last interaction time - try { - const lastInteraction = localStorage.getItem('zclaw-last-interaction'); - if (lastInteraction) { - const elapsed = Date.now() - parseInt(lastInteraction, 10); - const hoursSinceInteraction = elapsed / (1000 * 60 * 60); - - // Only greet on weekdays between 9am-6pm if idle for > 4 hours - const now = new Date(); - const isWeekday = now.getDay() >= 1 && now.getDay() <= 5; - const isWorkHours = now.getHours() >= 9 && now.getHours() <= 18; - - if (isWeekday && isWorkHours && hoursSinceInteraction > 4) { - return { - title: '闲置提醒', - content: `已有 ${Math.floor(hoursSinceInteraction)} 小时未交互。需要我帮你处理什么吗?`, - urgency: 'low', - source: 'idle-greeting', - timestamp: new Date().toISOString(), - }; - } - } - } catch { - // localStorage not available in test - } - return null; -} - -// === Heartbeat Engine === - -const HISTORY_STORAGE_KEY = 'zclaw-heartbeat-history'; - -export class HeartbeatEngine { - private config: HeartbeatConfig; - private timerId: ReturnType | null = null; - private checks: HeartbeatCheckFn[] = []; - private history: HeartbeatResult[] = []; - private agentId: string; - private onAlert?: (alerts: HeartbeatAlert[]) => void; - - constructor(agentId: string, config?: Partial) { - this.config = { ...DEFAULT_HEARTBEAT_CONFIG, ...config }; - this.agentId = agentId; - this.loadHistory(); - - // Register built-in checks - this.checks = [ - checkPendingTasks, - checkMemoryHealth, - checkIdleGreeting, - ]; - } - - // === Lifecycle === - - start(onAlert?: (alerts: HeartbeatAlert[]) => void): void { - if (this.timerId) this.stop(); - if (!this.config.enabled) return; - - this.onAlert = onAlert; - const intervalMs = this.config.intervalMinutes * 60 * 1000; - - this.timerId = setInterval(() => { - this.tick().catch(err => - console.warn('[Heartbeat] Tick failed:', err) - ); - }, intervalMs); - - console.log(`[Heartbeat] Started for ${this.agentId}, interval: ${this.config.intervalMinutes}min`); - } - - stop(): void { - if (this.timerId) { - clearInterval(this.timerId); - this.timerId = null; - console.log(`[Heartbeat] Stopped for ${this.agentId}`); - } - } - - isRunning(): boolean { - return this.timerId !== null; - } - - // === Single Tick === - - async tick(): Promise { - // Quiet hours check - if (this.isQuietHours()) { - const result: HeartbeatResult = { - status: 'ok', - alerts: [], - checkedItems: 0, - timestamp: new Date().toISOString(), - }; - return result; - } - - const alerts: HeartbeatAlert[] = []; - - for (const check of this.checks) { - try { - const alert = await check(this.agentId); - if (alert) { - alerts.push(alert); - if (alerts.length >= this.config.maxAlertsPerTick) break; - } - } catch (err) { - console.warn('[Heartbeat] Check failed:', err); - } - } - - // Filter by proactivity level - const filteredAlerts = this.filterByProactivity(alerts); - - const result: HeartbeatResult = { - status: filteredAlerts.length > 0 ? 'alert' : 'ok', - alerts: filteredAlerts, - checkedItems: this.checks.length, - timestamp: new Date().toISOString(), - }; - - // Store history - this.history.push(result); - if (this.history.length > 100) { - this.history = this.history.slice(-50); - } - this.saveHistory(); - - // Notify - if (filteredAlerts.length > 0 && this.onAlert) { - this.onAlert(filteredAlerts); - } - - return result; - } - - // === Custom Checks === - - registerCheck(check: HeartbeatCheckFn): void { - this.checks.push(check); - } - - // === History === - - getHistory(limit: number = 20): HeartbeatResult[] { - return this.history.slice(-limit); - } - - getLastResult(): HeartbeatResult | null { - return this.history.length > 0 ? this.history[this.history.length - 1] : null; - } - - // === Quiet Hours === - - isQuietHours(): boolean { - if (!this.config.quietHoursStart || !this.config.quietHoursEnd) return false; - - const now = new Date(); - const currentMinutes = now.getHours() * 60 + now.getMinutes(); - - const [startH, startM] = this.config.quietHoursStart.split(':').map(Number); - const [endH, endM] = this.config.quietHoursEnd.split(':').map(Number); - const startMinutes = startH * 60 + startM; - const endMinutes = endH * 60 + endM; - - if (startMinutes <= endMinutes) { - // Same-day range (e.g., 13:00-17:00) - return currentMinutes >= startMinutes && currentMinutes < endMinutes; - } else { - // Cross-midnight range (e.g., 22:00-08:00) - return currentMinutes >= startMinutes || currentMinutes < endMinutes; - } - } - - // === Config === - - getConfig(): HeartbeatConfig { - return { ...this.config }; - } - - updateConfig(updates: Partial): void { - const wasEnabled = this.config.enabled; - this.config = { ...this.config, ...updates }; - - // Restart if interval changed or enabled/disabled - if (this.timerId && (updates.intervalMinutes || updates.enabled === false)) { - this.stop(); - if (this.config.enabled) { - this.start(this.onAlert); - } - } else if (!wasEnabled && this.config.enabled) { - this.start(this.onAlert); - } - } - - // === Internal === - - private filterByProactivity(alerts: HeartbeatAlert[]): HeartbeatAlert[] { - switch (this.config.proactivityLevel) { - case 'silent': - return []; // Never alert - case 'light': - return alerts.filter(a => a.urgency === 'high'); - case 'standard': - return alerts.filter(a => a.urgency === 'high' || a.urgency === 'medium'); - case 'autonomous': - return alerts; // Show everything - default: - return alerts.filter(a => a.urgency === 'high'); - } - } - - private loadHistory(): void { - try { - const raw = localStorage.getItem(HISTORY_STORAGE_KEY); - if (raw) { - this.history = JSON.parse(raw); - } - } catch { - this.history = []; - } - } - - private saveHistory(): void { - try { - localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(this.history.slice(-50))); - } catch { - // silent - } - } -} - -// === Singleton === - -let _instances: Map = new Map(); - -export function getHeartbeatEngine(agentId: string, config?: Partial): HeartbeatEngine { - let engine = _instances.get(agentId); - if (!engine) { - engine = new HeartbeatEngine(agentId, config); - _instances.set(agentId, engine); - } - return engine; -} - -export function resetHeartbeatEngines(): void { - for (const engine of _instances.values()) { - engine.stop(); - } - _instances = new Map(); -} diff --git a/desktop/src/lib/intelligence-client.ts b/desktop/src/lib/intelligence-client.ts new file mode 100644 index 0000000..eb808e4 --- /dev/null +++ b/desktop/src/lib/intelligence-client.ts @@ -0,0 +1,955 @@ +/** + * 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 environment + * + * 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 { + intelligence, + type MemoryEntryInput, + type PersistentMemory, + type MemorySearchOptions as BackendSearchOptions, + type MemoryStats as BackendMemoryStats, + type HeartbeatConfig, + type HeartbeatResult, + type CompactableMessage, + type CompactionResult, + type CompactionCheck, + type CompactionConfig, + type MemoryEntryForAnalysis, + type ReflectionResult, + type ReflectionState, + type ReflectionConfig, + type IdentityFiles, + type IdentityChangeProposal, + type IdentitySnapshot, +} from './intelligence-backend'; + +// === Environment Detection === + +/** + * Check if running in Tauri environment + */ +export function isTauriEnv(): boolean { + return typeof window !== 'undefined' && '__TAURI__' in window; +} + +// === Frontend Types (for backward compatibility) === + +export type MemoryType = 'fact' | 'preference' | 'lesson' | 'context' | 'task'; +export type MemorySource = 'auto' | 'user' | 'reflection' | 'llm-reflection'; + +export interface MemoryEntry { + id: string; + agentId: string; + content: string; + type: MemoryType; + importance: number; + source: MemorySource; + tags: string[]; + createdAt: string; + lastAccessedAt: string; + accessCount: number; + conversationId?: string; +} + +export interface MemorySearchOptions { + agentId?: string; + type?: MemoryType; + types?: MemoryType[]; + tags?: string[]; + query?: string; + limit?: number; + minImportance?: number; +} + +export interface MemoryStats { + totalEntries: number; + byType: Record; + byAgent: Record; + oldestEntry: string | null; + newestEntry: string | null; +} + +// === Re-export types from intelligence-backend === + +export type { + HeartbeatConfig, + HeartbeatResult, + HeartbeatAlert, + CompactableMessage, + CompactionResult, + CompactionCheck, + CompactionConfig, + PatternObservation, + ImprovementSuggestion, + ReflectionResult, + ReflectionState, + ReflectionConfig, + IdentityFiles, + IdentityChangeProposal, + IdentitySnapshot, +} from './intelligence-backend'; + +// === Type Conversion Utilities === + +/** + * Convert backend PersistentMemory to frontend MemoryEntry format + */ +export function toFrontendMemory(backend: PersistentMemory): MemoryEntry { + return { + id: backend.id, + agentId: backend.agent_id, + content: backend.content, + type: backend.memory_type as MemoryType, + importance: backend.importance, + source: backend.source as MemorySource, + tags: parseTags(backend.tags), + createdAt: backend.created_at, + lastAccessedAt: backend.last_accessed_at, + accessCount: backend.access_count, + conversationId: backend.conversation_id ?? undefined, + }; +} + +/** + * Convert frontend MemoryEntry to backend MemoryEntryInput format + */ +export function toBackendMemoryInput(entry: Omit): MemoryEntryInput { + return { + agent_id: entry.agentId, + memory_type: entry.type, + content: entry.content, + importance: entry.importance, + source: entry.source, + tags: entry.tags, + conversation_id: entry.conversationId, + }; +} + +/** + * Convert frontend search options to backend format + */ +export function toBackendSearchOptions(options: MemorySearchOptions): BackendSearchOptions { + return { + agent_id: options.agentId, + memory_type: options.type, + tags: options.tags, + query: options.query, + limit: options.limit, + min_importance: options.minImportance, + }; +} + +/** + * Convert backend stats to frontend format + */ +export function toFrontendStats(backend: BackendMemoryStats): MemoryStats { + return { + totalEntries: backend.total_memories, + byType: backend.by_type, + byAgent: backend.by_agent, + oldestEntry: backend.oldest_memory, + newestEntry: backend.newest_memory, + }; +} + +/** + * Parse tags from backend (JSON string or array) + */ +function parseTags(tags: string | string[]): string[] { + if (Array.isArray(tags)) return tags; + if (!tags) return []; + try { + return JSON.parse(tags); + } catch { + return []; + } +} + +// === LocalStorage Fallback Implementation === + +const FALLBACK_STORAGE_KEY = 'zclaw-intelligence-fallback'; + +interface FallbackMemoryStore { + memories: MemoryEntry[]; +} + +function getFallbackStore(): FallbackMemoryStore { + try { + const stored = localStorage.getItem(FALLBACK_STORAGE_KEY); + if (stored) { + return JSON.parse(stored); + } + } catch { + // ignore + } + return { memories: [] }; +} + +function saveFallbackStore(store: FallbackMemoryStore): void { + try { + localStorage.setItem(FALLBACK_STORAGE_KEY, JSON.stringify(store)); + } catch { + console.warn('[IntelligenceClient] Failed to save to localStorage'); + } +} + +// Fallback Memory API +const fallbackMemory = { + async init(): Promise { + // No-op for localStorage + }, + + async store(entry: MemoryEntryInput): Promise { + const store = getFallbackStore(); + const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + const now = new Date().toISOString(); + + const memory: MemoryEntry = { + id, + agentId: entry.agent_id, + content: entry.content, + type: entry.memory_type as MemoryType, + importance: entry.importance ?? 5, + source: (entry.source as MemorySource) ?? 'auto', + tags: entry.tags ?? [], + createdAt: now, + lastAccessedAt: now, + accessCount: 0, + conversationId: entry.conversation_id, + }; + + store.memories.push(memory); + saveFallbackStore(store); + return id; + }, + + async get(id: string): Promise { + const store = getFallbackStore(); + return store.memories.find(m => m.id === id) ?? null; + }, + + async search(options: MemorySearchOptions): Promise { + const store = getFallbackStore(); + let results = store.memories; + + if (options.agentId) { + results = results.filter(m => m.agentId === options.agentId); + } + if (options.type) { + results = results.filter(m => m.type === options.type); + } + if (options.minImportance !== undefined) { + results = results.filter(m => m.importance >= options.minImportance!); + } + if (options.query) { + const queryLower = options.query.toLowerCase(); + results = results.filter(m => + m.content.toLowerCase().includes(queryLower) || + m.tags.some(t => t.toLowerCase().includes(queryLower)) + ); + } + if (options.limit) { + results = results.slice(0, options.limit); + } + + return results; + }, + + async delete(id: string): Promise { + const store = getFallbackStore(); + store.memories = store.memories.filter(m => m.id !== id); + saveFallbackStore(store); + }, + + async deleteAll(agentId: string): Promise { + const store = getFallbackStore(); + const before = store.memories.length; + store.memories = store.memories.filter(m => m.agentId !== agentId); + saveFallbackStore(store); + return before - store.memories.length; + }, + + async stats(): Promise { + const store = getFallbackStore(); + const byType: Record = {}; + const byAgent: Record = {}; + + for (const m of store.memories) { + byType[m.type] = (byType[m.type] ?? 0) + 1; + byAgent[m.agentId] = (byAgent[m.agentId] ?? 0) + 1; + } + + const sorted = [...store.memories].sort((a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + + return { + totalEntries: store.memories.length, + byType, + byAgent, + oldestEntry: sorted[0]?.createdAt ?? null, + newestEntry: sorted[sorted.length - 1]?.createdAt ?? null, + }; + }, + + async export(): Promise { + const store = getFallbackStore(); + return store.memories; + }, + + async import(memories: MemoryEntry[]): Promise { + const store = getFallbackStore(); + store.memories.push(...memories); + saveFallbackStore(store); + return memories.length; + }, + + async dbPath(): Promise { + return 'localStorage://zclaw-intelligence-fallback'; + }, +}; + +// Fallback Compactor API +const fallbackCompactor = { + async estimateTokens(text: string): Promise { + // Simple heuristic: ~4 chars per token for English, ~1.5 for CJK + const cjkChars = (text.match(/[\u4e00-\u9fff\u3040-\u30ff]/g) ?? []).length; + const otherChars = text.length - cjkChars; + return Math.ceil(cjkChars * 1.5 + otherChars / 4); + }, + + async estimateMessagesTokens(messages: CompactableMessage[]): Promise { + let total = 0; + for (const m of messages) { + total += await fallbackCompactor.estimateTokens(m.content); + } + return total; + }, + + async checkThreshold( + messages: CompactableMessage[], + config?: CompactionConfig + ): Promise { + const threshold = config?.soft_threshold_tokens ?? 15000; + const currentTokens = await fallbackCompactor.estimateMessagesTokens(messages); + + return { + should_compact: currentTokens >= threshold, + current_tokens: currentTokens, + threshold, + urgency: currentTokens >= (config?.hard_threshold_tokens ?? 20000) ? 'hard' : + currentTokens >= threshold ? 'soft' : 'none', + }; + }, + + async compact( + messages: CompactableMessage[], + _agentId: string, + _conversationId?: string, + config?: CompactionConfig + ): Promise { + // Simple rule-based compaction: keep last N messages + const keepRecent = config?.keep_recent_messages ?? 10; + const retained = messages.slice(-keepRecent); + + return { + compacted_messages: retained, + summary: `[Compacted ${messages.length - retained.length} earlier messages]`, + original_count: messages.length, + retained_count: retained.length, + flushed_memories: 0, + tokens_before_compaction: await fallbackCompactor.estimateMessagesTokens(messages), + tokens_after_compaction: await fallbackCompactor.estimateMessagesTokens(retained), + }; + }, +}; + +// Fallback Reflection API +const fallbackReflection = { + _conversationCount: 0, + _lastReflection: null as string | null, + + async init(_config?: ReflectionConfig): Promise { + // No-op + }, + + async recordConversation(): Promise { + fallbackReflection._conversationCount++; + }, + + async shouldReflect(): Promise { + return fallbackReflection._conversationCount >= 5; + }, + + async reflect(_agentId: string, _memories: MemoryEntryForAnalysis[]): Promise { + fallbackReflection._conversationCount = 0; + fallbackReflection._lastReflection = new Date().toISOString(); + + return { + patterns: [], + improvements: [], + identity_proposals: [], + new_memories: 0, + timestamp: new Date().toISOString(), + }; + }, + + async getHistory(_limit?: number): Promise { + return []; + }, + + async getState(): Promise { + return { + conversations_since_reflection: fallbackReflection._conversationCount, + last_reflection_time: fallbackReflection._lastReflection, + last_reflection_agent_id: null, + }; + }, +}; + +// Fallback Identity API +const fallbackIdentities = new Map(); +const fallbackProposals: IdentityChangeProposal[] = []; + +const fallbackIdentity = { + async get(agentId: string): Promise { + if (!fallbackIdentities.has(agentId)) { + fallbackIdentities.set(agentId, { + soul: '# Agent Soul\n\nA helpful AI assistant.', + instructions: '# Instructions\n\nBe helpful and concise.', + user_profile: '# User Profile\n\nNo profile yet.', + }); + } + return fallbackIdentities.get(agentId)!; + }, + + async getFile(agentId: string, file: string): Promise { + const files = await fallbackIdentity.get(agentId); + return files[file as keyof IdentityFiles] ?? ''; + }, + + async buildPrompt(agentId: string, memoryContext?: string): Promise { + const files = await fallbackIdentity.get(agentId); + let prompt = `${files.soul}\n\n## Instructions\n${files.instructions}\n\n## User Profile\n${files.user_profile}`; + if (memoryContext) { + prompt += `\n\n## Memory Context\n${memoryContext}`; + } + return prompt; + }, + + async updateUserProfile(agentId: string, content: string): Promise { + const files = await fallbackIdentity.get(agentId); + files.user_profile = content; + fallbackIdentities.set(agentId, files); + }, + + async appendUserProfile(agentId: string, addition: string): Promise { + const files = await fallbackIdentity.get(agentId); + files.user_profile += `\n\n${addition}`; + fallbackIdentities.set(agentId, files); + }, + + async proposeChange( + agentId: string, + file: 'soul' | 'instructions', + suggestedContent: string, + reason: string + ): Promise { + const files = await fallbackIdentity.get(agentId); + const proposal: IdentityChangeProposal = { + id: `prop_${Date.now()}`, + agent_id: agentId, + file, + reason, + current_content: files[file] ?? '', + suggested_content: suggestedContent, + status: 'pending', + created_at: new Date().toISOString(), + }; + fallbackProposals.push(proposal); + return proposal; + }, + + async approveProposal(proposalId: string): Promise { + const proposal = fallbackProposals.find(p => p.id === proposalId); + if (!proposal) throw new Error('Proposal not found'); + + proposal.status = 'approved'; + const files = await fallbackIdentity.get(proposal.agent_id); + files[proposal.file] = proposal.suggested_content; + fallbackIdentities.set(proposal.agent_id, files); + return files; + }, + + async rejectProposal(proposalId: string): Promise { + const proposal = fallbackProposals.find(p => p.id === proposalId); + if (proposal) { + proposal.status = 'rejected'; + } + }, + + async getPendingProposals(agentId?: string): Promise { + return fallbackProposals.filter(p => + p.status === 'pending' && (!agentId || p.agent_id === agentId) + ); + }, + + async updateFile(agentId: string, file: string, content: string): Promise { + const files = await fallbackIdentity.get(agentId); + if (file in files) { + // IdentityFiles has known properties, update safely + const key = file as keyof IdentityFiles; + if (key in files) { + files[key] = content; + fallbackIdentities.set(agentId, files); + } + } + }, + + async getSnapshots(_agentId: string, _limit?: number): Promise { + return []; + }, + + async restoreSnapshot(_agentId: string, _snapshotId: string): Promise { + // No-op for fallback + }, + + async listAgents(): Promise { + return Array.from(fallbackIdentities.keys()); + }, + + async deleteAgent(agentId: string): Promise { + fallbackIdentities.delete(agentId); + }, +}; + +// Fallback Heartbeat API +const fallbackHeartbeat = { + _configs: new Map(), + + async init(agentId: string, config?: HeartbeatConfig): Promise { + if (config) { + fallbackHeartbeat._configs.set(agentId, config); + } + }, + + async start(_agentId: string): Promise { + // No-op for fallback (no background tasks in browser) + }, + + async stop(_agentId: string): Promise { + // No-op + }, + + async tick(_agentId: string): Promise { + return { + status: 'ok', + alerts: [], + checked_items: 0, + timestamp: new Date().toISOString(), + }; + }, + + async getConfig(agentId: string): Promise { + return fallbackHeartbeat._configs.get(agentId) ?? { + enabled: false, + interval_minutes: 30, + quiet_hours_start: null, + quiet_hours_end: null, + notify_channel: 'ui', + proactivity_level: 'standard', + max_alerts_per_tick: 5, + }; + }, + + async updateConfig(agentId: string, config: HeartbeatConfig): Promise { + fallbackHeartbeat._configs.set(agentId, config); + }, + + async getHistory(_agentId: string, _limit?: number): Promise { + return []; + }, +}; + +// === Unified Client Export === + +/** + * Unified intelligence client that automatically selects backend or fallback + */ +export const intelligenceClient = { + memory: { + init: async (): Promise => { + if (isTauriEnv()) { + await intelligence.memory.init(); + } else { + await fallbackMemory.init(); + } + }, + + store: async (entry: MemoryEntryInput): Promise => { + if (isTauriEnv()) { + return intelligence.memory.store(entry); + } + return fallbackMemory.store(entry); + }, + + get: async (id: string): Promise => { + if (isTauriEnv()) { + const result = await intelligence.memory.get(id); + return result ? toFrontendMemory(result) : null; + } + return fallbackMemory.get(id); + }, + + search: async (options: MemorySearchOptions): Promise => { + if (isTauriEnv()) { + const results = await intelligence.memory.search(toBackendSearchOptions(options)); + return results.map(toFrontendMemory); + } + return fallbackMemory.search(options); + }, + + delete: async (id: string): Promise => { + if (isTauriEnv()) { + await intelligence.memory.delete(id); + } else { + await fallbackMemory.delete(id); + } + }, + + deleteAll: async (agentId: string): Promise => { + if (isTauriEnv()) { + return intelligence.memory.deleteAll(agentId); + } + return fallbackMemory.deleteAll(agentId); + }, + + stats: async (): Promise => { + if (isTauriEnv()) { + const stats = await intelligence.memory.stats(); + return toFrontendStats(stats); + } + return fallbackMemory.stats(); + }, + + export: async (): Promise => { + if (isTauriEnv()) { + const results = await intelligence.memory.export(); + return results.map(toFrontendMemory); + } + return fallbackMemory.export(); + }, + + import: async (memories: MemoryEntry[]): Promise => { + if (isTauriEnv()) { + // Convert to backend format + 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 intelligence.memory.import(backendMemories as PersistentMemory[]); + } + return fallbackMemory.import(memories); + }, + + dbPath: async (): Promise => { + if (isTauriEnv()) { + return intelligence.memory.dbPath(); + } + return fallbackMemory.dbPath(); + }, + }, + + heartbeat: { + init: async (agentId: string, config?: HeartbeatConfig): Promise => { + if (isTauriEnv()) { + await intelligence.heartbeat.init(agentId, config); + } else { + await fallbackHeartbeat.init(agentId, config); + } + }, + + start: async (agentId: string): Promise => { + if (isTauriEnv()) { + await intelligence.heartbeat.start(agentId); + } else { + await fallbackHeartbeat.start(agentId); + } + }, + + stop: async (agentId: string): Promise => { + if (isTauriEnv()) { + await intelligence.heartbeat.stop(agentId); + } else { + await fallbackHeartbeat.stop(agentId); + } + }, + + tick: async (agentId: string): Promise => { + if (isTauriEnv()) { + return intelligence.heartbeat.tick(agentId); + } + return fallbackHeartbeat.tick(agentId); + }, + + getConfig: async (agentId: string): Promise => { + if (isTauriEnv()) { + return intelligence.heartbeat.getConfig(agentId); + } + return fallbackHeartbeat.getConfig(agentId); + }, + + updateConfig: async (agentId: string, config: HeartbeatConfig): Promise => { + if (isTauriEnv()) { + await intelligence.heartbeat.updateConfig(agentId, config); + } else { + await fallbackHeartbeat.updateConfig(agentId, config); + } + }, + + getHistory: async (agentId: string, limit?: number): Promise => { + if (isTauriEnv()) { + return intelligence.heartbeat.getHistory(agentId, limit); + } + return fallbackHeartbeat.getHistory(agentId, limit); + }, + }, + + compactor: { + estimateTokens: async (text: string): Promise => { + if (isTauriEnv()) { + return intelligence.compactor.estimateTokens(text); + } + return fallbackCompactor.estimateTokens(text); + }, + + estimateMessagesTokens: async (messages: CompactableMessage[]): Promise => { + if (isTauriEnv()) { + return intelligence.compactor.estimateMessagesTokens(messages); + } + return fallbackCompactor.estimateMessagesTokens(messages); + }, + + checkThreshold: async ( + messages: CompactableMessage[], + config?: CompactionConfig + ): Promise => { + if (isTauriEnv()) { + return intelligence.compactor.checkThreshold(messages, config); + } + return fallbackCompactor.checkThreshold(messages, config); + }, + + compact: async ( + messages: CompactableMessage[], + agentId: string, + conversationId?: string, + config?: CompactionConfig + ): Promise => { + if (isTauriEnv()) { + return intelligence.compactor.compact(messages, agentId, conversationId, config); + } + return fallbackCompactor.compact(messages, agentId, conversationId, config); + }, + }, + + reflection: { + init: async (config?: ReflectionConfig): Promise => { + if (isTauriEnv()) { + await intelligence.reflection.init(config); + } else { + await fallbackReflection.init(config); + } + }, + + recordConversation: async (): Promise => { + if (isTauriEnv()) { + await intelligence.reflection.recordConversation(); + } else { + await fallbackReflection.recordConversation(); + } + }, + + shouldReflect: async (): Promise => { + if (isTauriEnv()) { + return intelligence.reflection.shouldReflect(); + } + return fallbackReflection.shouldReflect(); + }, + + reflect: async (agentId: string, memories: MemoryEntryForAnalysis[]): Promise => { + if (isTauriEnv()) { + return intelligence.reflection.reflect(agentId, memories); + } + return fallbackReflection.reflect(agentId, memories); + }, + + getHistory: async (limit?: number): Promise => { + if (isTauriEnv()) { + return intelligence.reflection.getHistory(limit); + } + return fallbackReflection.getHistory(limit); + }, + + getState: async (): Promise => { + if (isTauriEnv()) { + return intelligence.reflection.getState(); + } + return fallbackReflection.getState(); + }, + }, + + identity: { + get: async (agentId: string): Promise => { + if (isTauriEnv()) { + return intelligence.identity.get(agentId); + } + return fallbackIdentity.get(agentId); + }, + + getFile: async (agentId: string, file: string): Promise => { + if (isTauriEnv()) { + return intelligence.identity.getFile(agentId, file); + } + return fallbackIdentity.getFile(agentId, file); + }, + + buildPrompt: async (agentId: string, memoryContext?: string): Promise => { + if (isTauriEnv()) { + return intelligence.identity.buildPrompt(agentId, memoryContext); + } + return fallbackIdentity.buildPrompt(agentId, memoryContext); + }, + + updateUserProfile: async (agentId: string, content: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.updateUserProfile(agentId, content); + } else { + await fallbackIdentity.updateUserProfile(agentId, content); + } + }, + + appendUserProfile: async (agentId: string, addition: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.appendUserProfile(agentId, addition); + } else { + await fallbackIdentity.appendUserProfile(agentId, addition); + } + }, + + proposeChange: async ( + agentId: string, + file: 'soul' | 'instructions', + suggestedContent: string, + reason: string + ): Promise => { + if (isTauriEnv()) { + return intelligence.identity.proposeChange(agentId, file, suggestedContent, reason); + } + return fallbackIdentity.proposeChange(agentId, file, suggestedContent, reason); + }, + + approveProposal: async (proposalId: string): Promise => { + if (isTauriEnv()) { + return intelligence.identity.approveProposal(proposalId); + } + return fallbackIdentity.approveProposal(proposalId); + }, + + rejectProposal: async (proposalId: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.rejectProposal(proposalId); + } else { + await fallbackIdentity.rejectProposal(proposalId); + } + }, + + getPendingProposals: async (agentId?: string): Promise => { + if (isTauriEnv()) { + return intelligence.identity.getPendingProposals(agentId); + } + return fallbackIdentity.getPendingProposals(agentId); + }, + + updateFile: async (agentId: string, file: string, content: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.updateFile(agentId, file, content); + } else { + await fallbackIdentity.updateFile(agentId, file, content); + } + }, + + getSnapshots: async (agentId: string, limit?: number): Promise => { + if (isTauriEnv()) { + return intelligence.identity.getSnapshots(agentId, limit); + } + return fallbackIdentity.getSnapshots(agentId, limit); + }, + + restoreSnapshot: async (agentId: string, snapshotId: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.restoreSnapshot(agentId, snapshotId); + } else { + await fallbackIdentity.restoreSnapshot(agentId, snapshotId); + } + }, + + listAgents: async (): Promise => { + if (isTauriEnv()) { + return intelligence.identity.listAgents(); + } + return fallbackIdentity.listAgents(); + }, + + deleteAgent: async (agentId: string): Promise => { + if (isTauriEnv()) { + await intelligence.identity.deleteAgent(agentId); + } else { + await fallbackIdentity.deleteAgent(agentId); + } + }, + }, +}; + +export default intelligenceClient; diff --git a/desktop/src/lib/memory-extractor.ts b/desktop/src/lib/memory-extractor.ts index 3a43c12..433d7ad 100644 --- a/desktop/src/lib/memory-extractor.ts +++ b/desktop/src/lib/memory-extractor.ts @@ -15,8 +15,10 @@ * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.2 */ -import { getMemoryManager, type MemoryType } from './agent-memory'; -import { getAgentIdentityManager } from './agent-identity'; +import { + intelligenceClient, + type MemoryType, +} from './intelligence-client'; import { getLLMAdapter, llmExtract, @@ -159,20 +161,19 @@ export class MemoryExtractor { console.log(`[MemoryExtractor] After importance filtering (>= ${this.config.minImportanceThreshold}): ${extracted.length} items`); // Save to memory - const memoryManager = getMemoryManager(); let saved = 0; let skipped = 0; for (const item of extracted) { try { - await memoryManager.save({ - agentId, + await intelligenceClient.memory.store({ + agent_id: agentId, + memory_type: item.type, content: item.content, - type: item.type, importance: item.importance, source: 'auto', tags: item.tags, - conversationId, + conversation_id: conversationId, }); saved++; } catch { @@ -185,9 +186,8 @@ export class MemoryExtractor { const preferences = extracted.filter(e => e.type === 'preference' && e.importance >= 5); if (preferences.length > 0) { try { - const identityManager = getAgentIdentityManager(); const prefSummary = preferences.map(p => `- ${p.content}`).join('\n'); - identityManager.appendToUserProfile(agentId, `### 自动发现的偏好 (${new Date().toLocaleDateString('zh-CN')})\n${prefSummary}`); + await intelligenceClient.identity.appendUserProfile(agentId, `### 自动发现的偏好 (${new Date().toLocaleDateString('zh-CN')})\n${prefSummary}`); userProfileUpdated = true; } catch (err) { console.warn('[MemoryExtractor] Failed to update USER.md:', err); diff --git a/desktop/src/lib/memory-index.ts b/desktop/src/lib/memory-index.ts deleted file mode 100644 index 38d85f6..0000000 --- a/desktop/src/lib/memory-index.ts +++ /dev/null @@ -1,443 +0,0 @@ -/** - * Memory Index - High-performance indexing for agent memory retrieval - * - * Implements inverted index + LRU cache for sub-20ms retrieval on 1000+ memories. - * - * Performance targets: - * - Retrieval latency: <20ms (vs ~50ms with linear scan) - * - 1000 memories: smooth operation - * - Memory overhead: ~30% additional for indexes - * - * Reference: Task "Optimize ZCLAW Agent Memory Retrieval Performance" - */ - -import type { MemoryEntry, MemoryType } from './agent-memory'; - -// === Types === - -export interface IndexStats { - totalEntries: number; - keywordCount: number; - cacheHitRate: number; - cacheSize: number; - avgQueryTime: number; -} - -interface CacheEntry { - results: string[]; // memory IDs - timestamp: number; -} - -// === Tokenization (shared with agent-memory.ts) === - -export function tokenize(text: string): string[] { - return text - .toLowerCase() - .replace(/[^\w\u4e00-\u9fff\u3400-\u4dbf]+/g, ' ') - .split(/\s+/) - .filter(t => t.length > 0); -} - -// === LRU Cache Implementation === - -class LRUCache { - private cache: Map; - private maxSize: number; - - constructor(maxSize: number) { - this.cache = new Map(); - this.maxSize = maxSize; - } - - get(key: K): V | undefined { - const value = this.cache.get(key); - if (value !== undefined) { - // Move to end (most recently used) - this.cache.delete(key); - this.cache.set(key, value); - } - return value; - } - - set(key: K, value: V): void { - if (this.cache.has(key)) { - this.cache.delete(key); - } else if (this.cache.size >= this.maxSize) { - // Remove least recently used (first item) - const firstKey = this.cache.keys().next().value; - if (firstKey !== undefined) { - this.cache.delete(firstKey); - } - } - this.cache.set(key, value); - } - - clear(): void { - this.cache.clear(); - } - - get size(): number { - return this.cache.size; - } -} - -// === Memory Index Implementation === - -export class MemoryIndex { - // Inverted indexes - private keywordIndex: Map> = new Map(); // keyword -> memoryIds - private typeIndex: Map> = new Map(); // type -> memoryIds - private agentIndex: Map> = new Map(); // agentId -> memoryIds - private tagIndex: Map> = new Map(); // tag -> memoryIds - - // Pre-tokenized content cache - private tokenCache: Map = new Map(); // memoryId -> tokens - - // Query result cache - private queryCache: LRUCache; - - // Statistics - private cacheHits = 0; - private cacheMisses = 0; - private queryTimes: number[] = []; - - constructor(cacheSize = 100) { - this.queryCache = new LRUCache(cacheSize); - } - - // === Index Building === - - /** - * Build or update index for a memory entry. - * Call this when adding or updating a memory. - */ - index(entry: MemoryEntry): void { - const { id, agentId, type, tags, content } = entry; - - // Index by agent - if (!this.agentIndex.has(agentId)) { - this.agentIndex.set(agentId, new Set()); - } - this.agentIndex.get(agentId)!.add(id); - - // Index by type - if (!this.typeIndex.has(type)) { - this.typeIndex.set(type, new Set()); - } - this.typeIndex.get(type)!.add(id); - - // Index by tags - for (const tag of tags) { - const normalizedTag = tag.toLowerCase(); - if (!this.tagIndex.has(normalizedTag)) { - this.tagIndex.set(normalizedTag, new Set()); - } - this.tagIndex.get(normalizedTag)!.add(id); - } - - // Index by content keywords - const tokens = tokenize(content); - this.tokenCache.set(id, tokens); - - for (const token of tokens) { - if (!this.keywordIndex.has(token)) { - this.keywordIndex.set(token, new Set()); - } - this.keywordIndex.get(token)!.add(id); - } - - // Invalidate query cache on index change - this.queryCache.clear(); - } - - /** - * Remove a memory from all indexes. - */ - remove(memoryId: string): void { - // Remove from agent index - for (const [agentId, ids] of this.agentIndex) { - ids.delete(memoryId); - if (ids.size === 0) { - this.agentIndex.delete(agentId); - } - } - - // Remove from type index - for (const [type, ids] of this.typeIndex) { - ids.delete(memoryId); - if (ids.size === 0) { - this.typeIndex.delete(type); - } - } - - // Remove from tag index - for (const [tag, ids] of this.tagIndex) { - ids.delete(memoryId); - if (ids.size === 0) { - this.tagIndex.delete(tag); - } - } - - // Remove from keyword index - for (const [keyword, ids] of this.keywordIndex) { - ids.delete(memoryId); - if (ids.size === 0) { - this.keywordIndex.delete(keyword); - } - } - - // Remove token cache - this.tokenCache.delete(memoryId); - - // Invalidate query cache - this.queryCache.clear(); - } - - /** - * Rebuild all indexes from scratch. - * Use after bulk updates or data corruption. - */ - rebuild(entries: MemoryEntry[]): void { - this.clear(); - for (const entry of entries) { - this.index(entry); - } - } - - /** - * Clear all indexes. - */ - clear(): void { - this.keywordIndex.clear(); - this.typeIndex.clear(); - this.agentIndex.clear(); - this.tagIndex.clear(); - this.tokenCache.clear(); - this.queryCache.clear(); - this.cacheHits = 0; - this.cacheMisses = 0; - this.queryTimes = []; - } - - // === Fast Filtering === - - /** - * Get candidate memory IDs based on filter options. - * Uses indexes for O(1) lookups instead of O(n) scans. - */ - getCandidates(options: { - agentId?: string; - type?: MemoryType; - types?: MemoryType[]; - tags?: string[]; - }): Set | null { - const candidateSets: Set[] = []; - - // Filter by agent - if (options.agentId) { - const agentSet = this.agentIndex.get(options.agentId); - if (!agentSet) return new Set(); // Agent has no memories - candidateSets.push(agentSet); - } - - // Filter by single type - if (options.type) { - const typeSet = this.typeIndex.get(options.type); - if (!typeSet) return new Set(); // No memories of this type - candidateSets.push(typeSet); - } - - // Filter by multiple types - if (options.types && options.types.length > 0) { - const typeUnion = new Set(); - for (const t of options.types) { - const typeSet = this.typeIndex.get(t); - if (typeSet) { - for (const id of typeSet) { - typeUnion.add(id); - } - } - } - if (typeUnion.size === 0) return new Set(); - candidateSets.push(typeUnion); - } - - // Filter by tags (OR logic - match any tag) - if (options.tags && options.tags.length > 0) { - const tagUnion = new Set(); - for (const tag of options.tags) { - const normalizedTag = tag.toLowerCase(); - const tagSet = this.tagIndex.get(normalizedTag); - if (tagSet) { - for (const id of tagSet) { - tagUnion.add(id); - } - } - } - if (tagUnion.size === 0) return new Set(); - candidateSets.push(tagUnion); - } - - // Intersect all candidate sets - if (candidateSets.length === 0) { - return null; // No filters applied, return null to indicate "all" - } - - // Start with smallest set for efficiency - candidateSets.sort((a, b) => a.size - b.size); - let result = new Set(candidateSets[0]); - - for (let i = 1; i < candidateSets.length; i++) { - const nextSet = candidateSets[i]; - result = new Set([...result].filter(id => nextSet.has(id))); - if (result.size === 0) break; - } - - return result; - } - - // === Keyword Search === - - /** - * Get memory IDs that contain any of the query keywords. - * Returns a map of memoryId -> match count for ranking. - */ - searchKeywords(queryTokens: string[]): Map { - const matchCounts = new Map(); - - for (const token of queryTokens) { - const matchingIds = this.keywordIndex.get(token); - if (matchingIds) { - for (const id of matchingIds) { - matchCounts.set(id, (matchCounts.get(id) ?? 0) + 1); - } - } - - // Also check for partial matches (token is substring of indexed keyword) - for (const [keyword, ids] of this.keywordIndex) { - if (keyword.includes(token) || token.includes(keyword)) { - for (const id of ids) { - matchCounts.set(id, (matchCounts.get(id) ?? 0) + 1); - } - } - } - } - - return matchCounts; - } - - /** - * Get pre-tokenized content for a memory. - */ - getTokens(memoryId: string): string[] | undefined { - return this.tokenCache.get(memoryId); - } - - // === Query Cache === - - /** - * Generate cache key from query and options. - */ - private getCacheKey(query: string, options?: Record): string { - const opts = options ?? {}; - return `${query}|${opts.agentId ?? ''}|${opts.type ?? ''}|${(opts.types as string[])?.join(',') ?? ''}|${(opts.tags as string[])?.join(',') ?? ''}|${opts.minImportance ?? ''}|${opts.limit ?? ''}`; - } - - /** - * Get cached query results. - */ - getCached(query: string, options?: Record): string[] | null { - const key = this.getCacheKey(query, options); - const cached = this.queryCache.get(key); - if (cached) { - this.cacheHits++; - return cached.results; - } - this.cacheMisses++; - return null; - } - - /** - * Cache query results. - */ - setCached(query: string, options: Record | undefined, results: string[]): void { - const key = this.getCacheKey(query, options); - this.queryCache.set(key, { - results, - timestamp: Date.now(), - }); - } - - // === Statistics === - - /** - * Record query time for statistics. - */ - recordQueryTime(timeMs: number): void { - this.queryTimes.push(timeMs); - // Keep last 100 query times - if (this.queryTimes.length > 100) { - this.queryTimes.shift(); - } - } - - /** - * Get index statistics. - */ - getStats(): IndexStats { - const avgQueryTime = this.queryTimes.length > 0 - ? this.queryTimes.reduce((a, b) => a + b, 0) / this.queryTimes.length - : 0; - - const totalRequests = this.cacheHits + this.cacheMisses; - - return { - totalEntries: this.tokenCache.size, - keywordCount: this.keywordIndex.size, - cacheHitRate: totalRequests > 0 ? this.cacheHits / totalRequests : 0, - cacheSize: this.queryCache.size, - avgQueryTime, - }; - } - - /** - * Get index memory usage estimate. - */ - getMemoryUsage(): { estimated: number; breakdown: Record } { - let keywordIndexSize = 0; - for (const [keyword, ids] of this.keywordIndex) { - keywordIndexSize += keyword.length * 2 + ids.size * 50; // rough estimate - } - - return { - estimated: - keywordIndexSize + - this.typeIndex.size * 100 + - this.agentIndex.size * 100 + - this.tagIndex.size * 100 + - this.tokenCache.size * 200, - breakdown: { - keywordIndex: keywordIndexSize, - typeIndex: this.typeIndex.size * 100, - agentIndex: this.agentIndex.size * 100, - tagIndex: this.tagIndex.size * 100, - tokenCache: this.tokenCache.size * 200, - }, - }; - } -} - -// === Singleton === - -let _instance: MemoryIndex | null = null; - -export function getMemoryIndex(): MemoryIndex { - if (!_instance) { - _instance = new MemoryIndex(); - } - return _instance; -} - -export function resetMemoryIndex(): void { - _instance = null; -} diff --git a/desktop/src/lib/reflection-engine.ts b/desktop/src/lib/reflection-engine.ts deleted file mode 100644 index 8fff2d2..0000000 --- a/desktop/src/lib/reflection-engine.ts +++ /dev/null @@ -1,677 +0,0 @@ -/** - * Reflection Engine - Agent self-improvement through conversation analysis - * - * Periodically analyzes recent conversations to: - * - Identify behavioral patterns (positive and negative) - * - Generate improvement suggestions - * - Propose identity file changes (with user approval) - * - Create meta-memories about agent performance - * - * Phase 3 implementation: rule-based pattern detection. - * Phase 4 upgrade: LLM-powered deep reflection. - * - * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.2 - */ - -import { getMemoryManager, type MemoryEntry } from './agent-memory'; -import { getAgentIdentityManager, type IdentityChangeProposal } from './agent-identity'; -import { - getLLMAdapter, - llmReflect, - type LLMServiceAdapter, - type LLMProvider, -} from './llm-service'; -import { canAutoExecute } from './autonomy-manager'; - -// === Types === - -export interface ReflectionConfig { - triggerAfterConversations: number; // Reflect after N conversations (default 5) - triggerAfterHours: number; // Reflect after N hours (default 24) - allowSoulModification: boolean; // Can propose SOUL.md changes - requireApproval: boolean; // Identity changes need user OK - useLLM: boolean; // Use LLM for deep reflection (Phase 4) - llmProvider?: LLMProvider; // Preferred LLM provider - llmFallbackToRules: boolean; // Fall back to rules if LLM fails -} - -export interface PatternObservation { - observation: string; - frequency: number; - sentiment: 'positive' | 'negative' | 'neutral'; - evidence: string[]; -} - -export interface ImprovementSuggestion { - area: string; - suggestion: string; - priority: 'high' | 'medium' | 'low'; -} - -export interface ReflectionResult { - patterns: PatternObservation[]; - improvements: ImprovementSuggestion[]; - identityProposals: IdentityChangeProposal[]; - newMemories: number; - timestamp: string; -} - -// === Default Config === - -export const DEFAULT_REFLECTION_CONFIG: ReflectionConfig = { - triggerAfterConversations: 5, - triggerAfterHours: 24, - allowSoulModification: false, - requireApproval: true, - useLLM: true, // Enable LLM-powered deep reflection (Phase 4) - llmFallbackToRules: true, -}; - -// === Storage === - -const REFLECTION_STORAGE_KEY = 'zclaw-reflection-state'; -const REFLECTION_HISTORY_KEY = 'zclaw-reflection-history'; - -interface ReflectionState { - conversationsSinceReflection: number; - lastReflectionTime: string | null; - lastReflectionAgentId: string | null; -} - -// === Reflection Engine === - -export class ReflectionEngine { - private config: ReflectionConfig; - private state: ReflectionState; - private history: ReflectionResult[] = []; - private llmAdapter: LLMServiceAdapter | null = null; - - constructor(config?: Partial) { - this.config = { ...DEFAULT_REFLECTION_CONFIG, ...config }; - this.state = this.loadState(); - this.loadHistory(); - - // Initialize LLM adapter if configured - if (this.config.useLLM) { - try { - this.llmAdapter = getLLMAdapter(); - } catch (error) { - console.warn('[ReflectionEngine] Failed to initialize LLM adapter:', error); - } - } - } - - // === Trigger Management === - - /** - * Call after each conversation to track when reflection should trigger. - */ - recordConversation(): void { - this.state.conversationsSinceReflection++; - this.saveState(); - } - - /** - * Check if it's time for reflection. - */ - shouldReflect(): boolean { - // Conversation count trigger - if (this.state.conversationsSinceReflection >= this.config.triggerAfterConversations) { - return true; - } - - // Time-based trigger - if (this.state.lastReflectionTime) { - const elapsed = Date.now() - new Date(this.state.lastReflectionTime).getTime(); - const hoursSince = elapsed / (1000 * 60 * 60); - if (hoursSince >= this.config.triggerAfterHours) { - return true; - } - } else { - // Never reflected before, trigger after initial conversations - return this.state.conversationsSinceReflection >= 3; - } - - return false; - } - - /** - * Execute a reflection cycle for the given agent. - */ - async reflect(agentId: string, options?: { forceLLM?: boolean; skipAutonomyCheck?: boolean }): Promise { - console.log(`[Reflection] Starting reflection for agent: ${agentId}`); - - // Autonomy check - verify if reflection is allowed - if (!options?.skipAutonomyCheck) { - const { canProceed, decision } = canAutoExecute('reflection_run', 5); - if (!canProceed) { - console.log(`[Reflection] Autonomy check failed: ${decision.reason}`); - // Return empty result instead of throwing - return { - patterns: [], - improvements: [], - identityProposals: [], - newMemories: 0, - timestamp: new Date().toISOString(), - }; - } - console.log(`[Reflection] Autonomy check passed: ${decision.reason}`); - } - - // Try LLM-powered reflection if enabled - if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) { - try { - console.log('[Reflection] Using LLM-powered deep reflection'); - return await this.llmReflectImpl(agentId); - } catch (error) { - console.error('[Reflection] LLM reflection failed:', error); - if (!this.config.llmFallbackToRules) { - throw error; - } - console.log('[Reflection] Falling back to rule-based analysis'); - } - } - - // Rule-based reflection (original implementation) - return this.ruleBasedReflect(agentId); - } - - /** - * LLM-powered deep reflection implementation. - * Uses semantic analysis for pattern detection and improvement suggestions. - */ - private async llmReflectImpl(agentId: string): Promise { - const memoryMgr = getMemoryManager(); - const identityMgr = getAgentIdentityManager(); - - // 1. Gather context for LLM analysis - const allMemories = await memoryMgr.getAll(agentId, { limit: 100 }); - const context = this.buildReflectionContext(agentId, allMemories); - - // 2. Call LLM for deep reflection - const llmResponse = await llmReflect(context, this.llmAdapter!); - - // 3. Parse LLM response - const { patterns, improvements } = this.parseLLMResponse(llmResponse); - - // 4. Propose identity changes if patterns warrant it - const identityProposals: IdentityChangeProposal[] = []; - if (this.config.allowSoulModification) { - const proposals = this.proposeIdentityChanges(agentId, patterns, identityMgr); - identityProposals.push(...proposals); - } - - // 5. Save reflection insights as memories - let newMemories = 0; - for (const pattern of patterns.filter(p => p.frequency >= 2)) { - await memoryMgr.save({ - agentId, - content: `[LLM反思] ${pattern.observation} (出现${pattern.frequency}次, ${pattern.sentiment === 'positive' ? '正面' : pattern.sentiment === 'negative' ? '负面' : '中性'})`, - type: 'lesson', - importance: pattern.sentiment === 'negative' ? 8 : 5, - source: 'llm-reflection', - tags: ['reflection', 'pattern', 'llm'], - }); - newMemories++; - } - - for (const improvement of improvements.filter(i => i.priority === 'high')) { - await memoryMgr.save({ - agentId, - content: `[LLM建议] [${improvement.area}] ${improvement.suggestion}`, - type: 'lesson', - importance: 7, - source: 'llm-reflection', - tags: ['reflection', 'improvement', 'llm'], - }); - newMemories++; - } - - // 6. Build result - const result: ReflectionResult = { - patterns, - improvements, - identityProposals, - newMemories, - timestamp: new Date().toISOString(), - }; - - // 7. Update state and history - this.state.conversationsSinceReflection = 0; - this.state.lastReflectionTime = result.timestamp; - this.state.lastReflectionAgentId = agentId; - this.saveState(); - - this.history.push(result); - if (this.history.length > 20) { - this.history = this.history.slice(-10); - } - this.saveHistory(); - - console.log( - `[Reflection] LLM complete: ${patterns.length} patterns, ${improvements.length} improvements, ` + - `${identityProposals.length} proposals, ${newMemories} memories saved` - ); - - return result; - } - - /** - * Build context string for LLM reflection. - */ - private buildReflectionContext(agentId: string, memories: MemoryEntry[]): string { - const memorySummary = memories.slice(0, 50).map(m => - `[${m.type}] ${m.content} (重要性: ${m.importance}, 访问: ${m.accessCount}次)` - ).join('\n'); - - const typeStats = new Map(); - for (const m of memories) { - typeStats.set(m.type, (typeStats.get(m.type) || 0) + 1); - } - - const recentHistory = this.history.slice(-3).map(h => - `上次反思(${h.timestamp}): ${h.patterns.length}个模式, ${h.improvements.length}个建议` - ).join('\n'); - - return ` -Agent ID: ${agentId} -记忆总数: ${memories.length} -记忆类型分布: ${[...typeStats.entries()].map(([k, v]) => `${k}:${v}`).join(', ')} - -最近记忆: -${memorySummary} - -历史反思: -${recentHistory || '无'} -`; - } - - /** - * Parse LLM response into structured reflection data. - */ - private parseLLMResponse(response: string): { - patterns: PatternObservation[]; - improvements: ImprovementSuggestion[]; - } { - const patterns: PatternObservation[] = []; - const improvements: ImprovementSuggestion[] = []; - - try { - // Try to extract JSON from response - const jsonMatch = response.match(/\{[\s\S]*\}/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[0]); - - if (Array.isArray(parsed.patterns)) { - for (const p of parsed.patterns) { - patterns.push({ - observation: p.observation || p.observation || '未知模式', - frequency: p.frequency || 1, - sentiment: p.sentiment || 'neutral', - evidence: Array.isArray(p.evidence) ? p.evidence : [], - }); - } - } - - if (Array.isArray(parsed.improvements)) { - for (const i of parsed.improvements) { - improvements.push({ - area: i.area || '通用', - suggestion: i.suggestion || i.suggestion || '', - priority: i.priority || 'medium', - }); - } - } - } - } catch (error) { - console.warn('[Reflection] Failed to parse LLM response as JSON:', error); - - // Fallback: extract text patterns - if (response.includes('模式') || response.includes('pattern')) { - patterns.push({ - observation: 'LLM 分析完成,但未能解析结构化数据', - frequency: 1, - sentiment: 'neutral', - evidence: [response.slice(0, 200)], - }); - } - } - - // Ensure we have at least some output - if (patterns.length === 0) { - patterns.push({ - observation: 'LLM 反思完成,未检测到显著模式', - frequency: 1, - sentiment: 'neutral', - evidence: [], - }); - } - - return { patterns, improvements }; - } - - /** - * Rule-based reflection (original implementation). - */ - private async ruleBasedReflect(agentId: string): Promise { - const memoryMgr = getMemoryManager(); - const identityMgr = getAgentIdentityManager(); - - // 1. Analyze memory patterns - const allMemories = await memoryMgr.getAll(agentId, { limit: 100 }); - const patterns = this.analyzePatterns(allMemories); - - // 2. Generate improvement suggestions - const improvements = this.generateImprovements(patterns, allMemories); - - // 3. Propose identity changes if patterns warrant it - const identityProposals: IdentityChangeProposal[] = []; - if (this.config.allowSoulModification) { - const proposals = this.proposeIdentityChanges(agentId, patterns, identityMgr); - identityProposals.push(...proposals); - } - - // 4. Save reflection insights as memories - let newMemories = 0; - for (const pattern of patterns.filter(p => p.frequency >= 3)) { - await memoryMgr.save({ - agentId, - content: `反思观察: ${pattern.observation} (出现${pattern.frequency}次, ${pattern.sentiment === 'positive' ? '正面' : pattern.sentiment === 'negative' ? '负面' : '中性'})`, - type: 'lesson', - importance: pattern.sentiment === 'negative' ? 8 : 5, - source: 'reflection', - tags: ['reflection', 'pattern'], - }); - newMemories++; - } - - for (const improvement of improvements.filter(i => i.priority === 'high')) { - await memoryMgr.save({ - agentId, - content: `改进方向: [${improvement.area}] ${improvement.suggestion}`, - type: 'lesson', - importance: 7, - source: 'reflection', - tags: ['reflection', 'improvement'], - }); - newMemories++; - } - - // 5. Build result - const result: ReflectionResult = { - patterns, - improvements, - identityProposals, - newMemories, - timestamp: new Date().toISOString(), - }; - - // 6. Update state - this.state.conversationsSinceReflection = 0; - this.state.lastReflectionTime = result.timestamp; - this.state.lastReflectionAgentId = agentId; - this.saveState(); - - // 7. Store in history - this.history.push(result); - if (this.history.length > 20) { - this.history = this.history.slice(-10); - } - this.saveHistory(); - - console.log( - `[Reflection] Complete: ${patterns.length} patterns, ${improvements.length} improvements, ` + - `${identityProposals.length} proposals, ${newMemories} memories saved` - ); - - return result; - } - - // === Pattern Analysis === - - private analyzePatterns(memories: MemoryEntry[]): PatternObservation[] { - const patterns: PatternObservation[] = []; - - // Analyze memory type distribution - const typeCounts = new Map(); - for (const m of memories) { - typeCounts.set(m.type, (typeCounts.get(m.type) || 0) + 1); - } - - // Pattern: Too many tasks accumulating - const taskCount = typeCounts.get('task') || 0; - if (taskCount >= 5) { - patterns.push({ - observation: `积累了 ${taskCount} 个待办任务,可能存在任务管理不善`, - frequency: taskCount, - sentiment: 'negative', - evidence: memories.filter(m => m.type === 'task').slice(0, 3).map(m => m.content), - }); - } - - // Pattern: Strong preference accumulation - const prefCount = typeCounts.get('preference') || 0; - if (prefCount >= 5) { - patterns.push({ - observation: `已记录 ${prefCount} 个用户偏好,对用户习惯有较好理解`, - frequency: prefCount, - sentiment: 'positive', - evidence: memories.filter(m => m.type === 'preference').slice(0, 3).map(m => m.content), - }); - } - - // Pattern: Many lessons learned - const lessonCount = typeCounts.get('lesson') || 0; - if (lessonCount >= 5) { - patterns.push({ - observation: `积累了 ${lessonCount} 条经验教训,知识库在成长`, - frequency: lessonCount, - sentiment: 'positive', - evidence: memories.filter(m => m.type === 'lesson').slice(0, 3).map(m => m.content), - }); - } - - // Pattern: High-importance items being accessed frequently - const highAccess = memories.filter(m => m.accessCount >= 5 && m.importance >= 7); - if (highAccess.length >= 3) { - patterns.push({ - observation: `有 ${highAccess.length} 条高频访问的重要记忆,核心知识正在形成`, - frequency: highAccess.length, - sentiment: 'positive', - evidence: highAccess.slice(0, 3).map(m => m.content), - }); - } - - // Pattern: Low-importance memories accumulating - const lowImportance = memories.filter(m => m.importance <= 3); - if (lowImportance.length > 20) { - patterns.push({ - observation: `有 ${lowImportance.length} 条低重要性记忆,建议清理`, - frequency: lowImportance.length, - sentiment: 'neutral', - evidence: [], - }); - } - - // Pattern: Tag analysis - recurring topics - const tagCounts = new Map(); - for (const m of memories) { - for (const tag of m.tags) { - if (tag !== 'auto-extracted') { - tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); - } - } - } - - const frequentTags = [...tagCounts.entries()] - .filter(([, count]) => count >= 5) - .sort((a, b) => b[1] - a[1]); - - if (frequentTags.length > 0) { - patterns.push({ - observation: `反复出现的主题: ${frequentTags.slice(0, 5).map(([tag, count]) => `${tag}(${count}次)`).join(', ')}`, - frequency: frequentTags[0][1], - sentiment: 'neutral', - evidence: frequentTags.map(([tag]) => tag), - }); - } - - return patterns; - } - - // === Improvement Suggestions === - - private generateImprovements( - patterns: PatternObservation[], - memories: MemoryEntry[] - ): ImprovementSuggestion[] { - const improvements: ImprovementSuggestion[] = []; - - // Suggestion: Clear pending tasks - const taskPattern = patterns.find(p => p.observation.includes('待办任务')); - if (taskPattern) { - improvements.push({ - area: '任务管理', - suggestion: '清理已完成的任务记忆,对长期未处理的任务降低重要性或标记为已取消', - priority: 'high', - }); - } - - // Suggestion: Prune low-importance memories - const lowPattern = patterns.find(p => p.observation.includes('低重要性')); - if (lowPattern) { - improvements.push({ - area: '记忆管理', - suggestion: '执行记忆清理,移除30天以上未访问且重要性低于3的记忆', - priority: 'medium', - }); - } - - // Suggestion: User profile enrichment - const prefCount = memories.filter(m => m.type === 'preference').length; - if (prefCount < 3) { - improvements.push({ - area: '用户理解', - suggestion: '主动在对话中了解用户偏好(沟通风格、技术栈、工作习惯),丰富用户画像', - priority: 'medium', - }); - } - - // Suggestion: Knowledge consolidation - const factCount = memories.filter(m => m.type === 'fact').length; - if (factCount > 20) { - improvements.push({ - area: '知识整合', - suggestion: '合并相似的事实记忆,提高检索效率。可将相关事实整合为结构化的项目/用户档案', - priority: 'low', - }); - } - - return improvements; - } - - // === Identity Change Proposals === - - private proposeIdentityChanges( - agentId: string, - patterns: PatternObservation[], - identityMgr: ReturnType - ): IdentityChangeProposal[] { - const proposals: IdentityChangeProposal[] = []; - - // If many negative patterns, propose instruction update - const negativePatterns = patterns.filter(p => p.sentiment === 'negative'); - if (negativePatterns.length >= 2) { - const identity = identityMgr.getIdentity(agentId); - const additions = negativePatterns.map(p => - `- 注意: ${p.observation}` - ).join('\n'); - - const proposal = identityMgr.proposeChange( - agentId, - 'instructions', - identity.instructions + `\n\n## 自我反思改进\n${additions}`, - `基于 ${negativePatterns.length} 个负面模式观察,建议在指令中增加自我改进提醒` - ); - if (proposal) { - proposals.push(proposal); - } - } - - return proposals; - } - - // === History === - - getHistory(limit: number = 10): ReflectionResult[] { - return this.history.slice(-limit); - } - - getLastResult(): ReflectionResult | null { - return this.history.length > 0 ? this.history[this.history.length - 1] : null; - } - - // === Config === - - getConfig(): ReflectionConfig { - return { ...this.config }; - } - - updateConfig(updates: Partial): void { - this.config = { ...this.config, ...updates }; - } - - getState(): ReflectionState { - return { ...this.state }; - } - - // === Persistence === - - private loadState(): ReflectionState { - try { - const raw = localStorage.getItem(REFLECTION_STORAGE_KEY); - if (raw) return JSON.parse(raw); - } catch { /* silent */ } - return { - conversationsSinceReflection: 0, - lastReflectionTime: null, - lastReflectionAgentId: null, - }; - } - - private saveState(): void { - try { - localStorage.setItem(REFLECTION_STORAGE_KEY, JSON.stringify(this.state)); - } catch { /* silent */ } - } - - private loadHistory(): void { - try { - const raw = localStorage.getItem(REFLECTION_HISTORY_KEY); - if (raw) this.history = JSON.parse(raw); - } catch { - this.history = []; - } - } - - private saveHistory(): void { - try { - localStorage.setItem(REFLECTION_HISTORY_KEY, JSON.stringify(this.history.slice(-10))); - } catch { /* silent */ } - } -} - -// === Singleton === - -let _instance: ReflectionEngine | null = null; - -export function getReflectionEngine(config?: Partial): ReflectionEngine { - if (!_instance) { - _instance = new ReflectionEngine(config); - } - return _instance; -} - -export function resetReflectionEngine(): void { - _instance = null; -} diff --git a/desktop/src/lib/skill-adapter.ts b/desktop/src/lib/skill-adapter.ts index 57c278e..5792ccd 100644 --- a/desktop/src/lib/skill-adapter.ts +++ b/desktop/src/lib/skill-adapter.ts @@ -191,3 +191,17 @@ export function getCategories(skills: UISkillInfo[]): string[] { } return Array.from(categories); } + +// === Aliases for backward compatibility === + +/** + * Alias for UISkillInfo for backward compatibility + */ +export type SkillDisplay = UISkillInfo; + +/** + * Alias for adaptSkills for catalog adaptation + */ +export function adaptSkillsCatalog(skills: ConfigSkillInfo[]): UISkillInfo[] { + return adaptSkills(skills); +} diff --git a/desktop/src/lib/skill-discovery.ts b/desktop/src/lib/skill-discovery.ts index 69a1609..c219d24 100644 --- a/desktop/src/lib/skill-discovery.ts +++ b/desktop/src/lib/skill-discovery.ts @@ -11,7 +11,7 @@ * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.5.2 */ -import { getMemoryManager } from './agent-memory'; +import { intelligenceClient } from './intelligence-client'; import { canAutoExecute } from './autonomy-manager'; // === Types === @@ -295,8 +295,9 @@ export class SkillDiscoveryEngine { // 3. Check memory patterns for recurring needs try { - const memories = await getMemoryManager().search(skill.name, { + const memories = await intelligenceClient.memory.search({ agentId, + query: skill.name, limit: 5, minImportance: 3, }); diff --git a/desktop/src/store/chatStore.ts b/desktop/src/store/chatStore.ts index 204dc42..642d6ce 100644 --- a/desktop/src/store/chatStore.ts +++ b/desktop/src/store/chatStore.ts @@ -1,11 +1,8 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { getGatewayClient, AgentStreamDelta } from '../lib/gateway-client'; -import { getMemoryManager } from '../lib/agent-memory'; -import { getAgentIdentityManager } from '../lib/agent-identity'; +import { intelligenceClient } from '../lib/intelligence-client'; import { getMemoryExtractor } from '../lib/memory-extractor'; -import { getContextCompactor } from '../lib/context-compactor'; -import { getReflectionEngine } from '../lib/reflection-engine'; import { getAgentSwarm } from '../lib/agent-swarm'; import { getSkillDiscovery } from '../lib/skill-discovery'; @@ -300,21 +297,26 @@ export const useChatStore = create()( // Check context compaction threshold before adding new message try { - const compactor = getContextCompactor(); - const check = compactor.checkThreshold(get().messages.map(m => ({ role: m.role, content: m.content }))); - if (check.shouldCompact) { - console.log(`[Chat] Context compaction triggered (${check.urgency}): ${check.currentTokens} tokens`); - const result = await compactor.compact( - get().messages.map(m => ({ role: m.role, content: m.content, id: m.id, timestamp: m.timestamp })), + const messages = get().messages.map(m => ({ role: m.role, content: m.content })); + const check = await intelligenceClient.compactor.checkThreshold(messages); + if (check.should_compact) { + console.log(`[Chat] Context compaction triggered (${check.urgency}): ${check.current_tokens} tokens`); + const result = await intelligenceClient.compactor.compact( + get().messages.map(m => ({ + role: m.role, + content: m.content, + id: m.id, + timestamp: m.timestamp instanceof Date ? m.timestamp.toISOString() : m.timestamp + })), agentId, get().currentConversationId ?? undefined ); // Replace messages with compacted version - const compactedMsgs: Message[] = result.compactedMessages.map((m, i) => ({ + const compactedMsgs: Message[] = result.compacted_messages.map((m, i) => ({ id: m.id || `compacted_${i}_${Date.now()}`, role: m.role as Message['role'], content: m.content, - timestamp: m.timestamp || new Date(), + timestamp: m.timestamp ? new Date(m.timestamp) : new Date(), })); set({ messages: compactedMsgs }); } @@ -325,17 +327,16 @@ export const useChatStore = create()( // Build memory-enhanced content let enhancedContent = content; try { - const memoryMgr = getMemoryManager(); - const identityMgr = getAgentIdentityManager(); - const relevantMemories = await memoryMgr.search(content, { + const relevantMemories = await intelligenceClient.memory.search({ agentId, + query: content, limit: 8, minImportance: 3, }); const memoryContext = relevantMemories.length > 0 ? `\n\n## 相关记忆\n${relevantMemories.map(m => `- [${m.type}] ${m.content}`).join('\n')}` : ''; - const systemPrompt = identityMgr.buildSystemPrompt(agentId, memoryContext); + const systemPrompt = await intelligenceClient.identity.buildPrompt(agentId, memoryContext); if (systemPrompt) { enhancedContent = `\n${systemPrompt}\n\n\n${content}`; } @@ -426,13 +427,16 @@ export const useChatStore = create()( console.warn('[Chat] Memory extraction failed:', err) ); // Track conversation for reflection trigger - const reflectionEngine = getReflectionEngine(); - reflectionEngine.recordConversation(); - if (reflectionEngine.shouldReflect()) { - reflectionEngine.reflect(agentId).catch(err => - console.warn('[Chat] Reflection failed:', err) - ); - } + intelligenceClient.reflection.recordConversation().catch(err => + console.warn('[Chat] Recording conversation failed:', err) + ); + intelligenceClient.reflection.shouldReflect().then(shouldReflect => { + if (shouldReflect) { + intelligenceClient.reflection.reflect(agentId, []).catch(err => + console.warn('[Chat] Reflection failed:', err) + ); + } + }); }, onError: (error: string) => { set((state) => ({ diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index d74df3f..8463756 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -136,6 +136,7 @@ export interface ConfigStateSlice { modelsError: string | null; error: string | null; client: ConfigStoreClient | null; + isLoading: boolean; } // === Store Actions Slice === @@ -208,6 +209,7 @@ export const useConfigStore = create((set modelsError: null, error: null, client: null, + isLoading: false, // Client Injection setConfigStoreClient: (client: ConfigStoreClient) => { diff --git a/desktop/src/store/connectionStore.ts b/desktop/src/store/connectionStore.ts index aa5b02a..0443581 100644 --- a/desktop/src/store/connectionStore.ts +++ b/desktop/src/store/connectionStore.ts @@ -24,8 +24,6 @@ import { type LocalGatewayStatus, } from '../lib/tauri-gateway'; import { - performHealthCheck, - createHealthCheckScheduler, type HealthCheckResult, type HealthStatus, } from '../lib/health-check'; @@ -165,6 +163,8 @@ export const useConnectionStore = create((set, get) => { localGateway: getUnsupportedLocalGatewayStatus(), localGatewayBusy: false, isLoading: false, + healthStatus: 'unknown', + healthCheckResult: null, client, // === Actions === diff --git a/desktop/src/store/memoryGraphStore.ts b/desktop/src/store/memoryGraphStore.ts index e335f6a..69b0fbf 100644 --- a/desktop/src/store/memoryGraphStore.ts +++ b/desktop/src/store/memoryGraphStore.ts @@ -6,7 +6,11 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { getMemoryManager, type MemoryEntry, type MemoryType } from '../lib/agent-memory'; +import { + intelligenceClient, + type MemoryEntry, + type MemoryType, +} from '../lib/intelligence-client'; export type { MemoryType }; @@ -184,8 +188,10 @@ export const useMemoryGraphStore = create()( set({ isLoading: true, error: null }); try { - const mgr = getMemoryManager(); - const memories = await mgr.getAll(agentId, { limit: 200 }); + const memories = await intelligenceClient.memory.search({ + agentId, + limit: 200, + }); const nodes = memories.map((m, i) => memoryToNode(m, i, memories.length)); const edges = findRelatedMemories(memories);