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
149 lines
5.1 KiB
Rust
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()
|
|
}
|
|
}
|