//! Message sending (non-streaming, streaming, system prompt building) use tokio::sync::mpsc; use zclaw_types::{AgentId, Result}; /// Chat mode configuration passed from the frontend. /// Controls thinking, reasoning, plan mode, and sub-agent behavior. #[derive(Debug, Clone)] pub struct ChatModeConfig { pub thinking_enabled: Option, pub reasoning_effort: Option, pub plan_mode: Option, pub subagent_enabled: Option, } use zclaw_runtime::{AgentLoop, tool::builtin::PathValidator}; use super::Kernel; use super::super::MessageResponse; impl Kernel { /// Send a message to an agent pub async fn send_message( &self, agent_id: &AgentId, message: String, ) -> Result { self.send_message_with_chat_mode(agent_id, message, None).await } /// Send a message to an agent with optional chat mode configuration pub async fn send_message_with_chat_mode( &self, agent_id: &AgentId, message: String, chat_mode: Option, ) -> Result { let agent_config = self.registry.get(agent_id) .ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?; // Create or get session let session_id = self.memory.create_session(agent_id).await?; // Always use Kernel's current model configuration // This ensures user's "模型与 API" settings are respected let model = self.config.model().to_string(); // Create agent loop with model configuration let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); let tools = self.create_tool_registry(subagent_enabled); let mut loop_runner = AgentLoop::new( *agent_id, self.driver.clone(), tools, self.memory.clone(), ) .with_model(&model) .with_skill_executor(self.skill_executor.clone()) .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())) .with_compaction_threshold( agent_config.compaction_threshold .map(|t| t as usize) .unwrap_or_else(|| self.config.compaction_threshold()), ); // Set path validator from agent's workspace directory (if configured) if let Some(ref workspace) = agent_config.workspace { let path_validator = PathValidator::new().with_workspace(workspace.clone()); tracing::info!( "[Kernel] Setting path_validator with workspace: {} for agent {}", workspace.display(), agent_id ); loop_runner = loop_runner.with_path_validator(path_validator); } // Inject middleware chain if available if let Some(chain) = self.create_middleware_chain() { loop_runner = loop_runner.with_middleware_chain(chain); } // Apply chat mode configuration (thinking/reasoning/plan mode) if let Some(ref mode) = chat_mode { if mode.thinking_enabled.unwrap_or(false) { loop_runner = loop_runner.with_thinking_enabled(true); } if let Some(ref effort) = mode.reasoning_effort { loop_runner = loop_runner.with_reasoning_effort(effort.clone()); } if mode.plan_mode.unwrap_or(false) { loop_runner = loop_runner.with_plan_mode(true); } } // Build system prompt with skill information injected let system_prompt = self.build_system_prompt_with_skills( agent_config.system_prompt.as_ref(), subagent_enabled, ).await; let loop_runner = loop_runner.with_system_prompt(&system_prompt); // Run the loop let result = loop_runner.run(session_id, message).await?; // Track message count self.registry.increment_message_count(agent_id); Ok(MessageResponse { content: result.response, input_tokens: result.input_tokens, output_tokens: result.output_tokens, }) } /// Send a message with streaming pub async fn send_message_stream( &self, agent_id: &AgentId, message: String, ) -> Result> { self.send_message_stream_with_prompt(agent_id, message, None, None, None).await } /// Send a message with streaming, optional system prompt, optional session reuse, /// and optional chat mode configuration (thinking/reasoning/plan mode). pub async fn send_message_stream_with_prompt( &self, agent_id: &AgentId, message: String, system_prompt_override: Option, session_id_override: Option, chat_mode: Option, ) -> Result> { let agent_config = self.registry.get(agent_id) .ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?; // Reuse existing session or create new one let session_id = match session_id_override { Some(id) => { // Use get_or_create to ensure the frontend's session ID is persisted. // This is the critical bridge: without it, the kernel generates a // different UUID each turn, so conversation history is never found. tracing::debug!("Reusing frontend session ID: {}", id); self.memory.get_or_create_session(&id, agent_id).await? } None => self.memory.create_session(agent_id).await?, }; // Always use Kernel's current model configuration // This ensures user's "模型与 API" settings are respected let model = self.config.model().to_string(); // Create agent loop with model configuration let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false); let tools = self.create_tool_registry(subagent_enabled); let mut loop_runner = AgentLoop::new( *agent_id, self.driver.clone(), tools, self.memory.clone(), ) .with_model(&model) .with_skill_executor(self.skill_executor.clone()) .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())) .with_compaction_threshold( agent_config.compaction_threshold .map(|t| t as usize) .unwrap_or_else(|| self.config.compaction_threshold()), ); // Set path validator from agent's workspace directory (if configured) // This enables file_read / file_write tools to access the workspace if let Some(ref workspace) = agent_config.workspace { let path_validator = PathValidator::new().with_workspace(workspace.clone()); tracing::info!( "[Kernel] Setting path_validator with workspace: {} for agent {}", workspace.display(), agent_id ); loop_runner = loop_runner.with_path_validator(path_validator); } // Inject middleware chain if available if let Some(chain) = self.create_middleware_chain() { loop_runner = loop_runner.with_middleware_chain(chain); } // Apply chat mode configuration (thinking/reasoning/plan mode from frontend) if let Some(ref mode) = chat_mode { if mode.thinking_enabled.unwrap_or(false) { loop_runner = loop_runner.with_thinking_enabled(true); } if let Some(ref effort) = mode.reasoning_effort { loop_runner = loop_runner.with_reasoning_effort(effort.clone()); } if mode.plan_mode.unwrap_or(false) { loop_runner = loop_runner.with_plan_mode(true); } } // Use external prompt if provided, otherwise build default let system_prompt = match system_prompt_override { Some(prompt) => prompt, None => self.build_system_prompt_with_skills( agent_config.system_prompt.as_ref(), subagent_enabled, ).await, }; let loop_runner = loop_runner.with_system_prompt(&system_prompt); // Run with streaming self.registry.increment_message_count(agent_id); loop_runner.run_streaming(session_id, message).await } /// Build a system prompt with skill information injected. /// When `subagent_enabled` is true, adds sub-agent delegation instructions. pub(super) async fn build_system_prompt_with_skills( &self, base_prompt: Option<&String>, subagent_enabled: bool, ) -> String { // Get skill list asynchronously let skills = self.skills.list().await; let mut prompt = base_prompt .map(|p| p.clone()) .unwrap_or_else(|| "You are a helpful AI assistant.".to_string()); // Progressive skill loading (DeerFlow pattern): // If the SkillIndexMiddleware is registered in the middleware chain, // it will inject a lightweight index at priority 200. // We still inject a basic instruction block here for when middleware is not active. // // When middleware IS active, avoid duplicate injection by only keeping // the skill-use instructions (not the full list). let skill_index_active = { use zclaw_runtime::tool::SkillExecutor; !self.skill_executor.list_skill_index().is_empty() }; if !skills.is_empty() { if skill_index_active { // Middleware will inject the index — only add usage instructions prompt.push_str("\n\n## Skills\n\n"); prompt.push_str("You have access to specialized skills listed in the skill index above. "); prompt.push_str("Analyze user intent and autonomously call `skill_load` to inspect a skill, "); prompt.push_str("then `execute_skill` with the appropriate skill_id.\n\n"); prompt.push_str("- **IMPORTANT**: Autonomously decide when to use skills based on user intent.\n"); prompt.push_str("- Do not wait for explicit skill names — recognize the need and act.\n"); prompt.push_str("- If unsure about a skill, call `skill_load` first to understand its parameters.\n"); } else { // No middleware — inject full skill list as fallback prompt.push_str("\n\n## Available Skills\n\n"); prompt.push_str("You have access to specialized skills. Analyze user intent and autonomously call `execute_skill` with the appropriate skill_id.\n\n"); let categories = self.categorize_skills(&skills); for (category, category_skills) in categories { prompt.push_str(&format!("### {}\n", category)); for skill in category_skills { prompt.push_str(&format!( "- **{}**: {}", skill.id.as_str(), skill.description )); prompt.push('\n'); } prompt.push('\n'); } prompt.push_str("### When to use skills:\n"); prompt.push_str("- **IMPORTANT**: You should autonomously decide when to use skills based on your understanding of the user's intent.\n"); prompt.push_str("- Do not wait for explicit skill names - recognize the need and act.\n"); prompt.push_str("- Match user's request to the most appropriate skill's domain.\n\n"); prompt.push_str("### Example:\n"); prompt.push_str("User: 分析腾讯财报 -> Intent: Financial analysis -> Call: execute_skill(\"finance-tracker\", {...})\n"); } } // Sub-agent delegation instructions (Ultra mode only) if subagent_enabled { prompt.push_str("\n\n## Sub-Agent Delegation\n\n"); prompt.push_str("You can delegate complex sub-tasks to sub-agents using the `task` tool. This enables parallel execution of independent work.\n\n"); prompt.push_str("### When to use sub-agents:\n"); prompt.push_str("- Complex tasks that can be decomposed into independent parallel sub-tasks\n"); prompt.push_str("- Research tasks requiring multiple independent searches\n"); prompt.push_str("- Tasks requiring different expertise areas simultaneously\n\n"); prompt.push_str("### Guidelines:\n"); prompt.push_str("- Break complex work into clear, self-contained sub-tasks\n"); prompt.push_str("- Each sub-task should have a clear objective and expected output\n"); prompt.push_str("- Synthesize sub-agent results into a coherent final response\n"); prompt.push_str("- Maximum 3 concurrent sub-agents — batch if more are needed\n"); } // Clarification system — always enabled prompt.push_str("\n\n## Clarification System\n\n"); prompt.push_str("When you encounter any of the following situations, call `ask_clarification` to ask the user BEFORE proceeding:\n\n"); prompt.push_str("- **Missing information**: User's request is critical details you you need but don't have\n"); prompt.push_str("- **Ambiguous requirement**: Multiple valid interpretations exist\n"); prompt.push_str("- **Approach choice**: Several approaches with different trade-offs\n"); prompt.push_str("- **Risk confirmation**: Action could have significant consequences\n\n"); prompt.push_str("### Guidelines:\n"); prompt.push_str("- ALWAYS prefer asking over guessing\n"); prompt.push_str("- Provide clear options when possible\n"); prompt.push_str("- Include brief context about why you're asking\n"); prompt.push_str("- After receiving clarification, proceed immediately\n"); prompt } /// Categorize skills into logical groups /// /// Priority: /// 1. Use skill's `category` field if defined in SKILL.md /// 2. Fall back to pattern matching for backward compatibility pub(super) fn categorize_skills<'a>(&self, skills: &'a [zclaw_skills::SkillManifest]) -> Vec<(String, Vec<&'a zclaw_skills::SkillManifest>)> { let mut categories: std::collections::HashMap> = std::collections::HashMap::new(); // Fallback category patterns for skills without explicit category let fallback_patterns = [ ("开发工程", vec!["senior-developer", "frontend-developer", "backend-architect", "ai-engineer", "devops-automator", "rapid-prototyper", "lsp-index-engineer"]), ("测试质量", vec!["api-tester", "evidence-collector", "reality-checker", "performance-benchmarker", "test-results-analyzer", "accessibility-auditor", "code-review"]), ("安全合规", vec!["security-engineer", "legal-compliance-checker", "agentic-identity-trust"]), ("数据分析", vec!["analytics-reporter", "finance-tracker", "data-analysis", "sales-data-extraction-agent", "data-consolidation-agent", "report-distribution-agent"]), ("项目管理", vec!["senior-pm", "project-shepherd", "sprint-prioritizer", "experiment-tracker", "feedback-synthesizer", "trend-researcher", "agents-orchestrator"]), ("设计UX", vec!["ui-designer", "ux-architect", "ux-researcher", "visual-storyteller", "image-prompt-engineer", "whimsy-injector", "brand-guardian"]), ("内容营销", vec!["content-creator", "chinese-writing", "executive-summary-generator", "social-media-strategist"]), ("社交平台", vec!["twitter-engager", "instagram-curator", "tiktok-strategist", "reddit-community-builder", "zhihu-strategist", "xiaohongshu-specialist", "wechat-official-account", "growth-hacker", "app-store-optimizer"]), ("运营支持", vec!["studio-operations", "studio-producer", "support-responder", "workflow-optimizer", "infrastructure-maintainer", "tool-evaluator"]), ("XR/空间计算", vec!["visionos-spatial-engineer", "macos-spatial-metal-engineer", "xr-immersive-developer", "xr-interface-architect", "xr-cockpit-interaction-specialist", "terminal-integration-specialist"]), ("基础工具", vec!["web-search", "file-operations", "shell-command", "git", "translation", "feishu-docs"]), ]; // Categorize each skill for skill in skills { // Priority 1: Use skill's explicit category if let Some(ref category) = skill.category { if !category.is_empty() { categories.entry(category.clone()).or_default().push(skill); continue; } } // Priority 2: Fallback to pattern matching let skill_id = skill.id.as_str(); let mut categorized = false; for (category, patterns) in &fallback_patterns { if patterns.iter().any(|p| skill_id.contains(p) || *p == skill_id) { categories.entry(category.to_string()).or_default().push(skill); categorized = true; break; } } // Put uncategorized skills in "其他" if !categorized { categories.entry("其他".to_string()).or_default().push(skill); } } // Convert to ordered vector let mut result: Vec<(String, Vec<_>)> = categories.into_iter().collect(); result.sort_by(|a, b| { // Sort by predefined order let order = ["开发工程", "测试质量", "安全合规", "数据分析", "项目管理", "设计UX", "内容营销", "社交平台", "运营支持", "XR/空间计算", "基础工具", "其他"]; let a_idx = order.iter().position(|&x| x == a.0).unwrap_or(99); let b_idx = order.iter().position(|&x| x == b.0).unwrap_or(99); a_idx.cmp(&b_idx) }); result } }