1use std::time::Duration;
10
11use serde::Serialize;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
18#[serde(rename_all = "lowercase")]
19pub enum AgentModel {
20 Opus,
22 #[default]
24 Sonnet,
25 Haiku,
27}
28
29impl AgentModel {
30 pub fn all() -> &'static [AgentModel] {
32 &[AgentModel::Opus, AgentModel::Sonnet, AgentModel::Haiku]
33 }
34
35 pub fn id(self) -> &'static str {
38 match self {
39 AgentModel::Opus => "opus",
40 AgentModel::Sonnet => "sonnet",
41 AgentModel::Haiku => "haiku",
42 }
43 }
44
45 pub fn label(self) -> &'static str {
48 match self {
49 AgentModel::Opus => "Opus 4.8",
50 AgentModel::Sonnet => "Sonnet 4.6",
51 AgentModel::Haiku => "Haiku 4.5",
52 }
53 }
54
55 pub fn parse(s: &str) -> Option<AgentModel> {
58 match s.trim().to_ascii_lowercase().as_str() {
59 "opus" => Some(AgentModel::Opus),
60 "sonnet" => Some(AgentModel::Sonnet),
61 "haiku" => Some(AgentModel::Haiku),
62 _ => None,
63 }
64 }
65
66 pub fn next(self) -> AgentModel {
68 match self {
69 AgentModel::Opus => AgentModel::Sonnet,
70 AgentModel::Sonnet => AgentModel::Haiku,
71 AgentModel::Haiku => AgentModel::Opus,
72 }
73 }
74
75 pub fn prev(self) -> AgentModel {
78 match self {
79 AgentModel::Opus => AgentModel::Haiku,
80 AgentModel::Sonnet => AgentModel::Opus,
81 AgentModel::Haiku => AgentModel::Sonnet,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
92#[serde(rename_all = "lowercase")]
93pub enum Effort {
94 Low,
96 #[default]
98 Medium,
99 High,
101}
102
103impl Effort {
104 pub fn all() -> &'static [Effort] {
106 &[Effort::Low, Effort::Medium, Effort::High]
107 }
108
109 pub fn id(self) -> &'static str {
111 match self {
112 Effort::Low => "low",
113 Effort::Medium => "medium",
114 Effort::High => "high",
115 }
116 }
117
118 pub fn label(self) -> &'static str {
120 self.id()
121 }
122
123 pub fn parse(s: &str) -> Option<Effort> {
126 match s.trim().to_ascii_lowercase().as_str() {
127 "low" => Some(Effort::Low),
128 "medium" | "med" => Some(Effort::Medium),
129 "high" => Some(Effort::High),
130 _ => None,
131 }
132 }
133
134 pub fn next(self) -> Effort {
136 match self {
137 Effort::Low => Effort::Medium,
138 Effort::Medium => Effort::High,
139 Effort::High => Effort::Low,
140 }
141 }
142
143 pub fn prev(self) -> Effort {
146 match self {
147 Effort::Low => Effort::High,
148 Effort::Medium => Effort::Low,
149 Effort::High => Effort::Medium,
150 }
151 }
152
153 pub fn directive(self) -> Option<&'static str> {
156 match self {
157 Effort::Low => Some("Work quickly and keep your reasoning brief."),
158 Effort::Medium => None,
159 Effort::High => Some("Think carefully and review the diff thoroughly before writing."),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub struct AgentOptions {
167 pub model: AgentModel,
169 pub effort: Effort,
171 pub timeout: Option<Duration>,
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn model_parse_roundtrips_and_rejects_unknown() {
186 for &m in AgentModel::all() {
187 assert_eq!(AgentModel::parse(m.id()), Some(m));
188 }
189 assert_eq!(AgentModel::parse("OPUS"), Some(AgentModel::Opus));
190 assert_eq!(AgentModel::parse(" sonnet "), Some(AgentModel::Sonnet));
191 assert_eq!(AgentModel::parse("gpt"), None);
192 }
193
194 #[test]
195 fn model_cycle_visits_every_variant() {
196 let mut seen = vec![AgentModel::Opus];
197 let mut cur = AgentModel::Opus;
198 for _ in 0..AgentModel::all().len() - 1 {
199 cur = cur.next();
200 seen.push(cur);
201 }
202 assert_eq!(cur.next(), AgentModel::Opus); assert_eq!(seen.len(), AgentModel::all().len());
204 }
205
206 #[test]
207 fn model_serializes_lowercase() {
208 assert_eq!(
209 serde_json::to_string(&AgentModel::Sonnet).unwrap(),
210 "\"sonnet\""
211 );
212 }
213
214 #[test]
215 fn effort_parse_accepts_aliases() {
216 assert_eq!(Effort::parse("low"), Some(Effort::Low));
217 assert_eq!(Effort::parse("MED"), Some(Effort::Medium));
218 assert_eq!(Effort::parse("medium"), Some(Effort::Medium));
219 assert_eq!(Effort::parse("High"), Some(Effort::High));
220 assert_eq!(Effort::parse("max"), None);
221 }
222
223 #[test]
224 fn effort_directive_only_for_non_baseline() {
225 assert!(Effort::Low.directive().is_some());
226 assert!(Effort::Medium.directive().is_none());
227 assert!(Effort::High.directive().is_some());
228 }
229
230 #[test]
231 fn effort_cycle_wraps() {
232 assert_eq!(Effort::Low.next(), Effort::Medium);
233 assert_eq!(Effort::Medium.next(), Effort::High);
234 assert_eq!(Effort::High.next(), Effort::Low);
235 }
236
237 #[test]
238 fn prev_is_the_inverse_of_next() {
239 for &m in AgentModel::all() {
240 assert_eq!(m.next().prev(), m);
241 assert_eq!(m.prev().next(), m);
242 }
243 for &e in Effort::all() {
244 assert_eq!(e.next().prev(), e);
245 assert_eq!(e.prev().next(), e);
246 }
247 }
248
249 #[test]
250 fn defaults_are_sonnet_and_medium() {
251 let opts = AgentOptions::default();
252 assert_eq!(opts.model, AgentModel::Sonnet);
253 assert_eq!(opts.effort, Effort::Medium);
254 }
255}