1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum PermissionDecision {
16 Allow,
18 Deny(String),
20 Transform(serde_json::Value),
22}
23
24#[derive(Debug, Clone, PartialEq, Default)]
30pub enum PlanningMode {
31 #[default]
33 Immediate,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(tag = "kind", rename_all = "snake_case")]
38#[non_exhaustive]
52pub enum FinishReason {
53 NoMoreToolCalls,
55 BudgetExceeded,
57 ProviderStop(String),
59 Stuck {
61 repeated_call: String,
62 repeats: usize,
63 },
64 TranscriptLimit { chars: usize, limit: usize },
66 Cancelled,
68
69 PermissionDenialLimit,
73
74 WallClockExceeded { secs: u64 },
78}
79
80impl std::fmt::Display for FinishReason {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 FinishReason::NoMoreToolCalls => write!(f, "no_more_tool_calls"),
84 FinishReason::BudgetExceeded => write!(f, "budget_exceeded"),
85 FinishReason::ProviderStop(reason) => write!(f, "provider_stop:{reason}"),
86 FinishReason::Stuck {
87 repeated_call,
88 repeats,
89 } => write!(f, "stuck:{repeated_call}:{repeats}"),
90 FinishReason::TranscriptLimit { chars, limit } => {
91 write!(f, "transcript_limit:{chars}/{limit}")
92 }
93 FinishReason::Cancelled => write!(f, "cancelled"),
94 FinishReason::PermissionDenialLimit => write!(f, "permission_denial_limit"),
95 FinishReason::WallClockExceeded { secs } => {
96 write!(f, "wall_clock_exceeded:{secs}")
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn finish_reason_display_all_variants() {
108 assert_eq!(
111 FinishReason::NoMoreToolCalls.to_string(),
112 "no_more_tool_calls"
113 );
114 assert_eq!(FinishReason::BudgetExceeded.to_string(), "budget_exceeded");
115 assert_eq!(
116 FinishReason::ProviderStop("length".into()).to_string(),
117 "provider_stop:length"
118 );
119 assert_eq!(
120 FinishReason::Stuck {
121 repeated_call: "Read".into(),
122 repeats: 3
123 }
124 .to_string(),
125 "stuck:Read:3"
126 );
127 assert_eq!(
128 FinishReason::TranscriptLimit {
129 chars: 10_000,
130 limit: 8_000
131 }
132 .to_string(),
133 "transcript_limit:10000/8000"
134 );
135 assert_eq!(FinishReason::Cancelled.to_string(), "cancelled");
136 assert_eq!(
137 FinishReason::PermissionDenialLimit.to_string(),
138 "permission_denial_limit"
139 );
140 assert_eq!(
141 FinishReason::WallClockExceeded { secs: 120 }.to_string(),
142 "wall_clock_exceeded:120"
143 );
144 }
145
146 #[test]
147 fn planning_mode_default_is_immediate() {
148 assert_eq!(PlanningMode::default(), PlanningMode::Immediate);
149 }
150
151 #[test]
152 fn finish_reason_struct_variants_serialize_with_kind_tag() {
153 let json = serde_json::to_value(&FinishReason::NoMoreToolCalls).unwrap();
155 assert_eq!(json["kind"], "no_more_tool_calls");
156
157 let json = serde_json::to_value(&FinishReason::BudgetExceeded).unwrap();
158 assert_eq!(json["kind"], "budget_exceeded");
159
160 let json = serde_json::to_value(&FinishReason::Stuck {
161 repeated_call: "Bash".into(),
162 repeats: 5,
163 })
164 .unwrap();
165 assert_eq!(json["kind"], "stuck");
166 assert_eq!(json["repeated_call"], "Bash");
167 assert_eq!(json["repeats"], 5);
168
169 let json = serde_json::to_value(&FinishReason::TranscriptLimit {
170 chars: 1000,
171 limit: 500,
172 })
173 .unwrap();
174 assert_eq!(json["kind"], "transcript_limit");
175 assert_eq!(json["chars"], 1000);
176 assert_eq!(json["limit"], 500);
177
178 let json = serde_json::to_value(&FinishReason::Cancelled).unwrap();
179 assert_eq!(json["kind"], "cancelled");
180
181 let json = serde_json::to_value(&FinishReason::PermissionDenialLimit).unwrap();
182 assert_eq!(json["kind"], "permission_denial_limit");
183
184 let json = serde_json::to_value(&FinishReason::WallClockExceeded { secs: 300 }).unwrap();
185 assert_eq!(json["kind"], "wall_clock_exceeded");
186 assert_eq!(json["secs"], 300);
187 }
188
189 #[test]
190 fn finish_reason_deserializes_from_kind_tag() {
191 let r: FinishReason = serde_json::from_str(r#"{"kind":"no_more_tool_calls"}"#).unwrap();
193 assert!(matches!(r, FinishReason::NoMoreToolCalls));
194
195 let r: FinishReason =
196 serde_json::from_str(r#"{"kind":"stuck","repeated_call":"Read","repeats":3}"#).unwrap();
197 match r {
198 FinishReason::Stuck {
199 repeated_call,
200 repeats,
201 } => {
202 assert_eq!(repeated_call, "Read");
203 assert_eq!(repeats, 3);
204 }
205 other => panic!("expected Stuck, got {other:?}"),
206 }
207
208 let r: FinishReason = serde_json::from_str(r#"{"kind":"cancelled"}"#).unwrap();
209 assert!(matches!(r, FinishReason::Cancelled));
210
211 let r: FinishReason =
212 serde_json::from_str(r#"{"kind":"wall_clock_exceeded","secs":99}"#).unwrap();
213 assert!(matches!(r, FinishReason::WallClockExceeded { secs: 99 }));
214 }
215
216 #[test]
217 fn permission_decision_serializes_deny_and_allow() {
218 let deny = serde_json::to_value(PermissionDecision::Deny("blocked".into())).unwrap();
220 assert!(
222 deny.get("deny").is_some(),
223 "Deny must have 'deny' key, got {deny}"
224 );
225
226 let allow = serde_json::to_value(&PermissionDecision::Allow).unwrap();
227 assert_eq!(
228 allow,
229 serde_json::json!("allow"),
230 "Allow must serialize to string 'allow'"
231 );
232 }
233}