Skip to main content

forge_primitives/
question.rs

1//! UI-side `AskUserQuestion` request/response shapes - what the
2//! agent surfaces to the UI when a tool wants structured user input,
3//! and what the user picks.
4
5use serde::{Deserialize, Serialize};
6
7use crate::session_update::ToolCall;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct QuestionOption {
11    pub option_id: String,
12    pub label: String,
13    pub description: Option<String>,
14    pub preview: Option<String>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct QuestionPrompt {
19    pub question: String,
20    pub header: String,
21    pub multi_select: bool,
22    pub options: Vec<QuestionOption>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct QuestionRequest {
27    pub tool_call: ToolCall,
28    pub prompt: QuestionPrompt,
29    pub question_index: u64,
30    pub total_questions: u64,
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct QuestionAnnotation {
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub preview: Option<String>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub notes: Option<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(tag = "outcome", rename_all = "snake_case")]
43pub enum QuestionOutcome {
44    Answered { selected_option_ids: Vec<String>, annotation: Option<QuestionAnnotation> },
45    Cancelled,
46}
47
48#[cfg(test)]
49mod tests {
50    use super::QuestionAnnotation;
51
52    #[test]
53    fn annotation_omits_none_fields_rather_than_serializing_null() {
54        // The AskUserQuestion tool schema types `notes`/`preview` as
55        // optional strings and rejects an explicit `null`. A selected
56        // option carrying a preview with no user note must serialize
57        // without a `notes` key - not `"notes": null`, which the
58        // permission handler was feeding back as updated tool input.
59        let annotation = QuestionAnnotation { preview: Some("shown".to_owned()), notes: None };
60        let value = serde_json::to_value(&annotation).expect("serialize");
61        let obj = value.as_object().expect("annotation serializes to an object");
62        assert_eq!(obj.get("preview").and_then(serde_json::Value::as_str), Some("shown"));
63        assert!(
64            !obj.contains_key("notes"),
65            "None fields must be omitted, not serialized as null; got {value}",
66        );
67    }
68}