use crate::render::{RenderFormat, Rendered};
use crate::render_usage::usage_line;
use saya_harness::journal::{Journal, JournalWire};
use saya_types::{PauseReason, RunEvent, RunFailureCode};
use std::io::Write;
use std::sync::Arc;
pub(crate) fn journal_line(event: &RunEvent) -> String {
serde_json::to_string(event).unwrap_or_else(|_| format!("{event:?}"))
}
pub fn render_run_event(event: &RunEvent, format: RenderFormat) -> Rendered {
match format {
RenderFormat::Text => Rendered {
stdout: run_event_text(event),
stderr: String::new(),
},
RenderFormat::Json | RenderFormat::Ndjson => Rendered {
stdout: format!("{}\n", journal_line(event)),
stderr: String::new(),
},
}
}
pub(crate) fn print_run_event(event: &RunEvent, format: RenderFormat) {
let rendered = render_run_event(event, format);
print!("{}", rendered.stdout);
eprint!("{}", rendered.stderr);
let _ = std::io::stdout().flush();
}
pub(crate) fn run_wire(format: RenderFormat) -> JournalWire {
Arc::new(move |event: &RunEvent| print_run_event(event, format))
}
pub(crate) fn wired_journal(journal: Journal, format: RenderFormat) -> Journal {
journal.with_wire(run_wire(format))
}
pub(crate) fn run_event_text(event: &RunEvent) -> String {
match event {
RunEvent::RunStarted => "run started\n".into(),
RunEvent::PlanApproved { scopes: None } => "plan approved\n".into(),
RunEvent::PlanApproved {
scopes: Some(words),
} if words.is_empty() => "plan approved · (none)\n".into(),
RunEvent::PlanApproved {
scopes: Some(words),
} => {
format!("plan approved · {}\n", words.join(", "))
}
RunEvent::StepStarted { step } => format!("step {} started\n", step + 1),
RunEvent::StepCompleted { step } => format!("step {} completed\n", step + 1),
RunEvent::StepFailed { step } => format!("step {} failed\n", step + 1),
RunEvent::Paused { reason } => format!("run paused · {}\n", pause_reason_text(*reason)),
RunEvent::Completed => "run completed\n".into(),
RunEvent::Failed { code } => format!("run failed: {}\n", failure_code_cause(*code)),
RunEvent::Cancelled => "run cancelled\n".into(),
RunEvent::Usage {
endpoint,
tokens,
turns,
tool_calls,
cached_input_tokens,
cache_creation_input_tokens,
} => format!(
"usage · {endpoint} · tokens {} · cache reads {} · cache writes {} · turns {} · tool calls {}\n",
count_text(*tokens),
count_text(*cached_input_tokens),
count_text(*cache_creation_input_tokens),
count_text(*turns),
count_text(*tool_calls),
),
RunEvent::DownloadedBytes { bytes } => {
format!("download · {} bytes claimed so far\n", bytes)
}
_ => String::new(),
}
}
pub(crate) fn count_text(count: Option<u64>) -> String {
match count {
Some(count) => count.to_string(),
None => "unknown".into(),
}
}
pub(crate) fn failure_code_cause(code: RunFailureCode) -> &'static str {
match code {
RunFailureCode::SafetyQuery => "the safety gate refused a query",
RunFailureCode::ConnectionConfig => "a connection or configuration problem",
_ => "the provider or agent layer failed",
}
}
pub(crate) fn pause_reason_text(reason: PauseReason) -> &'static str {
match reason {
PauseReason::BudgetExhausted => "a declared budget tripped",
PauseReason::WallClockExceeded => "the wall-clock budget tripped",
PauseReason::StepFailedAfterRetry => "a step kept failing past the bounded retries",
PauseReason::StoreUnavailable => "the state store became unavailable",
PauseReason::UserPaused => "the run was paused by the user",
_ => "the process holding the run died",
}
}
pub(crate) struct RunShowStanza<'a> {
pub id: &'a str,
pub status: &'a str,
pub failure_cause: Option<&'a str>,
pub created_unix_ms: i64,
pub updated_unix_ms: i64,
pub spec: Option<(&'a str, &'a str)>,
pub paused: Option<PauseReason>,
pub deliverables: &'a [String],
pub usage: &'a [crate::render_usage::EndpointUsage],
}
pub(crate) fn run_show_text(stanza: RunShowStanza<'_>) -> String {
let RunShowStanza {
id,
status,
failure_cause,
created_unix_ms,
updated_unix_ms,
spec,
paused,
deliverables,
usage,
} = stanza;
let cause = match failure_cause {
Some(cause) => format!(" ({cause})"),
None => String::new(),
};
let mut text = format!(
"run {id}\nstatus: {status}{cause}\ncreated: {created}\nupdated: {updated}",
created = created_unix_ms,
updated = updated_unix_ms,
);
if let Some((goal, scopes)) = spec {
text.push_str(&format!("\ngoal: {goal}"));
text.push_str(&format!("\nscopes: {scopes}"));
}
if let Some(reason) = paused {
text.push_str(&format!("\npaused: {}", pause_reason_text(reason)));
}
if !deliverables.is_empty() {
text.push_str("\ndeliverables:");
for line in deliverables {
text.push_str(&format!("\n{line}"));
}
}
if !usage.is_empty() {
text.push_str("\nusage:");
for entry in usage {
text.push_str(&format!("\n{}", usage_line(entry)));
}
}
text
}
#[cfg(test)]
#[path = "render_run_tests.rs"]
mod tests;