Files
zclaw_openfang/desktop/src-tauri/src/kernel_commands/agent.rs
iven 1c0029001d
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
fix(identity): agent_update 同步写入 soul.md — 跨会话名字记忆
config.name 更新后新会话的 system prompt 看不到名字,因为
pre_conversation_hook 只读 soul.md。现在 agent_update 在 name
变更时同步更新 soul.md(含/替换"你的名字是X"),确保下次
会话的 system prompt 包含身份信息。
2026-04-23 14:17:36 +08:00

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(&current_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())
}