1use serde::Serialize;
12
13use crate::hooks::HookManager;
14use crate::plan_export::SchemaHeader;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum OutputFormat {
21 Text,
22 Json,
23}
24
25impl OutputFormat {
26 pub fn from_str(s: &str) -> Option<Self> {
28 match s {
29 "text" => Some(OutputFormat::Text),
30 "json" => Some(OutputFormat::Json),
31 _ => None,
32 }
33 }
34
35 pub fn is_json(&self) -> bool {
36 *self == OutputFormat::Json
37 }
38}
39
40#[derive(Debug, Clone, Serialize)]
44pub struct StepReport {
45 pub name: String,
46 pub step_type: String,
47 pub result: String,
48 pub duration_ms: u64,
49 pub input_tokens: u64,
50 pub output_tokens: u64,
51 pub anchor_breaches: u32,
52 pub chain_activations: u32,
53 pub was_retried: bool,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct UnitReport {
59 pub flow_name: String,
60 pub persona_name: String,
61 pub steps: Vec<StepReport>,
62 pub duration_ms: u64,
63 pub total_input_tokens: u64,
64 pub total_output_tokens: u64,
65 pub total_anchor_breaches: u32,
66 pub total_chain_activations: u32,
67}
68
69#[derive(Debug, Clone, Serialize)]
71pub struct ExecutionReport {
72 pub _schema: SchemaHeader,
73 pub axon_version: String,
74 pub source_file: String,
75 pub backend: String,
76 pub mode: String,
77 pub success: bool,
78 pub units: Vec<UnitReport>,
79 pub summary: ExecutionSummary,
80}
81
82#[derive(Debug, Clone, Serialize)]
84pub struct ExecutionSummary {
85 pub total_units: usize,
86 pub total_steps: usize,
87 pub total_duration_ms: u64,
88 pub avg_step_duration_ms: u64,
89 pub total_input_tokens: u64,
90 pub total_output_tokens: u64,
91 pub total_tokens: u64,
92 pub retried_steps: usize,
93}
94
95pub struct ReportBuilder {
99 source_file: String,
100 backend: String,
101 mode: String,
102 unit_reports: Vec<UnitReport>,
103 current_unit_steps: Vec<StepReport>,
105 current_flow_name: String,
106 current_persona_name: String,
107}
108
109impl ReportBuilder {
110 pub fn new(source_file: &str, backend: &str, mode: &str) -> Self {
111 ReportBuilder {
112 source_file: source_file.to_string(),
113 backend: backend.to_string(),
114 mode: mode.to_string(),
115 unit_reports: Vec::new(),
116 current_unit_steps: Vec::new(),
117 current_flow_name: String::new(),
118 current_persona_name: String::new(),
119 }
120 }
121
122 pub fn begin_unit(&mut self, flow_name: &str, persona_name: &str) {
124 self.current_flow_name = flow_name.to_string();
125 self.current_persona_name = persona_name.to_string();
126 self.current_unit_steps.clear();
127 }
128
129 pub fn record_step(&mut self, step: StepReport) {
131 self.current_unit_steps.push(step);
132 }
133
134 pub fn end_unit(&mut self, hooks: &HookManager) {
136 let unit_metrics = hooks.unit_metrics();
137 let um = unit_metrics.last();
138
139 self.unit_reports.push(UnitReport {
140 flow_name: self.current_flow_name.clone(),
141 persona_name: self.current_persona_name.clone(),
142 steps: std::mem::take(&mut self.current_unit_steps),
143 duration_ms: um.map(|u| u.duration_ms).unwrap_or(0),
144 total_input_tokens: um.map(|u| u.total_input_tokens).unwrap_or(0),
145 total_output_tokens: um.map(|u| u.total_output_tokens).unwrap_or(0),
146 total_anchor_breaches: um.map(|u| u.total_anchor_breaches).unwrap_or(0),
147 total_chain_activations: um.map(|u| u.total_chain_activations).unwrap_or(0),
148 });
149 }
150
151 pub fn build(self, success: bool, hooks: &HookManager) -> ExecutionReport {
153 ExecutionReport {
154 _schema: SchemaHeader::new("axon.report"),
155 axon_version: crate::runner::AXON_VERSION.to_string(),
156 source_file: self.source_file,
157 backend: self.backend,
158 mode: self.mode,
159 success,
160 units: self.unit_reports,
161 summary: ExecutionSummary {
162 total_units: hooks.unit_metrics().len(),
163 total_steps: hooks.total_steps(),
164 total_duration_ms: hooks.total_duration_ms(),
165 avg_step_duration_ms: hooks.avg_step_duration_ms(),
166 total_input_tokens: hooks.total_input_tokens(),
167 total_output_tokens: hooks.total_output_tokens(),
168 total_tokens: hooks.total_input_tokens() + hooks.total_output_tokens(),
169 retried_steps: hooks.retried_steps(),
170 },
171 }
172 }
173
174 pub fn to_json(report: &ExecutionReport) -> String {
176 serde_json::to_string_pretty(report).unwrap_or_else(|e| {
177 format!("{{\"error\": \"serialization failed: {e}\"}}")
178 })
179 }
180}
181
182#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::hooks::HookManager;
188
189 #[test]
190 fn output_format_parsing() {
191 assert_eq!(OutputFormat::from_str("text"), Some(OutputFormat::Text));
192 assert_eq!(OutputFormat::from_str("json"), Some(OutputFormat::Json));
193 assert_eq!(OutputFormat::from_str("xml"), None);
194 assert_eq!(OutputFormat::from_str(""), None);
195 }
196
197 #[test]
198 fn output_format_is_json() {
199 assert!(!OutputFormat::Text.is_json());
200 assert!(OutputFormat::Json.is_json());
201 }
202
203 #[test]
204 fn report_builder_empty() {
205 let hooks = HookManager::new();
206 let rb = ReportBuilder::new("test.axon", "anthropic", "stub");
207 let report = rb.build(true, &hooks);
208
209 assert_eq!(report.source_file, "test.axon");
210 assert_eq!(report.backend, "anthropic");
211 assert_eq!(report.mode, "stub");
212 assert!(report.success);
213 assert!(report.units.is_empty());
214 assert_eq!(report.summary.total_units, 0);
215 assert_eq!(report.summary.total_steps, 0);
216 }
217
218 #[test]
219 fn report_builder_with_steps() {
220 let mut hooks = HookManager::new();
221 let mut rb = ReportBuilder::new("demo.axon", "openai", "real");
222
223 hooks.on_unit_start("Analyze", "Expert");
224 rb.begin_unit("Analyze", "Expert");
225
226 hooks.on_step_start("Gather", "step");
227 hooks.on_step_end(100, 50, 0, 0, false);
228 rb.record_step(StepReport {
229 name: "Gather".into(),
230 step_type: "step".into(),
231 result: "gathered data".into(),
232 duration_ms: 0,
233 input_tokens: 100,
234 output_tokens: 50,
235 anchor_breaches: 0,
236 chain_activations: 0,
237 was_retried: false,
238 });
239
240 hooks.on_step_start("Summarize", "step");
241 hooks.on_step_end(200, 100, 1, 0, true);
242 rb.record_step(StepReport {
243 name: "Summarize".into(),
244 step_type: "step".into(),
245 result: "summary text".into(),
246 duration_ms: 0,
247 input_tokens: 200,
248 output_tokens: 100,
249 anchor_breaches: 1,
250 chain_activations: 0,
251 was_retried: true,
252 });
253
254 hooks.on_unit_end();
255 rb.end_unit(&hooks);
256
257 let report = rb.build(true, &hooks);
258 assert_eq!(report.units.len(), 1);
259 assert_eq!(report.units[0].flow_name, "Analyze");
260 assert_eq!(report.units[0].steps.len(), 2);
261 assert_eq!(report.units[0].steps[0].name, "Gather");
262 assert_eq!(report.units[0].steps[1].name, "Summarize");
263 assert!(report.units[0].steps[1].was_retried);
264 assert_eq!(report.summary.total_steps, 2);
265 assert_eq!(report.summary.total_input_tokens, 300);
266 assert_eq!(report.summary.total_output_tokens, 150);
267 assert_eq!(report.summary.total_tokens, 450);
268 assert_eq!(report.summary.retried_steps, 1);
269 }
270
271 #[test]
272 fn report_serializes_to_json() {
273 let hooks = HookManager::new();
274 let rb = ReportBuilder::new("test.axon", "anthropic", "stub");
275 let report = rb.build(true, &hooks);
276 let json = ReportBuilder::to_json(&report);
277
278 assert!(json.contains("\"axon_version\""));
279 assert!(json.contains("\"source_file\""));
280 assert!(json.contains("\"test.axon\""));
281 assert!(json.contains("\"summary\""));
282 assert!(json.contains("\"total_steps\""));
283 }
284
285 #[test]
286 fn report_multiple_units() {
287 let mut hooks = HookManager::new();
288 let mut rb = ReportBuilder::new("multi.axon", "gemini", "real");
289
290 hooks.on_unit_start("Flow1", "P1");
292 rb.begin_unit("Flow1", "P1");
293 hooks.on_step_start("S1", "step");
294 hooks.on_step_end(10, 5, 0, 0, false);
295 rb.record_step(StepReport {
296 name: "S1".into(),
297 step_type: "step".into(),
298 result: "r1".into(),
299 duration_ms: 0,
300 input_tokens: 10,
301 output_tokens: 5,
302 anchor_breaches: 0,
303 chain_activations: 0,
304 was_retried: false,
305 });
306 hooks.on_unit_end();
307 rb.end_unit(&hooks);
308
309 hooks.on_unit_start("Flow2", "P2");
311 rb.begin_unit("Flow2", "P2");
312 hooks.on_step_start("S2", "step");
313 hooks.on_step_end(20, 10, 0, 0, false);
314 rb.record_step(StepReport {
315 name: "S2".into(),
316 step_type: "step".into(),
317 result: "r2".into(),
318 duration_ms: 0,
319 input_tokens: 20,
320 output_tokens: 10,
321 anchor_breaches: 0,
322 chain_activations: 0,
323 was_retried: false,
324 });
325 hooks.on_unit_end();
326 rb.end_unit(&hooks);
327
328 let report = rb.build(true, &hooks);
329 assert_eq!(report.units.len(), 2);
330 assert_eq!(report.summary.total_units, 2);
331 assert_eq!(report.summary.total_tokens, 45);
332 }
333
334 #[test]
335 fn report_json_round_trip() {
336 let mut hooks = HookManager::new();
337 let mut rb = ReportBuilder::new("rt.axon", "anthropic", "stub");
338
339 hooks.on_unit_start("F", "P");
340 rb.begin_unit("F", "P");
341 hooks.on_step_start("S", "step");
342 hooks.on_step_end(42, 21, 0, 0, false);
343 rb.record_step(StepReport {
344 name: "S".into(),
345 step_type: "step".into(),
346 result: "hello world".into(),
347 duration_ms: 0,
348 input_tokens: 42,
349 output_tokens: 21,
350 anchor_breaches: 0,
351 chain_activations: 0,
352 was_retried: false,
353 });
354 hooks.on_unit_end();
355 rb.end_unit(&hooks);
356
357 let report = rb.build(true, &hooks);
358 let json = ReportBuilder::to_json(&report);
359
360 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
362 assert_eq!(parsed["source_file"], "rt.axon");
363 assert_eq!(parsed["success"], true);
364 assert_eq!(parsed["units"][0]["flow_name"], "F");
365 assert_eq!(parsed["units"][0]["steps"][0]["result"], "hello world");
366 assert_eq!(parsed["summary"]["total_tokens"], 63);
367 }
368}