feat(intelligence): complete Phase 2-3 migration to Rust

Phase 2 - Core Engines:
- Heartbeat Engine: Periodic proactive checks with quiet hours support
- Context Compactor: Token estimation and message summarization
  - CJK character handling (1.5 tokens per char)
  - Rule-based summary generation

Phase 3 - Advanced Features:
- Reflection Engine: Pattern analysis and improvement suggestions
- Agent Identity: SOUL.md/AGENTS.md/USER.md management
  - Proposal-based changes (requires user approval)
  - Snapshot history for rollback

All modules include:
- Tauri commands for frontend integration
- Unit tests
- Re-exported types via mod.rs

Reference: docs/plans/INTELLIGENCE-LAYER-MIGRATION.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 00:52:44 +08:00
parent 0db8a2822f
commit ef8f5cdb43
6 changed files with 2235 additions and 1 deletions

View File

@@ -0,0 +1,568 @@
//! Reflection Engine - Agent self-improvement through conversation analysis
//!
//! Periodically analyzes recent conversations to:
//! - Identify behavioral patterns (positive and negative)
//! - Generate improvement suggestions
//! - Propose identity file changes (with user approval)
//! - Create meta-memories about agent performance
//!
//! Phase 3 of Intelligence Layer Migration.
//! Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.2
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// === Types ===
/// Reflection configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflectionConfig {
#[serde(default = "default_trigger_conversations")]
pub trigger_after_conversations: usize,
#[serde(default = "default_trigger_hours")]
pub trigger_after_hours: u64,
#[serde(default)]
pub allow_soul_modification: bool,
#[serde(default = "default_require_approval")]
pub require_approval: bool,
#[serde(default = "default_use_llm")]
pub use_llm: bool,
#[serde(default = "default_llm_fallback")]
pub llm_fallback_to_rules: bool,
}
fn default_trigger_conversations() -> usize { 5 }
fn default_trigger_hours() -> u64 { 24 }
fn default_require_approval() -> bool { true }
fn default_use_llm() -> bool { true }
fn default_llm_fallback() -> bool { true }
impl Default for ReflectionConfig {
fn default() -> Self {
Self {
trigger_after_conversations: 5,
trigger_after_hours: 24,
allow_soul_modification: false,
require_approval: true,
use_llm: true,
llm_fallback_to_rules: true,
}
}
}
/// Observed pattern from analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternObservation {
pub observation: String,
pub frequency: usize,
pub sentiment: Sentiment,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Sentiment {
Positive,
Negative,
Neutral,
}
/// Improvement suggestion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImprovementSuggestion {
pub area: String,
pub suggestion: String,
pub priority: Priority,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
High,
Medium,
Low,
}
/// Identity change proposal
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityChangeProposal {
pub agent_id: String,
pub field: String,
pub current_value: String,
pub proposed_value: String,
pub reason: String,
}
/// Result of reflection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflectionResult {
pub patterns: Vec<PatternObservation>,
pub improvements: Vec<ImprovementSuggestion>,
pub identity_proposals: Vec<IdentityChangeProposal>,
pub new_memories: usize,
pub timestamp: String,
}
/// Reflection state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflectionState {
pub conversations_since_reflection: usize,
pub last_reflection_time: Option<String>,
pub last_reflection_agent_id: Option<String>,
}
impl Default for ReflectionState {
fn default() -> Self {
Self {
conversations_since_reflection: 0,
last_reflection_time: None,
last_reflection_agent_id: None,
}
}
}
// === Memory Entry (simplified for analysis) ===
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntryForAnalysis {
pub memory_type: String,
pub content: String,
pub importance: usize,
pub access_count: usize,
pub tags: Vec<String>,
}
// === Reflection Engine ===
pub struct ReflectionEngine {
config: ReflectionConfig,
state: ReflectionState,
history: Vec<ReflectionResult>,
}
impl ReflectionEngine {
pub fn new(config: Option<ReflectionConfig>) -> Self {
Self {
config: config.unwrap_or_default(),
state: ReflectionState::default(),
history: Vec::new(),
}
}
/// Record that a conversation happened
pub fn record_conversation(&mut self) {
self.state.conversations_since_reflection += 1;
}
/// Check if it's time for reflection
pub fn should_reflect(&self) -> bool {
// Conversation count trigger
if self.state.conversations_since_reflection >= self.config.trigger_after_conversations {
return true;
}
// Time-based trigger
if let Some(last_time) = &self.state.last_reflection_time {
if let Ok(last) = DateTime::parse_from_rfc3339(last_time) {
let elapsed = Utc::now().signed_duration_since(last);
let hours_since = elapsed.num_hours() as u64;
if hours_since >= self.config.trigger_after_hours {
return true;
}
}
} else {
// Never reflected before, trigger after initial conversations
return self.state.conversations_since_reflection >= 3;
}
false
}
/// Execute reflection cycle
pub fn reflect(&mut self, agent_id: &str, memories: &[MemoryEntryForAnalysis]) -> ReflectionResult {
// 1. Analyze memory patterns
let patterns = self.analyze_patterns(memories);
// 2. Generate improvement suggestions
let improvements = self.generate_improvements(&patterns, memories);
// 3. Propose identity changes if patterns warrant it
let identity_proposals: Vec<IdentityChangeProposal> = if self.config.allow_soul_modification {
self.propose_identity_changes(agent_id, &patterns)
} else {
vec![]
};
// 4. Count new memories that would be saved
let new_memories = patterns.iter()
.filter(|p| p.frequency >= 3)
.count()
+ improvements.iter()
.filter(|i| matches!(i.priority, Priority::High))
.count();
// 5. Build result
let result = ReflectionResult {
patterns,
improvements,
identity_proposals,
new_memories,
timestamp: Utc::now().to_rfc3339(),
};
// 6. Update state
self.state.conversations_since_reflection = 0;
self.state.last_reflection_time = Some(result.timestamp.clone());
self.state.last_reflection_agent_id = Some(agent_id.to_string());
// 7. Store in history
self.history.push(result.clone());
if self.history.len() > 20 {
self.history = self.history.split_off(10);
}
result
}
/// Analyze patterns in memories
fn analyze_patterns(&self, memories: &[MemoryEntryForAnalysis]) -> Vec<PatternObservation> {
let mut patterns = Vec::new();
// Analyze memory type distribution
let mut type_counts: HashMap<String, usize> = HashMap::new();
for m in memories {
*type_counts.entry(m.memory_type.clone()).or_insert(0) += 1;
}
// Pattern: Too many tasks accumulating
let task_count = *type_counts.get("task").unwrap_or(&0);
if task_count >= 5 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "task")
.take(3)
.map(|m| m.content.clone())
.collect();
patterns.push(PatternObservation {
observation: format!("积累了 {} 个待办任务,可能存在任务管理不善", task_count),
frequency: task_count,
sentiment: Sentiment::Negative,
evidence,
});
}
// Pattern: Strong preference accumulation
let pref_count = *type_counts.get("preference").unwrap_or(&0);
if pref_count >= 5 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "preference")
.take(3)
.map(|m| m.content.clone())
.collect();
patterns.push(PatternObservation {
observation: format!("已记录 {} 个用户偏好,对用户习惯有较好理解", pref_count),
frequency: pref_count,
sentiment: Sentiment::Positive,
evidence,
});
}
// Pattern: Many lessons learned
let lesson_count = *type_counts.get("lesson").unwrap_or(&0);
if lesson_count >= 5 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "lesson")
.take(3)
.map(|m| m.content.clone())
.collect();
patterns.push(PatternObservation {
observation: format!("积累了 {} 条经验教训,知识库在成长", lesson_count),
frequency: lesson_count,
sentiment: Sentiment::Positive,
evidence,
});
}
// Pattern: High-importance items being accessed frequently
let high_access: Vec<_> = memories
.iter()
.filter(|m| m.access_count >= 5 && m.importance >= 7)
.collect();
if high_access.len() >= 3 {
let evidence: Vec<String> = high_access.iter().take(3).map(|m| m.content.clone()).collect();
patterns.push(PatternObservation {
observation: format!("有 {} 条高频访问的重要记忆,核心知识正在形成", high_access.len()),
frequency: high_access.len(),
sentiment: Sentiment::Positive,
evidence,
});
}
// Pattern: Low-importance memories accumulating
let low_importance_count = memories.iter().filter(|m| m.importance <= 3).count();
if low_importance_count > 20 {
patterns.push(PatternObservation {
observation: format!("有 {} 条低重要性记忆,建议清理", low_importance_count),
frequency: low_importance_count,
sentiment: Sentiment::Neutral,
evidence: vec![],
});
}
// Pattern: Tag analysis - recurring topics
let mut tag_counts: HashMap<String, usize> = HashMap::new();
for m in memories {
for tag in &m.tags {
if tag != "auto-extracted" {
*tag_counts.entry(tag.clone()).or_insert(0) += 1;
}
}
}
let mut frequent_tags: Vec<_> = tag_counts
.iter()
.filter(|(_, count)| **count >= 5)
.map(|(tag, count)| (tag.clone(), *count))
.collect();
frequent_tags.sort_by(|a, b| b.1.cmp(&a.1));
if !frequent_tags.is_empty() {
let tag_str: Vec<String> = frequent_tags
.iter()
.take(5)
.map(|(tag, count)| format!("{}({}次)", tag, count))
.collect();
patterns.push(PatternObservation {
observation: format!("反复出现的主题: {}", tag_str.join(", ")),
frequency: frequent_tags[0].1,
sentiment: Sentiment::Neutral,
evidence: frequent_tags.iter().take(5).map(|(t, _)| t.clone()).collect(),
});
}
patterns
}
/// Generate improvement suggestions
fn generate_improvements(
&self,
patterns: &[PatternObservation],
memories: &[MemoryEntryForAnalysis],
) -> Vec<ImprovementSuggestion> {
let mut improvements = Vec::new();
// Suggestion: Clear pending tasks
if patterns.iter().any(|p| p.observation.contains("待办任务")) {
improvements.push(ImprovementSuggestion {
area: "任务管理".to_string(),
suggestion: "清理已完成的任务记忆,对长期未处理的任务降低重要性或标记为已取消".to_string(),
priority: Priority::High,
});
}
// Suggestion: Prune low-importance memories
if patterns.iter().any(|p| p.observation.contains("低重要性")) {
improvements.push(ImprovementSuggestion {
area: "记忆管理".to_string(),
suggestion: "执行记忆清理移除30天以上未访问且重要性低于3的记忆".to_string(),
priority: Priority::Medium,
});
}
// Suggestion: User profile enrichment
let pref_count = memories.iter().filter(|m| m.memory_type == "preference").count();
if pref_count < 3 {
improvements.push(ImprovementSuggestion {
area: "用户理解".to_string(),
suggestion: "主动在对话中了解用户偏好(沟通风格、技术栈、工作习惯),丰富用户画像".to_string(),
priority: Priority::Medium,
});
}
// Suggestion: Knowledge consolidation
let fact_count = memories.iter().filter(|m| m.memory_type == "fact").count();
if fact_count > 20 {
improvements.push(ImprovementSuggestion {
area: "知识整合".to_string(),
suggestion: "合并相似的事实记忆,提高检索效率。可将相关事实整合为结构化的项目/用户档案".to_string(),
priority: Priority::Low,
});
}
improvements
}
/// Propose identity changes based on patterns
fn propose_identity_changes(
&self,
agent_id: &str,
patterns: &[PatternObservation],
) -> Vec<IdentityChangeProposal> {
let mut proposals = Vec::new();
// If many negative patterns, propose instruction update
let negative_patterns: Vec<_> = patterns
.iter()
.filter(|p| matches!(p.sentiment, Sentiment::Negative))
.collect();
if negative_patterns.len() >= 2 {
let additions: Vec<String> = negative_patterns
.iter()
.map(|p| format!("- 注意: {}", p.observation))
.collect();
proposals.push(IdentityChangeProposal {
agent_id: agent_id.to_string(),
field: "instructions".to_string(),
current_value: "...".to_string(),
proposed_value: format!("\n\n## 自我反思改进\n{}", additions.join("\n")),
reason: format!(
"基于 {} 个负面模式观察,建议在指令中增加自我改进提醒",
negative_patterns.len()
),
});
}
proposals
}
/// Get reflection history
pub fn get_history(&self, limit: usize) -> Vec<&ReflectionResult> {
self.history.iter().rev().take(limit).collect()
}
/// Get last reflection result
pub fn get_last_result(&self) -> Option<&ReflectionResult> {
self.history.last()
}
/// Get current state
pub fn get_state(&self) -> &ReflectionState {
&self.state
}
/// Get configuration
pub fn get_config(&self) -> &ReflectionConfig {
&self.config
}
/// Update configuration
pub fn update_config(&mut self, config: ReflectionConfig) {
self.config = config;
}
}
// === Tauri Commands ===
use std::sync::Arc;
use tokio::sync::Mutex;
pub type ReflectionEngineState = Arc<Mutex<ReflectionEngine>>;
/// Initialize reflection engine
#[tauri::command]
pub async fn reflection_init(
config: Option<ReflectionConfig>,
) -> Result<ReflectionEngineState, String> {
Ok(Arc::new(Mutex::new(ReflectionEngine::new(config))))
}
/// Record a conversation
#[tauri::command]
pub async fn reflection_record_conversation(
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<(), String> {
let mut engine = state.lock().await;
engine.record_conversation();
Ok(())
}
/// Check if reflection should run
#[tauri::command]
pub async fn reflection_should_reflect(
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<bool, String> {
let engine = state.lock().await;
Ok(engine.should_reflect())
}
/// Execute reflection
#[tauri::command]
pub async fn reflection_reflect(
agent_id: String,
memories: Vec<MemoryEntryForAnalysis>,
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<ReflectionResult, String> {
let mut engine = state.lock().await;
Ok(engine.reflect(&agent_id, &memories))
}
/// Get reflection history
#[tauri::command]
pub async fn reflection_get_history(
limit: Option<usize>,
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<Vec<ReflectionResult>, String> {
let engine = state.lock().await;
Ok(engine.get_history(limit.unwrap_or(10)).into_iter().cloned().collect())
}
/// Get reflection state
#[tauri::command]
pub async fn reflection_get_state(
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<ReflectionState, String> {
let engine = state.lock().await;
Ok(engine.get_state().clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_reflect_initial() {
let mut engine = ReflectionEngine::new(None);
assert!(!engine.should_reflect());
// After 3 conversations
for _ in 0..3 {
engine.record_conversation();
}
assert!(engine.should_reflect());
}
#[test]
fn test_analyze_patterns() {
let engine = ReflectionEngine::new(None);
let memories = vec![
MemoryEntryForAnalysis {
memory_type: "task".to_string(),
content: "Task 1".to_string(),
importance: 7,
access_count: 1,
tags: vec![],
},
MemoryEntryForAnalysis {
memory_type: "task".to_string(),
content: "Task 2".to_string(),
importance: 8,
access_count: 2,
tags: vec![],
},
];
let patterns = engine.analyze_patterns(&memories);
// Should not trigger (only 2 tasks, threshold is 5)
assert!(!patterns.iter().any(|p| p.observation.contains("待办任务")));
}
}