story-kit: merge 301_story_dedicated_token_usage_page_in_web_ui

This commit is contained in:
Dave
2026-03-19 11:34:08 +00:00
parent 586d06b840
commit a6ac6497e9
8 changed files with 534 additions and 8 deletions

View File

@@ -507,8 +507,11 @@ impl AgentPool {
&& let Some(agent) = agents.get(&key_clone)
&& let Some(ref pr) = agent.project_root
{
let model = config_clone
.find_agent(&aname)
.and_then(|a| a.model.clone());
let record = super::token_usage::build_record(
&sid, &aname, usage.clone(),
&sid, &aname, model, usage.clone(),
);
if let Err(e) = super::token_usage::append_record(pr, &record) {
slog_error!(

View File

@@ -12,6 +12,8 @@ pub struct TokenUsageRecord {
pub story_id: String,
pub agent_name: String,
pub timestamp: String,
#[serde(default)]
pub model: Option<String>,
pub usage: TokenUsage,
}
@@ -69,11 +71,17 @@ pub fn read_all(project_root: &Path) -> Result<Vec<TokenUsageRecord>, String> {
}
/// Build a `TokenUsageRecord` from the parts available at completion time.
pub fn build_record(story_id: &str, agent_name: &str, usage: TokenUsage) -> TokenUsageRecord {
pub fn build_record(
story_id: &str,
agent_name: &str,
model: Option<String>,
usage: TokenUsage,
) -> TokenUsageRecord {
TokenUsageRecord {
story_id: story_id.to_string(),
agent_name: agent_name.to_string(),
timestamp: Utc::now().to_rfc3339(),
model,
usage,
}
}
@@ -102,7 +110,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let root = dir.path();
let record = build_record("42_story_foo", "coder-1", sample_usage());
let record = build_record("42_story_foo", "coder-1", None, sample_usage());
append_record(root, &record).unwrap();
let records = read_all(root).unwrap();
@@ -117,8 +125,8 @@ mod tests {
let dir = TempDir::new().unwrap();
let root = dir.path();
let r1 = build_record("s1", "coder-1", sample_usage());
let r2 = build_record("s2", "coder-2", sample_usage());
let r1 = build_record("s1", "coder-1", None, sample_usage());
let r2 = build_record("s2", "coder-2", None, sample_usage());
append_record(root, &r1).unwrap();
append_record(root, &r2).unwrap();

View File

@@ -130,6 +130,26 @@ struct TokenCostResponse {
agents: Vec<AgentCostEntry>,
}
/// A single token usage record in the all-usage response.
#[derive(Object, Serialize)]
struct TokenUsageRecordResponse {
story_id: String,
agent_name: String,
model: Option<String>,
timestamp: String,
input_tokens: u64,
output_tokens: u64,
cache_creation_input_tokens: u64,
cache_read_input_tokens: u64,
total_cost_usd: f64,
}
/// Response for the all token usage endpoint.
#[derive(Object, Serialize)]
struct AllTokenUsageResponse {
records: Vec<TokenUsageRecordResponse>,
}
/// Returns true if the story file exists in `work/5_done/` or `work/6_archived/`.
///
/// Used to exclude agents for already-archived stories from the `list_agents`
@@ -532,6 +552,42 @@ impl AgentsApi {
agents,
}))
}
/// Get all token usage records across all stories.
///
/// Returns the full history from the persistent token_usage.jsonl log.
#[oai(path = "/token-usage", method = "get")]
async fn get_all_token_usage(
&self,
) -> OpenApiResult<Json<AllTokenUsageResponse>> {
let project_root = self
.ctx
.agents
.get_project_root(&self.ctx.state)
.map_err(bad_request)?;
let records = crate::agents::token_usage::read_all(&project_root)
.map_err(|e| bad_request(format!("Failed to read token usage: {e}")))?;
let response_records: Vec<TokenUsageRecordResponse> = records
.into_iter()
.map(|r| TokenUsageRecordResponse {
story_id: r.story_id,
agent_name: r.agent_name,
model: r.model,
timestamp: r.timestamp,
input_tokens: r.usage.input_tokens,
output_tokens: r.usage.output_tokens,
cache_creation_input_tokens: r.usage.cache_creation_input_tokens,
cache_read_input_tokens: r.usage.cache_read_input_tokens,
total_cost_usd: r.usage.total_cost_usd,
})
.collect();
Ok(Json(AllTokenUsageResponse {
records: response_records,
}))
}
}
#[cfg(test)]

View File

@@ -3862,7 +3862,7 @@ mod tests {
total_cost_usd: 1.57,
};
let record =
crate::agents::token_usage::build_record("42_story_foo", "coder-1", usage);
crate::agents::token_usage::build_record("42_story_foo", "coder-1", None, usage);
crate::agents::token_usage::append_record(root, &record).unwrap();
let result = tool_get_token_usage(&json!({}), &ctx).unwrap();
@@ -3888,8 +3888,8 @@ mod tests {
cache_read_input_tokens: 0,
total_cost_usd: 0.5,
};
let r1 = crate::agents::token_usage::build_record("10_story_a", "coder-1", usage.clone());
let r2 = crate::agents::token_usage::build_record("20_story_b", "coder-2", usage);
let r1 = crate::agents::token_usage::build_record("10_story_a", "coder-1", None, usage.clone());
let r2 = crate::agents::token_usage::build_record("20_story_b", "coder-2", None, usage);
crate::agents::token_usage::append_record(root, &r1).unwrap();
crate::agents::token_usage::append_record(root, &r2).unwrap();

View File

@@ -1194,6 +1194,7 @@ mod tests {
story_id: story_id.to_string(),
agent_name: agent_name.to_string(),
timestamp: ts,
model: None,
usage: make_usage(cost),
}
}