Skip to main content

agy_bridge/hooks/
interactive.rs

1//! Interactive question/answer types for agent-user interactions.
2
3use serde::{Deserialize, Serialize};
4
5use super::types::HookResult;
6
7// ── Interactive Q&A types (Python SDK parity) ───────────────────────────────
8
9/// A single selectable option in an interactive question prompt.
10///
11/// Maps to the Python SDK's `AskQuestionOption`.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct AskQuestionOption {
14    /// Human-readable label displayed to the user.
15    pub label: String,
16    /// Machine-readable value returned when this option is selected.
17    pub value: String,
18}
19
20/// A question with its list of selectable options.
21///
22/// Maps to the Python SDK's `AskQuestionEntry`.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct AskQuestionEntry {
25    /// The question text to present to the user.
26    pub question: String,
27    /// Available answer options.
28    pub options: Vec<AskQuestionOption>,
29}
30
31/// Full specification for an interactive ask-user interaction.
32///
33/// Contains one or more questions to present to the user in sequence.
34/// Maps to the Python SDK's `AskQuestionInteractionSpec`.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct AskQuestionInteractionSpec {
37    /// The questions to present to the user.
38    pub entries: Vec<AskQuestionEntry>,
39}
40
41/// The user's response to an interactive question prompt.
42///
43/// Maps to the Python SDK's `QuestionResponse`.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct QuestionResponse {
46    /// The selected answer values, one per question entry.
47    pub answers: Vec<String>,
48}
49
50/// Combined hook result and optional question response.
51///
52/// Returned by hooks that involve interactive Q&A. The `hook_result`
53/// determines whether the agent should proceed, and `response` carries
54/// the user's answers (if any).
55///
56/// Maps to the Python SDK's `QuestionHookResult`.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct QuestionHookResult {
59    /// The hook decision (allow/deny).
60    pub hook_result: HookResult,
61    /// The user's answers, if a question was asked and answered.
62    pub response: Option<QuestionResponse>,
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn ask_question_option_serde_roundtrip() {
71        let opt = AskQuestionOption {
72            label: "Yes".into(),
73            value: "y".into(),
74        };
75        let json = serde_json::to_string(&opt).unwrap();
76        let parsed: AskQuestionOption = serde_json::from_str(&json).unwrap();
77        assert_eq!(parsed, opt);
78    }
79
80    #[test]
81    fn ask_question_entry_serde_roundtrip() {
82        let entry = AskQuestionEntry {
83            question: "Continue?".into(),
84            options: vec![
85                AskQuestionOption {
86                    label: "Yes".into(),
87                    value: "y".into(),
88                },
89                AskQuestionOption {
90                    label: "No".into(),
91                    value: "n".into(),
92                },
93            ],
94        };
95        let json = serde_json::to_string(&entry).unwrap();
96        let parsed: AskQuestionEntry = serde_json::from_str(&json).unwrap();
97        assert_eq!(parsed, entry);
98    }
99
100    #[test]
101    fn ask_question_interaction_spec_serde_roundtrip() {
102        let spec = AskQuestionInteractionSpec {
103            entries: vec![
104                AskQuestionEntry {
105                    question: "Q1?".into(),
106                    options: vec![AskQuestionOption {
107                        label: "A".into(),
108                        value: "a".into(),
109                    }],
110                },
111                AskQuestionEntry {
112                    question: "Q2?".into(),
113                    options: vec![],
114                },
115            ],
116        };
117        let json = serde_json::to_string(&spec).unwrap();
118        let parsed: AskQuestionInteractionSpec = serde_json::from_str(&json).unwrap();
119        assert_eq!(parsed, spec);
120    }
121
122    #[test]
123    fn question_response_serde_roundtrip() {
124        let resp = QuestionResponse {
125            answers: vec!["yes".into(), "fast".into()],
126        };
127        let json = serde_json::to_string(&resp).unwrap();
128        let parsed: QuestionResponse = serde_json::from_str(&json).unwrap();
129        assert_eq!(parsed, resp);
130    }
131
132    #[test]
133    fn question_hook_result_with_response() {
134        let result = QuestionHookResult {
135            hook_result: HookResult::allow_with_message("confirmed"),
136            response: Some(QuestionResponse {
137                answers: vec!["answer1".into()],
138            }),
139        };
140        let json = serde_json::to_string(&result).unwrap();
141        let parsed: QuestionHookResult = serde_json::from_str(&json).unwrap();
142        assert_eq!(parsed.hook_result, result.hook_result);
143        assert!(parsed.response.is_some());
144    }
145
146    #[test]
147    fn question_hook_result_without_response() {
148        let result = QuestionHookResult {
149            hook_result: HookResult::deny("cancelled"),
150            response: None,
151        };
152        let json = serde_json::to_string(&result).unwrap();
153        let parsed: QuestionHookResult = serde_json::from_str(&json).unwrap();
154        assert!(!parsed.hook_result.allow);
155        assert!(parsed.response.is_none());
156    }
157}