/** * 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. * * Domain methods are installed from mixin modules following the same pattern * used by gateway-api.ts for GatewayClient: * - kernel-agent.ts → installAgentMethods() * - kernel-chat.ts → installChatMethods() * - kernel-hands.ts → installHandMethods() * - kernel-skills.ts → installSkillMethods() * - kernel-triggers.ts → installTriggerMethods() * - kernel-a2a.ts → installA2aMethods() */ import { invoke } from '@tauri-apps/api/core'; import { createLogger } from './logger'; // Re-export all types from the shared types module export type { UnlistenFn } from '@tauri-apps/api/event'; export type { ConnectionState, KernelStatus, AgentInfo, CreateAgentRequest, CreateAgentResponse, ChatResponse, EventCallback, StreamCallbacks, StreamEventDelta, StreamEventToolStart, StreamEventToolEnd, StreamEventIterationStart, StreamEventComplete, StreamEventError, StreamEventHandStart, StreamEventHandEnd, StreamChatEvent, StreamChunkPayload, KernelConfig, } from './kernel-types'; import type { ConnectionState, KernelStatus, KernelConfig, EventCallback, } from './kernel-types'; const log = createLogger('KernelClient'); // === Tauri Runtime Detection === /** * 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 (e) { // Try without plugin prefix - some Tauri versions don't use it log.debug('probeTauriAvailability: kernel_status invoke failed', { error: e }); try { // Just checking if invoke function exists is enough log.debug('probeTauriAvailability: Tauri invoke available'); _tauriAvailable = true; return true; } catch (e) { log.debug('probeTauriAvailability: secondary invoke check failed', { error: e }); _tauriAvailable = false; return false; } } } // === KernelClient Core Class === /** * ZCLAW Kernel Client * * Provides a GatewayClient-compatible interface that uses Tauri commands * to communicate with the internal ZCLAW Kernel instead of external WebSocket. * * Domain-specific methods (agents, chat, hands, skills, triggers, a2a) * are installed at the bottom of this file via mixin functions. */ export class KernelClient { private state: ConnectionState = 'disconnected'; private eventListeners = new Map>(); private kernelStatus: KernelStatus | null = null; /** @internal stored as _defaultAgentId so mixin methods can access via getDefaultAgentId() */ 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'); } // === 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.baseUrl, defaultModel: status.model, }; } /** * REST API compatibility stubs */ 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 () => {}; } // === Internal helpers (non-private so mixin modules can use them via `this`) === setState(state: ConnectionState): void { this.state = state; this.onStateChange?.(state); this.emitEvent('state', state); } emitEvent(event: string, payload: unknown): void { const listeners = this.eventListeners.get(event); if (listeners) { for (const cb of listeners) { try { cb(payload); } catch (e) { log.debug('Event listener threw error', { error: e }); } } } } log(level: string, message: string): void { this.onLog?.(level, message); } /** * Format error for consistent logging */ formatError(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } } // === Install domain methods from mixin modules === import { installAgentMethods } from './kernel-agent'; import { installChatMethods } from './kernel-chat'; import { installHandMethods } from './kernel-hands'; import { installSkillMethods } from './kernel-skills'; import { installTriggerMethods } from './kernel-triggers'; import { installA2aMethods } from './kernel-a2a'; installAgentMethods(KernelClient); installChatMethods(KernelClient); installHandMethods(KernelClient); installSkillMethods(KernelClient); installTriggerMethods(KernelClient); installA2aMethods(KernelClient); // === API Method Type Declarations === // These methods are installed at runtime by the mixin modules above. // We declare them here so TypeScript knows they exist on KernelClient. export interface KernelClient { // Agent management (kernel-agent.ts) listAgents(): Promise; getAgent(agentId: string): Promise; createAgent(request: import('./kernel-types').CreateAgentRequest): Promise; deleteAgent(agentId: string): Promise; listClones(): Promise<{ clones: Array<{ id: string; name: string; [key: string]: unknown }> }>; createClone(opts: { name: string; role?: string; model?: string; personality?: string; communicationStyle?: string; [key: string]: unknown }): Promise<{ clone: { id: string; name: string; [key: string]: unknown } }>; deleteClone(id: string): Promise; updateClone(id: string, updates: Record): Promise<{ clone: unknown }>; // Chat (kernel-chat.ts) chat(message: string, opts?: { sessionKey?: string; agentId?: string }): Promise<{ runId: string; sessionId?: string; response?: string }>; chatStream(message: string, callbacks: import('./kernel-types').StreamCallbacks, opts?: { sessionKey?: string; agentId?: string; thinking_enabled?: boolean; reasoning_effort?: string; plan_mode?: boolean; subagent_enabled?: boolean }): Promise<{ runId: string }>; cancelStream(sessionId: string): Promise; fetchDefaultAgentId(): Promise; setDefaultAgentId(agentId: string): void; getDefaultAgentId(): string; // Hands (kernel-hands.ts) 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[]; }[] }>; 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; }>; triggerHand(name: string, params?: Record, autonomyLevel?: string): Promise<{ runId: string; status: string }>; getHandStatus(name: string, runId: string): Promise<{ status: string; result?: unknown }>; approveHand(name: string, runId: string, approved: boolean, reason?: string): Promise<{ status: string }>; cancelHand(name: string, runId: string): Promise<{ status: string }>; 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; }[] }>; listApprovals(status?: string): Promise<{ approvals: Array<{ id: string; handId: string; status: string; createdAt: string; input: Record; }> }>; respondToApproval(approvalId: string, approved: boolean, reason?: string): Promise; // Skills (kernel-skills.ts) listSkills(): Promise<{ skills: { id: string; name: string; description: string; version: string; capabilities: string[]; tags: string[]; mode: string; enabled: boolean; triggers: string[]; category?: string; }[] }>; 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; }[] }>; 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; } }>; 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; } }>; deleteSkill(id: string): Promise; executeSkill(id: string, input?: Record): Promise<{ success: boolean; output?: unknown; error?: string; durationMs?: number; }>; // Triggers (kernel-triggers.ts) listTriggers(): Promise<{ triggers?: Array<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; }> }>; getTrigger(id: string): Promise<{ id: string; name: string; handId: string; triggerType: string; enabled: boolean; createdAt: string; modifiedAt: string; description?: string; tags: string[]; } | null>; 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>; 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[]; }>; deleteTrigger(id: string): Promise; executeTrigger(id: string, input?: Record): Promise>; // A2A (kernel-a2a.ts) a2aSend(from: string, to: string, payload: unknown, messageType?: string): Promise; a2aBroadcast(from: string, payload: unknown): Promise; a2aDiscover(capability: string): Promise; role: string; priority: number; }>>; a2aDelegateTask(from: string, to: string, task: string, timeoutMs?: number): Promise; } // === 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(); }