1use std::path::Path;
4
5use anyhow::{Context, Result};
6
7use crate::types::{SessionMetrics, SessionSummary};
8
9const METRICS_FILE: &str = "session_metrics.json";
11
12pub fn save_metrics(metrics: &SessionMetrics, session_dir: &Path) -> Result<()> {
15 let path = session_dir.join(METRICS_FILE);
16 let json = serde_json::to_string_pretty(metrics)?;
17 std::fs::write(&path, json)?;
18 tracing::debug!(path = %path.display(), turns = metrics.total_turns, "session_metrics saved");
19 Ok(())
20}
21
22pub fn load_metrics(session_dir: &Path) -> Result<SessionMetrics> {
24 let path = session_dir.join(METRICS_FILE);
25 let content = std::fs::read_to_string(&path)
26 .with_context(|| format!("Failed to read metrics file: {}", path.display()))?;
27 let metrics: SessionMetrics = serde_json::from_str(&content)
28 .with_context(|| format!("Failed to parse metrics file: {}", path.display()))?;
29 Ok(metrics)
30}
31
32pub fn try_load_metrics(session_dir: &Path) -> Option<SessionMetrics> {
34 load_metrics(session_dir).ok()
35}
36
37pub fn list_all_metrics(base_dir: &Path) -> Result<Vec<SessionSummary>> {
40 let sessions_dir = base_dir.join("sessions");
41 if !sessions_dir.exists() {
42 return Ok(Vec::new());
43 }
44
45 let mut summaries = Vec::new();
46 for entry in std::fs::read_dir(&sessions_dir)? {
47 let entry = entry?;
48 let path = entry.path();
49 if !path.is_dir() {
50 continue;
51 }
52
53 if let Some(metrics) = try_load_metrics(&path) {
54 let product = metrics
55 .custom
56 .get("product")
57 .and_then(|v| v.as_str())
58 .map(|s| s.to_string());
59
60 summaries.push(SessionSummary {
61 session_id: metrics.session_id,
62 node_id: metrics.node_id,
63 created_at: metrics.created_at,
64 model: metrics.model,
65 total_turns: metrics.total_turns,
66 total_chars: metrics.total_chars,
67 outcome: metrics.outcome,
68 product,
69 });
70 }
71 }
72
73 summaries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
75 Ok(summaries)
76}