1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! AskUserQuestion tool: interactive prompts during execution.
//!
//! Allows the agent to ask the user questions and collect structured
//! responses. Used for gathering preferences, clarifying ambiguity,
//! and making implementation decisions.
use async_trait::async_trait;
use serde_json::json;
use std::io::Write;
use super::{Tool, ToolContext, ToolResult};
use crate::error::ToolError;
pub struct AskUserQuestionTool;
#[async_trait]
impl Tool for AskUserQuestionTool {
fn name(&self) -> &'static str {
"AskUserQuestion"
}
fn description(&self) -> &'static str {
"Ask the user a question to gather preferences or clarify requirements. \
Present 2-4 options for the user to choose from."
}
fn input_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"required": ["questions"],
"properties": {
"questions": {
"type": "array",
"description": "Questions to ask (1-4)",
"items": {
"type": "object",
"required": ["question", "options"],
"properties": {
"question": {
"type": "string",
"description": "The question to ask"
},
"options": {
"type": "array",
"description": "Available choices (2-4)",
"items": {
"type": "object",
"required": ["label", "description"],
"properties": {
"label": {
"type": "string",
"description": "Short choice label"
},
"description": {
"type": "string",
"description": "What this choice means"
}
}
}
}
}
}
}
}
})
}
fn is_read_only(&self) -> bool {
true
}
fn is_concurrency_safe(&self) -> bool {
false // Requires user interaction.
}
async fn call(
&self,
input: serde_json::Value,
_ctx: &ToolContext,
) -> Result<ToolResult, ToolError> {
let questions = input
.get("questions")
.and_then(|v| v.as_array())
.ok_or_else(|| ToolError::InvalidInput("'questions' array is required".into()))?;
let mut answers = Vec::new();
for q in questions {
let question_text = q.get("question").and_then(|v| v.as_str()).unwrap_or("?");
let options = q
.get("options")
.and_then(|v| v.as_array())
.ok_or_else(|| ToolError::InvalidInput("'options' array required".into()))?;
// Display the question.
eprintln!("\n{question_text}");
for (i, opt) in options.iter().enumerate() {
let label = opt.get("label").and_then(|v| v.as_str()).unwrap_or("?");
let desc = opt
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("");
let letter = (b'A' + i as u8) as char;
eprintln!(" {letter}) {label} — {desc}");
}
eprint!("Choice: ");
let _ = std::io::stderr().flush();
// Read user input from stdin.
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to read input: {e}")))?;
let choice = line.trim().to_uppercase();
// Map letter to option label.
let selected = if choice.len() == 1 {
let idx = choice.as_bytes()[0].wrapping_sub(b'A') as usize;
if idx < options.len() {
options[idx]
.get("label")
.and_then(|v| v.as_str())
.unwrap_or(&choice)
.to_string()
} else {
choice
}
} else {
choice
};
answers.push(format!("{question_text}={selected}"));
}
let result = format!(
"User has answered your questions: {}. You can now continue with the user's answers in mind.",
answers
.iter()
.map(|a| format!("\"{a}\""))
.collect::<Vec<_>>()
.join(", ")
);
Ok(ToolResult::success(result))
}
}