diff --git a/crates/zclaw-kernel/src/kernel/messaging.rs b/crates/zclaw-kernel/src/kernel/messaging.rs index 0962d45..cdfc9ba 100644 --- a/crates/zclaw-kernel/src/kernel/messaging.rs +++ b/crates/zclaw-kernel/src/kernel/messaging.rs @@ -41,9 +41,12 @@ impl Kernel { // Create or get session let session_id = self.memory.create_session(agent_id).await?; - // Always use Kernel's current model configuration - // This ensures user's "模型与 API" settings are respected - let model = self.config.model().to_string(); + // Use agent-level model if configured, otherwise fall back to global config + let model = if !agent_config.model.model.is_empty() { + agent_config.model.model.clone() + } else { + self.config.model().to_string() + }; // Create agent loop with model configuration let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); @@ -147,9 +150,12 @@ impl Kernel { None => self.memory.create_session(agent_id).await?, }; - // Always use Kernel's current model configuration - // This ensures user's "模型与 API" settings are respected - let model = self.config.model().to_string(); + // Use agent-level model if configured, otherwise fall back to global config + let model = if !agent_config.model.model.is_empty() { + agent_config.model.model.clone() + } else { + self.config.model().to_string() + }; // Create agent loop with model configuration let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); diff --git a/desktop/src-tauri/src/intelligence/identity.rs b/desktop/src-tauri/src/intelligence/identity.rs index 28f993f..a556f27 100644 --- a/desktop/src-tauri/src/intelligence/identity.rs +++ b/desktop/src-tauri/src/intelligence/identity.rs @@ -221,13 +221,39 @@ impl AgentIdentityManager { } } - /// Get identity files for an agent (creates default if not exists) + /// Get identity files for an agent. + /// + /// Returns empty soul/instructions for agents without explicit identity files. + /// This ensures `build_system_prompt()` returns an empty string when no + /// identity has been configured, allowing `agent_config.system_prompt` to + /// take precedence via the `system_prompt_override` flow in `messaging.rs`. + /// + /// The default soul/instructions are only used when explicitly requested + /// via `get_identity_or_default()` (e.g., for the DEFAULT agent). pub fn get_identity(&mut self, agent_id: &str) -> IdentityFiles { if let Some(existing) = self.identities.get(agent_id) { return existing.clone(); } - // Initialize with defaults + // Return empty identity — do NOT auto-populate with defaults. + // The identity system should be opt-in: only agents with explicit + // SOUL.md / USER.md files get identity-based prompts. + let empty = IdentityFiles { + soul: String::new(), + instructions: String::new(), + user_profile: default_user_profile(), + heartbeat: None, + }; + empty + } + + /// Get identity files for an agent, falling back to defaults. + /// Used for the DEFAULT ZCLAW agent which should always have a personality. + pub fn get_identity_or_default(&mut self, agent_id: &str) -> IdentityFiles { + if let Some(existing) = self.identities.get(agent_id) { + return existing.clone(); + } + let defaults = IdentityFiles { soul: default_soul(), instructions: default_instructions(), @@ -741,9 +767,20 @@ mod tests { use super::*; #[test] - fn test_get_identity_creates_default() { + fn test_get_identity_returns_empty_for_new_agent() { let mut manager = AgentIdentityManager::new(); - let identity = manager.get_identity("test-agent"); + let agent_id = format!("test-empty-{}", std::process::id()); + let identity = manager.get_identity(&agent_id); + // New agents without explicit identity files should have empty soul/instructions + assert!(identity.soul.is_empty()); + assert!(identity.instructions.is_empty()); + } + + #[test] + fn test_get_identity_or_default_creates_default() { + let mut manager = AgentIdentityManager::new(); + let agent_id = format!("test-default-{}", std::process::id()); + let identity = manager.get_identity_or_default(&agent_id); assert!(!identity.soul.is_empty()); assert!(!identity.instructions.is_empty()); } @@ -751,16 +788,18 @@ mod tests { #[test] fn test_update_user_profile() { let mut manager = AgentIdentityManager::new(); - manager.update_user_profile("test-agent", "New profile content"); - let identity = manager.get_identity("test-agent"); + let agent_id = format!("test-profile-{}", std::process::id()); + manager.update_user_profile(&agent_id, "New profile content"); + let identity = manager.get_identity(&agent_id); assert_eq!(identity.user_profile, "New profile content"); } #[test] fn test_proposal_flow() { let mut manager = AgentIdentityManager::new(); + let agent_id = format!("test-proposal-{}", std::process::id()); let proposal = manager.propose_change( - "test-agent", + &agent_id, IdentityFile::Soul, "New soul content", "Test proposal", @@ -775,17 +814,18 @@ mod tests { let result = manager.approve_proposal(&proposal.id); assert!(result.is_ok()); - let identity = manager.get_identity("test-agent"); + let identity = manager.get_identity(&agent_id); assert_eq!(identity.soul, "New soul content"); } #[test] fn test_snapshots() { let mut manager = AgentIdentityManager::new(); - manager.update_user_profile("test-agent", "First update"); - manager.update_user_profile("test-agent", "Second update"); + let agent_id = format!("test-snapshots-{}", std::process::id()); + manager.update_user_profile(&agent_id, "First update"); + manager.update_user_profile(&agent_id, "Second update"); - let snapshots = manager.get_snapshots("test-agent", 10); + let snapshots = manager.get_snapshots(&agent_id, 10); assert!(snapshots.len() >= 2); } } diff --git a/desktop/src-tauri/src/kernel_commands/agent.rs b/desktop/src-tauri/src/kernel_commands/agent.rs index 5b0fe4c..243cc3a 100644 --- a/desktop/src-tauri/src/kernel_commands/agent.rs +++ b/desktop/src-tauri/src/kernel_commands/agent.rs @@ -7,6 +7,7 @@ use zclaw_types::{AgentConfig, AgentId, AgentInfo}; use super::{validate_agent_id, KernelState}; use crate::intelligence::validation::validate_string_length; +use crate::intelligence::identity::IdentityManagerState; // --------------------------------------------------------------------------- // Request / Response types @@ -71,6 +72,7 @@ pub struct AgentUpdateRequest { #[tauri::command] pub async fn agent_create( state: State<'_, KernelState>, + identity_state: State<'_, IdentityManagerState>, request: CreateAgentRequest, ) -> Result { // Input validation @@ -90,6 +92,10 @@ pub async fn agent_create( 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()) @@ -114,8 +120,30 @@ pub async fn agent_create( .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: id.to_string(), + id: agent_id_str, name: request.name, state: "running".to_string(), }) diff --git a/desktop/src/store/agentStore.ts b/desktop/src/store/agentStore.ts index f5c5947..9f59055 100644 --- a/desktop/src/store/agentStore.ts +++ b/desktop/src/store/agentStore.ts @@ -137,9 +137,18 @@ export const useAgentStore = create((set, get) => ({ // Actions loadClones: async () => { - const client = getClient(); + let client = getClient(); + + // Retry up to 3 times with short delay if client isn't ready yet. + // This handles the race where connected fires before coordinator + // injects the client into the store. + for (let attempt = 0; attempt < 3 && !client; attempt++) { + await new Promise((r) => setTimeout(r, 300)); + client = getClient(); + } + if (!client) { - log.warn('[AgentStore] Client not initialized, skipping loadClones'); + log.warn('[AgentStore] Client not initialized after retries, skipping loadClones'); return; } diff --git a/desktop/src/store/chat/conversationStore.ts b/desktop/src/store/chat/conversationStore.ts index c7e4faf..26338ca 100644 --- a/desktop/src/store/chat/conversationStore.ts +++ b/desktop/src/store/chat/conversationStore.ts @@ -325,11 +325,19 @@ export const useConversationStore = create()( upsertActiveConversation: (currentMessages: ChatMessage[]) => { const state = get(); + const currentId = state.currentConversationId || null; const conversations = upsertActiveConversation( [...state.conversations], currentMessages, state.sessionKey, state.currentConversationId, state.currentAgent, ); - set({ conversations }); + // If this was a new conversation (no prior currentConversationId), + // persist the generated ID so subsequent upserts update in-place + // instead of creating duplicate entries. + if (!currentId && conversations.length > 0) { + set({ conversations, currentConversationId: conversations[0].id }); + } else { + set({ conversations }); + } return conversations; },