/** * 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 { saasClient } from '../lib/saas-client'; import { useChatStore } from './chatStore'; import { useConversationStore } from './chat/conversationStore'; import { createLogger } from '../lib/logger'; const log = createLogger('AgentStore'); // === 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 welcomeMessage?: string; // 模板预设欢迎语 quickCommands?: Array<{ label: string; command: string }>; // 模板预设快捷命令 } 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) { log.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) { log.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) { set({ error: 'Client not initialized' }); return undefined; } set({ isLoading: true, error: null }); try { // Step 1: Call backend to get server-processed config (tools merge, model fallback) const config = await saasClient.createAgentFromTemplate(template.id); // Step 2: Create clone with merged data from backend const result = await client.createClone({ name: config.name, emoji: config.emoji, personality: config.personality, scenarios: template.scenarios, communicationStyle: config.communication_style, model: config.model, }); const cloneId = result?.clone?.id; if (cloneId) { // Persist SOUL.md via identity system if (config.soul_content) { try { const { intelligenceClient } = await import('../lib/intelligence-client'); await intelligenceClient.identity.updateFile(cloneId, 'soul', config.soul_content); } catch (e) { log.warn('Failed to persist soul_content:', e); } } // Persist system_prompt via identity system if (config.system_prompt) { try { const { intelligenceClient } = await import('../lib/intelligence-client'); await intelligenceClient.identity.updateFile(cloneId, 'system', config.system_prompt); } catch (e) { log.warn('Failed to persist system_prompt:', e); } } // Persist temperature / max_tokens / tools / source_template_id / welcomeMessage / quickCommands const metadata: Record = {}; if (config.temperature != null) metadata.temperature = config.temperature; if (config.max_tokens != null) metadata.maxTokens = config.max_tokens; if (config.tools?.length) metadata.tools = config.tools; metadata.source_template_id = template.id; if (config.welcome_message) metadata.welcomeMessage = config.welcome_message; if (config.quick_commands?.length) metadata.quickCommands = config.quick_commands; if (Object.keys(metadata).length > 0) { try { await client.updateClone(cloneId, metadata); } catch (e) { log.warn('Failed to persist clone metadata:', e); } } } await get().loadClones(); // Return a fresh clone from the store (immutable — no in-place mutation) const freshClone = get().clones.find((c) => c.id === cloneId); if (freshClone) { return { ...freshClone, ...(config.welcome_message ? { welcomeMessage: config.welcome_message } : {}), ...(config.quick_commands?.length ? { quickCommands: config.quick_commands } : {}), }; } return result?.clone as Clone | undefined; } catch (error) { set({ error: String(error) }); return undefined; } finally { set({ isLoading: false }); } }, updateClone: async (id: string, updates: Partial) => { const client = getClient(); if (!client) { log.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) { log.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) { log.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;