story-kit: merge 314_story_show_token_cost_per_story_in_bot_status_command_output

This commit is contained in:
Dave
2026-03-19 19:57:09 +00:00
parent 3cb4f32634
commit 060d5a40a4

View File

@@ -270,6 +270,16 @@ pub fn build_pipeline_status(project_root: &std::path::Path, agents: &AgentPool)
.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: std::collections::HashMap<String, f64> =
crate::agents::token_usage::read_all(project_root)
.unwrap_or_default()
.into_iter()
.fold(std::collections::HashMap::new(), |mut map, r| {
*map.entry(r.story_id).or_insert(0.0) += r.usage.total_cost_usd;
map
});
let config = ProjectConfig::load(project_root).ok();
let mut out = String::from("**Pipeline Status**\n\n");
@@ -291,6 +301,11 @@ pub fn build_pipeline_status(project_root: &std::path::Path, agents: &AgentPool)
} else {
for (story_id, name) in &items {
let display = story_short_label(story_id, name.as_deref());
let cost_suffix = cost_by_story
.get(story_id)
.filter(|&&c| c > 0.0)
.map(|c| format!(" — ${c:.2}"))
.unwrap_or_default();
if let Some(agent) = active_map.get(story_id) {
let model_str = config
.as_ref()
@@ -298,11 +313,11 @@ pub fn build_pipeline_status(project_root: &std::path::Path, agents: &AgentPool)
.and_then(|ac| ac.model.as_deref())
.unwrap_or("?");
out.push_str(&format!(
"{display}{} ({model_str})\n",
"{display}{cost_suffix}{} ({model_str})\n",
agent.agent_name
));
} else {
out.push_str(&format!("{display}\n"));
out.push_str(&format!("{display}{cost_suffix}\n"));
}
}
}
@@ -1037,6 +1052,109 @@ mod tests {
);
}
// -- token cost in status output ----------------------------------------
#[test]
fn status_shows_cost_when_token_usage_exists() {
use std::io::Write;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let stage_dir = tmp.path().join(".story_kit/work/2_current");
std::fs::create_dir_all(&stage_dir).unwrap();
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
let mut f = std::fs::File::create(&story_path).unwrap();
writeln!(f, "---\nname: Register all bot commands\n---\n").unwrap();
// Write token usage for this story.
let usage = crate::agents::TokenUsage {
input_tokens: 100,
output_tokens: 200,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
total_cost_usd: 0.29,
};
let record = crate::agents::token_usage::build_record(
"293_story_register_all_bot_commands",
"coder-1",
None,
usage,
);
crate::agents::token_usage::append_record(tmp.path(), &record).unwrap();
let agents = AgentPool::new_test(3000);
let output = build_pipeline_status(tmp.path(), &agents);
assert!(
output.contains("293 — Register all bot commands — $0.29"),
"output must show cost next to story: {output}"
);
}
#[test]
fn status_no_cost_when_no_usage() {
use std::io::Write;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let stage_dir = tmp.path().join(".story_kit/work/2_current");
std::fs::create_dir_all(&stage_dir).unwrap();
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
let mut f = std::fs::File::create(&story_path).unwrap();
writeln!(f, "---\nname: Register all bot commands\n---\n").unwrap();
// No token usage written.
let agents = AgentPool::new_test(3000);
let output = build_pipeline_status(tmp.path(), &agents);
assert!(
!output.contains("$"),
"output must not show cost when no usage exists: {output}"
);
}
#[test]
fn status_aggregates_multiple_records_per_story() {
use std::io::Write;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let stage_dir = tmp.path().join(".story_kit/work/2_current");
std::fs::create_dir_all(&stage_dir).unwrap();
let story_path = stage_dir.join("293_story_register_all_bot_commands.md");
let mut f = std::fs::File::create(&story_path).unwrap();
writeln!(f, "---\nname: Register all bot commands\n---\n").unwrap();
// Write two records for the same story — costs should be summed.
for cost in [0.10, 0.19] {
let usage = crate::agents::TokenUsage {
input_tokens: 50,
output_tokens: 100,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
total_cost_usd: cost,
};
let record = crate::agents::token_usage::build_record(
"293_story_register_all_bot_commands",
"coder-1",
None,
usage,
);
crate::agents::token_usage::append_record(tmp.path(), &record).unwrap();
}
let agents = AgentPool::new_test(3000);
let output = build_pipeline_status(tmp.path(), &agents);
assert!(
output.contains("293 — Register all bot commands — $0.29"),
"output must show aggregated cost: {output}"
);
}
// -- commands registry --------------------------------------------------
#[test]