Files
zclaw_openfang/crates/zclaw-runtime/src/tool/builtin/ask_clarification.rs
iven 9060935401 perf(runtime): Hermes Phase 1-3 — prompt caching + parallel tools + smart retry
Phase 1: Anthropic prompt caching
- Add cache_control ephemeral on system prompt blocks
- Track cache_creation/cache_read tokens in CompletionResponse + StreamChunk

Phase 2A: Parallel tool execution
- Add ToolConcurrency enum (ReadOnly/Exclusive/Interactive)
- JoinSet + Semaphore(3) for bounded parallel tool calls
- 7 tools annotated with correct concurrency level
- AtomicU32 for lock-free failure tracking in ToolErrorMiddleware

Phase 2B: Tool output pruning
- prune_tool_outputs() trims old ToolResult > 2000 chars to 500 chars
- Integrated into CompactionMiddleware before token estimation

Phase 3: Error classification + smart retry
- LlmErrorKind + ClassifiedLlmError for structured error mapping
- RetryDriver decorator with jittered exponential backoff
- Kernel wraps all LLM calls with RetryDriver
- CONTEXT_OVERFLOW recovery triggers emergency compaction in loop_runner
2026-04-24 08:39:56 +08:00

149 lines
5.1 KiB
Rust

//! 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, ToolConcurrency};
/// 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"]
})
}
fn concurrency(&self) -> ToolConcurrency {
ToolConcurrency::Interactive
}
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()
}
}