1use std::fmt::Write;
6
7use crate::state::{ManagerPhaseSnapshot, TickSnapshot, WorkResultSnapshot, WorkerResultSnapshot};
8
9#[derive(Debug, Clone)]
15pub struct SnapshotOutput {
16 pub content: String,
18 pub item_count: usize,
20}
21
22impl SnapshotOutput {
23 pub fn new(content: String, item_count: usize) -> Self {
24 Self {
25 content,
26 item_count,
27 }
28 }
29
30 pub fn empty() -> Self {
31 Self {
32 content: String::new(),
33 item_count: 0,
34 }
35 }
36}
37
38pub trait SnapshotFormatter: Send + Sync {
46 fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput;
48
49 fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput;
51
52 fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput;
54
55 fn format_history(&self, history: &[TickSnapshot]) -> SnapshotOutput {
57 let mut output = String::new();
58 let mut count = 0;
59
60 for snapshot in history {
61 let tick_output = self.format_tick(snapshot);
62 if !tick_output.content.is_empty() {
63 output.push_str(&tick_output.content);
64 output.push('\n');
65 count += 1;
66 }
67 }
68
69 SnapshotOutput::new(output, count)
70 }
71
72 fn name(&self) -> &str;
74}
75
76#[derive(Debug, Clone, Default)]
84pub struct ConsoleFormatter {
85 pub show_prompts: bool,
87 pub show_raw_responses: bool,
89 pub show_idle: bool,
91 pub max_prompts: usize,
93}
94
95impl ConsoleFormatter {
96 pub fn new() -> Self {
97 Self {
98 show_prompts: true,
99 show_raw_responses: true,
100 show_idle: false,
101 max_prompts: 1, }
103 }
104
105 pub fn with_all_prompts(mut self) -> Self {
107 self.max_prompts = 0;
108 self
109 }
110
111 pub fn without_prompts(mut self) -> Self {
113 self.show_prompts = false;
114 self.show_raw_responses = false;
115 self
116 }
117
118 pub fn with_idle(mut self) -> Self {
120 self.show_idle = true;
121 self
122 }
123}
124
125impl SnapshotFormatter for ConsoleFormatter {
126 fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
127 let has_manager = snapshot.manager_phase.is_some();
128 let has_action = snapshot.worker_results.iter().any(|r| {
129 matches!(
130 r.result,
131 WorkResultSnapshot::Acted { .. } | WorkResultSnapshot::Done { .. }
132 )
133 });
134
135 if !has_manager && !has_action {
137 return SnapshotOutput::empty();
138 }
139
140 let mut output = String::new();
141 writeln!(
142 output,
143 "\n--- Tick {} ({:?}) ---",
144 snapshot.tick, snapshot.duration
145 )
146 .unwrap();
147
148 if let Some(manager) = &snapshot.manager_phase {
150 let manager_output = self.format_manager_phase(manager);
151 output.push_str(&manager_output.content);
152 }
153
154 for wr in &snapshot.worker_results {
156 let worker_output = self.format_worker_result(wr);
157 if !worker_output.content.is_empty() {
158 output.push_str(&worker_output.content);
159 }
160 }
161
162 SnapshotOutput::new(output, 1)
163 }
164
165 fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
166 let mut output = String::new();
167
168 writeln!(output, " Manager:").unwrap();
169 writeln!(
170 output,
171 " Requests: {} workers",
172 phase.batch_request.requests.len()
173 )
174 .unwrap();
175
176 for req in &phase.batch_request.requests {
177 writeln!(
178 output,
179 " W{}: candidates={:?}",
180 req.worker_id.0, req.context.candidates
181 )
182 .unwrap();
183 writeln!(output, " query: {}", req.query).unwrap();
184 writeln!(
185 output,
186 " context: tick={}, progress={:.1}%",
187 req.context.global.tick,
188 req.context.global.progress * 100.0
189 )
190 .unwrap();
191 }
192
193 writeln!(output, " Responses: {}", phase.responses.len()).unwrap();
194
195 for (i, (wid, resp)) in phase.responses.iter().enumerate() {
196 writeln!(
197 output,
198 " W{}: tool={}, target={}, confidence={:.2}",
199 wid.0, resp.tool, resp.target, resp.confidence
200 )
201 .unwrap();
202
203 if let Some(reason) = &resp.reasoning {
204 writeln!(output, " reasoning: {}", reason).unwrap();
205 }
206
207 let show_this = self.max_prompts == 0 || i < self.max_prompts;
209 if show_this {
210 if self.show_prompts {
211 if let Some(prompt) = &resp.prompt {
212 writeln!(output, " --- Prompt ---").unwrap();
213 for line in prompt.lines() {
214 writeln!(output, " {}", line).unwrap();
215 }
216 }
217 }
218
219 if self.show_raw_responses {
220 if let Some(raw) = &resp.raw_response {
221 writeln!(output, " --- Raw Response ---").unwrap();
222 writeln!(output, " {}", raw.trim()).unwrap();
223 }
224 }
225 }
226 }
227
228 writeln!(output, " Guidances: {}", phase.guidances.len()).unwrap();
229 for (wid, guidance) in &phase.guidances {
230 let action_names: Vec<_> = guidance.actions.iter().map(|a| &a.name).collect();
231 writeln!(output, " W{}: {:?}", wid.0, action_names).unwrap();
232 }
233
234 SnapshotOutput::new(output, phase.responses.len())
235 }
236
237 fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
238 let mut output = String::new();
239
240 match &result.result {
241 WorkResultSnapshot::Acted { action_result, .. } => {
242 let action_name = result
243 .guidance_received
244 .as_ref()
245 .and_then(|g| g.actions.first())
246 .map(|a| a.name.as_str())
247 .unwrap_or("unknown");
248 writeln!(
249 output,
250 " W{}: Acted - {} (success={})",
251 result.worker_id.0, action_name, action_result.success
252 )
253 .unwrap();
254 }
255 WorkResultSnapshot::NeedsGuidance { reason, .. } => {
256 writeln!(
257 output,
258 " W{}: NeedsGuidance - {}",
259 result.worker_id.0, reason
260 )
261 .unwrap();
262 }
263 WorkResultSnapshot::Escalate { reason, .. } => {
264 writeln!(output, " W{}: Escalate - {:?}", result.worker_id.0, reason).unwrap();
265 }
266 WorkResultSnapshot::Done { success, message } => {
267 writeln!(
268 output,
269 " W{}: Done (success={}) - {}",
270 result.worker_id.0,
271 success,
272 message.as_deref().unwrap_or("(no message)")
273 )
274 .unwrap();
275 }
276 WorkResultSnapshot::Continuing { progress } => {
277 writeln!(
278 output,
279 " W{}: Continuing ({:.1}%)",
280 result.worker_id.0,
281 progress * 100.0
282 )
283 .unwrap();
284 }
285 WorkResultSnapshot::Idle => {
286 if self.show_idle {
287 writeln!(output, " W{}: Idle", result.worker_id.0).unwrap();
288 }
289 }
290 }
291
292 let count = if output.is_empty() { 0 } else { 1 };
293 SnapshotOutput::new(output, count)
294 }
295
296 fn name(&self) -> &str {
297 "console"
298 }
299}
300
301#[derive(Debug, Clone, Default)]
309pub struct CompactFormatter;
310
311impl CompactFormatter {
312 pub fn new() -> Self {
313 Self
314 }
315}
316
317impl SnapshotFormatter for CompactFormatter {
318 fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
319 let manager_str = if let Some(m) = &snapshot.manager_phase {
320 format!(
321 "M(req={},resp={})",
322 m.batch_request.requests.len(),
323 m.responses.len()
324 )
325 } else {
326 "M(-)".to_string()
327 };
328
329 let mut acted = 0;
330 let mut done = 0;
331 let mut idle = 0;
332
333 for wr in &snapshot.worker_results {
334 match &wr.result {
335 WorkResultSnapshot::Acted { .. } => acted += 1,
336 WorkResultSnapshot::Done { .. } => done += 1,
337 WorkResultSnapshot::Idle => idle += 1,
338 _ => {}
339 }
340 }
341
342 let content = format!(
343 "T{:04} [{:?}] {} W(acted={},done={},idle={})",
344 snapshot.tick, snapshot.duration, manager_str, acted, done, idle
345 );
346
347 SnapshotOutput::new(content, 1)
348 }
349
350 fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
351 let content = format!(
352 "Manager: req={} resp={} guidance={} errors={}",
353 phase.batch_request.requests.len(),
354 phase.responses.len(),
355 phase.guidances.len(),
356 phase.llm_errors
357 );
358 SnapshotOutput::new(content, 1)
359 }
360
361 fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
362 let content = match &result.result {
363 WorkResultSnapshot::Acted { action_result, .. } => {
364 let action = result
365 .guidance_received
366 .as_ref()
367 .and_then(|g| g.actions.first())
368 .map(|a| a.name.as_str())
369 .unwrap_or("?");
370 format!(
371 "W{}: {} ({})",
372 result.worker_id.0,
373 action,
374 if action_result.success { "ok" } else { "fail" }
375 )
376 }
377 WorkResultSnapshot::Done { success, .. } => {
378 format!(
379 "W{}: DONE ({})",
380 result.worker_id.0,
381 if *success { "ok" } else { "fail" }
382 )
383 }
384 WorkResultSnapshot::NeedsGuidance { .. } => {
385 format!("W{}: NEEDS_GUIDANCE", result.worker_id.0)
386 }
387 WorkResultSnapshot::Escalate { .. } => {
388 format!("W{}: ESCALATE", result.worker_id.0)
389 }
390 WorkResultSnapshot::Continuing { progress } => {
391 format!("W{}: CONT({:.0}%)", result.worker_id.0, progress * 100.0)
392 }
393 WorkResultSnapshot::Idle => {
394 format!("W{}: IDLE", result.worker_id.0)
395 }
396 };
397
398 SnapshotOutput::new(content, 1)
399 }
400
401 fn name(&self) -> &str {
402 "compact"
403 }
404}
405
406#[derive(Debug, Clone, Default)]
414pub struct JsonFormatter {
415 pub pretty: bool,
417}
418
419impl JsonFormatter {
420 pub fn new() -> Self {
421 Self { pretty: false }
422 }
423
424 pub fn pretty() -> Self {
425 Self { pretty: true }
426 }
427}
428
429impl SnapshotFormatter for JsonFormatter {
430 fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
431 let obj = serde_json::json!({
432 "tick": snapshot.tick,
433 "duration_us": snapshot.duration.as_micros(),
434 "has_manager": snapshot.manager_phase.is_some(),
435 "worker_count": snapshot.worker_results.len(),
436 "manager": snapshot.manager_phase.as_ref().map(|m| {
437 serde_json::json!({
438 "requests": m.batch_request.requests.len(),
439 "responses": m.responses.len(),
440 "guidances": m.guidances.len(),
441 "llm_errors": m.llm_errors,
442 })
443 }),
444 "workers": snapshot.worker_results.iter().map(|wr| {
445 let (status, success) = match &wr.result {
446 WorkResultSnapshot::Acted { action_result, .. } => ("acted", Some(action_result.success)),
447 WorkResultSnapshot::Done { success, .. } => ("done", Some(*success)),
448 WorkResultSnapshot::NeedsGuidance { .. } => ("needs_guidance", None),
449 WorkResultSnapshot::Escalate { .. } => ("escalate", None),
450 WorkResultSnapshot::Continuing { .. } => ("continuing", None),
451 WorkResultSnapshot::Idle => ("idle", None),
452 };
453 serde_json::json!({
454 "worker_id": wr.worker_id.0,
455 "status": status,
456 "success": success,
457 })
458 }).collect::<Vec<_>>(),
459 });
460
461 let content = if self.pretty {
462 serde_json::to_string_pretty(&obj).unwrap_or_default()
463 } else {
464 serde_json::to_string(&obj).unwrap_or_default()
465 };
466
467 SnapshotOutput::new(content, 1)
468 }
469
470 fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
471 let obj = serde_json::json!({
472 "requests": phase.batch_request.requests.iter().map(|r| {
473 serde_json::json!({
474 "worker_id": r.worker_id.0,
475 "query": r.query,
476 "candidates": r.context.candidates.iter().map(|c| &c.name).collect::<Vec<_>>(),
477 })
478 }).collect::<Vec<_>>(),
479 "responses": phase.responses.iter().map(|(wid, resp)| {
480 serde_json::json!({
481 "worker_id": wid.0,
482 "tool": resp.tool,
483 "target": resp.target,
484 "confidence": resp.confidence,
485 "reasoning": resp.reasoning,
486 "has_prompt": resp.prompt.is_some(),
487 "has_raw_response": resp.raw_response.is_some(),
488 })
489 }).collect::<Vec<_>>(),
490 "guidances": phase.guidances.iter().map(|(wid, g)| {
491 serde_json::json!({
492 "worker_id": wid.0,
493 "actions": g.actions.iter().map(|a| &a.name).collect::<Vec<_>>(),
494 })
495 }).collect::<Vec<_>>(),
496 "llm_errors": phase.llm_errors,
497 });
498
499 let content = if self.pretty {
500 serde_json::to_string_pretty(&obj).unwrap_or_default()
501 } else {
502 serde_json::to_string(&obj).unwrap_or_default()
503 };
504
505 SnapshotOutput::new(content, phase.responses.len())
506 }
507
508 fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
509 let (status, details) = match &result.result {
510 WorkResultSnapshot::Acted { action_result, .. } => {
511 let action = result
512 .guidance_received
513 .as_ref()
514 .and_then(|g| g.actions.first())
515 .map(|a| a.name.clone());
516 (
517 "acted",
518 serde_json::json!({
519 "action": action,
520 "success": action_result.success,
521 "duration_us": action_result.duration.as_micros(),
522 "error": action_result.error,
523 }),
524 )
525 }
526 WorkResultSnapshot::Done { success, message } => (
527 "done",
528 serde_json::json!({
529 "success": success,
530 "message": message,
531 }),
532 ),
533 WorkResultSnapshot::NeedsGuidance { reason, .. } => (
534 "needs_guidance",
535 serde_json::json!({
536 "reason": reason,
537 }),
538 ),
539 WorkResultSnapshot::Escalate { reason, context } => (
540 "escalate",
541 serde_json::json!({
542 "reason": format!("{:?}", reason),
543 "context": context,
544 }),
545 ),
546 WorkResultSnapshot::Continuing { progress } => (
547 "continuing",
548 serde_json::json!({
549 "progress": progress,
550 }),
551 ),
552 WorkResultSnapshot::Idle => ("idle", serde_json::json!({})),
553 };
554
555 let obj = serde_json::json!({
556 "worker_id": result.worker_id.0,
557 "status": status,
558 "details": details,
559 });
560
561 let content = if self.pretty {
562 serde_json::to_string_pretty(&obj).unwrap_or_default()
563 } else {
564 serde_json::to_string(&obj).unwrap_or_default()
565 };
566
567 SnapshotOutput::new(content, 1)
568 }
569
570 fn name(&self) -> &str {
571 "json"
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::state::ActionResultSnapshot;
579 use crate::types::WorkerId;
580 use std::time::Duration;
581
582 fn sample_tick_snapshot() -> TickSnapshot {
583 TickSnapshot {
584 tick: 42,
585 duration: Duration::from_micros(1500),
586 manager_phase: None,
587 worker_results: vec![WorkerResultSnapshot {
588 worker_id: WorkerId(0),
589 guidance_received: None,
590 result: WorkResultSnapshot::Acted {
591 action_result: ActionResultSnapshot {
592 success: true,
593 output_debug: Some("test output".to_string()),
594 duration: Duration::from_micros(500),
595 error: None,
596 },
597 state_delta: None,
598 },
599 }],
600 }
601 }
602
603 #[test]
604 fn test_console_formatter() {
605 let formatter = ConsoleFormatter::new();
606 let snapshot = sample_tick_snapshot();
607 let output = formatter.format_tick(&snapshot);
608
609 assert!(output.content.contains("Tick 42"));
610 assert!(output.content.contains("Acted"));
611 assert_eq!(output.item_count, 1);
612 }
613
614 #[test]
615 fn test_compact_formatter() {
616 let formatter = CompactFormatter::new();
617 let snapshot = sample_tick_snapshot();
618 let output = formatter.format_tick(&snapshot);
619
620 assert!(output.content.contains("T0042"));
621 assert!(output.content.contains("acted=1"));
622 assert_eq!(output.item_count, 1);
623 }
624
625 #[test]
626 fn test_json_formatter() {
627 let formatter = JsonFormatter::new();
628 let snapshot = sample_tick_snapshot();
629 let output = formatter.format_tick(&snapshot);
630
631 let parsed: serde_json::Value = serde_json::from_str(&output.content).unwrap();
633 assert_eq!(parsed["tick"], 42);
634 assert_eq!(parsed["worker_count"], 1);
635 }
636}