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
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:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user