diff --git a/crates/zclaw-kernel/src/kernel/messaging.rs b/crates/zclaw-kernel/src/kernel/messaging.rs index 2b52640..0962d45 100644 --- a/crates/zclaw-kernel/src/kernel/messaging.rs +++ b/crates/zclaw-kernel/src/kernel/messaging.rs @@ -275,7 +275,7 @@ impl Kernel { 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"); + prompt.push_str("User: 分析腾讯财报 -> Intent: Financial analysis -> Call: execute_skill(\"finance-tracker\", {...})\n"); } } @@ -294,6 +294,19 @@ impl Kernel { 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 } diff --git a/crates/zclaw-runtime/src/tool/builtin.rs b/crates/zclaw-runtime/src/tool/builtin.rs index 0497a42..92fe20f 100644 --- a/crates/zclaw-runtime/src/tool/builtin.rs +++ b/crates/zclaw-runtime/src/tool/builtin.rs @@ -8,6 +8,7 @@ mod execute_skill; mod skill_load; mod path_validator; mod task; +mod ask_clarification; pub use file_read::FileReadTool; pub use file_write::FileWriteTool; @@ -17,6 +18,7 @@ pub use execute_skill::ExecuteSkillTool; pub use skill_load::SkillLoadTool; pub use path_validator::{PathValidator, PathValidatorConfig}; pub use task::TaskTool; +pub use ask_clarification::AskClarificationTool; use crate::tool::ToolRegistry; @@ -28,4 +30,5 @@ pub fn register_builtin_tools(registry: &mut ToolRegistry) { registry.register(Box::new(WebFetchTool::new())); registry.register(Box::new(ExecuteSkillTool::new())); registry.register(Box::new(SkillLoadTool::new())); + registry.register(Box::new(AskClarificationTool::new())); } diff --git a/crates/zclaw-runtime/src/tool/builtin/ask_clarification.rs b/crates/zclaw-runtime/src/tool/builtin/ask_clarification.rs new file mode 100644 index 0000000..d0f8dac --- /dev/null +++ b/crates/zclaw-runtime/src/tool/builtin/ask_clarification.rs @@ -0,0 +1,144 @@ +//! Ask clarification tool — allows the LLM to ask the user for clarification +//! instead of guessing when requirements are ambiguous. +//! +//! Inspired by DeerFlow's `ask_clarification` + ClarificationMiddleware pattern. +//! When the LLM encounters an ambiguous or incomplete request, it calls this tool +//! to present a structured question to the user and halt execution. + +use async_trait::async_trait; +use serde_json::{json, Value}; +use zclaw_types::{Result, ZclawError}; + +use crate::tool::{Tool, ToolContext}; + +/// Clarification type — categorizes the reason for asking. +#[derive(Debug, Clone, PartialEq)] +pub enum ClarificationType { + /// Missing information needed to proceed + MissingInfo, + /// Requirement is ambiguous — multiple interpretations possible + AmbiguousRequirement, + /// Multiple approaches available — user needs to choose + ApproachChoice, + /// Risky operation needs explicit confirmation + RiskConfirmation, + /// Agent has a suggestion but wants user approval + Suggestion, +} + +impl ClarificationType { + fn from_str(s: &str) -> Self { + match s { + "missing_info" => Self::MissingInfo, + "ambiguous_requirement" => Self::AmbiguousRequirement, + "approach_choice" => Self::ApproachChoice, + "risk_confirmation" => Self::RiskConfirmation, + "suggestion" => Self::Suggestion, + _ => Self::MissingInfo, + } + } + + fn as_str(&self) -> &'static str { + match self { + Self::MissingInfo => "missing_info", + Self::AmbiguousRequirement => "ambiguous_requirement", + Self::ApproachChoice => "approach_choice", + Self::RiskConfirmation => "risk_confirmation", + Self::Suggestion => "suggestion", + } + } +} + +pub struct AskClarificationTool; + +impl AskClarificationTool { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl Tool for AskClarificationTool { + fn name(&self) -> &str { + "ask_clarification" + } + + fn description(&self) -> &str { + "Ask the user for clarification when the request is ambiguous, incomplete, \ + or involves a choice between approaches. Use this instead of guessing. \ + The conversation will pause and wait for the user's response." + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The specific question to ask the user" + }, + "clarification_type": { + "type": "string", + "enum": ["missing_info", "ambiguous_requirement", "approach_choice", "risk_confirmation", "suggestion"], + "description": "The type of clarification needed" + }, + "options": { + "type": "array", + "items": { "type": "string" }, + "description": "Suggested options for the user to choose from (optional)" + }, + "context": { + "type": "string", + "description": "Brief context about why clarification is needed" + } + }, + "required": ["question", "clarification_type"] + }) + } + + async fn execute(&self, input: Value, _context: &ToolContext) -> Result { + let question = input["question"].as_str() + .ok_or_else(|| ZclawError::InvalidInput("Missing 'question' parameter".into()))?; + + let clarification_type = input["clarification_type"].as_str() + .map(ClarificationType::from_str) + .unwrap_or(ClarificationType::MissingInfo); + + let options: Vec = input["options"].as_array() + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default(); + + let context = input["context"].as_str().unwrap_or(""); + + tracing::info!( + "[AskClarification] type={}, question={:?}, options={:?}", + clarification_type.as_str(), question, options + ); + + // Return a structured result that the frontend can render as a clarification card. + // The LLM will see this result and incorporate the user's answer in the next turn. + let mut result = json!({ + "status": "clarification_needed", + "clarification_type": clarification_type.as_str(), + "question": question, + }); + + if !options.is_empty() { + result["options"] = json!(options); + } + if !context.is_empty() { + result["context"] = json!(context); + } + + // Include a note telling the LLM to wait for the user's response + result["instruction"] = json!("已向用户提问。请等待用户回复后继续。在用户回复前,不要采取任何行动。"); + + Ok(result) + } +} + +impl Default for AskClarificationTool { + fn default() -> Self { + Self::new() + } +}