huskies: merge 1017

This commit is contained in:
dave
2026-05-13 23:51:12 +00:00
parent 29e800da21
commit 52180bc402
8 changed files with 651 additions and 35 deletions
+21 -24
View File
@@ -8,43 +8,36 @@ use super::status::story_short_label;
/// Show token spend: 24h total, top 5 stories, agent-type breakdown, and
/// all-time total.
pub(super) fn handle_cost(ctx: &CommandContext) -> Option<String> {
let records = match crate::agents::token_usage::read_all(ctx.effective_root()) {
Ok(r) => r,
Err(e) => return Some(format!("Failed to read token usage: {e}")),
};
let rollups = crate::service::agents::cost_rollup::all_rollups(ctx.effective_root());
if records.is_empty() {
if rollups.is_empty() {
return Some("**Token Spend**\n\nNo usage records found.".to_string());
}
let now = chrono::Utc::now();
let cutoff = now - chrono::Duration::hours(24);
// Partition into 24h window and all-time
let mut recent = Vec::new();
let mut all_time_cost = 0.0;
for r in &records {
all_time_cost += r.usage.total_cost_usd;
if let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&r.timestamp)
&& ts >= cutoff
{
// Partition into 24h window (stories that completed recently) and all-time.
let mut recent: Vec<&crate::service::agents::cost_rollup::CostRollup> = Vec::new();
let mut all_time_cost = 0.0_f64;
for r in &rollups {
all_time_cost += r.total_cost_usd;
if r.recorded_at >= cutoff {
recent.push(r);
}
}
// 24h total
let recent_cost: f64 = recent.iter().map(|r| r.usage.total_cost_usd).sum();
let recent_cost: f64 = recent.iter().map(|r| r.total_cost_usd).sum();
let mut out = String::from("**Token Spend**\n\n");
out.push_str(&format!("**Last 24h:** ${:.2}\n", recent_cost));
out.push_str(&format!("**All-time:** ${:.2}\n\n", all_time_cost));
// Top 5 most expensive stories (last 24h)
let mut story_costs: HashMap<&str, f64> = HashMap::new();
for r in &recent {
*story_costs.entry(r.story_id.as_str()).or_default() += r.usage.total_cost_usd;
}
let mut story_list: Vec<(&str, f64)> = story_costs.into_iter().collect();
let mut story_list: Vec<(&str, f64)> = recent
.iter()
.map(|r| (r.story_id.as_str(), r.total_cost_usd))
.collect();
story_list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
story_list.truncate(5);
@@ -59,13 +52,15 @@ pub(super) fn handle_cost(ctx: &CommandContext) -> Option<String> {
}
out.push('\n');
// Breakdown by agent type (last 24h)
// Breakdown by agent type (last 24h) — derived from per-agent data in rollups.
// Agent names follow pattern "coder-1", "qa-1", "mergemaster" — extract
// the type as everything before the last '-' digit, or the full name.
let mut type_costs: HashMap<String, f64> = HashMap::new();
for r in &recent {
let agent_type = extract_agent_type(&r.agent_name);
*type_costs.entry(agent_type).or_default() += r.usage.total_cost_usd;
for rollup in &recent {
for agent in &rollup.agents {
let agent_type = extract_agent_type(&agent.agent_name);
*type_costs.entry(agent_type).or_default() += agent.total_cost_usd;
}
}
let mut type_list: Vec<(String, f64)> = type_costs.into_iter().collect();
type_list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
@@ -108,6 +103,8 @@ mod tests {
for r in records {
crate::agents::token_usage::append_record(root, r).unwrap();
}
// Pre-populate the register so the cost command can read from it.
crate::service::agents::cost_rollup::init_from_disk(root);
}
fn make_usage(cost: f64) -> crate::agents::TokenUsage {
+7 -8
View File
@@ -104,14 +104,13 @@ pub(crate) fn build_status_from_items(
.map(|a| (a.story_id.clone(), a))
.collect();
// Read token usage once for all stories to avoid repeated file I/O.
let cost_by_story: HashMap<String, f64> = crate::agents::token_usage::read_all(project_root)
.unwrap_or_default()
.into_iter()
.fold(HashMap::new(), |mut map, r| {
*map.entry(r.story_id).or_insert(0.0) += r.usage.total_cost_usd;
map
});
// Build a per-story cost map from the in-memory rollup register.
// Only completed stories have entries; in-progress stories show no cost.
let cost_by_story: HashMap<String, f64> =
crate::service::agents::cost_rollup::all_rollups(project_root)
.into_iter()
.map(|r| (r.story_id, r.total_cost_usd))
.collect();
let config = ProjectConfig::load(project_root).ok();
+2
View File
@@ -181,6 +181,7 @@ fn status_shows_cost_when_token_usage_exists() {
usage,
);
crate::agents::token_usage::append_record(tmp.path(), &record).unwrap();
crate::service::agents::cost_rollup::init_from_disk(tmp.path());
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);
@@ -239,6 +240,7 @@ fn status_aggregates_multiple_records_per_story() {
);
crate::agents::token_usage::append_record(tmp.path(), &record).unwrap();
}
crate::service::agents::cost_rollup::init_from_disk(tmp.path());
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);