use rho_sdk::ToolCallId;
use super::usage_cost::{format_token_count, format_usd};
pub(super) fn compaction_call_id() -> ToolCallId {
ToolCallId::from_string("rho:compact").expect("static compaction call id")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct CompactionDisplayFacts {
pub(super) previous_messages: usize,
pub(super) current_messages: usize,
pub(super) previous_tokens: u64,
pub(super) current_tokens: u64,
pub(super) cost_usd_micros: Option<u64>,
}
impl CompactionDisplayFacts {
pub(super) fn from_outcome(outcome: &rho_sdk::CompactionOutcome) -> Self {
Self {
previous_messages: outcome.previous_messages(),
current_messages: outcome.current_messages(),
previous_tokens: outcome.previous_tokens(),
current_tokens: outcome.current_tokens(),
cost_usd_micros: outcome.cost_usd_micros(),
}
}
pub(super) fn removed_tokens(self) -> u64 {
self.previous_tokens.saturating_sub(self.current_tokens)
}
pub(super) fn removed_messages(self) -> usize {
self.previous_messages.saturating_sub(self.current_messages)
}
pub(super) fn reduced(self) -> bool {
self.removed_tokens() > 0 || self.removed_messages() > 0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum CompactionUiOutcome {
Completed(CompactionDisplayFacts),
Unchanged { detail: String },
Failed { detail: String },
Cancelled,
}
impl CompactionUiOutcome {
pub(super) fn ok(&self) -> bool {
!matches!(self, Self::Failed { .. })
}
pub(super) fn display_lines(&self) -> Vec<String> {
match self {
Self::Completed(facts) => completed_display_lines(*facts),
Self::Unchanged { detail } => unchanged_display_lines(detail.clone()),
Self::Failed { detail } => failed_display_lines(detail.clone()),
Self::Cancelled => cancelled_display_lines(),
}
}
}
pub(super) fn running_display_lines() -> Vec<String> {
vec!["compact".into(), "shrinking context…".into()]
}
pub(super) fn completed_display_lines(facts: CompactionDisplayFacts) -> Vec<String> {
let mut lines = vec!["compact".into()];
let has_token_signal = facts.previous_tokens > 0 || facts.current_tokens > 0;
if has_token_signal {
lines.push(token_summary_line(facts));
}
if !has_token_signal
|| facts.previous_messages != facts.current_messages
|| facts.previous_messages > 0
{
lines.push(message_summary_line(facts));
}
if let Some(cost) = facts.cost_usd_micros.filter(|cost| *cost > 0) {
lines.push(format!("cost {}", format_usd(cost)));
}
if !facts.reduced() && lines.len() == 1 {
lines.push("no reduction".into());
}
lines
}
pub(super) fn failed_display_lines(detail: impl Into<String>) -> Vec<String> {
vec!["compact".into(), "failed".into(), detail.into()]
}
pub(super) fn cancelled_display_lines() -> Vec<String> {
vec!["compact".into(), "cancelled".into()]
}
pub(super) fn unchanged_display_lines(detail: impl Into<String>) -> Vec<String> {
vec!["compact".into(), detail.into()]
}
fn token_summary_line(facts: CompactionDisplayFacts) -> String {
let mut line = format!(
"{} → {} tokens",
format_token_count(facts.previous_tokens),
format_token_count(facts.current_tokens),
);
let removed = facts.removed_tokens();
if removed > 0 {
let percent = reduction_percent(facts.previous_tokens, removed);
line.push_str(&format!(
" (−{} · {percent}%)",
format_token_count(removed)
));
} else if facts.previous_tokens == facts.current_tokens && facts.previous_tokens > 0 {
line.push_str(" (no change)");
}
line
}
fn message_summary_line(facts: CompactionDisplayFacts) -> String {
let mut line = format!(
"{} → {} messages",
facts.previous_messages, facts.current_messages
);
let removed = facts.removed_messages();
if removed > 0 {
line.push_str(&format!(" (−{removed})"));
}
line
}
fn reduction_percent(previous: u64, removed: u64) -> u64 {
if previous == 0 {
return 0;
}
((removed as f64 * 100.0) / previous as f64).round() as u64
}
#[cfg(test)]
#[path = "compaction_display_tests.rs"]
mod tests;