Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Split monolithic chatStore.ts (908 lines) into 4 focused stores: - chatStore.ts: facade layer, owns messages[], backward-compatible selectors - conversationStore.ts: conversation CRUD, agent switching, IndexedDB persistence - streamStore.ts: streaming orchestration, chat mode, suggestions - messageStore.ts: token tracking Key fixes from 3-round deep audit: - C1: Fix Rust serde camelCase vs TS snake_case mismatch (toolStart/toolEnd/iterationStart) - C2: Fix IDB async rehydration race with persist.hasHydrated() subscribe - C3: Add sessionKey to partialize to survive page refresh - H3: Fix IDB migration retry on failure (don't set migrated=true in catch) - M3: Fix ToolCallStep deduplication (toolStart creates, toolEnd updates) - M-NEW-2: Clear sessionKey on cancelStream Also adds: - Rust backend stream cancellation via AtomicBool + cancel_stream command - IndexedDB storage adapter with one-time localStorage migration - HMR cleanup for cross-store subscriptions
265 lines
7.9 KiB
Rust
265 lines
7.9 KiB
Rust
//! 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;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<String>,
|
|
#[serde(default)]
|
|
pub system_prompt: Option<String>,
|
|
#[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<PathBuf>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
pub description: Option<String>,
|
|
pub system_prompt: Option<String>,
|
|
pub model: Option<String>,
|
|
pub provider: Option<String>,
|
|
pub max_tokens: Option<u32>,
|
|
pub temperature: Option<f32>,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Commands
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Create a new agent
|
|
// @connected
|
|
#[tauri::command]
|
|
pub async fn agent_create(
|
|
state: State<'_, KernelState>,
|
|
request: CreateAgentRequest,
|
|
) -> Result<CreateAgentResponse, 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())?;
|
|
|
|
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(workspace) = request.workspace {
|
|
config.workspace = Some(workspace);
|
|
}
|
|
|
|
let id = kernel.spawn_agent(config)
|
|
.await
|
|
.map_err(|e| format!("Failed to create agent: {}", e))?;
|
|
|
|
Ok(CreateAgentResponse {
|
|
id: id.to_string(),
|
|
name: request.name,
|
|
state: "running".to_string(),
|
|
})
|
|
}
|
|
|
|
/// List all agents
|
|
// @connected
|
|
#[tauri::command]
|
|
pub async fn agent_list(
|
|
state: State<'_, KernelState>,
|
|
) -> Result<Vec<AgentInfo>, 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
|
|
// @connected
|
|
#[tauri::command]
|
|
pub async fn agent_get(
|
|
state: State<'_, KernelState>,
|
|
agent_id: String,
|
|
) -> Result<Option<AgentInfo>, 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())?;
|
|
|
|
Ok(kernel.get_agent(&id))
|
|
}
|
|
|
|
/// 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>,
|
|
agent_id: String,
|
|
updates: AgentUpdateRequest,
|
|
) -> Result<AgentInfo, 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())?;
|
|
|
|
// 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 {
|
|
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))
|
|
}
|
|
|
|
/// Export an agent configuration as JSON
|
|
// @reserved: 暂无前端集成
|
|
#[tauri::command]
|
|
pub async fn agent_export(
|
|
state: State<'_, KernelState>,
|
|
agent_id: String,
|
|
) -> Result<String, 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 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))
|
|
}
|
|
|
|
/// Import an agent from JSON configuration
|
|
// @reserved: 暂无前端集成
|
|
#[tauri::command]
|
|
pub async fn agent_import(
|
|
state: State<'_, KernelState>,
|
|
config_json: String,
|
|
) -> Result<AgentInfo, String> {
|
|
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))?;
|
|
|
|
// 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())
|
|
}
|