1use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "openapi")]
11use utoipa::ToSchema;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[cfg_attr(feature = "openapi", derive(ToSchema))]
27#[cfg_attr(feature = "openapi", schema(rename_all = "snake_case"))]
31pub enum ExecutionPhase {
32 Commentary,
35 FinalAnswer,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "openapi", derive(ToSchema))]
48#[cfg_attr(feature = "openapi", schema(rename_all = "snake_case"))]
49pub enum PhaseSource {
50 Provider,
52 Derived,
54}
55
56impl PhaseSource {
57 pub fn as_str(&self) -> &'static str {
58 match self {
59 Self::Provider => "provider",
60 Self::Derived => "derived",
61 }
62 }
63
64 pub fn from_str_opt(s: &str) -> Option<Self> {
65 match s {
66 "provider" => Some(Self::Provider),
67 "derived" => Some(Self::Derived),
68 _ => None,
69 }
70 }
71}
72
73impl Serialize for PhaseSource {
74 fn serialize<S: serde::Serializer>(
75 &self,
76 serializer: S,
77 ) -> std::result::Result<S::Ok, S::Error> {
78 serializer.serialize_str(self.as_str())
79 }
80}
81
82impl<'de> Deserialize<'de> for PhaseSource {
83 fn deserialize<D: serde::Deserializer<'de>>(
84 deserializer: D,
85 ) -> std::result::Result<Self, D::Error> {
86 let s = String::deserialize(deserializer)?;
87 Self::from_str_opt(&s)
88 .ok_or_else(|| serde::de::Error::unknown_variant(&s, &["provider", "derived"]))
89 }
90}
91
92impl ExecutionPhase {
93 pub fn from_has_tool_calls(has_tool_calls: bool) -> Self {
95 if has_tool_calls {
96 Self::Commentary
97 } else {
98 Self::FinalAnswer
99 }
100 }
101
102 pub fn from_provider_str(s: &str) -> Option<Self> {
105 match s {
106 "commentary" | "in_progress" => Some(Self::Commentary),
107 "final_answer" | "completed" => Some(Self::FinalAnswer),
108 _ => None,
109 }
110 }
111
112 pub fn as_provider_str(&self) -> &'static str {
114 match self {
115 Self::Commentary => "commentary",
116 Self::FinalAnswer => "final_answer",
117 }
118 }
119
120 pub fn refine_streamed_hint(current: Option<Self>, incoming: Self) -> Option<Self> {
129 Some(current.unwrap_or(incoming))
131 }
132}
133
134impl std::fmt::Display for ExecutionPhase {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.write_str(self.as_provider_str())
137 }
138}
139
140impl Serialize for ExecutionPhase {
141 fn serialize<S: serde::Serializer>(
142 &self,
143 serializer: S,
144 ) -> std::result::Result<S::Ok, S::Error> {
145 serializer.serialize_str(self.as_provider_str())
146 }
147}
148
149impl<'de> Deserialize<'de> for ExecutionPhase {
150 fn deserialize<D: serde::Deserializer<'de>>(
151 deserializer: D,
152 ) -> std::result::Result<Self, D::Error> {
153 let s = String::deserialize(deserializer)?;
154 match s.as_str() {
155 "commentary" | "in_progress" => Ok(Self::Commentary),
156 "final_answer" | "completed" => Ok(Self::FinalAnswer),
157 other => Err(serde::de::Error::unknown_variant(
158 other,
159 &["commentary", "final_answer", "in_progress", "completed"],
160 )),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_execution_phase_from_provider_str() {
171 assert_eq!(
172 ExecutionPhase::from_provider_str("commentary"),
173 Some(ExecutionPhase::Commentary)
174 );
175 assert_eq!(
176 ExecutionPhase::from_provider_str("final_answer"),
177 Some(ExecutionPhase::FinalAnswer)
178 );
179 assert_eq!(
180 ExecutionPhase::from_provider_str("in_progress"),
181 Some(ExecutionPhase::Commentary)
182 );
183 assert_eq!(
184 ExecutionPhase::from_provider_str("completed"),
185 Some(ExecutionPhase::FinalAnswer)
186 );
187 assert_eq!(ExecutionPhase::from_provider_str("unknown"), None);
188 }
189
190 #[test]
191 fn tool_call_presence_derives_execution_phase() {
192 assert_eq!(
193 ExecutionPhase::from_has_tool_calls(true),
194 ExecutionPhase::Commentary
195 );
196 assert_eq!(
197 ExecutionPhase::from_has_tool_calls(false),
198 ExecutionPhase::FinalAnswer
199 );
200 }
201
202 #[test]
203 fn streamed_hint_keeps_first_classification() {
204 use ExecutionPhase::{Commentary, FinalAnswer};
205 for (current, incoming, expected) in [
206 (None, Commentary, Commentary),
207 (None, FinalAnswer, FinalAnswer),
208 (Some(Commentary), Commentary, Commentary),
209 (Some(Commentary), FinalAnswer, Commentary),
210 (Some(FinalAnswer), Commentary, FinalAnswer),
211 (Some(FinalAnswer), FinalAnswer, FinalAnswer),
212 ] {
213 assert_eq!(
214 ExecutionPhase::refine_streamed_hint(current, incoming),
215 Some(expected)
216 );
217 }
218 }
219
220 #[test]
221 fn phase_wire_values_accept_legacy_spellings_and_reject_invalid_input() {
222 for (input, phase, canonical) in [
223 ("commentary", ExecutionPhase::Commentary, "commentary"),
224 ("in_progress", ExecutionPhase::Commentary, "commentary"),
225 ("final_answer", ExecutionPhase::FinalAnswer, "final_answer"),
226 ("completed", ExecutionPhase::FinalAnswer, "final_answer"),
227 ] {
228 let decoded: ExecutionPhase = serde_json::from_value(serde_json::json!(input)).unwrap();
229 assert_eq!(decoded, phase);
230 assert_eq!(
231 serde_json::to_value(decoded).unwrap(),
232 serde_json::json!(canonical)
233 );
234 assert_eq!(decoded.as_provider_str(), canonical);
235 assert_eq!(decoded.to_string(), canonical);
236 }
237 for input in [
238 serde_json::json!("bogus"),
239 serde_json::json!("Commentary"),
240 serde_json::Value::Null,
241 serde_json::json!(42),
242 ] {
243 assert!(serde_json::from_value::<ExecutionPhase>(input).is_err());
244 }
245 }
246}