//! Agent CRUD commands: create, list, get, delete, update, export, import use std::path::PathBuf; use serde::{Deserialize, Serialize}; use tauri::State; use zclaw_types::{AgentConfig, AgentId, AgentInfo}; use super::{validate_agent_id, KernelState}; use crate::intelligence::validation::validate_string_length; use crate::intelligence::identity::{IdentityFile, IdentityManagerState}; // --------------------------------------------------------------------------- // Request / Response types // --------------------------------------------------------------------------- fn default_provider() -> String { "openai".to_string() } fn default_model() -> String { "gpt-4o-mini".to_string() } fn default_max_tokens() -> u32 { 4096 } fn default_temperature() -> f32 { 0.7 } /// Agent creation request #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAgentRequest { pub name: String, #[serde(default)] pub description: Option, #[serde(default)] pub system_prompt: Option, #[serde(default)] pub soul: Option, #[serde(default = "default_provider")] pub provider: String, #[serde(default = "default_model")] pub model: String, #[serde(default = "default_max_tokens")] pub max_tokens: u32, #[serde(default = "default_temperature")] pub temperature: f32, #[serde(default)] pub workspace: Option, } /// Agent creation response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAgentResponse { pub id: String, pub name: String, pub state: String, } /// Agent update request #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentUpdateRequest { pub name: Option, pub description: Option, pub system_prompt: Option, pub model: Option, pub provider: Option, pub max_tokens: Option, pub temperature: Option, } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- /// Create a new agent // @reserved: agent CRUD management // @connected #[tauri::command] pub async fn agent_create( state: State<'_, KernelState>, identity_state: State<'_, IdentityManagerState>, request: CreateAgentRequest, ) -> Result { // Input validation let name_trimmed = request.name.trim(); if name_trimmed.is_empty() { return Err("Agent name cannot be empty".to_string()); } if request.temperature < 0.0 || request.temperature > 2.0 { return Err(format!("Temperature must be between 0 and 2, got {}", request.temperature)); } if request.max_tokens == 0 { return Err("max_tokens must be greater than 0".to_string()); } let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; // Capture identity-relevant fields before moving request let soul_content = request.soul.clone(); let system_prompt_content = request.system_prompt.clone(); let mut config = AgentConfig::new(&request.name) .with_description(request.description.unwrap_or_default()) .with_system_prompt(request.system_prompt.unwrap_or_default()) .with_model(zclaw_types::ModelConfig { provider: request.provider, model: request.model, api_key_env: None, base_url: None, }) .with_max_tokens(request.max_tokens) .with_temperature(request.temperature); if let Some(soul) = request.soul { config = config.with_soul(soul); } if let Some(workspace) = request.workspace { config.workspace = Some(workspace); } let id = kernel.spawn_agent(config) .await .map_err(|e| format!("Failed to create agent: {}", e))?; let agent_id_str = id.to_string(); // Auto-populate identity files from creation parameters. // This ensures the identity system has content for `build_system_prompt()`. { let mut identity_mgr = identity_state.lock().await; if let Some(soul) = soul_content { if !soul.is_empty() { if let Err(e) = identity_mgr.update_file(&agent_id_str, "soul", &soul) { tracing::warn!("[agent_create] Failed to write soul to identity: {}", e); } } } if let Some(prompt) = system_prompt_content { if !prompt.is_empty() { if let Err(e) = identity_mgr.update_file(&agent_id_str, "instructions", &prompt) { tracing::warn!("[agent_create] Failed to write instructions to identity: {}", e); } } } } Ok(CreateAgentResponse { id: agent_id_str, name: request.name, state: "running".to_string(), }) } /// List all agents // @reserved: agent CRUD management // @connected #[tauri::command] pub async fn agent_list( state: State<'_, KernelState>, ) -> Result, String> { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; Ok(kernel.list_agents()) } /// Get agent info (with optional UserProfile from memory store) // @reserved: agent CRUD management // @connected #[tauri::command] pub async fn agent_get( state: State<'_, KernelState>, agent_id: String, ) -> Result, String> { let agent_id = validate_agent_id(&agent_id)?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let id: AgentId = agent_id.parse() .map_err(|_| "Invalid agent ID format".to_string())?; let mut info = kernel.get_agent(&id); // Extend with UserProfile if available (reads from same MemoryStore pool as middleware writes to) if let Some(ref mut agent_info) = info { let memory_store = kernel.memory(); let profile_store = zclaw_memory::UserProfileStore::new(memory_store.pool()); match profile_store.get(&agent_id).await { Ok(Some(profile)) => { match serde_json::to_value(&profile) { Ok(val) => agent_info.user_profile = Some(val), Err(e) => tracing::warn!("[agent_get] Failed to serialize UserProfile: {}", e), } } Ok(None) => { tracing::debug!("[agent_get] No UserProfile found for agent {}", agent_id); } Err(e) => { tracing::warn!("[agent_get] Failed to read UserProfile: {}", e); } } } Ok(info) } /// Delete an agent // @connected #[tauri::command] pub async fn agent_delete( state: State<'_, KernelState>, agent_id: String, ) -> Result<(), String> { let agent_id = validate_agent_id(&agent_id)?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let id: AgentId = agent_id.parse() .map_err(|_| "Invalid agent ID format".to_string())?; kernel.kill_agent(&id) .await .map_err(|e| format!("Failed to delete agent: {}", e)) } /// Update an agent's configuration // @connected #[tauri::command] pub async fn agent_update( state: State<'_, KernelState>, identity_state: State<'_, IdentityManagerState>, agent_id: String, updates: AgentUpdateRequest, ) -> Result { let agent_id = validate_agent_id(&agent_id)?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let id: AgentId = agent_id.parse() .map_err(|_| "Invalid agent ID format".to_string())?; // Get existing config let mut config = kernel.get_agent_config(&id) .ok_or_else(|| format!("Agent not found: {}", agent_id))?; // Apply updates if let Some(name) = updates.name { // Sync name to identity soul so next session's system prompt includes it let mut identity_mgr = identity_state.lock().await; let current_soul = identity_mgr.get_file(&agent_id, IdentityFile::Soul); let updated_soul = if current_soul.is_empty() { format!("# ZCLAW 人格\n\n你的名字是{}。\n\n你是一个成长性的中文 AI 助手。", name) } else if current_soul.contains("你的名字是") { let re = regex::Regex::new(r"你的名字是[^\n]+").unwrap(); re.replace(¤t_soul, format!("你的名字是{}", name)).to_string() } else { format!("你的名字是{}。\n\n{}", name, current_soul) }; let _ = identity_mgr.update_file(&agent_id, "soul", &updated_soul); drop(identity_mgr); config.name = name; } if let Some(description) = updates.description { config.description = Some(description); } if let Some(system_prompt) = updates.system_prompt { config.system_prompt = Some(system_prompt); } if let Some(model) = updates.model { config.model.model = model; } if let Some(provider) = updates.provider { config.model.provider = provider; } if let Some(max_tokens) = updates.max_tokens { config.max_tokens = Some(max_tokens); } if let Some(temperature) = updates.temperature { config.temperature = Some(temperature); } // Save updated config kernel.update_agent(config) .await .map_err(|e| format!("Failed to update agent: {}", e))?; // Return updated info kernel.get_agent(&id) .ok_or_else(|| format!("Agent not found after update: {}", agent_id)) } /// @reserved — no frontend UI yet /// Export an agent configuration as JSON #[tauri::command] pub async fn agent_export( state: State<'_, KernelState>, agent_id: String, ) -> Result { let agent_id = validate_agent_id(&agent_id)?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let id: AgentId = agent_id.parse() .map_err(|_| "Invalid agent ID format".to_string())?; let config = kernel.get_agent_config(&id) .ok_or_else(|| format!("Agent not found: {}", agent_id))?; serde_json::to_string_pretty(&config) .map_err(|e| format!("Failed to serialize agent config: {}", e)) } /// @reserved — no frontend UI yet /// Import an agent from JSON configuration #[tauri::command] pub async fn agent_import( state: State<'_, KernelState>, config_json: String, ) -> Result { validate_string_length(&config_json, "config_json", 1_000_000) .map_err(|e| format!("{}", e))?; let mut config: AgentConfig = serde_json::from_str(&config_json) .map_err(|e| format!("Invalid agent config JSON: {}", e))?; // Validate system_prompt length to prevent excessive token consumption const MAX_SYSTEM_PROMPT_LEN: usize = 50_000; if let Some(ref prompt) = config.system_prompt { if prompt.len() > MAX_SYSTEM_PROMPT_LEN { return Err(format!( "system_prompt too long: {} chars (max {})", prompt.len(), MAX_SYSTEM_PROMPT_LEN )); } } // Regenerate ID to avoid collisions config.id = AgentId::new(); let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let new_id = kernel.spawn_agent(config).await .map_err(|e| format!("Failed to import agent: {}", e))?; kernel.get_agent(&new_id) .ok_or_else(|| "Agent was created but could not be retrieved".to_string()) }