/** * ZCLAW Kernel Client (Tauri Internal) * * Client for communicating with the internal ZCLAW Kernel via Tauri commands. * This replaces the external ZCLAW Gateway WebSocket connection. * * Phase 5 of Intelligence Layer Migration. */ import { invoke } from '@tauri-apps/api/core'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { createLogger } from './logger'; const log = createLogger('KernelClient'); // Re-export UnlistenFn for external use export type { UnlistenFn }; // === Types === export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'; export interface KernelStatus { initialized: boolean; agentCount: number; databaseUrl: string | null; defaultProvider: string | null; defaultModel: string | null; } export interface AgentInfo { id: string; name: string; description?: string; state: string; model?: string; provider?: string; } export interface CreateAgentRequest { name: string; description?: string; systemPrompt?: string; provider?: string; model?: string; maxTokens?: number; temperature?: number; } export interface CreateAgentResponse { id: string; name: string; state: string; } export interface ChatResponse { content: string; inputTokens: number; outputTokens: number; } export interface EventCallback { (payload: unknown): void; } export interface StreamCallbacks { onDelta: (delta: string) => void; onTool?: (tool: string, input: string, output: string) => void; onHand?: (name: string, status: string, result?: unknown) => void; onComplete: (inputTokens?: number, outputTokens?: number) => void; onError: (error: string) => void; } // === Streaming Types (match Rust StreamChatEvent) === export interface StreamEventDelta { type: 'delta'; delta: string; } export interface StreamEventToolStart { type: 'tool_start'; name: string; input: unknown; } export interface StreamEventToolEnd { type: 'tool_end'; name: string; output: unknown; } export interface StreamEventIterationStart { type: 'iteration_start'; iteration: number; maxIterations: number; } export interface StreamEventComplete { type: 'complete'; inputTokens: number; outputTokens: number; } export interface StreamEventError { type: 'error'; message: string; } export interface StreamEventHandStart { type: 'handStart'; name: string; params: unknown; } export interface StreamEventHandEnd { type: 'handEnd'; name: string; result: unknown; } export type StreamChatEvent = | StreamEventDelta | StreamEventToolStart | StreamEventToolEnd | StreamEventIterationStart | StreamEventHandStart | StreamEventHandEnd | StreamEventComplete | StreamEventError; export interface StreamChunkPayload { sessionId: string; event: StreamChatEvent; } export interface KernelConfig { provider?: string; model?: string; apiKey?: string; baseUrl?: string; apiProtocol?: string; // openai, anthropic, custom } /** * Check if running in Tauri environment * NOTE: This checks synchronously. For more reliable detection, * use probeTauriAvailability() which actually tries to call a Tauri command. */ export function isTauriRuntime(): boolean { const result = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; log.debug('isTauriRuntime() check:', result, 'window exists:', typeof window !== 'undefined', '__TAURI_INTERNALS__ exists:', typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window); return result; } /** * Probe if Tauri is actually available by trying to invoke a command. * This is more reliable than checking __TAURI_INTERNALS__ which may not be set * immediately when the page loads. */ let _tauriAvailable: boolean | null = null; export async function probeTauriAvailability(): Promise { if (_tauriAvailable !== null) { return _tauriAvailable; } // First check if window.__TAURI_INTERNALS__ exists if (typeof window === 'undefined' || !('__TAURI_INTERNALS__' in window)) { log.debug('probeTauriAvailability: __TAURI_INTERNALS__ not found'); _tauriAvailable = false; return false; } // Try to actually invoke a simple command to verify Tauri is working try { // Use kernel_status as a lightweight health check await invoke('kernel_status'); log.debug('probeTauriAvailability: kernel_status succeeded'); _tauriAvailable = true; return true; } catch { // Try without plugin prefix - some Tauri versions don't use it try { // Just checking if invoke function exists is enough log.debug('probeTauriAvailability: Tauri invoke available'); _tauriAvailable = true; return true; } catch { log.debug('probeTauriAvailability: Tauri invoke failed'); _tauriAvailable = false; return false; } } } /** * ZCLAW Kernel Client * * Provides a GatewayClient-compatible interface that uses Tauri commands * to communicate with the internal ZCLAW Kernel instead of external WebSocket. */ export class KernelClient { private state: ConnectionState = 'disconnected'; private eventListeners = new Map>(); private kernelStatus: KernelStatus | null = null; private defaultAgentId: string = ''; private config: KernelConfig = {}; // State change callbacks onStateChange?: (state: ConnectionState) => void; onLog?: (level: string, message: string) => void; constructor(opts?: { url?: string; token?: string; autoReconnect?: boolean; reconnectInterval?: number; requestTimeout?: number; kernelConfig?: KernelConfig; }) { // Store kernel config if provided if (opts?.kernelConfig) { this.config = opts.kernelConfig; } } updateOptions(opts?: { url?: string; token?: string; autoReconnect?: boolean; reconnectInterval?: number; requestTimeout?: number; kernelConfig?: KernelConfig; }): void { if (opts?.kernelConfig) { this.config = opts.kernelConfig; } } /** * Set kernel configuration (must be called before connect) */ setConfig(config: KernelConfig): void { this.config = config; } getState(): ConnectionState { return this.state; } /** * Initialize and connect to the internal Kernel */ async connect(): Promise { // Always try to (re)initialize - backend will handle config changes // by rebooting the kernel if needed this.setState('connecting'); try { // Validate that we have required config if (!this.config.provider || !this.config.model || !this.config.apiKey) { throw new Error('请先在"模型与 API"设置页面配置模型'); } // Initialize the kernel via Tauri command with config const configRequest = { provider: this.config.provider, model: this.config.model, apiKey: this.config.apiKey, baseUrl: this.config.baseUrl || null, apiProtocol: this.config.apiProtocol || 'openai', }; log.debug('Initializing with config:', { provider: configRequest.provider, model: configRequest.model, hasApiKey: !!configRequest.apiKey, baseUrl: configRequest.baseUrl, apiProtocol: configRequest.apiProtocol, }); const status = await invoke('kernel_init', { configRequest, }); this.kernelStatus = status; // Get or create default agent using the configured model const agents = await this.listAgents(); if (agents.length > 0) { this.defaultAgentId = agents[0].id; } else { // Create a default agent with the user's configured model // For Coding Plan providers, add a coding-focused system prompt const isCodingPlan = this.config.provider?.includes('coding') || this.config.baseUrl?.includes('coding.dashscope'); const systemPrompt = isCodingPlan ? '你是一个专业的编程助手。你可以帮助用户解决编程问题、写代码、调试、解释技术概念等。请用中文回答问题。' : '你是 ZCLAW 智能助手,可以帮助用户解决各种问题。请用中文回答。'; const agent = await this.createAgent({ name: 'Default Agent', description: 'ZCLAW default assistant', systemPrompt, provider: this.config.provider, model: this.config.model, }); this.defaultAgentId = agent.id; } this.setState('connected'); this.emitEvent('connected', { version: '0.1.0-internal' }); this.log('info', 'Connected to internal ZCLAW Kernel'); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); this.setState('disconnected'); this.log('error', `Failed to initialize kernel: ${errorMessage}`); throw new Error(`Failed to initialize kernel: ${errorMessage}`); } } /** * Connect using REST API (compatibility with GatewayClient) */ async connectRest(): Promise { return this.connect(); } /** * Disconnect from kernel (no-op for internal kernel) */ disconnect(): void { this.setState('disconnected'); this.kernelStatus = null; this.log('info', 'Disconnected from internal kernel'); } // === Agent Management === /** * List all agents */ async listAgents(): Promise { return invoke('agent_list'); } /** * Get agent by ID */ async getAgent(agentId: string): Promise { return invoke('agent_get', { agentId }); } /** * Create a new agent */ async createAgent(request: CreateAgentRequest): Promise { return invoke('agent_create', { request: { name: request.name, description: request.description, systemPrompt: request.systemPrompt, provider: request.provider || 'anthropic', model: request.model || 'claude-sonnet-4-20250514', maxTokens: request.maxTokens || 4096, temperature: request.temperature || 0.7, }, }); } /** * Delete an agent */ async deleteAgent(agentId: string): Promise { return invoke('agent_delete', { agentId }); } // === Clone/Agent Adaptation (GatewayClient interface compatibility) === /** * List clones — maps to listAgents() with field adaptation */ async listClones(): Promise<{ clones: any[] }> { const agents = await this.listAgents(); const clones = agents.map((agent) => ({ id: agent.id, name: agent.name, role: agent.description, model: agent.model, createdAt: new Date().toISOString(), })); return { clones }; } /** * Create clone — maps to createAgent() */ async createClone(opts: { name: string; role?: string; model?: string; personality?: string; communicationStyle?: string; [key: string]: unknown; }): Promise<{ clone: any }> { const response = await this.createAgent({ name: opts.name, description: opts.role, model: opts.model, }); const clone = { id: response.id, name: response.name, role: opts.role, model: opts.model, personality: opts.personality, communicationStyle: opts.communicationStyle, createdAt: new Date().toISOString(), }; return { clone }; } /** * Delete clone — maps to deleteAgent() */ async deleteClone(id: string): Promise { return this.deleteAgent(id); } /** * Update clone — maps to kernel agent_update */ async updateClone(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, }, }); // Return updated clone representation const clone = { id, name: updates.name, role: updates.description || updates.role, model: updates.model, personality: updates.personality, communicationStyle: updates.communicationStyle, systemPrompt: updates.systemPrompt, }; return { clone }; } // === Chat === /** * Send a message and get a response */ async chat( message: string, opts?: { sessionKey?: string; agentId?: string; } ): Promise<{ runId: string; sessionId?: string; response?: string }> { const agentId = opts?.agentId || this.defaultAgentId; if (!agentId) { throw new Error('No agent available'); } const response = await invoke('agent_chat', { request: { agentId, message, }, }); return { runId: `run_${Date.now()}`, sessionId: opts?.sessionKey, response: response.content, }; } /** * Send a message with streaming response via Tauri events */ async chatStream( message: string, callbacks: StreamCallbacks, opts?: { sessionKey?: string; agentId?: string; } ): Promise<{ runId: string }> { const runId = crypto.randomUUID(); const sessionId = opts?.sessionKey || runId; const agentId = opts?.agentId || this.defaultAgentId; if (!agentId) { callbacks.onError('No agent available'); return { runId }; } let unlisten: UnlistenFn | null = null; try { // Set up event listener for stream chunks unlisten = await listen('stream:chunk', (event) => { const payload = event.payload; // Only process events for this session if (payload.sessionId !== sessionId) { return; } const streamEvent = payload.event; switch (streamEvent.type) { case 'delta': callbacks.onDelta(streamEvent.delta); break; case 'tool_start': log.debug('Tool started:', streamEvent.name, streamEvent.input); if (callbacks.onTool) { callbacks.onTool( streamEvent.name, JSON.stringify(streamEvent.input), '' ); } break; case 'tool_end': log.debug('Tool ended:', streamEvent.name, streamEvent.output); if (callbacks.onTool) { callbacks.onTool( streamEvent.name, '', JSON.stringify(streamEvent.output) ); } break; case 'handStart': log.debug('Hand started:', streamEvent.name, streamEvent.params); if (callbacks.onHand) { callbacks.onHand(streamEvent.name, 'running', undefined); } break; case 'handEnd': log.debug('Hand ended:', streamEvent.name, streamEvent.result); if (callbacks.onHand) { callbacks.onHand(streamEvent.name, 'completed', streamEvent.result); } break; case 'iteration_start': log.debug('Iteration started:', streamEvent.iteration, '/', streamEvent.maxIterations); // Don't need to notify user about iterations break; case 'complete': log.debug('Stream complete:', streamEvent.inputTokens, streamEvent.outputTokens); callbacks.onComplete(streamEvent.inputTokens, streamEvent.outputTokens); // Clean up listener if (unlisten) { unlisten(); unlisten = null; } break; case 'error': log.error('Stream error:', streamEvent.message); callbacks.onError(streamEvent.message); // Clean up listener if (unlisten) { unlisten(); unlisten = null; } break; } }); // Invoke the streaming command await invoke('agent_chat_stream', { request: { agentId, sessionId, message, }, }); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); callbacks.onError(errorMessage); // Clean up listener on error if (unlisten) { unlisten(); } } return { runId }; } /** * Cancel a stream (no-op for internal kernel) */ cancelStream(_runId: string): void { // No-op: internal kernel doesn't support stream cancellation } // === Default Agent === /** * Fetch default agent ID (returns current default) */ async fetchDefaultAgentId(): Promise { return this.defaultAgentId; } /** * Set default agent ID */ setDefaultAgentId(agentId: string): void { this.defaultAgentId = agentId; } /** * Get default agent ID */ getDefaultAgentId(): string { return this.defaultAgentId; } // === GatewayClient Compatibility === /** * Health check */ async health(): Promise<{ status: string; version?: string }> { if (this.kernelStatus?.initialized) { return { status: 'ok', version: '0.1.0-internal' }; } return { status: 'not_initialized' }; } /** * Get status */ async status(): Promise> { const status = await invoke('kernel_status'); return { initialized: status.initialized, agentCount: status.agentCount, defaultProvider: status.defaultProvider, defaultModel: status.defaultModel, }; } // === Hands API === /** * List all available hands */ async listHands(): Promise<{ hands: { id?: string; name: string; description?: string; status?: string; requirements_met?: boolean; category?: string; icon?: string; tool_count?: number; tools?: string[]; metric_count?: number; metrics?: string[]; }[] }> { const hands = await invoke>('hand_list'); return { hands: hands || [] }; } /** * Get hand details */ async getHand(name: string): Promise<{ id?: string; name?: string; description?: string; status?: string; requirements_met?: boolean; category?: string; icon?: string; provider?: string; model?: string; requirements?: { description?: string; name?: string; met?: boolean; satisfied?: boolean; details?: string; hint?: string }[]; tools?: string[]; metrics?: string[]; config?: Record; tool_count?: number; metric_count?: number; }> { try { return await invoke('hand_get', { name }); } catch { // Hand not found or kernel not initialized return {}; } } /** * Trigger/execute a hand */ async triggerHand(name: string, params?: Record, autonomyLevel?: string): Promise<{ runId: string; status: string }> { const result = await invoke<{ instance_id: string; status: string }>('hand_execute', { id: name, input: params || {}, ...(autonomyLevel ? { autonomyLevel } : {}), }); return { runId: result.instance_id, status: result.status }; } /** * Get hand run status */ async getHandStatus(name: string, runId: string): Promise<{ status: string; result?: unknown }> { try { return await invoke('hand_run_status', { handName: name, runId }); } catch { return { status: 'unknown' }; } } /** * Approve a hand execution */ async approveHand(name: string, runId: string, approved: boolean, reason?: string): Promise<{ status: string }> { return await invoke('hand_approve', { handName: name, runId, approved, reason }); } /** * Cancel a hand execution */ async cancelHand(name: string, runId: string): Promise<{ status: string }> { return await invoke('hand_cancel', { handName: name, runId }); } /** * List hand runs (execution history) */ async listHandRuns(name: string, opts?: { limit?: number; offset?: number }): Promise<{ runs: { runId?: string; run_id?: string; id?: string; status?: string; startedAt?: string; started_at?: string; completedAt?: string; completed_at?: string; result?: unknown; error?: string; }[] }> { // Hand run history try { return await invoke('hand_run_list', { handName: name, ...opts }); } catch { return { runs: [] }; } } // === Skills API === /** * List all discovered skills */ async listSkills(): Promise<{ skills: { id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; }[] }> { const skills = await invoke>('skill_list'); return { skills: skills || [] }; } /** * Refresh skills from directory */ async refreshSkills(skillDir?: string): Promise<{ skills: { id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; }[] }> { const skills = await invoke>('skill_refresh', { skillDir: skillDir || null }); return { skills: skills || [] }; } /** * Create a new skill */ async createSkill(skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: { id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; } }> { const result = await invoke<{ id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; }>('skill_create', { request: { name: skill.name, description: skill.description, triggers: skill.triggers.map(t => t.pattern || t.type), actions: skill.actions.map(a => a.type), enabled: skill.enabled, }, }); return { skill: result }; } /** * Update an existing skill */ async updateSkill(id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: { id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; } }> { const result = await invoke<{ id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; }>('skill_update', { id, request: { name: updates.name, description: updates.description, triggers: updates.triggers?.map(t => t.pattern || t.type), actions: updates.actions?.map(a => a.type), enabled: updates.enabled, }, }); return { skill: result }; } /** * Delete a skill */ async deleteSkill(id: string): Promise { await invoke('skill_delete', { id }); } /** async executeSkill(id: string, input?: Record): Promise<{ success: boolean; output?: unknown; error?: string; durationMs?: number; }> { // Autonomy check before executing skill const { canAutoExecute, getAutonomyManager } = await import('./autonomy-manager'); const { canProceed, decision } = canAutoExecute('skill_install', 5); if (!canProceed) { return { success: false, error: `自主授权拒绝: ${decision.reason}`, }; } const autonomyLevel = getAutonomyManager().getConfig().level; return invoke('skill_execute', { id, context: {}, input: input || {}, autonomyLevel, }); } // === Triggers API === /** * List all triggers * Returns empty array on error for graceful degradation */ async listTriggers(): Promise<{ triggers?: Array<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; }> }> { try { const triggers = await invoke>('trigger_list'); return { triggers }; } catch (error) { this.log('error', `[TriggersAPI] listTriggers failed: ${this.formatError(error)}`); return { triggers: [] }; } } /** * Get a single trigger by ID * Returns null on error for graceful degradation */ async getTrigger(id: string): Promise<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; } | null> { try { return await invoke<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; } | null>('trigger_get', { id }); } catch (error) { this.log('error', `[TriggersAPI] getTrigger(${id}) failed: ${this.formatError(error)}`); return null; } } /** * Create a new trigger * Returns null on error for graceful degradation */ async createTrigger(trigger: { id: string; name: string; handId: string; triggerType: { type: string; cron?: string; pattern?: string; path?: string; secret?: string; events?: string[] }; enabled?: boolean; description?: string; tags?: string[]; }): Promise<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; } | null> { try { return await invoke<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; }>('trigger_create', { request: trigger }); } catch (error) { this.log('error', `[TriggersAPI] createTrigger(${trigger.id}) failed: ${this.formatError(error)}`); return null; } } /** * Update an existing trigger * Throws on error as this is a mutation operation that callers need to handle */ async updateTrigger(id: string, updates: { name?: string; enabled?: boolean; handId?: string; triggerType?: { type: string; cron?: string; pattern?: string; path?: string; secret?: string; events?: string[] }; }): Promise<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; }> { try { return await invoke<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; }>('trigger_update', { id, updates }); } catch (error) { this.log('error', `[TriggersAPI] updateTrigger(${id}) failed: ${this.formatError(error)}`); throw error; } } /** * Delete a trigger * Throws on error as this is a destructive operation that callers need to handle */ async deleteTrigger(id: string): Promise { try { await invoke('trigger_delete', { id }); } catch (error) { this.log('error', `[TriggersAPI] deleteTrigger(${id}) failed: ${this.formatError(error)}`); throw error; } } /** * Execute a trigger * Throws on error as callers need to know if execution failed */ async executeTrigger(id: string, input?: Record): Promise> { try { return await invoke>('trigger_execute', { id, input: input || {} }); } catch (error) { this.log('error', `[TriggersAPI] executeTrigger(${id}) failed: ${this.formatError(error)}`); throw error; } } // === Approvals API === async listApprovals(_status?: string): Promise<{ approvals: Array<{ id: string; handId: string; status: string; createdAt: string; input: Record; }> }> { try { const approvals = await invoke; }>>('approval_list'); return { approvals }; } catch (error) { log.error('listApprovals error:', error); return { approvals: [] }; } } async respondToApproval(approvalId: string, approved: boolean, reason?: string): Promise { return invoke('approval_respond', { id: approvalId, approved, reason }); } /** * REST API compatibility methods */ public getRestBaseUrl(): string { return ''; // Internal kernel doesn't use REST } public async restGet(_path: string): Promise { throw new Error('REST API not available for internal kernel'); } public async restPost(_path: string, _body?: unknown): Promise { throw new Error('REST API not available for internal kernel'); } public async restPut(_path: string, _body?: unknown): Promise { throw new Error('REST API not available for internal kernel'); } public async restDelete(_path: string): Promise { throw new Error('REST API not available for internal kernel'); } public async restPatch(_path: string, _body?: unknown): Promise { throw new Error('REST API not available for internal kernel'); } // === Events === /** * Subscribe to events */ on(event: string, callback: EventCallback): () => void { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } this.eventListeners.get(event)!.add(callback); return () => { this.eventListeners.get(event)?.delete(callback); }; } /** * Subscribe to agent stream events (GatewayClient compatibility) * Note: KernelClient handles streaming via chatStream callbacks directly, * so this is a no-op that returns an empty unsubscribe function. */ onAgentStream(_callback: (delta: { stream: 'assistant' | 'tool' | 'lifecycle' | 'hand' | 'workflow'; delta?: string; content?: string; runId?: string }) => void): () => void { // KernelClient uses chatStream callbacks for streaming, not a separate event stream // Return empty unsubscribe for compatibility return () => {}; } // === A2A (Agent-to-Agent) API === /** * Send a direct A2A message from one agent to another */ /** * Send a direct A2A message from one agent to another */ async a2aSend(from: string, to: string, payload: unknown, messageType?: string): Promise { await invoke('agent_a2a_send', { from, to, payload, messageType: messageType || 'notification', }); } /** * Broadcast a message from an agent to all other agents */ async a2aBroadcast(from: string, payload: unknown): Promise { await invoke('agent_a2a_broadcast', { from, payload }); } /** * Discover agents that have a specific capability */ async a2aDiscover(capability: string): Promise; role: string; priority: number; }>> { return await invoke('agent_a2a_discover', { capability }); } /** * Delegate a task to another agent and wait for response */ async a2aDelegateTask(from: string, to: string, task: string, timeoutMs?: number): Promise { return await invoke('agent_a2a_delegate_task', { from, to, task, timeoutMs: timeoutMs || 30000, }); } // === Internal === private setState(state: ConnectionState): void { this.state = state; this.onStateChange?.(state); this.emitEvent('state', state); } private emitEvent(event: string, payload: unknown): void { const listeners = this.eventListeners.get(event); if (listeners) { for (const cb of listeners) { try { cb(payload); } catch { /* ignore listener errors */ } } } } private log(level: string, message: string): void { this.onLog?.(level, message); } /** * Format error for consistent logging */ private formatError(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } } // === Singleton === let _client: KernelClient | null = null; /** * Get the kernel client singleton */ export function getKernelClient(opts?: ConstructorParameters[0]): KernelClient { if (!_client) { _client = new KernelClient(opts); } else if (opts) { _client.updateOptions(opts); } return _client; } /** * Check if internal kernel mode is available */ export function isInternalKernelAvailable(): boolean { return isTauriRuntime(); }