use super::error::Result;
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use crate::brain::agent::FollowUpQuestionInfo;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::Value;
pub const MAX_OPTIONS: usize = 8;
pub struct FollowUpQuestionTool;
#[derive(Debug, Deserialize)]
struct FollowUpInput {
question: String,
options: Vec<String>,
}
#[async_trait]
impl Tool for FollowUpQuestionTool {
fn name(&self) -> &str {
"follow_up_question"
}
fn description(&self) -> &str {
"Ask the user a discrete-choice question with up to 8 button options. \
Use this ONLY when you cannot proceed without the user picking from a short \
list of specific values (e.g. \"which file did you mean?\", \"target environment?\"). \
Do not use it for general questions, confirmations (use the normal approval flow), \
or anything you could resolve yourself by reading code or running a tool. Returns \
the chosen option string. Call this tool silently. Do not repeat the question or \
options in surrounding prose."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to display above the option buttons. Keep it under 200 chars.",
"maxLength": 500
},
"options": {
"type": "array",
"items": { "type": "string" },
"minItems": 2,
"maxItems": MAX_OPTIONS,
"description": "Between 2 and 8 distinct option strings. Each becomes one clickable button. Recommended under 40 chars for clean rendering on all channels."
}
},
"required": ["question", "options"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![]
}
fn requires_approval(&self) -> bool {
false
}
async fn execute(&self, input: Value, context: &ToolExecutionContext) -> Result<ToolResult> {
let parsed: FollowUpInput = serde_json::from_value(input)?;
let question = parsed.question.trim();
if question.is_empty() {
return Ok(ToolResult::error(
"follow_up_question requires a non-empty question.".into(),
));
}
let options: Vec<String> = parsed
.options
.into_iter()
.map(|o| o.trim().to_string())
.filter(|o| !o.is_empty())
.collect();
if options.len() < 2 {
return Ok(ToolResult::error(
"follow_up_question needs at least 2 non-empty options. If you only have one \
option, just do it instead of asking."
.into(),
));
}
if options.len() > MAX_OPTIONS {
return Ok(ToolResult::error(format!(
"Too many options ({}). Cap is {}. Narrow the question.",
options.len(),
MAX_OPTIONS
)));
}
let mut seen = std::collections::HashSet::new();
for opt in &options {
if !seen.insert(opt.as_str()) {
return Ok(ToolResult::error(format!(
"Duplicate option '{}'. Options must be distinct.",
opt
)));
}
}
let cb = match context.question_callback.as_ref() {
Some(c) => c.clone(),
None => {
return Ok(ToolResult::error(
"This channel does not support follow_up_question (no interactive surface). \
Ask the question in plain text instead."
.into(),
));
}
};
let info = FollowUpQuestionInfo {
session_id: context.session_id,
question: question.to_string(),
options: options.clone(),
};
match cb(info).await {
Ok(answer) => Ok(ToolResult::success(format!("User chose: {}", answer))),
Err(e) => Ok(ToolResult::error(format!(
"follow_up_question failed: {}",
e
))),
}
}
}