Files
zclaw_openfang/crates/zclaw-runtime/src/growth.rs
iven e94235c4f9
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
fix(growth): Evolution Engine 穷尽审计 3CRITICAL + 3HIGH 全部修复
C-01: ExperienceExtractor 接入 ExperienceStore
- GrowthIntegration.new() 创建 ExperienceExtractor 时注入 ExperienceStore
- 经验持久化路径打通:extract_combined → persist_experiences → ExperienceStore

C-02+C-03: 进化触发链路全链路接通
- create_middleware_chain() 注册 EvolutionMiddleware (priority 78)
- MemoryMiddleware 持有 Arc<EvolutionMiddleware> 共享引用
- after_completion 中调用 check_evolution() → 推送 PendingEvolution
- EvolutionMiddleware 在下次对话前注入进化建议到 system prompt

H-01: FeedbackCollector loaded 标志修复
- load() 失败时保留 loaded=false,下次 save 重试
- 日志级别 debug → warn

H-03: FeedbackCollector 内部可变性
- EvolutionEngine.feedback 改为 Arc<Mutex<FeedbackCollector>>
- submit_feedback() 从 &mut self → &self,支持中间件 &self 调用路径
- GrowthIntegration.initialize() 从 &mut self → &self

H-05: 删除空测试 test_parse_empty_response (无 assert)

H-06: infer_experiences_from_memories() fallback
- Outcome::Success → Outcome::Partial (反映推断不确定性)
2026-04-19 00:43:02 +08:00

523 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<Arc<UserProfileStore>>,
/// Evolution engine for L2 skill generation (optional)
evolution_engine: Option<EvolutionEngine>,
/// 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<VikingAdapter>) -> 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<dyn LlmDriverForExtraction>) -> 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<UserProfileStore>) -> 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<Vec<AggregatedPattern>> {
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<String> {
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<usize> {
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<ExtractedFactBatch>)` on success.
pub async fn extract_combined(
&self,
agent_id: &AgentId,
messages: &[Message],
session_id: &SessionId,
) -> Result<Option<(usize, ExtractedFactBatch)>> {
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<Fact> = 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<RetrievalResult> {
self.retriever.retrieve(agent_id, query).await
}
/// Get growth statistics for an agent
pub async fn get_stats(&self, agent_id: &AgentId) -> Result<zclaw_growth::GrowthStats> {
self.tracker.get_stats(agent_id).await
}
/// Warm up cache with hot memories
pub async fn warmup_cache(&self, agent_id: &AgentId) -> Result<usize> {
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);
}
}