feat(runtime): DeerFlow 模式中间件链 Phase 1-4 全部完成
借鉴 DeerFlow 架构,实现完整中间件链系统: Phase 1 - Agent 中间件链基础设施 - MiddlewareChain Clone 支持 - LoopRunner 双路径集成 (middleware/legacy) - Kernel create_middleware_chain() 工厂方法 Phase 2 - 技能按需注入 - SkillIndexMiddleware (priority 200) - SkillLoadTool 工具 - SkillDetail/SkillIndexEntry 结构体 - KernelSkillExecutor trait 扩展 Phase 3 - Guardrail 安全护栏 - GuardrailMiddleware (priority 400, fail_open) - ShellExecRule / FileWriteRule / WebFetchRule Phase 4 - 记忆闭环统一 - MemoryMiddleware (priority 150, 30s 防抖) - after_completion 双路径调用 中间件注册顺序: 100 Compaction | 150 Memory | 200 SkillIndex 400 Guardrail | 500 LoopGuard | 700 TokenCalibration 向后兼容:Option<MiddlewareChain> 默认 None 走旧路径
This commit is contained in:
115
crates/zclaw-runtime/src/middleware/memory.rs
Normal file
115
crates/zclaw-runtime/src/middleware/memory.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
//! 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};
|
||||
|
||||
/// 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
|
||||
pub struct MemoryMiddleware {
|
||||
growth: GrowthIntegration,
|
||||
/// 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,
|
||||
debounce_secs: 30,
|
||||
last_extraction: std::sync::Mutex::new(std::collections::HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
match self.growth.enhance_prompt(
|
||||
&ctx.agent_id,
|
||||
&ctx.system_prompt,
|
||||
&ctx.user_input,
|
||||
).await {
|
||||
Ok(enhanced) => {
|
||||
ctx.system_prompt = enhanced;
|
||||
Ok(MiddlewareDecision::Continue)
|
||||
}
|
||||
Err(e) => {
|
||||
// Non-fatal: memory retrieval failure should not block the loop
|
||||
tracing::warn!("[MemoryMiddleware] Prompt enhancement failed: {}", e);
|
||||
Ok(MiddlewareDecision::Continue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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!(
|
||||
"[MemoryMiddleware] Skipping extraction for agent {} (debounced)",
|
||||
agent_key
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ctx.messages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self.growth.process_conversation(
|
||||
&ctx.agent_id,
|
||||
&ctx.messages,
|
||||
ctx.session_id.clone(),
|
||||
).await {
|
||||
Ok(count) => {
|
||||
tracing::info!(
|
||||
"[MemoryMiddleware] Extracted {} memories for agent {}",
|
||||
count,
|
||||
agent_key
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// Non-fatal: extraction failure should not affect the response
|
||||
tracing::warn!("[MemoryMiddleware] Memory extraction failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user