//! Growth System Integration for ZCLAW Runtime //! //! This module provides integration between the AgentLoop and the Growth System, //! enabling automatic memory retrieval before conversations and memory extraction //! after conversations. //! //! **Note (2026-03-30)**: GrowthIntegration IS wired into the Kernel's middleware //! chain (MemoryMiddleware + CompactionMiddleware). In the Tauri desktop deployment, //! `kernel_commands::kernel_init()` bridges the persistent SqliteStorage to the Kernel //! via `set_viking()` + `set_extraction_driver()`, so the middleware chain and the //! Tauri intelligence_hooks share the same persistent storage backend. use std::sync::Arc; use zclaw_growth::{ AggregatedPattern, CombinedExtraction, EvolutionConfig, EvolutionEngine, ExperienceExtractor, ExperienceStore, GrowthTracker, InjectionFormat, LlmDriverForExtraction, MemoryExtractor, MemoryRetriever, PromptInjector, RetrievalResult, UserProfileUpdater, VikingAdapter, }; use zclaw_memory::{ExtractedFactBatch, Fact, FactCategory, UserProfileStore}; use zclaw_types::{AgentId, Message, Result, SessionId}; /// Growth system integration for AgentLoop /// /// This struct wraps the growth system components and provides /// a simplified interface for integration with the agent loop. pub struct GrowthIntegration { /// Memory retriever for fetching relevant memories retriever: MemoryRetriever, /// Memory extractor for extracting memories from conversations extractor: MemoryExtractor, /// Prompt injector for injecting memories into prompts injector: PromptInjector, /// Growth tracker for tracking growth metrics tracker: GrowthTracker, /// Experience extractor for structured experience persistence experience_extractor: ExperienceExtractor, /// Profile updater for incremental user profile updates profile_updater: UserProfileUpdater, /// User profile store (optional, for profile updates) profile_store: Option>, /// Evolution engine for L2 skill generation (optional) evolution_engine: Option, /// Configuration config: GrowthConfigInner, } /// Internal configuration for growth integration #[derive(Debug, Clone)] struct GrowthConfigInner { /// Enable/disable growth system pub enabled: bool, /// Auto-extract after each conversation pub auto_extract: bool, } impl Default for GrowthConfigInner { fn default() -> Self { Self { enabled: true, auto_extract: true, } } } impl GrowthIntegration { /// Create a new growth integration with in-memory storage pub fn in_memory() -> Self { let viking = Arc::new(VikingAdapter::in_memory()); Self::new(viking) } /// Create a new growth integration with the given Viking adapter pub fn new(viking: Arc) -> Self { // Create extractor without LLM driver - can be set later let extractor = MemoryExtractor::new_without_driver() .with_viking(viking.clone()); let retriever = MemoryRetriever::new(viking.clone()); let injector = PromptInjector::new(); let tracker = GrowthTracker::new(viking.clone()); let evolution_engine = Some(EvolutionEngine::new(viking.clone())); Self { retriever, extractor, injector, tracker, experience_extractor: ExperienceExtractor::new() .with_store(Arc::new(ExperienceStore::new(viking))), profile_updater: UserProfileUpdater::new(), profile_store: None, evolution_engine, config: GrowthConfigInner::default(), } } /// Set the injection format pub fn with_format(mut self, format: InjectionFormat) -> Self { self.injector = self.injector.with_format(format); self } /// Set the LLM driver for memory extraction pub fn with_llm_driver(mut self, driver: Arc) -> Self { self.extractor = self.extractor.with_llm_driver(driver); self } /// Enable or disable growth system pub fn set_enabled(&mut self, enabled: bool) { self.config.enabled = enabled; } /// Check if growth system is enabled pub fn is_enabled(&self) -> bool { self.config.enabled } /// 启动时初始化:从持久化存储恢复进化引擎的信任度记录 /// /// **注意**:FeedbackCollector 内部已实现 lazy-load(首次 save() 时自动加载), /// 所以此方法为可选优化 — 提前加载可避免首次反馈提交时的延迟。 pub async fn initialize(&self) -> Result<()> { if let Some(ref engine) = self.evolution_engine { match engine.load_feedback().await { Ok(count) => { if count > 0 { tracing::info!( "[GrowthIntegration] Loaded {} trust records from storage", count ); } } Err(e) => { tracing::warn!( "[GrowthIntegration] Failed to load trust records: {}", e ); } } } Ok(()) } /// Enable or disable auto extraction pub fn set_auto_extract(&mut self, auto_extract: bool) { self.config.auto_extract = auto_extract; } /// Set the user profile store for incremental profile updates pub fn with_profile_store(mut self, store: Arc) -> Self { self.profile_store = Some(store); self } /// Set the evolution engine configuration pub fn with_evolution_config(self, config: EvolutionConfig) -> Self { let engine = self.evolution_engine.unwrap_or_else(|| { EvolutionEngine::new(Arc::new(VikingAdapter::in_memory())) }); Self { evolution_engine: Some(engine.with_config(config)), ..self } } /// Enable or disable the evolution engine pub fn set_evolution_enabled(&mut self, enabled: bool) { if let Some(ref mut engine) = self.evolution_engine { engine.set_enabled(enabled); } } /// L2 检查:是否有可进化的模式 /// 在 extract_combined 之后调用,返回可固化的经验模式列表 pub async fn check_evolution( &self, agent_id: &AgentId, ) -> Result> { match &self.evolution_engine { Some(engine) => engine.check_evolvable_patterns(&agent_id.to_string()).await, None => Ok(Vec::new()), } } /// Enhance system prompt with retrieved memories /// /// This method: /// 1. Retrieves relevant memories based on user input /// 2. Injects them into the system prompt using configured format /// /// Returns the enhanced prompt or the original if growth is disabled pub async fn enhance_prompt( &self, agent_id: &AgentId, base_prompt: &str, user_input: &str, ) -> Result { if !self.config.enabled { return Ok(base_prompt.to_string()); } tracing::debug!( "[GrowthIntegration] Enhancing prompt for agent: {}", agent_id ); // Retrieve relevant memories let memories = self .retriever .retrieve(agent_id, user_input) .await .unwrap_or_else(|e| { tracing::warn!("[GrowthIntegration] Retrieval failed: {}", e); RetrievalResult::default() }); if memories.is_empty() { tracing::debug!("[GrowthIntegration] No memories retrieved"); return Ok(base_prompt.to_string()); } tracing::info!( "[GrowthIntegration] Injecting {} memories ({} tokens)", memories.total_count(), memories.total_tokens ); // Inject memories into prompt let enhanced = self.injector.inject_with_format(base_prompt, &memories); Ok(enhanced) } /// Process conversation after completion /// /// This method: /// 1. Extracts memories from the conversation using LLM (if driver available) /// 2. Stores the extracted memories /// 3. Updates growth metrics /// /// Returns the number of memories extracted pub async fn process_conversation( &self, agent_id: &AgentId, messages: &[Message], session_id: SessionId, ) -> Result { if !self.config.enabled || !self.config.auto_extract { return Ok(0); } tracing::debug!( "[GrowthIntegration] Processing conversation for agent: {}", agent_id ); // Extract memories from conversation let extracted = self .extractor .extract(messages, session_id.clone()) .await .unwrap_or_else(|e| { tracing::warn!("[GrowthIntegration] Extraction failed: {}", e); Vec::new() }); if extracted.is_empty() { tracing::debug!("[GrowthIntegration] No memories extracted"); return Ok(0); } tracing::info!( "[GrowthIntegration] Extracted {} memories", extracted.len() ); // Store extracted memories let count = extracted.len(); self.extractor .store_memories(&agent_id.to_string(), &extracted) .await?; // Track learning event self.tracker .record_learning(agent_id, &session_id.to_string(), count) .await?; Ok(count) } /// Combined extraction: single LLM call that produces stored memories, /// structured experiences, and profile signals — all in one pass. /// /// Returns `(memory_count, Option)` on success. pub async fn extract_combined( &self, agent_id: &AgentId, messages: &[Message], session_id: &SessionId, ) -> Result> { if !self.config.enabled || !self.config.auto_extract { return Ok(None); } // 单次 LLM 提取:memories + experiences + profile_signals let combined = self .extractor .extract_combined(messages, session_id.clone()) .await .unwrap_or_else(|e| { tracing::warn!("[GrowthIntegration] Combined extraction failed: {}", e); CombinedExtraction::default() }); if combined.memories.is_empty() && combined.experiences.is_empty() && !combined.profile_signals.has_any_signal() { return Ok(None); } let mem_count = combined.memories.len(); // Store raw memories self.extractor .store_memories(&agent_id.to_string(), &combined.memories) .await?; // Track learning event self.tracker .record_learning(agent_id, &session_id.to_string(), mem_count) .await?; // Persist structured experiences (L1 enhancement) if let Ok(exp_count) = self .experience_extractor .persist_experiences(&agent_id.to_string(), &combined) .await { if exp_count > 0 { tracing::debug!( "[GrowthIntegration] Persisted {} structured experiences", exp_count ); } } // Update user profile from extraction signals (L1 enhancement) if let Some(profile_store) = &self.profile_store { let updates = self.profile_updater.collect_updates(&combined); let user_id = agent_id.to_string(); for update in updates { let result = match update.kind { zclaw_growth::ProfileUpdateKind::SetField => { profile_store .update_field(&user_id, &update.field, &update.value) .await } zclaw_growth::ProfileUpdateKind::AppendArray => { match update.field.as_str() { "recent_topic" => { profile_store .add_recent_topic(&user_id, &update.value, 10) .await } "pain_point" => { profile_store .add_pain_point(&user_id, &update.value, 10) .await } "preferred_tool" => { profile_store .add_preferred_tool(&user_id, &update.value, 10) .await } _ => { tracing::warn!( "[GrowthIntegration] Unknown array field: {}", update.field ); Ok(()) } } } }; if let Err(e) = result { tracing::warn!( "[GrowthIntegration] Profile update failed for {}: {}", update.field, e ); } } } // Convert extracted memories to structured facts let facts: Vec = combined .memories .into_iter() .map(|m| { let category = match m.memory_type { zclaw_growth::types::MemoryType::Preference => FactCategory::Preference, zclaw_growth::types::MemoryType::Knowledge => FactCategory::Knowledge, zclaw_growth::types::MemoryType::Experience => FactCategory::Behavior, _ => FactCategory::General, }; Fact::new(m.content, category, f64::from(m.confidence)) .with_source(session_id.to_string()) }) .collect(); let batch = ExtractedFactBatch { facts, agent_id: agent_id.to_string(), session_id: session_id.to_string(), } .deduplicate() .filter_by_confidence(0.7); if batch.is_empty() { return Ok(Some((mem_count, ExtractedFactBatch { facts: vec![], agent_id: agent_id.to_string(), session_id: session_id.to_string(), }))); } Ok(Some((mem_count, batch))) } /// Retrieve memories for a query without injection pub async fn retrieve_memories( &self, agent_id: &AgentId, query: &str, ) -> Result { self.retriever.retrieve(agent_id, query).await } /// Get growth statistics for an agent pub async fn get_stats(&self, agent_id: &AgentId) -> Result { self.tracker.get_stats(agent_id).await } /// Warm up cache with hot memories pub async fn warmup_cache(&self, agent_id: &AgentId) -> Result { self.retriever.warmup_cache(agent_id).await } /// Clear the semantic index pub async fn clear_index(&self) { self.retriever.clear_index().await; } } impl Default for GrowthIntegration { fn default() -> Self { Self::in_memory() } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_growth_integration_creation() { let growth = GrowthIntegration::in_memory(); assert!(growth.is_enabled()); } #[tokio::test] async fn test_enhance_prompt_empty() { let growth = GrowthIntegration::in_memory(); let agent_id = AgentId::new(); let base = "You are helpful."; let user_input = "Hello"; let enhanced = growth .enhance_prompt(&agent_id, base, user_input) .await .unwrap(); // Without any stored memories, should return base prompt assert_eq!(enhanced, base); } #[tokio::test] async fn test_disabled_growth() { let mut growth = GrowthIntegration::in_memory(); growth.set_enabled(false); let agent_id = AgentId::new(); let base = "You are helpful."; let enhanced = growth .enhance_prompt(&agent_id, base, "test") .await .unwrap(); assert_eq!(enhanced, base); } #[tokio::test] async fn test_process_conversation_disabled() { let mut growth = GrowthIntegration::in_memory(); growth.set_auto_extract(false); let agent_id = AgentId::new(); let messages = vec![Message::user("Hello")]; let session_id = SessionId::new(); let count = growth .process_conversation(&agent_id, &messages, session_id) .await .unwrap(); assert_eq!(count, 0); } }