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 (反映推断不确定性)
189 lines
6.9 KiB
Rust
189 lines
6.9 KiB
Rust
//! Memory middleware — unified pre/post hooks for memory retrieval and extraction.
|
|
//!
|
|
//! This middleware unifies the memory lifecycle:
|
|
//! - `before_completion`: retrieves relevant memories and injects them into the system prompt
|
|
//! - `after_completion`: extracts learnings from the conversation and stores them
|
|
//!
|
|
//! It replaces both the inline `GrowthIntegration` calls in `AgentLoop` and the
|
|
//! `intelligence_hooks` calls in the Tauri desktop layer.
|
|
|
|
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` → `extract_combined()` for memory extraction + evolution check
|
|
pub struct MemoryMiddleware {
|
|
growth: GrowthIntegration,
|
|
/// Shared EvolutionMiddleware for pushing evolution suggestions
|
|
evolution_mw: Option<std::sync::Arc<EvolutionMiddleware>>,
|
|
/// Minimum seconds between extractions for the same agent (debounce).
|
|
debounce_secs: u64,
|
|
/// Timestamp of last extraction per agent (for debouncing).
|
|
last_extraction: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
|
|
}
|
|
|
|
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<EvolutionMiddleware>) -> 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;
|
|
self
|
|
}
|
|
|
|
/// Check if enough time has passed since the last extraction for this agent.
|
|
fn should_extract(&self, agent_id: &str) -> bool {
|
|
let now = std::time::Instant::now();
|
|
let mut map = self.last_extraction.lock().unwrap_or_else(|e| e.into_inner());
|
|
if let Some(last) = map.get(agent_id) {
|
|
if now.duration_since(*last).as_secs() < self.debounce_secs {
|
|
return false;
|
|
}
|
|
}
|
|
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]
|
|
impl AgentMiddleware for MemoryMiddleware {
|
|
fn name(&self) -> &str { "memory" }
|
|
fn priority(&self) -> i32 { 150 }
|
|
|
|
async fn before_completion(&self, ctx: &mut MiddlewareContext) -> Result<MiddlewareDecision> {
|
|
tracing::debug!(
|
|
"[MemoryMiddleware] before_completion for query: {:?}",
|
|
ctx.user_input.chars().take(50).collect::<String>()
|
|
);
|
|
|
|
let base = &ctx.system_prompt;
|
|
match self.growth.enhance_prompt(&ctx.agent_id, base, &ctx.user_input).await {
|
|
Ok(enhanced) => {
|
|
if enhanced != *base {
|
|
tracing::info!(
|
|
"[MemoryMiddleware] Injected memories into system prompt for agent {}",
|
|
ctx.agent_id
|
|
);
|
|
ctx.system_prompt = enhanced;
|
|
} else {
|
|
tracing::debug!(
|
|
"[MemoryMiddleware] No relevant memories found for query: {:?}",
|
|
ctx.user_input.chars().take(50).collect::<String>()
|
|
);
|
|
}
|
|
Ok(MiddlewareDecision::Continue)
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
"[MemoryMiddleware] Memory retrieval failed (non-fatal): {}",
|
|
e
|
|
);
|
|
Ok(MiddlewareDecision::Continue)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn after_completion(&self, ctx: &MiddlewareContext) -> Result<()> {
|
|
let agent_key = ctx.agent_id.to_string();
|
|
if !self.should_extract(&agent_key) {
|
|
tracing::debug!(
|
|
"[MemoryMiddleware] Skipping extraction for agent {} (debounced)",
|
|
agent_key
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
if ctx.messages.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
match self.growth.extract_combined(
|
|
&ctx.agent_id,
|
|
&ctx.messages,
|
|
&ctx.session_id,
|
|
).await {
|
|
Ok(Some((mem_count, facts))) => {
|
|
tracing::info!(
|
|
"[MemoryMiddleware] Extracted {} memories + {} structured facts for agent {}",
|
|
mem_count,
|
|
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) => {
|
|
tracing::warn!("[MemoryMiddleware] Combined extraction failed: {}", e);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|