use std::fmt::Write;
use crate::state::{ManagerPhaseSnapshot, TickSnapshot, WorkResultSnapshot, WorkerResultSnapshot};
#[derive(Debug, Clone)]
pub struct SnapshotOutput {
pub content: String,
pub item_count: usize,
}
impl SnapshotOutput {
pub fn new(content: String, item_count: usize) -> Self {
Self {
content,
item_count,
}
}
pub fn empty() -> Self {
Self {
content: String::new(),
item_count: 0,
}
}
}
pub trait SnapshotFormatter: Send + Sync {
fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput;
fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput;
fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput;
fn format_history(&self, history: &[TickSnapshot]) -> SnapshotOutput {
let mut output = String::new();
let mut count = 0;
for snapshot in history {
let tick_output = self.format_tick(snapshot);
if !tick_output.content.is_empty() {
output.push_str(&tick_output.content);
output.push('\n');
count += 1;
}
}
SnapshotOutput::new(output, count)
}
fn name(&self) -> &str;
}
#[derive(Debug, Clone, Default)]
pub struct ConsoleFormatter {
pub show_prompts: bool,
pub show_raw_responses: bool,
pub show_idle: bool,
pub max_prompts: usize,
}
impl ConsoleFormatter {
pub fn new() -> Self {
Self {
show_prompts: true,
show_raw_responses: true,
show_idle: false,
max_prompts: 1, }
}
pub fn with_all_prompts(mut self) -> Self {
self.max_prompts = 0;
self
}
pub fn without_prompts(mut self) -> Self {
self.show_prompts = false;
self.show_raw_responses = false;
self
}
pub fn with_idle(mut self) -> Self {
self.show_idle = true;
self
}
}
impl SnapshotFormatter for ConsoleFormatter {
fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
let has_manager = snapshot.manager_phase.is_some();
let has_action = snapshot.worker_results.iter().any(|r| {
matches!(
r.result,
WorkResultSnapshot::Acted { .. } | WorkResultSnapshot::Done { .. }
)
});
if !has_manager && !has_action {
return SnapshotOutput::empty();
}
let mut output = String::new();
writeln!(
output,
"\n--- Tick {} ({:?}) ---",
snapshot.tick, snapshot.duration
)
.unwrap();
if let Some(manager) = &snapshot.manager_phase {
let manager_output = self.format_manager_phase(manager);
output.push_str(&manager_output.content);
}
for wr in &snapshot.worker_results {
let worker_output = self.format_worker_result(wr);
if !worker_output.content.is_empty() {
output.push_str(&worker_output.content);
}
}
SnapshotOutput::new(output, 1)
}
fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
let mut output = String::new();
writeln!(output, " Manager:").unwrap();
writeln!(
output,
" Requests: {} workers",
phase.batch_request.requests.len()
)
.unwrap();
for req in &phase.batch_request.requests {
writeln!(
output,
" W{}: candidates={:?}",
req.worker_id.0, req.context.candidates
)
.unwrap();
writeln!(output, " query: {}", req.query).unwrap();
writeln!(
output,
" context: tick={}, progress={:.1}%",
req.context.global.tick,
req.context.global.progress * 100.0
)
.unwrap();
}
writeln!(output, " Responses: {}", phase.responses.len()).unwrap();
for (i, (wid, resp)) in phase.responses.iter().enumerate() {
writeln!(
output,
" W{}: tool={}, target={}, confidence={:.2}",
wid.0, resp.tool, resp.target, resp.confidence
)
.unwrap();
if let Some(reason) = &resp.reasoning {
writeln!(output, " reasoning: {}", reason).unwrap();
}
let show_this = self.max_prompts == 0 || i < self.max_prompts;
if show_this {
if self.show_prompts {
if let Some(prompt) = &resp.prompt {
writeln!(output, " --- Prompt ---").unwrap();
for line in prompt.lines() {
writeln!(output, " {}", line).unwrap();
}
}
}
if self.show_raw_responses {
if let Some(raw) = &resp.raw_response {
writeln!(output, " --- Raw Response ---").unwrap();
writeln!(output, " {}", raw.trim()).unwrap();
}
}
}
}
writeln!(output, " Guidances: {}", phase.guidances.len()).unwrap();
for (wid, guidance) in &phase.guidances {
let action_names: Vec<_> = guidance.actions.iter().map(|a| &a.name).collect();
writeln!(output, " W{}: {:?}", wid.0, action_names).unwrap();
}
SnapshotOutput::new(output, phase.responses.len())
}
fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
let mut output = String::new();
match &result.result {
WorkResultSnapshot::Acted { action_result, .. } => {
let action_name = result
.guidance_received
.as_ref()
.and_then(|g| g.actions.first())
.map(|a| a.name.as_str())
.unwrap_or("unknown");
writeln!(
output,
" W{}: Acted - {} (success={})",
result.worker_id.0, action_name, action_result.success
)
.unwrap();
}
WorkResultSnapshot::NeedsGuidance { reason, .. } => {
writeln!(
output,
" W{}: NeedsGuidance - {}",
result.worker_id.0, reason
)
.unwrap();
}
WorkResultSnapshot::Escalate { reason, .. } => {
writeln!(output, " W{}: Escalate - {:?}", result.worker_id.0, reason).unwrap();
}
WorkResultSnapshot::Done { success, message } => {
writeln!(
output,
" W{}: Done (success={}) - {}",
result.worker_id.0,
success,
message.as_deref().unwrap_or("(no message)")
)
.unwrap();
}
WorkResultSnapshot::Continuing { progress } => {
writeln!(
output,
" W{}: Continuing ({:.1}%)",
result.worker_id.0,
progress * 100.0
)
.unwrap();
}
WorkResultSnapshot::Idle => {
if self.show_idle {
writeln!(output, " W{}: Idle", result.worker_id.0).unwrap();
}
}
}
let count = if output.is_empty() { 0 } else { 1 };
SnapshotOutput::new(output, count)
}
fn name(&self) -> &str {
"console"
}
}
#[derive(Debug, Clone, Default)]
pub struct CompactFormatter;
impl CompactFormatter {
pub fn new() -> Self {
Self
}
}
impl SnapshotFormatter for CompactFormatter {
fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
let manager_str = if let Some(m) = &snapshot.manager_phase {
format!(
"M(req={},resp={})",
m.batch_request.requests.len(),
m.responses.len()
)
} else {
"M(-)".to_string()
};
let mut acted = 0;
let mut done = 0;
let mut idle = 0;
for wr in &snapshot.worker_results {
match &wr.result {
WorkResultSnapshot::Acted { .. } => acted += 1,
WorkResultSnapshot::Done { .. } => done += 1,
WorkResultSnapshot::Idle => idle += 1,
_ => {}
}
}
let content = format!(
"T{:04} [{:?}] {} W(acted={},done={},idle={})",
snapshot.tick, snapshot.duration, manager_str, acted, done, idle
);
SnapshotOutput::new(content, 1)
}
fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
let content = format!(
"Manager: req={} resp={} guidance={} errors={}",
phase.batch_request.requests.len(),
phase.responses.len(),
phase.guidances.len(),
phase.llm_errors
);
SnapshotOutput::new(content, 1)
}
fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
let content = match &result.result {
WorkResultSnapshot::Acted { action_result, .. } => {
let action = result
.guidance_received
.as_ref()
.and_then(|g| g.actions.first())
.map(|a| a.name.as_str())
.unwrap_or("?");
format!(
"W{}: {} ({})",
result.worker_id.0,
action,
if action_result.success { "ok" } else { "fail" }
)
}
WorkResultSnapshot::Done { success, .. } => {
format!(
"W{}: DONE ({})",
result.worker_id.0,
if *success { "ok" } else { "fail" }
)
}
WorkResultSnapshot::NeedsGuidance { .. } => {
format!("W{}: NEEDS_GUIDANCE", result.worker_id.0)
}
WorkResultSnapshot::Escalate { .. } => {
format!("W{}: ESCALATE", result.worker_id.0)
}
WorkResultSnapshot::Continuing { progress } => {
format!("W{}: CONT({:.0}%)", result.worker_id.0, progress * 100.0)
}
WorkResultSnapshot::Idle => {
format!("W{}: IDLE", result.worker_id.0)
}
};
SnapshotOutput::new(content, 1)
}
fn name(&self) -> &str {
"compact"
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonFormatter {
pub pretty: bool,
}
impl JsonFormatter {
pub fn new() -> Self {
Self { pretty: false }
}
pub fn pretty() -> Self {
Self { pretty: true }
}
}
impl SnapshotFormatter for JsonFormatter {
fn format_tick(&self, snapshot: &TickSnapshot) -> SnapshotOutput {
let obj = serde_json::json!({
"tick": snapshot.tick,
"duration_us": snapshot.duration.as_micros(),
"has_manager": snapshot.manager_phase.is_some(),
"worker_count": snapshot.worker_results.len(),
"manager": snapshot.manager_phase.as_ref().map(|m| {
serde_json::json!({
"requests": m.batch_request.requests.len(),
"responses": m.responses.len(),
"guidances": m.guidances.len(),
"llm_errors": m.llm_errors,
})
}),
"workers": snapshot.worker_results.iter().map(|wr| {
let (status, success) = match &wr.result {
WorkResultSnapshot::Acted { action_result, .. } => ("acted", Some(action_result.success)),
WorkResultSnapshot::Done { success, .. } => ("done", Some(*success)),
WorkResultSnapshot::NeedsGuidance { .. } => ("needs_guidance", None),
WorkResultSnapshot::Escalate { .. } => ("escalate", None),
WorkResultSnapshot::Continuing { .. } => ("continuing", None),
WorkResultSnapshot::Idle => ("idle", None),
};
serde_json::json!({
"worker_id": wr.worker_id.0,
"status": status,
"success": success,
})
}).collect::<Vec<_>>(),
});
let content = if self.pretty {
serde_json::to_string_pretty(&obj).unwrap_or_default()
} else {
serde_json::to_string(&obj).unwrap_or_default()
};
SnapshotOutput::new(content, 1)
}
fn format_manager_phase(&self, phase: &ManagerPhaseSnapshot) -> SnapshotOutput {
let obj = serde_json::json!({
"requests": phase.batch_request.requests.iter().map(|r| {
serde_json::json!({
"worker_id": r.worker_id.0,
"query": r.query,
"candidates": r.context.candidates.iter().map(|c| &c.name).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
"responses": phase.responses.iter().map(|(wid, resp)| {
serde_json::json!({
"worker_id": wid.0,
"tool": resp.tool,
"target": resp.target,
"confidence": resp.confidence,
"reasoning": resp.reasoning,
"has_prompt": resp.prompt.is_some(),
"has_raw_response": resp.raw_response.is_some(),
})
}).collect::<Vec<_>>(),
"guidances": phase.guidances.iter().map(|(wid, g)| {
serde_json::json!({
"worker_id": wid.0,
"actions": g.actions.iter().map(|a| &a.name).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
"llm_errors": phase.llm_errors,
});
let content = if self.pretty {
serde_json::to_string_pretty(&obj).unwrap_or_default()
} else {
serde_json::to_string(&obj).unwrap_or_default()
};
SnapshotOutput::new(content, phase.responses.len())
}
fn format_worker_result(&self, result: &WorkerResultSnapshot) -> SnapshotOutput {
let (status, details) = match &result.result {
WorkResultSnapshot::Acted { action_result, .. } => {
let action = result
.guidance_received
.as_ref()
.and_then(|g| g.actions.first())
.map(|a| a.name.clone());
(
"acted",
serde_json::json!({
"action": action,
"success": action_result.success,
"duration_us": action_result.duration.as_micros(),
"error": action_result.error,
}),
)
}
WorkResultSnapshot::Done { success, message } => (
"done",
serde_json::json!({
"success": success,
"message": message,
}),
),
WorkResultSnapshot::NeedsGuidance { reason, .. } => (
"needs_guidance",
serde_json::json!({
"reason": reason,
}),
),
WorkResultSnapshot::Escalate { reason, context } => (
"escalate",
serde_json::json!({
"reason": format!("{:?}", reason),
"context": context,
}),
),
WorkResultSnapshot::Continuing { progress } => (
"continuing",
serde_json::json!({
"progress": progress,
}),
),
WorkResultSnapshot::Idle => ("idle", serde_json::json!({})),
};
let obj = serde_json::json!({
"worker_id": result.worker_id.0,
"status": status,
"details": details,
});
let content = if self.pretty {
serde_json::to_string_pretty(&obj).unwrap_or_default()
} else {
serde_json::to_string(&obj).unwrap_or_default()
};
SnapshotOutput::new(content, 1)
}
fn name(&self) -> &str {
"json"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::ActionResultSnapshot;
use crate::types::WorkerId;
use std::time::Duration;
fn sample_tick_snapshot() -> TickSnapshot {
TickSnapshot {
tick: 42,
duration: Duration::from_micros(1500),
manager_phase: None,
worker_results: vec![WorkerResultSnapshot {
worker_id: WorkerId(0),
guidance_received: None,
result: WorkResultSnapshot::Acted {
action_result: ActionResultSnapshot {
success: true,
output_debug: Some("test output".to_string()),
duration: Duration::from_micros(500),
error: None,
},
state_delta: None,
},
}],
}
}
#[test]
fn test_console_formatter() {
let formatter = ConsoleFormatter::new();
let snapshot = sample_tick_snapshot();
let output = formatter.format_tick(&snapshot);
assert!(output.content.contains("Tick 42"));
assert!(output.content.contains("Acted"));
assert_eq!(output.item_count, 1);
}
#[test]
fn test_compact_formatter() {
let formatter = CompactFormatter::new();
let snapshot = sample_tick_snapshot();
let output = formatter.format_tick(&snapshot);
assert!(output.content.contains("T0042"));
assert!(output.content.contains("acted=1"));
assert_eq!(output.item_count, 1);
}
#[test]
fn test_json_formatter() {
let formatter = JsonFormatter::new();
let snapshot = sample_tick_snapshot();
let output = formatter.format_tick(&snapshot);
let parsed: serde_json::Value = serde_json::from_str(&output.content).unwrap();
assert_eq!(parsed["tick"], 42);
assert_eq!(parsed["worker_count"], 1);
}
}