1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::event::FlowRunId;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum FormKind {
12 Confirm {
13 prompt: String,
14 },
15 SingleSelect {
16 prompt: String,
17 options: Vec<String>,
18 },
19 MultiSelect {
20 prompt: String,
21 options: Vec<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 min: Option<usize>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 max: Option<usize>,
26 },
27 Text {
28 prompt: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 placeholder: Option<String>,
31 #[serde(default)]
32 multiline: bool,
33 },
34}
35
36impl FormKind {
37 pub fn prompt(&self) -> &str {
38 match self {
39 Self::Confirm { prompt }
40 | Self::SingleSelect { prompt, .. }
41 | Self::MultiSelect { prompt, .. }
42 | Self::Text { prompt, .. } => prompt,
43 }
44 }
45
46 pub fn discriminator(&self) -> &'static str {
47 match self {
48 Self::Confirm { .. } => "confirm",
49 Self::SingleSelect { .. } => "single_select",
50 Self::MultiSelect { .. } => "multi_select",
51 Self::Text { .. } => "text",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct FormQuestion {
58 pub id: String,
59 #[serde(flatten)]
60 pub kind: FormKind,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub struct CompositeForm {
65 pub questions: Vec<FormQuestion>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub enum FormSubmission {
70 Submitted { answers: Vec<FormAnswer> },
71 Rejected,
72}
73
74#[derive(Debug, Clone)]
75pub struct PendingForm {
76 pub form_id: String,
77 pub run_id: FlowRunId,
78 pub tool_use_id: String,
79 pub form: CompositeForm,
80 pub kind: FormKind,
81 pub emitted_at: DateTime<Utc>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87#[serde(tag = "kind", rename_all = "snake_case")]
88pub enum FormAnswer {
89 Confirmed {
90 value: bool,
91 },
92 Selected {
93 index: usize,
94 label: String,
95 },
96 MultiSelected {
97 indices: Vec<usize>,
98 labels: Vec<String>,
99 },
100 TextEntered {
101 text: String,
102 },
103 Cancelled,
104}
105
106impl FormAnswer {
107 pub fn discriminator(&self) -> &'static str {
108 match self {
109 Self::Confirmed { .. } => "confirmed",
110 Self::Selected { .. } => "selected",
111 Self::MultiSelected { .. } => "multi_selected",
112 Self::TextEntered { .. } => "text_entered",
113 Self::Cancelled => "cancelled",
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn form_kind_serializes_with_tag() {
124 let k = FormKind::SingleSelect {
125 prompt: "pick".into(),
126 options: vec!["a".into(), "b".into()],
127 };
128 let s = serde_json::to_string(&k).unwrap();
129 assert!(s.contains(r#""kind":"single_select""#));
130 assert!(s.contains(r#""prompt":"pick""#));
131 }
132
133 #[test]
134 fn form_kind_round_trip_confirm() {
135 let k = FormKind::Confirm {
136 prompt: "sure?".into(),
137 };
138 let s = serde_json::to_string(&k).unwrap();
139 let back: FormKind = serde_json::from_str(&s).unwrap();
140 assert_eq!(back, k);
141 }
142
143 #[test]
144 fn form_kind_round_trip_multi_select_omits_empty_bounds() {
145 let k = FormKind::MultiSelect {
146 prompt: "tags".into(),
147 options: vec!["a".into()],
148 min: None,
149 max: Some(2),
150 };
151 let s = serde_json::to_string(&k).unwrap();
152 assert!(!s.contains("\"min\""));
153 assert!(s.contains("\"max\":2"));
154 let back: FormKind = serde_json::from_str(&s).unwrap();
155 assert_eq!(back, k);
156 }
157
158 #[test]
159 fn form_answer_cancelled_serializes_as_tag_only() {
160 let a = FormAnswer::Cancelled;
161 let s = serde_json::to_string(&a).unwrap();
162 assert_eq!(s, r#"{"kind":"cancelled"}"#);
163 }
164
165 #[test]
166 fn form_answer_round_trip_multi_selected() {
167 let a = FormAnswer::MultiSelected {
168 indices: vec![0, 2],
169 labels: vec!["a".into(), "c".into()],
170 };
171 let s = serde_json::to_string(&a).unwrap();
172 let back: FormAnswer = serde_json::from_str(&s).unwrap();
173 assert_eq!(back, a);
174 }
175
176 #[test]
177 fn discriminators_are_stable() {
178 assert_eq!(
179 FormKind::Text {
180 prompt: "".into(),
181 placeholder: None,
182 multiline: false,
183 }
184 .discriminator(),
185 "text"
186 );
187 assert_eq!(FormAnswer::Cancelled.discriminator(), "cancelled");
188 }
189}