use rho_sdk::ToolCallId;
use rho_tools::tool_card::{ToolCard, ToolFact, ToolFamily, ToolHeader, ToolStatus};
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)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum CompactionUiOutcome {
Completed(CompactionDisplayFacts),
Unchanged { detail: String },
Failed { detail: String },
Cancelled,
}
impl CompactionUiOutcome {
pub(super) fn card(&self) -> ToolCard {
match self {
Self::Completed(facts) => completed_card(*facts),
Self::Unchanged { detail } => unchanged_card(detail.clone()),
Self::Failed { detail } => failed_card(detail.clone()),
Self::Cancelled => cancelled_card(),
}
}
}
pub(super) fn running_card() -> ToolCard {
ToolCard::new(
ToolStatus::Running,
ToolFamily::Default,
ToolHeader::call("compact", None),
)
.with_facts(vec![ToolFact::Meta {
text: "shrinking context…".into(),
}])
}
pub(super) fn completed_card(facts: CompactionDisplayFacts) -> ToolCard {
let mut card = ToolCard::new(
ToolStatus::Ok,
ToolFamily::Default,
ToolHeader::call("compact", None),
);
let has_token_signal = facts.previous_tokens > 0 || facts.current_tokens > 0;
if has_token_signal {
card.push_fact(ToolFact::Meta {
text: token_summary_line(facts),
});
}
if !has_token_signal
|| facts.previous_messages != facts.current_messages
|| facts.previous_messages > 0
{
card.push_fact(ToolFact::Meta {
text: message_summary_line(facts),
});
}
if let Some(cost) = facts.cost_usd_micros.filter(|cost| *cost > 0) {
card.push_fact(ToolFact::Meta {
text: format!("cost {}", format_usd(cost)),
});
}
card
}
pub(super) fn failed_card(detail: impl Into<String>) -> ToolCard {
ToolCard::new(
ToolStatus::Error,
ToolFamily::Default,
ToolHeader::call("compact", None),
)
.with_facts(vec![
ToolFact::Meta {
text: "failed".into(),
},
ToolFact::Error {
text: detail.into(),
},
])
}
pub(super) fn cancelled_card() -> ToolCard {
ToolCard::new(
ToolStatus::Interrupted,
ToolFamily::Default,
ToolHeader::call("compact", None),
)
.with_facts(vec![ToolFact::Meta {
text: "cancelled".into(),
}])
}
pub(super) fn unchanged_card(detail: impl Into<String>) -> ToolCard {
ToolCard::new(
ToolStatus::Ok,
ToolFamily::Default,
ToolHeader::call("compact", None),
)
.with_facts(vec![ToolFact::Meta {
text: 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;