/** * Agent Store - Manages clones/agents, usage statistics, and plugin status * * Extracted from gatewayStore.ts for Phase 11 Store Refactoring. * This store focuses on agent/clone CRUD operations and related metadata. */ import { create } from 'zustand'; import type { GatewayClient } from '../lib/gateway-client'; import type { AgentTemplateFull } from '../lib/saas-client'; import { useChatStore } from './chatStore'; import { useConversationStore } from './chat/conversationStore'; // === Types === export interface Clone { id: string; name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; workspaceResolvedPath?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; createdAt: string; bootstrapReady?: boolean; bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>; updatedAt?: string; // 人格相关字段 emoji?: string; // Agent emoji, e.g., "🦞", "🤖", "💻" personality?: string; // 人格风格: professional, friendly, creative, concise communicationStyle?: string; // 沟通风格描述 notes?: string; // 用户备注 onboardingCompleted?: boolean; // 是否完成首次引导 source_template_id?: string; // 模板来源 ID } export interface UsageStats { totalSessions: number; totalMessages: number; totalTokens: number; byModel: Record; } export interface PluginStatus { id: string; name?: string; status: 'active' | 'inactive' | 'error' | 'loading'; version?: string; description?: string; } export interface CloneCreateOptions { name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; // 人格相关字段 emoji?: string; personality?: string; communicationStyle?: string; notes?: string; } // === Store State === export interface AgentStateSlice { clones: Clone[]; usageStats: UsageStats | null; pluginStatus: PluginStatus[]; isLoading: boolean; error: string | null; } // === Store Actions === export interface AgentActionsSlice { loadClones: () => Promise; createClone: (opts: CloneCreateOptions) => Promise; createFromTemplate: (template: AgentTemplateFull) => Promise; updateClone: (id: string, updates: Partial) => Promise; deleteClone: (id: string) => Promise; loadUsageStats: () => Promise; loadPluginStatus: () => Promise; setError: (error: string | null) => void; clearError: () => void; } // === Store Interface === export type AgentStore = AgentStateSlice & AgentActionsSlice; // === Client Injection === // For coordinator to inject client - avoids direct import coupling let _client: GatewayClient | null = null; /** * Sets the gateway client for the agent store. * Called by the coordinator during initialization. */ export const setAgentStoreClient = (client: unknown): void => { _client = client as GatewayClient; }; /** * Gets the gateway client. * Returns null if not set (coordinator must initialize first). */ const getClient = (): GatewayClient | null => _client; // === Store Implementation === export const useAgentStore = create((set, get) => ({ // Initial state clones: [], usageStats: null, pluginStatus: [], isLoading: false, error: null, // Actions loadClones: async () => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized, skipping loadClones'); return; } try { set({ isLoading: true, error: null }); const result = await client.listClones(); const clones = result?.clones || result?.agents || []; set({ clones, isLoading: false }); // Set default agent ID if we have agents and none is set if (clones.length > 0 && clones[0].id) { const currentDefault = client.getDefaultAgentId(); // Only set if the default doesn't exist in the list const defaultExists = clones.some((c: Clone) => c.id === currentDefault); if (!defaultExists) { client.setDefaultAgentId(clones[0].id); } } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); // Don't throw - clone loading is non-critical } }, createClone: async (opts: CloneCreateOptions) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return undefined; } try { set({ isLoading: true, error: null }); const result = await client.createClone(opts); await get().loadClones(); // Refresh the list return result?.clone; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); return undefined; } }, createFromTemplate: async (template: AgentTemplateFull) => { const client = getClient(); if (!client) return undefined; set({ isLoading: true, error: null }); try { const result = await client.createClone({ name: template.name, emoji: template.emoji, personality: template.personality, scenarios: template.scenarios, communicationStyle: template.communication_style, model: template.model, }); const cloneId = result?.clone?.id; // Persist SOUL.md via identity system if (cloneId && template.soul_content) { try { const { intelligenceClient } = await import('../lib/intelligence-client'); await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content); } catch (e) { console.warn('Failed to persist soul_content:', e); } } await get().loadClones(); return result?.clone; } catch (error) { set({ error: String(error) }); return undefined; } finally { set({ isLoading: false }); } }, updateClone: async (id: string, updates: Partial) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return undefined; } try { set({ isLoading: true, error: null }); const result = await client.updateClone(id, updates); await get().loadClones(); // Refresh the list return result?.clone; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); return undefined; } }, deleteClone: async (id: string) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return; } try { set({ isLoading: true, error: null }); await client.deleteClone(id); await get().loadClones(); // Refresh the list } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); } }, loadUsageStats: async () => { try { const { conversations } = useConversationStore.getState(); const tokenData = useChatStore.getState().getTotalTokens(); let totalMessages = 0; for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === 'user' || message.role === 'assistant') { totalMessages += 1; } } } const stats: UsageStats = { totalSessions: conversations.length, totalMessages, totalTokens: tokenData.total, byModel: {}, }; set({ usageStats: stats }); } catch { // Usage stats are non-critical, ignore errors silently } }, loadPluginStatus: async () => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized, skipping loadPluginStatus'); return; } try { const result = await client.getPluginStatus(); set({ pluginStatus: result?.plugins || [] }); } catch { // Plugin status is non-critical, ignore errors silently } }, setError: (error: string | null) => { set({ error }); }, clearError: () => { set({ error: null }); }, })); // === Selectors === /** * Get a clone by ID */ export const selectCloneById = (id: string) => (state: AgentStore): Clone | undefined => state.clones.find((clone) => clone.id === id); /** * Get all active plugins */ export const selectActivePlugins = (state: AgentStore): PluginStatus[] => state.pluginStatus.filter((plugin) => plugin.status === 'active'); /** * Check if any operation is in progress */ export const selectIsLoading = (state: AgentStore): boolean => state.isLoading;