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
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 (反映推断不确定性)
523 lines
17 KiB
Rust
523 lines
17 KiB
Rust
//! 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);
|
||
}
|
||
}
|