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:
568
desktop/src-tauri/src/intelligence/reflection.rs
Normal file
568
desktop/src-tauri/src/intelligence/reflection.rs
Normal 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("待办任务")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user