//! Reflection Engine - Agent self-improvement through conversation analysis //! //! Periodically analyzes recent conversations to: //! - Identify behavioral patterns (positive and negative) //! - Generate improvement suggestions //! - Propose identity file changes (with user approval) //! - Create meta-memories about agent performance //! //! Phase 3 of Intelligence Layer Migration. //! Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.2 //! //! NOTE: Some methods are reserved for future self-improvement features. // NOTE: #[tauri::command] functions are registered via invoke_handler! at runtime, // which the Rust compiler does not track as "use". Module-level allow required // for Tauri-commanded functions. Genuinely unused methods annotated individually. #![allow(dead_code)] use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; // Re-export from zclaw-runtime for LLM integration use zclaw_runtime::driver::{CompletionRequest, ContentBlock, LlmDriver}; // === Types === /// Reflection configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReflectionConfig { #[serde(default = "default_trigger_conversations")] pub trigger_after_conversations: usize, #[serde(default = "default_trigger_hours")] pub trigger_after_hours: u64, #[serde(default)] pub allow_soul_modification: bool, #[serde(default = "default_require_approval")] pub require_approval: bool, #[serde(default = "default_use_llm")] pub use_llm: bool, #[serde(default = "default_llm_fallback")] pub llm_fallback_to_rules: bool, } fn default_trigger_conversations() -> usize { 5 } fn default_trigger_hours() -> u64 { 24 } fn default_require_approval() -> bool { true } fn default_use_llm() -> bool { true } fn default_llm_fallback() -> bool { true } impl Default for ReflectionConfig { fn default() -> Self { Self { trigger_after_conversations: 5, trigger_after_hours: 24, allow_soul_modification: true, // Allow soul modification by default for self-evolution require_approval: true, use_llm: true, llm_fallback_to_rules: true, } } } /// Observed pattern from analysis #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PatternObservation { pub observation: String, pub frequency: usize, pub sentiment: Sentiment, pub evidence: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Sentiment { Positive, Negative, Neutral, } /// Improvement suggestion #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImprovementSuggestion { pub area: String, pub suggestion: String, pub priority: Priority, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Priority { High, Medium, Low, } /// Identity change proposal #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdentityChangeProposal { pub agent_id: String, pub field: String, pub current_value: String, pub proposed_value: String, pub reason: String, } /// Result of reflection #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReflectionResult { pub patterns: Vec, pub improvements: Vec, pub identity_proposals: Vec, pub new_memories: usize, pub timestamp: String, /// P2-07: Whether rules-based fallback was used instead of LLM #[serde(default)] pub used_fallback: bool, } /// Reflection state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReflectionState { pub conversations_since_reflection: usize, pub last_reflection_time: Option, pub last_reflection_agent_id: Option, } impl Default for ReflectionState { fn default() -> Self { Self { conversations_since_reflection: 0, last_reflection_time: None, last_reflection_agent_id: None, } } } // === Memory Entry (simplified for analysis) === #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MemoryEntryForAnalysis { pub memory_type: String, pub content: String, pub importance: usize, pub access_count: usize, pub tags: Vec, } // === Reflection Engine === pub struct ReflectionEngine { config: ReflectionConfig, state: ReflectionState, history: Vec, } impl ReflectionEngine { pub fn new(config: Option) -> Self { Self { config: config.unwrap_or_default(), state: ReflectionState::default(), history: Vec::new(), } } /// Record that a conversation happened pub fn record_conversation(&mut self) { self.state.conversations_since_reflection += 1; } /// Check if it's time for reflection pub fn should_reflect(&self) -> bool { // Conversation count trigger if self.state.conversations_since_reflection >= self.config.trigger_after_conversations { return true; } // Time-based trigger if let Some(last_time) = &self.state.last_reflection_time { if let Ok(last) = DateTime::parse_from_rfc3339(last_time) { let elapsed = Utc::now().signed_duration_since(last); let hours_since = elapsed.num_hours() as u64; if hours_since >= self.config.trigger_after_hours { return true; } } } else { // Never reflected before, trigger after initial conversations return self.state.conversations_since_reflection >= 3; } false } /// Execute reflection cycle pub async fn reflect( &mut self, agent_id: &str, memories: &[MemoryEntryForAnalysis], driver: Option>, ) -> ReflectionResult { // P2-07: Track whether rules-based fallback was used let mut used_fallback = !self.config.use_llm; // 1. Analyze memory patterns (LLM if configured, rules fallback) let patterns = if self.config.use_llm { if let Some(ref llm) = driver { match self.analyze_patterns_with_llm(memories, llm).await { Ok(p) => p, Err(e) => { tracing::warn!("[reflection] LLM analysis failed, falling back to rules: {}", e); used_fallback = true; if self.config.llm_fallback_to_rules { self.analyze_patterns(memories) } else { Vec::new() } } } } else { tracing::debug!("[reflection] use_llm=true but no driver available, using rules"); used_fallback = true; self.analyze_patterns(memories) } } else { self.analyze_patterns(memories) }; // 2. Generate improvement suggestions let improvements = self.generate_improvements(&patterns, memories); // 3. Propose identity changes if patterns warrant it let identity_proposals: Vec = if self.config.allow_soul_modification { self.propose_identity_changes(agent_id, &patterns) } else { vec![] }; // 4. Count new memories (would be saved) // Include LLM-generated patterns and high-priority improvements let new_memories = patterns.iter() .filter(|p| p.frequency >= 1 || p.frequency >= 2) .count() + improvements.iter() .filter(|i| matches!(i.priority, Priority::High)) .count(); // Include all LLM-proposed improvements // 5. Build result let result = ReflectionResult { patterns, improvements, identity_proposals, new_memories, timestamp: Utc::now().to_rfc3339(), used_fallback, // P2-07: expose fallback status to callers }; // 6. Update state self.state.conversations_since_reflection = 0; self.state.last_reflection_time = Some(result.timestamp.clone()); self.state.last_reflection_agent_id = Some(agent_id.to_string()); // 7. Store in history self.history.push(result.clone()); if self.history.len() > 20 { self.history = self.history.split_off(10); } // 8. Persist result, state, and history to VikingStorage (fire-and-forget) let state_to_persist = self.state.clone(); let result_to_persist = result.clone(); let agent_id_owned = agent_id.to_string(); tokio::spawn(async move { if let Ok(storage) = crate::viking_commands::get_storage().await { // Persist state as JSON string let state_key = format!("reflection:state:{}", agent_id_owned); if let Ok(state_json) = serde_json::to_string(&state_to_persist) { if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json( &*storage, &state_key, &state_json, ).await { tracing::warn!("[reflection] Failed to persist state: {}", e); } } // Persist latest result as JSON string let result_key = format!("reflection:latest:{}", agent_id_owned); if let Ok(result_json) = serde_json::to_string(&result_to_persist) { if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json( &*storage, &result_key, &result_json, ).await { tracing::warn!("[reflection] Failed to persist result: {}", e); } } // Persist full history array (append new result) let history_key = format!("reflection:history:{}", agent_id_owned); let mut history: Vec = match zclaw_growth::VikingStorage::get_metadata_json( &*storage, &history_key, ).await { Ok(Some(json)) => serde_json::from_str(&json).unwrap_or_default(), _ => Vec::new(), }; history.push(result_to_persist); // Keep last 20 entries if history.len() > 20 { history = history.split_off(history.len() - 20); } if let Ok(history_json) = serde_json::to_string(&history) { if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json( &*storage, &history_key, &history_json, ).await { tracing::warn!("[reflection] Failed to persist history: {}", e); } } } }); result } /// Analyze patterns using LLM for deeper behavioral insights async fn analyze_patterns_with_llm( &self, memories: &[MemoryEntryForAnalysis], driver: &Arc, ) -> Result, String> { if memories.is_empty() { return Ok(Vec::new()); } // Build memory summary for the prompt let memory_summary: String = memories.iter().enumerate().map(|(i, m)| { format!("{}. [{}] (重要性:{}, 访问:{}) {}", i + 1, m.memory_type, m.importance, m.access_count, m.content) }).collect::>().join("\n"); let system_prompt = r#"你是行为分析专家。分析以下 Agent 记忆条目,识别行为模式和趋势。 请返回 JSON 数组,每个元素包含: - "observation": string — 模式描述(中文) - "frequency": number — 该模式出现的频率估计(1-10) - "sentiment": "positive" | "negative" | "neutral" — 情感倾向 - "evidence": string[] — 支持该观察的证据(记忆内容摘要,最多3条) 只返回 JSON 数组,不要其他内容。如果没有明显模式,返回空数组。"# .to_string(); let request = CompletionRequest { model: driver.provider().to_string(), system: Some(system_prompt), messages: vec![zclaw_types::Message::assistant( format!("分析以下记忆条目:\n\n{}", memory_summary) )], max_tokens: Some(2048), temperature: Some(0.3), stream: false, ..Default::default() }; let response = driver.complete(request).await .map_err(|e| format!("LLM 调用失败: {}", e))?; // Extract text from response let text = response.content.iter() .filter_map(|block| match block { ContentBlock::Text { text } => Some(text.as_str()), _ => None, }) .collect::>() .join(""); // Parse JSON response (handle markdown code blocks) let json_str = extract_json_from_llm_response(&text); serde_json::from_str::>(&json_str) .map_err(|e| format!("解析 LLM 响应失败: {} — 原始响应: {}", e, &text[..text.len().min(200)])) } /// Analyze patterns in memories (rule-based fallback) fn analyze_patterns(&self, memories: &[MemoryEntryForAnalysis]) -> Vec { let mut patterns = Vec::new(); // Analyze memory type distribution let mut type_counts: HashMap = HashMap::new(); for m in memories { *type_counts.entry(m.memory_type.clone()).or_insert(0) += 1; } // Pattern: Too many tasks accumulating let task_count = *type_counts.get("task").unwrap_or(&0); if task_count >= 3 { let evidence: Vec = memories .iter() .filter(|m| m.memory_type == "task") .take(3) .map(|m| m.content.clone()) .collect(); patterns.push(PatternObservation { observation: format!("积累了 {} 个待办任务,可能存在任务管理不善", task_count), frequency: task_count, sentiment: Sentiment::Negative, evidence, }); } // Pattern: Strong preference accumulation let pref_count = *type_counts.get("preference").unwrap_or(&0); if pref_count >= 3 { let evidence: Vec = memories .iter() .filter(|m| m.memory_type == "preference") .take(3) .map(|m| m.content.clone()) .collect(); patterns.push(PatternObservation { observation: format!("已记录 {} 个用户偏好,对用户习惯有较好理解", pref_count), frequency: pref_count, sentiment: Sentiment::Positive, evidence, }); } // Pattern: Many lessons learned let lesson_count = *type_counts.get("lesson").unwrap_or(&0); if lesson_count >= 3 { let evidence: Vec = memories .iter() .filter(|m| m.memory_type == "lesson") .take(3) .map(|m| m.content.clone()) .collect(); patterns.push(PatternObservation { observation: format!("积累了 {} 条经验教训,知识库在成长", lesson_count), frequency: lesson_count, sentiment: Sentiment::Positive, evidence, }); } // Pattern: High-importance items being accessed frequently let high_access: Vec<_> = memories .iter() .filter(|m| m.access_count >= 5 && m.importance >= 7) .collect(); if high_access.len() >= 2 { let evidence: Vec = high_access.iter().take(3).map(|m| m.content.clone()).collect(); patterns.push(PatternObservation { observation: format!("有 {} 条高频访问的重要记忆,核心知识正在形成", high_access.len()), frequency: high_access.len(), sentiment: Sentiment::Positive, evidence, }); } // Pattern: Low-importance memories accumulating let low_importance_count = memories.iter().filter(|m| m.importance <= 3).count(); if low_importance_count > 15 { patterns.push(PatternObservation { observation: format!("有 {} 条低重要性记忆,可考虑清理", low_importance_count), frequency: low_importance_count, sentiment: Sentiment::Neutral, evidence: vec![], }); } // Pattern: Tag analysis - recurring topics let mut tag_counts: HashMap = HashMap::new(); for m in memories { for tag in &m.tags { if tag != "auto-extracted" { *tag_counts.entry(tag.clone()).or_insert(0) += 1; } } } let mut frequent_tags: Vec<_> = tag_counts .iter() .filter(|(_, count)| **count >= 5) .map(|(tag, count)| (tag.clone(), *count)) .collect(); frequent_tags.sort_by(|a, b| b.1.cmp(&a.1)); if !frequent_tags.is_empty() { let tag_str: Vec = frequent_tags .iter() .take(5) .map(|(tag, count)| format!("{}({}次)", tag, count)) .collect(); patterns.push(PatternObservation { observation: format!("反复出现的主题: {}", tag_str.join(", ")), frequency: frequent_tags[0].1, sentiment: Sentiment::Neutral, evidence: frequent_tags.iter().take(5).map(|(t, _)| t.clone()).collect(), }); } patterns } /// Generate improvement suggestions fn generate_improvements( &self, patterns: &[PatternObservation], memories: &[MemoryEntryForAnalysis], ) -> Vec { let mut improvements = Vec::new(); // Suggestion: Clear pending tasks if patterns.iter().any(|p| p.observation.contains("待办任务")) { improvements.push(ImprovementSuggestion { area: "任务管理".to_string(), suggestion: "清理已完成的任务记忆,对长期未处理的任务降低重要性或标记为已取消".to_string(), priority: Priority::High, }); } // Suggestion: Prune low-importance memories if patterns.iter().any(|p| p.observation.contains("低重要性")) { improvements.push(ImprovementSuggestion { area: "记忆管理".to_string(), suggestion: "执行记忆清理,移除30天以上未访问且重要性低于3的记忆".to_string(), priority: Priority::Medium, }); } // Suggestion: User profile enrichment let pref_count = memories.iter().filter(|m| m.memory_type == "preference").count(); if pref_count < 3 { improvements.push(ImprovementSuggestion { area: "用户理解".to_string(), suggestion: "主动在对话中了解用户偏好(沟通风格、技术栈、工作习惯),丰富用户画像".to_string(), priority: Priority::Medium, }); } // Suggestion: Knowledge consolidation let fact_count = memories.iter().filter(|m| m.memory_type == "fact").count(); if fact_count > 20 { improvements.push(ImprovementSuggestion { area: "知识整合".to_string(), suggestion: "合并相似的事实记忆,提高检索效率。可将相关事实整合为结构化的项目/用户档案".to_string(), priority: Priority::Low, }); } improvements } /// Propose identity changes based on patterns fn propose_identity_changes( &self, agent_id: &str, patterns: &[PatternObservation], ) -> Vec { let mut proposals = Vec::new(); // If many negative patterns, propose instruction update let negative_patterns: Vec<_> = patterns .iter() .filter(|p| matches!(p.sentiment, Sentiment::Negative)) .collect(); if negative_patterns.len() >= 2 { let additions: Vec = negative_patterns .iter() .map(|p| format!("- 注意: {}", p.observation)) .collect(); proposals.push(IdentityChangeProposal { agent_id: agent_id.to_string(), field: "instructions".to_string(), current_value: "...".to_string(), proposed_value: format!("\n\n## 自我反思改进\n{}", additions.join("\n")), reason: format!( "基于 {} 个负面模式观察,建议在指令中增加自我改进提醒", negative_patterns.len() ), }); } proposals } /// Get reflection history pub fn get_history(&self, limit: usize) -> Vec<&ReflectionResult> { self.history.iter().rev().take(limit).collect() } /// Get current state pub fn get_state(&self) -> &ReflectionState { &self.state } /// Get configuration pub fn get_config(&self) -> &ReflectionConfig { &self.config } /// Update configuration pub fn update_config(&mut self, config: ReflectionConfig) { self.config = config; } /// Restore state from VikingStorage metadata (called during init) /// /// Spawns an async task to read persisted state and result from VikingStorage. /// Results are placed in global caches, consumed one-shot by intelligence_hooks. pub fn restore_state(&self, agent_id: &str) { let rt = tokio::runtime::Handle::current(); let state_key = format!("reflection:state:{}", agent_id); let result_key = format!("reflection:latest:{}", agent_id); let agent_id_owned = agent_id.to_string(); rt.spawn(async move { match crate::viking_commands::get_storage().await { Ok(storage) => { // Restore state match zclaw_growth::VikingStorage::get_metadata_json( &*storage, &state_key, ).await { Ok(Some(state_json)) => { if let Ok(persisted_state) = serde_json::from_str::(&state_json) { tracing::info!( "[reflection] Restored state for {}: {} conversations since last reflection", agent_id_owned, persisted_state.conversations_since_reflection ); let cache = get_state_cache(); if let Ok(mut cache) = cache.write() { cache.insert(agent_id_owned.clone(), persisted_state); } } } Ok(None) => { tracing::debug!("[reflection] No persisted state for {}", agent_id_owned); } Err(e) => { tracing::warn!("[reflection] Failed to read state: {}", e); } } // Restore latest result into history match zclaw_growth::VikingStorage::get_metadata_json( &*storage, &result_key, ).await { Ok(Some(result_json)) => { if let Ok(persisted_result) = serde_json::from_str::(&result_json) { let cache = get_result_cache(); if let Ok(mut cache) = cache.write() { cache.insert(agent_id_owned.clone(), persisted_result); } } } Ok(None) => {} Err(e) => { tracing::warn!("[reflection] Failed to read result: {}", e); } } } Err(e) => { tracing::warn!("[reflection] Storage unavailable during restore: {}", e); } } }); } /// Apply a restored state (called from intelligence_hooks after restore completes) pub fn apply_restored_state(&mut self, state: ReflectionState) { self.state = state; } /// Apply a restored latest result to history pub fn apply_restored_result(&mut self, result: ReflectionResult) { self.history.push(result); } } // === State Restoration Cache === use std::sync::RwLock as StdRwLock; use std::sync::OnceLock as StdOnceLock; /// Temporary cache for restored reflection state (bridges async init ↔ sync apply) static REFLECTION_STATE_CACHE: StdOnceLock>> = StdOnceLock::new(); /// Temporary cache for restored reflection result static REFLECTION_RESULT_CACHE: StdOnceLock>> = StdOnceLock::new(); fn get_state_cache() -> &'static StdRwLock> { REFLECTION_STATE_CACHE.get_or_init(|| StdRwLock::new(HashMap::new())) } fn get_result_cache() -> &'static StdRwLock> { REFLECTION_RESULT_CACHE.get_or_init(|| StdRwLock::new(HashMap::new())) } /// Pop restored state from cache (one-shot, removes after read) pub fn pop_restored_state(agent_id: &str) -> Option { let cache = get_state_cache(); if let Ok(mut cache) = cache.write() { cache.remove(agent_id) } else { None } } /// Pop restored result from cache (one-shot, removes after read) pub fn pop_restored_result(agent_id: &str) -> Option { let cache = get_result_cache(); if let Ok(mut cache) = cache.write() { cache.remove(agent_id) } else { None } } /// Peek restored state from cache (non-destructive read) pub fn peek_restored_state(agent_id: &str) -> Option { let cache = get_state_cache(); cache.read().ok()?.get(agent_id).cloned() } /// Peek restored result from cache (non-destructive read) pub fn peek_restored_result(agent_id: &str) -> Option { let cache = get_result_cache(); cache.read().ok()?.get(agent_id).cloned() } // === Tauri Commands === use tokio::sync::Mutex; pub type ReflectionEngineState = Arc>; /// Initialize reflection engine with config /// Updates the shared state with new configuration // @connected #[tauri::command] pub async fn reflection_init( config: Option, state: tauri::State<'_, ReflectionEngineState>, ) -> Result { let mut engine = state.lock().await; if let Some(cfg) = config { engine.update_config(cfg); } Ok(true) } /// Record a conversation // @connected #[tauri::command] pub async fn reflection_record_conversation( state: tauri::State<'_, ReflectionEngineState>, ) -> Result<(), String> { let mut engine = state.lock().await; engine.record_conversation(); Ok(()) } /// Check if reflection should run // @connected #[tauri::command] pub async fn reflection_should_reflect( state: tauri::State<'_, ReflectionEngineState>, ) -> Result { let engine = state.lock().await; Ok(engine.should_reflect()) } /// Execute reflection // @connected #[tauri::command] pub async fn reflection_reflect( agent_id: String, memories: Vec, state: tauri::State<'_, ReflectionEngineState>, kernel_state: tauri::State<'_, crate::kernel_commands::KernelState>, ) -> Result { let driver = { let kernel_lock = kernel_state.lock().await; kernel_lock.as_ref().map(|k| k.driver()) }; let mut engine = state.lock().await; Ok(engine.reflect(&agent_id, &memories, driver).await) } /// Get reflection history /// /// Returns in-memory history first. If empty and an agent_id is provided, /// falls back to the persisted history array from VikingStorage metadata, /// then to the single latest result for backward compatibility. // @connected #[tauri::command] pub async fn reflection_get_history( limit: Option, agent_id: Option, state: tauri::State<'_, ReflectionEngineState>, ) -> Result, String> { let limit = limit.unwrap_or(10); let engine = state.lock().await; let mut results: Vec = engine.get_history(limit) .into_iter() .cloned() .collect(); // If no in-memory results and we have an agent_id, load persisted history if results.is_empty() { if let Some(ref aid) = agent_id { if let Ok(storage) = crate::viking_commands::get_storage().await { let history_key = format!("reflection:history:{}", aid); match zclaw_growth::VikingStorage::get_metadata_json(&*storage, &history_key).await { Ok(Some(json)) => { if let Ok(mut persisted) = serde_json::from_str::>(&json) { persisted.reverse(); persisted.truncate(limit); results = persisted; } } Ok(None) => { // Fallback: try loading single latest result (pre-history format) let latest_key = format!("reflection:latest:{}", aid); if let Ok(Some(json)) = zclaw_growth::VikingStorage::get_metadata_json( &*storage, &latest_key, ).await { if let Ok(persisted) = serde_json::from_str::(&json) { results.push(persisted); } } } Err(e) => { tracing::warn!("[reflection] Failed to load persisted history: {}", e); } } } } } Ok(results) } /// Get reflection state // @connected #[tauri::command] pub async fn reflection_get_state( state: tauri::State<'_, ReflectionEngineState>, ) -> Result { let engine = state.lock().await; Ok(engine.get_state().clone()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_should_reflect_initial() { let mut engine = ReflectionEngine::new(None); assert!(!engine.should_reflect()); // After 3 conversations for _ in 0..3 { engine.record_conversation(); } assert!(engine.should_reflect()); } #[test] fn test_analyze_patterns() { let engine = ReflectionEngine::new(None); let memories = vec![ MemoryEntryForAnalysis { memory_type: "task".to_string(), content: "Task 1".to_string(), importance: 7, access_count: 1, tags: vec![], }, MemoryEntryForAnalysis { memory_type: "task".to_string(), content: "Task 2".to_string(), importance: 8, access_count: 2, tags: vec![], }, ]; let patterns = engine.analyze_patterns(&memories); // Should not trigger (only 2 tasks, threshold is 3) assert!(!patterns.iter().any(|p| p.observation.contains("待办任务"))); } #[tokio::test] async fn test_reflection_cycle_full() { // Use config with use_llm=false to test rules-based path without driver let config = ReflectionConfig { use_llm: false, trigger_after_conversations: 5, ..Default::default() }; let mut engine = ReflectionEngine::new(Some(config)); // Record 5 conversations for _ in 0..5 { engine.record_conversation(); } assert!(engine.should_reflect(), "Should trigger after 5 conversations"); // Provide memories with enough task entries to exceed threshold (5) let mut memories = Vec::new(); for i in 0..6 { memories.push(MemoryEntryForAnalysis { memory_type: "task".to_string(), content: format!("待办任务 {}", i), importance: 7, access_count: 1, tags: vec!["任务".to_string()], }); } // Add some preferences and knowledge memories.push(MemoryEntryForAnalysis { memory_type: "preferences".to_string(), content: "用户偏好简洁回复".to_string(), importance: 8, access_count: 3, tags: vec!["偏好".to_string()], }); memories.push(MemoryEntryForAnalysis { memory_type: "knowledge".to_string(), content: "用户熟悉 Rust 编程".to_string(), importance: 6, access_count: 2, tags: vec!["知识".to_string()], }); // Run reflection (no LLM driver, rules-based) let result = engine.reflect("agent-test", &memories, None).await; // Verify result structure assert!(!result.patterns.is_empty(), "Should detect patterns from memories"); assert!(!result.improvements.is_empty(), "Should generate improvements"); assert!(!result.timestamp.is_empty(), "Should have timestamp"); assert!(result.used_fallback, "Should use rules-based fallback when no LLM driver"); // Verify state reset after reflection assert!(!engine.should_reflect(), "Counter should reset after reflection"); // Verify history stored assert_eq!(engine.history.len(), 1, "History should contain 1 reflection result"); } #[tokio::test] async fn test_reflection_generates_identity_proposals() { let config = ReflectionConfig { use_llm: false, allow_soul_modification: true, trigger_after_conversations: 3, ..Default::default() }; let mut engine = ReflectionEngine::new(Some(config)); for _ in 0..3 { engine.record_conversation(); } // High-frequency preference pattern should trigger identity proposal let memories: Vec = (0..7) .map(|i| MemoryEntryForAnalysis { memory_type: "preferences".to_string(), content: format!("偏好项目 {}", i), importance: 8, access_count: 5, tags: vec!["偏好".to_string()], }) .collect(); let result = engine.reflect("agent-identity-test", &memories, None).await; // Identity proposals are generated when patterns are strong enough // (rules-based may not always produce proposals, but result structure should be valid) assert!(result.identity_proposals.len() <= 5, "Identity proposals should be bounded"); } } // === Helpers === /// Extract JSON from LLM response, handling markdown code blocks and extra text fn extract_json_from_llm_response(text: &str) -> String { let trimmed = text.trim(); // Try to find JSON array in markdown code block if let Some(start) = trimmed.find("```json") { if let Some(content_start) = trimmed[start..].find('\n') { if let Some(end) = trimmed[content_start..].find("```") { return trimmed[content_start + 1..content_start + end].trim().to_string(); } } } // Try to find bare JSON array if let Some(start) = trimmed.find('[') { if let Some(end) = trimmed.rfind(']') { return trimmed[start..end + 1].to_string(); } } trimmed.to_string() }