Skip to main content

swarm_engine_core/snapshot/
formatter.rs

1//! Snapshot Formatter - スナップショットのフォーマット機能
2//!
3//! TickSnapshot を様々な形式で出力するための trait と実装。
4
5use std::fmt::Write;
6
7use crate::state::{ManagerPhaseSnapshot, TickSnapshot, WorkResultSnapshot, WorkerResultSnapshot};
8
9// ============================================================================
10// SnapshotOutput - フォーマット結果
11// ============================================================================
12
13/// フォーマット出力の結果
14#[derive(Debug, Clone)]
15pub struct SnapshotOutput {
16    /// フォーマットされた文字列
17    pub content: String,
18    /// 出力に含まれる要素数(tick数、worker数など)
19    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
38// ============================================================================
39// SnapshotFormatter Trait
40// ============================================================================
41
42/// スナップショットのフォーマッタ trait
43///
44/// 様々な出力形式(Console, JSON, Compact等)に対応するための抽象化。
45pub trait SnapshotFormatter: Send + Sync {
46    /// 単一の TickSnapshot をフォーマット
47    fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput;
48
49    /// ManagerPhaseSnapshot をフォーマット
50    fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput;
51
52    /// WorkerResultSnapshot をフォーマット
53    fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput;
54
55    /// 複数の TickSnapshot をまとめてフォーマット
56    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    /// フォーマッタの名前
73    fn name(&self) -> &str;
74}
75
76// ============================================================================
77// ConsoleFormatter - 人間が読みやすい形式
78// ============================================================================
79
80/// コンソール出力用フォーマッタ
81///
82/// 人間が読みやすい形式で出力。デバッグやverboseモードで使用。
83#[derive(Debug, Clone, Default)]
84pub struct ConsoleFormatter {
85    /// プロンプトを表示するか
86    pub show_prompts: bool,
87    /// 生レスポンスを表示するか
88    pub show_raw_responses: bool,
89    /// Idle Worker を表示するか
90    pub show_idle: bool,
91    /// 最大表示プロンプト数(0 = 全て)
92    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, // デフォルトは最初の1つのみ
102        }
103    }
104
105    /// 全てのプロンプトを表示
106    pub fn with_all_prompts(mut self) -> Self {
107        self.max_prompts = 0;
108        self
109    }
110
111    /// プロンプト表示を無効化
112    pub fn without_prompts(mut self) -> Self {
113        self.show_prompts = false;
114        self.show_raw_responses = false;
115        self
116    }
117
118    /// Idle Worker も表示
119    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        // Manager活動またはアクションがない場合はスキップ
136        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        // Manager phase
149        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        // Worker results
155        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            // プロンプトと生レスポンスの表示
208            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// ============================================================================
302// CompactFormatter - 1行サマリー形式
303// ============================================================================
304
305/// コンパクト出力用フォーマッタ
306///
307/// 1 Tick = 1行の簡潔な形式。ログファイルやリアルタイムモニタリング向け。
308#[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// ============================================================================
407// JsonFormatter - 構造化 JSON 形式
408// ============================================================================
409
410/// JSON出力用フォーマッタ
411///
412/// 機械可読なJSON形式で出力。分析ツールやログ集約システム向け。
413#[derive(Debug, Clone, Default)]
414pub struct JsonFormatter {
415    /// 整形出力するか
416    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        // JSON としてパースできることを確認
632        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}