feat: ask_clarification tool + clarification system prompt + progressive skill loading fix
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- New ask_clarification tool (crates/zclaw-runtime/src/tool/builtin/ask_clarification.rs)
  with 5 clarification types: missing_info, ambiguous_requirement, approach_choice, risk_confirmation, suggestion
- Registered as built-in tool in builtin.rs
- Added clarification system prompt instructions to messaging.rs system prompt
- Fixed messaging.rs skill injection: when SkillIndexMiddleware is active,
  only inject usage instructions (not full skill list), avoiding duplicate injection
- Fixed pre-existing unicode arrow character causing string literal parse error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-06 13:19:10 +08:00
parent c3ab7985d2
commit 14c3c963c2
3 changed files with 161 additions and 1 deletions

View File

@@ -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
}

View File

@@ -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()));
}

View File

@@ -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<Value> {
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<String> = 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()
}
}