diff --git a/crates/zclaw-growth/src/evolution_engine.rs b/crates/zclaw-growth/src/evolution_engine.rs index 0ce4165..0c6f0bb 100644 --- a/crates/zclaw-growth/src/evolution_engine.rs +++ b/crates/zclaw-growth/src/evolution_engine.rs @@ -42,7 +42,7 @@ impl Default for EvolutionConfig { /// 进化引擎中枢 pub struct EvolutionEngine { viking: Arc, - feedback: FeedbackCollector, + feedback: Arc>, config: EvolutionConfig, } @@ -50,7 +50,9 @@ impl EvolutionEngine { pub fn new(viking: Arc) -> Self { Self { viking: viking.clone(), - feedback: FeedbackCollector::with_viking(viking), + feedback: Arc::new(tokio::sync::Mutex::new( + FeedbackCollector::with_viking(viking), + )), config: EvolutionConfig::default(), } } @@ -61,7 +63,9 @@ impl EvolutionEngine { let viking = experience_store.viking().clone(); Self { viking: viking.clone(), - feedback: FeedbackCollector::with_viking(viking), + feedback: Arc::new(tokio::sync::Mutex::new( + FeedbackCollector::with_viking(viking), + )), config: EvolutionConfig::default(), } } @@ -144,18 +148,21 @@ impl EvolutionEngine { // ----------------------------------------------------------------------- /// 提交反馈并获取信任度更新,自动持久化 - pub async fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate { - let update = self.feedback.submit_feedback(entry); + pub async fn submit_feedback(&self, entry: FeedbackEntry) -> TrustUpdate { + let mut feedback = self.feedback.lock().await; + let update = feedback.submit_feedback(entry); // 非阻塞持久化:失败仅打日志,不影响返回值 - if let Err(e) = self.feedback.save().await { + if let Err(e) = feedback.save().await { tracing::warn!("[EvolutionEngine] Failed to persist trust records: {}", e); } update } /// 获取需要优化的进化产物 - pub fn get_artifacts_needing_optimization(&self) -> Vec { + pub async fn get_artifacts_needing_optimization(&self) -> Vec { self.feedback + .lock() + .await .get_artifacts_needing_optimization() .iter() .map(|r| r.artifact_id.clone()) @@ -163,8 +170,10 @@ impl EvolutionEngine { } /// 获取建议归档的进化产物 - pub fn get_artifacts_to_archive(&self) -> Vec { + pub async fn get_artifacts_to_archive(&self) -> Vec { self.feedback + .lock() + .await .get_artifacts_to_archive() .iter() .map(|r| r.artifact_id.clone()) @@ -172,22 +181,21 @@ impl EvolutionEngine { } /// 获取推荐产物 - pub fn get_recommended_artifacts(&self) -> Vec { + pub async fn get_recommended_artifacts(&self) -> Vec { self.feedback + .lock() + .await .get_recommended_artifacts() .iter() .map(|r| r.artifact_id.clone()) .collect() } - /// 获取反馈收集器的引用(用于高级查询) - pub fn feedback(&self) -> &FeedbackCollector { - &self.feedback - } - /// 启动时加载已持久化的信任度记录 - pub async fn load_feedback(&mut self) -> Result { + pub async fn load_feedback(&self) -> Result { self.feedback + .lock() + .await .load() .await .map_err(|e| zclaw_types::ZclawError::Internal(e)) diff --git a/crates/zclaw-growth/src/extractor.rs b/crates/zclaw-growth/src/extractor.rs index 1d5c2e1..470a9ee 100644 --- a/crates/zclaw-growth/src/extractor.rs +++ b/crates/zclaw-growth/src/extractor.rs @@ -501,7 +501,7 @@ fn infer_experiences_from_memories( pain_pattern: m.category.clone(), context: content.clone(), solution_steps: Vec::new(), - outcome: crate::types::Outcome::Success, + outcome: crate::types::Outcome::Partial, confidence: m.confidence * 0.7, // 降低推断置信度 tools_used: m.keywords.clone(), industry_context: None, diff --git a/crates/zclaw-growth/src/feedback_collector.rs b/crates/zclaw-growth/src/feedback_collector.rs index 4f7f29a..92e87d2 100644 --- a/crates/zclaw-growth/src/feedback_collector.rs +++ b/crates/zclaw-growth/src/feedback_collector.rs @@ -137,13 +137,18 @@ impl FeedbackCollector { pub async fn save(&mut self) -> Result { // 首次保存前自动加载已有记录,防止丢失历史数据 if !self.loaded { - if let Err(e) = self.load().await { - tracing::debug!( - "[FeedbackCollector] Auto-load before save failed (non-fatal): {}", - e - ); + match self.load().await { + Ok(_) => { + self.loaded = true; + } + Err(e) => { + // 加载失败时保留 loaded=false,下次 save 会重试 + tracing::warn!( + "[FeedbackCollector] Auto-load before save failed, will retry next save: {}", + e + ); + } } - self.loaded = true; } let viking = match &self.viking { diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index 488f188..13c621d 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -252,6 +252,12 @@ impl Kernel { growth = growth.with_llm_driver(driver.clone()); } + // Evolution middleware — shared with MemoryMiddleware for pushing evolution candidates + let evolution_mw = std::sync::Arc::new( + zclaw_runtime::middleware::evolution::EvolutionMiddleware::new() + ); + chain.register(evolution_mw.clone()); + // Compaction middleware — only register when threshold > 0 let threshold = self.config.compaction_threshold(); if threshold > 0 { @@ -269,10 +275,11 @@ impl Kernel { chain.register(Arc::new(mw)); } - // Memory middleware — auto-extract memories after conversations + // Memory middleware — auto-extract memories + check evolution after conversations { use std::sync::Arc; - let mw = zclaw_runtime::middleware::memory::MemoryMiddleware::new(growth); + let mw = zclaw_runtime::middleware::memory::MemoryMiddleware::new(growth) + .with_evolution(evolution_mw); chain.register(Arc::new(mw)); } diff --git a/crates/zclaw-runtime/src/growth.rs b/crates/zclaw-runtime/src/growth.rs index 7916d89..d85d2d4 100644 --- a/crates/zclaw-runtime/src/growth.rs +++ b/crates/zclaw-runtime/src/growth.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use zclaw_growth::{ AggregatedPattern, CombinedExtraction, EvolutionConfig, EvolutionEngine, - ExperienceExtractor, GrowthTracker, InjectionFormat, + ExperienceExtractor, ExperienceStore, GrowthTracker, InjectionFormat, LlmDriverForExtraction, MemoryExtractor, MemoryRetriever, PromptInjector, RetrievalResult, UserProfileUpdater, VikingAdapter, }; @@ -79,14 +79,15 @@ impl GrowthIntegration { let retriever = MemoryRetriever::new(viking.clone()); let injector = PromptInjector::new(); let tracker = GrowthTracker::new(viking.clone()); - let evolution_engine = Some(EvolutionEngine::new(viking)); + let evolution_engine = Some(EvolutionEngine::new(viking.clone())); Self { retriever, extractor, injector, tracker, - experience_extractor: ExperienceExtractor::new(), + experience_extractor: ExperienceExtractor::new() + .with_store(Arc::new(ExperienceStore::new(viking))), profile_updater: UserProfileUpdater::new(), profile_store: None, evolution_engine, @@ -120,10 +121,8 @@ impl GrowthIntegration { /// /// **注意**:FeedbackCollector 内部已实现 lazy-load(首次 save() 时自动加载), /// 所以此方法为可选优化 — 提前加载可避免首次反馈提交时的延迟。 - /// 在中间件持有 GrowthIntegration 的场景中,由于 `&self` 限制无法调用此方法, - /// lazy-load 机制会兜底处理。 - pub async fn initialize(&mut self) -> Result<()> { - if let Some(ref mut engine) = self.evolution_engine { + pub async fn initialize(&self) -> Result<()> { + if let Some(ref engine) = self.evolution_engine { match engine.load_feedback().await { Ok(count) => { if count > 0 { diff --git a/crates/zclaw-runtime/src/middleware/memory.rs b/crates/zclaw-runtime/src/middleware/memory.rs index bd996d1..4375d91 100644 --- a/crates/zclaw-runtime/src/middleware/memory.rs +++ b/crates/zclaw-runtime/src/middleware/memory.rs @@ -11,14 +11,17 @@ use async_trait::async_trait; use zclaw_types::Result; use crate::growth::GrowthIntegration; use crate::middleware::{AgentMiddleware, MiddlewareContext, MiddlewareDecision}; +use crate::middleware::evolution::EvolutionMiddleware; /// Middleware that handles memory retrieval (pre-completion) and extraction (post-completion). /// /// Wraps `GrowthIntegration` and delegates: /// - `before_completion` → `enhance_prompt()` for memory injection -/// - `after_completion` → `process_conversation()` for memory extraction +/// - `after_completion` → `extract_combined()` for memory extraction + evolution check pub struct MemoryMiddleware { growth: GrowthIntegration, + /// Shared EvolutionMiddleware for pushing evolution suggestions + evolution_mw: Option>, /// Minimum seconds between extractions for the same agent (debounce). debounce_secs: u64, /// Timestamp of last extraction per agent (for debouncing). @@ -29,11 +32,18 @@ impl MemoryMiddleware { pub fn new(growth: GrowthIntegration) -> Self { Self { growth, + evolution_mw: None, debounce_secs: 30, last_extraction: std::sync::Mutex::new(std::collections::HashMap::new()), } } + /// Attach a shared EvolutionMiddleware for pushing evolution suggestions. + pub fn with_evolution(mut self, mw: std::sync::Arc) -> Self { + self.evolution_mw = Some(mw); + self + } + /// Set the debounce interval in seconds. pub fn with_debounce_secs(mut self, secs: u64) -> Self { self.debounce_secs = secs; @@ -52,6 +62,49 @@ impl MemoryMiddleware { map.insert(agent_id.to_string(), now); true } + + /// Check for evolvable patterns and push suggestions to EvolutionMiddleware. + async fn check_and_push_evolution(&self, agent_id: &zclaw_types::AgentId) { + let evolution_mw = match &self.evolution_mw { + Some(mw) => mw, + None => return, + }; + + match self.growth.check_evolution(agent_id).await { + Ok(patterns) if !patterns.is_empty() => { + for pattern in &patterns { + let trigger = pattern + .common_steps + .first() + .cloned() + .unwrap_or_else(|| pattern.pain_pattern.clone()); + evolution_mw.add_pending( + crate::middleware::evolution::PendingEvolution { + pattern_name: pattern.pain_pattern.clone(), + trigger_suggestion: trigger, + description: format!( + "基于 {} 次重复经验,自动固化技能", + pattern.total_reuse + ), + }, + ).await; + } + tracing::info!( + "[MemoryMiddleware] Pushed {} evolution candidates for agent {}", + patterns.len(), + agent_id + ); + } + Ok(_) => { + tracing::debug!("[MemoryMiddleware] No evolvable patterns found"); + } + Err(e) => { + tracing::debug!( + "[MemoryMiddleware] Evolution check failed (non-fatal): {}", e + ); + } + } + } } #[async_trait] @@ -65,11 +118,6 @@ impl AgentMiddleware for MemoryMiddleware { ctx.user_input.chars().take(50).collect::() ); - // Retrieve relevant memories and inject into system prompt. - // The SqliteStorage retriever now uses FTS5-only matching — if FTS5 finds - // no relevant results, no memories are returned (no scope-based fallback). - // This prevents irrelevant high-importance memories from leaking into - // unrelated conversations. let base = &ctx.system_prompt; match self.growth.enhance_prompt(&ctx.agent_id, base, &ctx.user_input).await { Ok(enhanced) => { @@ -88,7 +136,6 @@ impl AgentMiddleware for MemoryMiddleware { Ok(MiddlewareDecision::Continue) } Err(e) => { - // Non-fatal: retrieval failure should not block the conversation tracing::warn!( "[MemoryMiddleware] Memory retrieval failed (non-fatal): {}", e @@ -99,7 +146,6 @@ impl AgentMiddleware for MemoryMiddleware { } async fn after_completion(&self, ctx: &MiddlewareContext) -> Result<()> { - // Debounce: skip extraction if called too recently for this agent let agent_key = ctx.agent_id.to_string(); if !self.should_extract(&agent_key) { tracing::debug!( @@ -113,8 +159,6 @@ impl AgentMiddleware for MemoryMiddleware { return Ok(()); } - // Combined extraction: single LLM call produces both memories and structured facts. - // Avoids double LLM extraction ( process_conversation + extract_structured_facts). match self.growth.extract_combined( &ctx.agent_id, &ctx.messages, @@ -127,12 +171,14 @@ impl AgentMiddleware for MemoryMiddleware { facts.len(), agent_key ); + + // Check for evolvable patterns after successful extraction + self.check_and_push_evolution(&ctx.agent_id).await; } Ok(None) => { tracing::debug!("[MemoryMiddleware] No memories or facts extracted"); } Err(e) => { - // Non-fatal: extraction failure should not affect the response tracing::warn!("[MemoryMiddleware] Combined extraction failed: {}", e); } } diff --git a/desktop/src-tauri/src/intelligence/extraction_adapter.rs b/desktop/src-tauri/src/intelligence/extraction_adapter.rs index 38aaeed..6132e04 100644 --- a/desktop/src-tauri/src/intelligence/extraction_adapter.rs +++ b/desktop/src-tauri/src/intelligence/extraction_adapter.rs @@ -335,22 +335,6 @@ mod tests { assert!(!is_extraction_driver_configured()); } - #[test] - fn test_parse_empty_response() { - // We cannot create a real LlmDriver easily in tests, so we test the - // parsing logic via a minimal helper. - struct DummyDriver; - impl TauriExtractionDriver { - fn parse_response_test( - &self, - response_text: &str, - extraction_type: MemoryType, - ) -> Vec { - self.parse_response(response_text, extraction_type) - } - } - } - #[test] fn test_parse_valid_json_response() { let response = r#"```json