Skip to main content

agent_session/
types.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4//! Data types for agent session representation.
5
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::path::PathBuf;
9use std::time::{Duration, Instant, SystemTime};
10
11use crate::{discover_session_files, parse_session_file};
12
13/// Token usage statistics for a model or session.
14#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
15pub struct TokenUsage {
16    pub input_tokens: i64,
17    pub output_tokens: i64,
18    pub cache_creation_tokens: i64,
19    pub cache_read_tokens: i64,
20    pub total_tokens: i64,
21}
22
23impl TokenUsage {
24    pub(crate) fn add(
25        &mut self,
26        input: i64,
27        output: i64,
28        cache_creation: i64,
29        cache_read: i64,
30        total: i64,
31    ) {
32        self.input_tokens += input;
33        self.output_tokens += output;
34        self.cache_creation_tokens += cache_creation;
35        self.cache_read_tokens += cache_read;
36        self.total_tokens += if total > 0 {
37            total
38        } else {
39            input + output + cache_creation + cache_read
40        };
41    }
42}
43
44/// A parsed agent session with metadata, token usage, and tool invocations.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AgentSession {
47    pub agent_type: String,
48    pub session_id: String,
49    pub conversation_id: Option<String>,
50    pub display_id: String,
51    pub path: PathBuf,
52    pub updated: SystemTime,
53    pub start_timestamp_ms: Option<u64>,
54    pub end_timestamp_ms: Option<u64>,
55    pub model: Option<String>,
56    pub usage: TokenUsage,
57    pub model_usage: BTreeMap<String, TokenUsage>,
58    pub tools: BTreeMap<String, usize>,
59    pub files: BTreeMap<String, usize>,
60    pub prompt_preview: Option<String>,
61    pub duration_ms: u64,
62    pub cwd: Option<String>,
63    pub last_message_at: Option<String>,
64}
65
66/// A candidate session file discovered on disk.
67#[derive(Debug, Clone)]
68pub struct SessionCandidate {
69    pub agent: &'static str,
70    pub path: PathBuf,
71    pub updated: SystemTime,
72}
73
74/// Statistics about a session directory.
75#[derive(Debug, Clone)]
76pub struct SessionDirStat {
77    pub agent: &'static str,
78    pub dir: PathBuf,
79    pub sessions: usize,
80    pub bytes: u64,
81}
82
83/// Cache for discovered and parsed sessions.
84#[derive(Default)]
85pub struct SessionCache {
86    entries: HashMap<PathBuf, CacheEntry>,
87    cached_sessions: Vec<AgentSession>,
88    last_refresh: Option<Instant>,
89    last_limit: usize,
90}
91
92struct CacheEntry {
93    mtime: SystemTime,
94    session: Option<AgentSession>,
95}
96
97impl SessionCache {
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    pub fn discover_cached(&mut self, limit: usize, max_age: Duration) -> Vec<AgentSession> {
103        let target = limit.clamp(1, 25);
104        if self.last_limit < target
105            || self
106                .last_refresh
107                .is_none_or(|last| last.elapsed() >= max_age)
108        {
109            self.refresh(target);
110        }
111        self.cached_sessions.iter().take(target).cloned().collect()
112    }
113
114    fn refresh(&mut self, limit: usize) {
115        let mut candidates = discover_session_files();
116        candidates.sort_by_key(|candidate| std::cmp::Reverse(candidate.updated));
117        let target = limit.clamp(1, 25);
118        let mut live_paths = HashSet::new();
119        let mut sessions = Vec::new();
120        let mut seen = HashSet::new();
121
122        for candidate in candidates
123            .into_iter()
124            .take(target.saturating_mul(3).clamp(10, 75))
125        {
126            live_paths.insert(candidate.path.clone());
127            let session = match self.entries.get(&candidate.path) {
128                Some(entry) if entry.mtime == candidate.updated => entry.session.clone(),
129                _ => {
130                    let parsed = parse_session_file(&candidate);
131                    self.entries.insert(
132                        candidate.path.clone(),
133                        CacheEntry {
134                            mtime: candidate.updated,
135                            session: parsed.clone(),
136                        },
137                    );
138                    parsed
139                }
140            };
141            if let Some(session) = session
142                && seen.insert(session.display_id.clone())
143            {
144                sessions.push(session);
145                if sessions.len() >= target {
146                    break;
147                }
148            }
149        }
150        self.entries.retain(|path, _| live_paths.contains(path));
151        self.cached_sessions = sessions;
152        self.last_refresh = Some(Instant::now());
153        self.last_limit = target;
154    }
155}