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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//! `ask_user` tool: lets the agent ask the human a single clarifying question
//! when the task direction is genuinely ambiguous.
//!
//! The tool is **rate-limited** (max `max_asks` per session), **user-disableable**
//! via `UiConfig::allow_clarification`, and **safe in headless mode** — it never
//! blocks on stdin when there is no TTY, instead returning a null answer so the
//! agent can proceed with its best assumption.
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde_json::Value;
use std::io::IsTerminal;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::tools::Tool;
/// A tool that asks the human user one clarifying question.
///
/// Construct with [`ClarificationTool::new`] passing `enabled` (from config) and
/// `max_asks` (session-level rate limit). The internal `asked` counter is atomic
/// so the tool is safe to share across async tasks.
pub struct ClarificationTool {
enabled: bool,
max_asks: u32,
asked: AtomicU32,
}
impl ClarificationTool {
/// Create a new clarification tool.
///
/// * `enabled` — whether user clarification is allowed at all (config flag).
/// * `max_asks` — maximum number of questions permitted per session.
pub fn new(enabled: bool, max_asks: u32) -> Self {
Self {
enabled,
max_asks,
asked: AtomicU32::new(0),
}
}
/// Returns how many questions have been asked so far (mainly for testing).
pub fn asked_count(&self) -> u32 {
self.asked.load(Ordering::SeqCst)
}
}
impl Default for ClarificationTool {
fn default() -> Self {
Self::new(true, 3)
}
}
#[async_trait]
impl Tool for ClarificationTool {
fn name(&self) -> &str {
"ask_user"
}
fn description(&self) -> &str {
"Ask the human user ONE concise clarifying question when the task \
direction is genuinely ambiguous and you cannot proceed with a \
reasonable assumption. Use very sparingly — most tasks can be \
completed by making a sensible default choice. The response \
contains an \"answer\" field (string or null) and a \"note\" field \
with guidance when no answer is available."
}
fn schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "A single, concise clarifying question for the user. \
Keep it under 200 characters when possible."
}
},
"required": ["question"]
})
}
async fn execute(&self, args: Value) -> Result<Value> {
let question = args
.get("question")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required field: question"))?;
// --- Guard 1: disabled by config ---
if !self.enabled {
return Ok(serde_json::json!({
"answer": null,
"note": "user clarification is disabled; proceed with your best assumption"
}));
}
// --- Guard 2: rate limit exhausted ---
if self.asked.load(Ordering::SeqCst) >= self.max_asks {
return Ok(serde_json::json!({
"answer": null,
"note": "clarification limit reached for this session; proceed with your best assumption"
}));
}
// --- Guard 3: TUI mode (NEVER block on stdin — the TUI owns the ---
// --- terminal in raw mode, so a blocking read_line() would fight ---
// --- its event loop). We check the same global atomic the rest ---
// --- of the agent uses (`crate::output::is_tui_active()`). ---
// Checked BEFORE the headless guard because the TUI may have a
// terminal stdin (is_terminal() == true) but still must not block.
if crate::output::is_tui_active() {
return Ok(serde_json::json!({
"answer": null,
"note": "running in TUI mode; interactive clarification is not available — proceed with your best assumption"
}));
}
// --- Guard 4: headless / non-interactive (NEVER block on stdin) ---
if !std::io::stdin().is_terminal() {
return Ok(serde_json::json!({
"answer": null,
"note": "running non-interactively; no user available — proceed with your best assumption"
}));
}
// --- Interactive path: ask the user ---
// Increment the counter *before* reading so a concurrent call cannot
// exceed the limit.
self.asked.fetch_add(1, Ordering::SeqCst);
eprintln!();
eprintln!("┌─ Clarification requested ─────────────────────────────");
eprintln!("│ {question}");
eprintln!("└─ (type your answer and press Enter, or empty to skip) ─");
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(|e| anyhow!("failed to read user input: {e}"))?;
let trimmed = line.trim();
if trimmed.is_empty() {
return Ok(serde_json::json!({
"answer": null,
"note": "user skipped the question; proceed with your best assumption"
}));
}
Ok(serde_json::json!({ "answer": trimmed }))
}
fn metadata(&self) -> crate::safety::ToolMetadata {
crate::safety::ToolMetadata {
read_only: true,
destructive: false,
risk_level: crate::safety::RiskLevel::Low,
network_access: false,
shell_execution: false,
}
}
}
#[cfg(test)]
#[path = "../../tests/unit/tools/clarify/clarify_test.rs"]
mod tests;