/** * kernel-agent.ts - Agent & Clone management methods for KernelClient * * Installed onto KernelClient.prototype via installAgentMethods(). */ import { invoke } from '@tauri-apps/api/core'; import type { KernelClient } from './kernel-client'; import type { AgentInfo, CreateAgentRequest, CreateAgentResponse } from './kernel-types'; export function installAgentMethods(ClientClass: { prototype: KernelClient }): void { const proto = ClientClass.prototype as unknown as Record; // ─── Agent Management ─── /** * List all agents */ proto.listAgents = async function (this: KernelClient): Promise { return invoke('agent_list'); }; /** * Get agent by ID */ proto.getAgent = async function (this: KernelClient, agentId: string): Promise { return invoke('agent_get', { agentId }); }; /** * Create a new agent */ proto.createAgent = async function (this: KernelClient, request: CreateAgentRequest): Promise { return invoke('agent_create', { request: { name: request.name, description: request.description, systemPrompt: request.systemPrompt, soul: request.soul, provider: request.provider || 'anthropic', model: request.model || 'claude-sonnet-4-20250514', maxTokens: request.maxTokens || 4096, temperature: request.temperature || 0.7, }, }); }; /** * Delete an agent */ proto.deleteAgent = async function (this: KernelClient, agentId: string): Promise { return invoke('agent_delete', { agentId }); }; // ─── Clone/Agent Adaptation (GatewayClient interface compatibility) ─── /** * List clones — maps to listAgents() with field adaptation * Maps all available AgentInfo fields to Clone interface properties */ proto.listClones = async function (this: KernelClient): Promise<{ clones: any[] }> { const agents = await this.listAgents(); const clones = agents.map((agent) => { // Parse personality/emoji/nickname from SOUL.md content const soulLines = (agent.soul || '').split('\n'); let emoji: string | undefined; let personality: string | undefined; let nickname: string | undefined; for (const line of soulLines) { if (!emoji || !nickname) { // Parse header line: "> 🦞 Nickname" or "> 🦞" const headerMatch = line.match(/^>\s*(\p{Emoji_Presentation}|\p{Extended_Pictographic})?\s*(.+)$/u); if (headerMatch) { if (headerMatch[1] && !emoji) emoji = headerMatch[1]; if (headerMatch[2]?.trim() && !nickname) nickname = headerMatch[2].trim(); } // Also check emoji without nickname if (!emoji) { const emojiOnly = line.match(/^>\s*(\p{Emoji_Presentation}|\p{Extended_Pictographic})\s*$/u); if (emojiOnly) emoji = emojiOnly[1]; } } if (!personality) { const match = line.match(/##\s*(?:性格|核心特质|沟通风格)/); if (match) personality = line.trim(); } } // Parse userName/userRole from userProfile let userName: string | undefined; let userRole: string | undefined; if (agent.userProfile && typeof agent.userProfile === 'object') { const profile = agent.userProfile as Record; userName = profile.userName as string | undefined || profile.name as string | undefined; userRole = profile.userRole as string | undefined || profile.role as string | undefined; } return { id: agent.id, name: agent.name, role: agent.description, nickname, model: agent.model, soul: agent.soul, systemPrompt: agent.systemPrompt, temperature: agent.temperature, maxTokens: agent.maxTokens, emoji, personality, userName, userRole, createdAt: agent.createdAt || new Date().toISOString(), updatedAt: agent.updatedAt, }; }); return { clones }; }; /** * Create clone — maps to createAgent() */ proto.createClone = async function (this: KernelClient, opts: { name: string; role?: string; model?: string; personality?: string; communicationStyle?: string; scenarios?: string[]; emoji?: string; notes?: string; [key: string]: unknown; }): Promise<{ clone: any }> { // Build soul content from personality data const soulParts: string[] = []; if (opts.personality) soulParts.push(`## 性格\n${opts.personality}`); if (opts.communicationStyle) soulParts.push(`## 沟通风格\n${opts.communicationStyle}`); if (opts.scenarios?.length) soulParts.push(`## 使用场景\n${opts.scenarios.join(', ')}`); if (opts.notes) soulParts.push(`## 备注\n${opts.notes}`); const soul = soulParts.length > 0 ? soulParts.join('\n\n') : undefined; const response = await this.createAgent({ name: opts.name, description: opts.role, model: opts.model, soul, }); const clone = { id: response.id, name: response.name, role: opts.role, model: opts.model, personality: opts.personality, communicationStyle: opts.communicationStyle, emoji: opts.emoji, scenarios: opts.scenarios, createdAt: new Date().toISOString(), }; return { clone }; }; /** * Delete clone — maps to deleteAgent() */ proto.deleteClone = async function (this: KernelClient, id: string): Promise { return this.deleteAgent(id); }; /** * Update clone — maps to kernel agent_update + identity system for nickname/userName */ proto.updateClone = async function (this: KernelClient, id: string, updates: Record): Promise<{ clone: unknown }> { await invoke('agent_update', { agentId: id, updates: { name: updates.name as string | undefined, description: updates.description as string | undefined, systemPrompt: updates.systemPrompt as string | undefined, model: updates.model as string | undefined, provider: updates.provider as string | undefined, maxTokens: updates.maxTokens as number | undefined, temperature: updates.temperature as number | undefined, }, }); // Sync nickname/emoji to SOUL.md via identity system const nickname = updates.nickname as string | undefined; const emoji = updates.emoji as string | undefined; if (nickname || emoji) { try { const currentSoul = await invoke('identity_get_file', { agentId: id, file: 'soul' }); const soul = currentSoul || ''; // Inject or update nickname line in SOUL.md header const lines = soul.split('\n'); const headerIdx = lines.findIndex((l: string) => l.startsWith('> ')); if (headerIdx >= 0) { // Update existing header line let header = lines[headerIdx]; if (emoji && !header.match(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/u)) { header = `> ${emoji} ${header.slice(2)}`; } lines[headerIdx] = header; } else if (emoji || nickname) { // Add header line after title const label = nickname || ''; const icon = emoji || ''; const titleIdx = lines.findIndex((l: string) => l.startsWith('# ')); if (titleIdx >= 0) { lines.splice(titleIdx + 1, 0, `> ${icon} ${label}`.trim()); } } await invoke('identity_update_file', { agentId: id, file: 'soul', content: lines.join('\n') }); } catch { // Identity system update is non-critical } } // Sync userName/userRole to USER.md via identity system const userName = updates.userName as string | undefined; const userRole = updates.userRole as string | undefined; if (userName || userRole) { try { const currentProfile = await invoke('identity_get_file', { agentId: id, file: 'user_profile' }); const profile = currentProfile || '# 用户档案\n'; const profileLines = profile.split('\n'); // Update or add userName if (userName) { const nameIdx = profileLines.findIndex((l: string) => l.includes('姓名') || l.includes('userName')); if (nameIdx >= 0) { profileLines[nameIdx] = `- 姓名:${userName}`; } else { const sectionIdx = profileLines.findIndex((l: string) => l.startsWith('## 基本信息')); if (sectionIdx >= 0) { profileLines.splice(sectionIdx + 1, 0, '', `- 姓名:${userName}`); } else { profileLines.push('', '## 基本信息', '', `- 姓名:${userName}`); } } } // Update or add userRole if (userRole) { const roleIdx = profileLines.findIndex((l: string) => l.includes('角色') || l.includes('userRole')); if (roleIdx >= 0) { profileLines[roleIdx] = `- 角色:${userRole}`; } else { profileLines.push(`- 角色:${userRole}`); } } await invoke('identity_update_file', { agentId: id, file: 'user_profile', content: profileLines.join('\n') }); } catch { // Identity system update is non-critical } } // Return updated clone representation const clone = { id, name: updates.name, role: updates.description || updates.role, nickname: updates.nickname, model: updates.model, emoji: updates.emoji, personality: updates.personality, communicationStyle: updates.communicationStyle, systemPrompt: updates.systemPrompt, userName: updates.userName, userRole: updates.userRole, }; return { clone }; }; } // === Agent ID Resolution === /** * Cached kernel default agent UUID. * The conversationStore's DEFAULT_AGENT has id="1", but VikingStorage * stores data under kernel UUIDs. This cache bridges the gap. */ let _cachedDefaultKernelAgentId: string | null = null; /** * Resolve an agent ID to the kernel's actual agent UUID. * - If already a UUID (8-4-4 hex pattern), return as-is. * - If "1" or undefined, query agent_list and cache the first kernel agent's UUID. * - Falls back to the original ID if kernel has no agents. */ export async function resolveKernelAgentId(agentId: string | undefined): Promise { if (agentId && /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(agentId)) { return agentId; } if (_cachedDefaultKernelAgentId) { return _cachedDefaultKernelAgentId; } try { const agents = await invoke<{ id: string }[]>('agent_list'); if (agents.length > 0) { _cachedDefaultKernelAgentId = agents[0].id; return _cachedDefaultKernelAgentId; } } catch { // Kernel may not be available } return agentId || '1'; } /** Invalidate cache when kernel reconnects (new instance may have different UUIDs) */ export function invalidateKernelAgentIdCache(): void { _cachedDefaultKernelAgentId = null; }