fix: 4 pre-release bug fixes — identity override, model config, agent sync, auto-identity
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

P1: identity.rs get_identity() returns empty soul/instructions for agents
without explicit identity files. This prevents the default ZCLAW personality
from overriding agent_config.system_prompt. New get_identity_or_default()
method added for the DEFAULT agent.

P2: messaging.rs now uses agent_config.model.model when available, falling
back to global Kernel config. This allows per-agent model selection.

P2: agentStore.ts loadClones retries up to 3 times (300ms interval) when
getClient() returns null, handling the coordinator initialization race.

P2: agent_create Tauri command auto-populates identity files (soul +
instructions) from creation parameters, ensuring build_system_prompt()
has content for new agents.

Also fixes conversationStore upsertActiveConversation to persist generated
conversation IDs, preventing duplicate entries on new conversations.
This commit is contained in:
iven
2026-04-08 21:47:46 +08:00
parent 8eeb616f61
commit adcce0d70c
5 changed files with 112 additions and 21 deletions

View File

@@ -41,9 +41,12 @@ impl Kernel {
// Create or get session // Create or get session
let session_id = self.memory.create_session(agent_id).await?; let session_id = self.memory.create_session(agent_id).await?;
// Always use Kernel's current model configuration // Use agent-level model if configured, otherwise fall back to global config
// This ensures user's "模型与 API" settings are respected let model = if !agent_config.model.model.is_empty() {
let model = self.config.model().to_string(); agent_config.model.model.clone()
} else {
self.config.model().to_string()
};
// Create agent loop with model configuration // Create agent loop with model configuration
let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); 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?, None => self.memory.create_session(agent_id).await?,
}; };
// Always use Kernel's current model configuration // Use agent-level model if configured, otherwise fall back to global config
// This ensures user's "模型与 API" settings are respected let model = if !agent_config.model.model.is_empty() {
let model = self.config.model().to_string(); agent_config.model.model.clone()
} else {
self.config.model().to_string()
};
// Create agent loop with model configuration // Create agent loop with model configuration
let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false);

View File

@@ -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 { pub fn get_identity(&mut self, agent_id: &str) -> IdentityFiles {
if let Some(existing) = self.identities.get(agent_id) { if let Some(existing) = self.identities.get(agent_id) {
return existing.clone(); 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 { let defaults = IdentityFiles {
soul: default_soul(), soul: default_soul(),
instructions: default_instructions(), instructions: default_instructions(),
@@ -741,9 +767,20 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_get_identity_creates_default() { fn test_get_identity_returns_empty_for_new_agent() {
let mut manager = AgentIdentityManager::new(); 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.soul.is_empty());
assert!(!identity.instructions.is_empty()); assert!(!identity.instructions.is_empty());
} }
@@ -751,16 +788,18 @@ mod tests {
#[test] #[test]
fn test_update_user_profile() { fn test_update_user_profile() {
let mut manager = AgentIdentityManager::new(); let mut manager = AgentIdentityManager::new();
manager.update_user_profile("test-agent", "New profile content"); let agent_id = format!("test-profile-{}", std::process::id());
let identity = manager.get_identity("test-agent"); manager.update_user_profile(&agent_id, "New profile content");
let identity = manager.get_identity(&agent_id);
assert_eq!(identity.user_profile, "New profile content"); assert_eq!(identity.user_profile, "New profile content");
} }
#[test] #[test]
fn test_proposal_flow() { fn test_proposal_flow() {
let mut manager = AgentIdentityManager::new(); let mut manager = AgentIdentityManager::new();
let agent_id = format!("test-proposal-{}", std::process::id());
let proposal = manager.propose_change( let proposal = manager.propose_change(
"test-agent", &agent_id,
IdentityFile::Soul, IdentityFile::Soul,
"New soul content", "New soul content",
"Test proposal", "Test proposal",
@@ -775,17 +814,18 @@ mod tests {
let result = manager.approve_proposal(&proposal.id); let result = manager.approve_proposal(&proposal.id);
assert!(result.is_ok()); 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"); assert_eq!(identity.soul, "New soul content");
} }
#[test] #[test]
fn test_snapshots() { fn test_snapshots() {
let mut manager = AgentIdentityManager::new(); let mut manager = AgentIdentityManager::new();
manager.update_user_profile("test-agent", "First update"); let agent_id = format!("test-snapshots-{}", std::process::id());
manager.update_user_profile("test-agent", "Second update"); 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); assert!(snapshots.len() >= 2);
} }
} }

View File

@@ -7,6 +7,7 @@ use zclaw_types::{AgentConfig, AgentId, AgentInfo};
use super::{validate_agent_id, KernelState}; use super::{validate_agent_id, KernelState};
use crate::intelligence::validation::validate_string_length; use crate::intelligence::validation::validate_string_length;
use crate::intelligence::identity::IdentityManagerState;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Request / Response types // Request / Response types
@@ -71,6 +72,7 @@ pub struct AgentUpdateRequest {
#[tauri::command] #[tauri::command]
pub async fn agent_create( pub async fn agent_create(
state: State<'_, KernelState>, state: State<'_, KernelState>,
identity_state: State<'_, IdentityManagerState>,
request: CreateAgentRequest, request: CreateAgentRequest,
) -> Result<CreateAgentResponse, String> { ) -> Result<CreateAgentResponse, String> {
// Input validation // Input validation
@@ -90,6 +92,10 @@ pub async fn agent_create(
let kernel = kernel_lock.as_ref() let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; .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) let mut config = AgentConfig::new(&request.name)
.with_description(request.description.unwrap_or_default()) .with_description(request.description.unwrap_or_default())
.with_system_prompt(request.system_prompt.unwrap_or_default()) .with_system_prompt(request.system_prompt.unwrap_or_default())
@@ -114,8 +120,30 @@ pub async fn agent_create(
.await .await
.map_err(|e| format!("Failed to create agent: {}", e))?; .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 { Ok(CreateAgentResponse {
id: id.to_string(), id: agent_id_str,
name: request.name, name: request.name,
state: "running".to_string(), state: "running".to_string(),
}) })

View File

@@ -137,9 +137,18 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
// Actions // Actions
loadClones: async () => { 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) { if (!client) {
log.warn('[AgentStore] Client not initialized, skipping loadClones'); log.warn('[AgentStore] Client not initialized after retries, skipping loadClones');
return; return;
} }

View File

@@ -325,11 +325,19 @@ export const useConversationStore = create<ConversationState>()(
upsertActiveConversation: (currentMessages: ChatMessage[]) => { upsertActiveConversation: (currentMessages: ChatMessage[]) => {
const state = get(); const state = get();
const currentId = state.currentConversationId || null;
const conversations = upsertActiveConversation( const conversations = upsertActiveConversation(
[...state.conversations], currentMessages, state.sessionKey, [...state.conversations], currentMessages, state.sessionKey,
state.currentConversationId, state.currentAgent, state.currentConversationId, state.currentAgent,
); );
// 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 }); set({ conversations });
}
return conversations; return conversations;
}, },