huskies: merge 1017
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user