//! Intelligence Hooks - Pre/Post conversation integration //! //! Bridges the intelligence layer modules (identity, memory, heartbeat, reflection) //! into the kernel's chat flow at the Tauri command boundary. //! //! Architecture: kernel_commands.rs → intelligence_hooks → intelligence modules → Viking/Kernel use tracing::debug; use crate::intelligence::identity::IdentityManagerState; use crate::intelligence::heartbeat::HeartbeatEngineState; use crate::intelligence::reflection::{MemoryEntryForAnalysis, ReflectionEngineState}; /// Run pre-conversation intelligence hooks /// /// 1. Build memory context from VikingStorage (FTS5 + TF-IDF + Embedding) /// 2. Build identity-enhanced system prompt (SOUL.md + instructions) /// /// Returns the enhanced system prompt that should be passed to the kernel. pub async fn pre_conversation_hook( agent_id: &str, user_message: &str, identity_state: &IdentityManagerState, ) -> Result { // Step 1: Build memory context from Viking storage let memory_context = build_memory_context(agent_id, user_message).await .unwrap_or_default(); // Step 2: Build identity-enhanced system prompt let enhanced_prompt = build_identity_prompt(agent_id, &memory_context, identity_state) .await .unwrap_or_default(); Ok(enhanced_prompt) } /// Run post-conversation intelligence hooks /// /// 1. Record interaction for heartbeat engine /// 2. Record conversation for reflection engine, trigger reflection if needed pub async fn post_conversation_hook( agent_id: &str, _user_message: &str, _heartbeat_state: &HeartbeatEngineState, reflection_state: &ReflectionEngineState, ) { // Step 1: Record interaction for heartbeat crate::intelligence::heartbeat::record_interaction(agent_id); debug!("[intelligence_hooks] Recorded interaction for agent: {}", agent_id); // Step 2: Record conversation for reflection let mut engine = reflection_state.lock().await; // Apply restored state on first call (one-shot after app restart) if let Some(restored_state) = crate::intelligence::reflection::pop_restored_state(agent_id) { engine.apply_restored_state(restored_state); } if let Some(restored_result) = crate::intelligence::reflection::pop_restored_result(agent_id) { engine.apply_restored_result(restored_result); } engine.record_conversation(); debug!( "[intelligence_hooks] Conversation count updated for agent: {}", agent_id ); if engine.should_reflect() { debug!( "[intelligence_hooks] Reflection threshold reached for agent: {}", agent_id ); // Query actual memories from VikingStorage for reflection analysis let memories = query_memories_for_reflection(agent_id).await .unwrap_or_default(); debug!( "[intelligence_hooks] Fetched {} memories for reflection", memories.len() ); let reflection_result = engine.reflect(agent_id, &memories); debug!( "[intelligence_hooks] Reflection completed: {} patterns, {} suggestions", reflection_result.patterns.len(), reflection_result.improvements.len() ); } } /// Build memory context by searching VikingStorage for relevant memories async fn build_memory_context( agent_id: &str, user_message: &str, ) -> Result { // Try Viking storage (has FTS5 + TF-IDF + Embedding) let storage = crate::viking_commands::get_storage().await?; // FindOptions from zclaw_growth let options = zclaw_growth::FindOptions { scope: Some(format!("agent://{}", agent_id)), limit: Some(8), min_similarity: Some(0.2), }; // find is on the VikingStorage trait — call via trait to dispatch correctly let results: Vec = zclaw_growth::VikingStorage::find(storage.as_ref(), user_message, options) .await .map_err(|e| format!("Memory search failed: {}", e))?; if results.is_empty() { return Ok(String::new()); } // Format memories into context string let mut context = String::from("## 相关记忆\n\n"); let mut token_estimate: usize = 0; let max_tokens: usize = 500; for entry in &results { // Prefer overview (L1 summary) over full content // overview is Option — use as_deref to get Option<&str> let overview_str = entry.overview.as_deref().unwrap_or(""); let text = if !overview_str.is_empty() { overview_str } else { &entry.content }; // Truncate long entries let truncated = if text.len() > 100 { format!("{}...", &text[..100]) } else { text.to_string() }; // Simple token estimate (~1.5 tokens per CJK char, ~0.25 per other) let tokens: usize = truncated.chars() .map(|c: char| if c.is_ascii() { 1 } else { 2 }) .sum(); if token_estimate + tokens > max_tokens { break; } context.push_str(&format!("- [{}] {}\n", entry.memory_type, truncated)); token_estimate += tokens; } Ok(context) } /// Build identity-enhanced system prompt async fn build_identity_prompt( agent_id: &str, memory_context: &str, identity_state: &IdentityManagerState, ) -> Result { // IdentityManagerState is Arc> // tokio::sync::Mutex::lock() returns MutexGuard directly let mut manager = identity_state.lock().await; let prompt = manager.build_system_prompt( agent_id, if memory_context.is_empty() { None } else { Some(memory_context) }, ); Ok(prompt) } /// Query agent memories from VikingStorage and convert to MemoryEntryForAnalysis /// for the reflection engine. /// /// Fetches up to 50 recent memories scoped to the given agent, without token /// truncation (unlike build_memory_context which is size-limited for prompts). async fn query_memories_for_reflection( agent_id: &str, ) -> Result, String> { let storage = crate::viking_commands::get_storage().await?; let options = zclaw_growth::FindOptions { scope: Some(format!("agent://{}", agent_id)), limit: Some(50), min_similarity: Some(0.0), // Fetch all, no similarity filter }; let results: Vec = zclaw_growth::VikingStorage::find(storage.as_ref(), "", options) .await .map_err(|e| format!("Memory query for reflection failed: {}", e))?; let memories: Vec = results .into_iter() .map(|entry| MemoryEntryForAnalysis { memory_type: entry.memory_type.to_string(), content: entry.content, importance: entry.importance as usize, access_count: entry.access_count as usize, tags: entry.keywords, }) .collect(); Ok(memories) }