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
config.name 更新后新会话的 system prompt 看不到名字,因为 pre_conversation_hook 只读 soul.md。现在 agent_update 在 name 变更时同步更新 soul.md(含/替换"你的名字是X"),确保下次 会话的 system prompt 包含身份信息。
363 lines
12 KiB
Rust
363 lines
12 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;
|
|
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<String>,
|
|
#[serde(default)]
|
|
pub system_prompt: Option<String>,
|
|
#[serde(default)]
|
|
pub soul: 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
|
|
// @reserved: agent CRUD management
|
|
// @connected
|
|
#[tauri::command]
|
|
pub async fn agent_create(
|
|
state: State<'_, KernelState>,
|
|
identity_state: State<'_, IdentityManagerState>,
|
|
request: CreateAgentRequest,
|
|
) -> Result<CreateAgentResponse, String> {
|
|
// 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<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 (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<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())?;
|
|
|
|
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<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 {
|
|
// 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<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))
|
|
}
|
|
|
|
/// @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<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))?;
|
|
|
|
// 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())
|
|
}
|