Skip to main content

telos_agent/
metrics.rs

1//! Session-level metrics — lightweight counters updated by the runtime.
2//!
3//! [`SessionMetrics`] is embedded in [`AgentSession`](crate::AgentSession) and
4//! accumulates counters across all turns. Callers can snapshot it via
5//! [`AgentSession::metrics`](crate::AgentSession::metrics) to feed their own
6//! monitoring stack (Prometheus, CloudWatch, etc.).
7
8use std::sync::Arc;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11/// Accumulated metrics for an agent session, updated internally by the runtime.
12///
13/// All counters are monotonic across all turns. Clone is cheap — the struct
14/// wraps `Arc`s so snapshots share the same underlying counters.
15#[derive(Debug, Clone)]
16pub struct SessionMetrics {
17    inner: Arc<MetricsInner>,
18}
19
20#[derive(Debug)]
21struct MetricsInner {
22    total_input_tokens: AtomicUsize,
23    total_output_tokens: AtomicUsize,
24    total_prompt_cache_hit_tokens: AtomicUsize,
25    total_prompt_cache_miss_tokens: AtomicUsize,
26    total_tool_calls: AtomicUsize,
27    total_tool_errors: AtomicUsize,
28    total_iterations: AtomicUsize,
29    compaction_count: AtomicUsize,
30    turn_count: AtomicUsize,
31    retry_count: AtomicUsize,
32}
33
34impl SessionMetrics {
35    /// Create a fresh metrics accumulator.
36    pub(crate) fn new() -> Self {
37        Self::with_values(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
38    }
39
40    /// Restore metrics from persisted counter values.
41    pub(crate) fn with_values(
42        total_input_tokens: usize,
43        total_output_tokens: usize,
44        total_prompt_cache_hit_tokens: usize,
45        total_prompt_cache_miss_tokens: usize,
46        total_tool_calls: usize,
47        total_tool_errors: usize,
48        total_iterations: usize,
49        compaction_count: usize,
50        turn_count: usize,
51        retry_count: usize,
52    ) -> Self {
53        Self {
54            inner: Arc::new(MetricsInner {
55                total_input_tokens: AtomicUsize::new(total_input_tokens),
56                total_output_tokens: AtomicUsize::new(total_output_tokens),
57                total_prompt_cache_hit_tokens: AtomicUsize::new(total_prompt_cache_hit_tokens),
58                total_prompt_cache_miss_tokens: AtomicUsize::new(total_prompt_cache_miss_tokens),
59                total_tool_calls: AtomicUsize::new(total_tool_calls),
60                total_tool_errors: AtomicUsize::new(total_tool_errors),
61                total_iterations: AtomicUsize::new(total_iterations),
62                compaction_count: AtomicUsize::new(compaction_count),
63                turn_count: AtomicUsize::new(turn_count),
64                retry_count: AtomicUsize::new(retry_count),
65            }),
66        }
67    }
68
69    // -- Read methods (public API) --
70
71    /// Total input tokens consumed across all turns.
72    pub fn total_input_tokens(&self) -> usize {
73        self.inner.total_input_tokens.load(Ordering::Relaxed)
74    }
75
76    /// Total output tokens produced across all turns.
77    pub fn total_output_tokens(&self) -> usize {
78        self.inner.total_output_tokens.load(Ordering::Relaxed)
79    }
80
81    /// Total prompt tokens served from cache across all turns.
82    pub fn total_prompt_cache_hit_tokens(&self) -> usize {
83        self.inner.total_prompt_cache_hit_tokens.load(Ordering::Relaxed)
84    }
85
86    /// Total prompt tokens not served from cache across all turns.
87    pub fn total_prompt_cache_miss_tokens(&self) -> usize {
88        self.inner.total_prompt_cache_miss_tokens.load(Ordering::Relaxed)
89    }
90
91    /// Total number of tool calls executed.
92    pub fn total_tool_calls(&self) -> usize {
93        self.inner.total_tool_calls.load(Ordering::Relaxed)
94    }
95
96    /// Total number of tool calls that resulted in errors.
97    pub fn total_tool_errors(&self) -> usize {
98        self.inner.total_tool_errors.load(Ordering::Relaxed)
99    }
100
101    /// Total number of model ⇄ tool iterations across all turns.
102    pub fn total_iterations(&self) -> usize {
103        self.inner.total_iterations.load(Ordering::Relaxed)
104    }
105
106    /// Number of times compaction was triggered.
107    pub fn compaction_count(&self) -> usize {
108        self.inner.compaction_count.load(Ordering::Relaxed)
109    }
110
111    /// Number of turns completed.
112    pub fn turn_count(&self) -> usize {
113        self.inner.turn_count.load(Ordering::Relaxed)
114    }
115
116    /// Number of provider retries across all turns.
117    pub fn retry_count(&self) -> usize {
118        self.inner.retry_count.load(Ordering::Relaxed)
119    }
120
121    // -- Update methods (used by the runtime) --
122
123    pub(crate) fn add_input_tokens(&self, n: usize) {
124        self.inner.total_input_tokens.fetch_add(n, Ordering::Relaxed);
125    }
126
127    pub(crate) fn add_output_tokens(&self, n: usize) {
128        self.inner.total_output_tokens.fetch_add(n, Ordering::Relaxed);
129    }
130
131    pub(crate) fn add_prompt_cache_hit_tokens(&self, n: usize) {
132        self.inner.total_prompt_cache_hit_tokens.fetch_add(n, Ordering::Relaxed);
133    }
134
135    pub(crate) fn add_prompt_cache_miss_tokens(&self, n: usize) {
136        self.inner.total_prompt_cache_miss_tokens.fetch_add(n, Ordering::Relaxed);
137    }
138
139    pub(crate) fn add_tool_call(&self) {
140        self.inner.total_tool_calls.fetch_add(1, Ordering::Relaxed);
141    }
142
143    pub(crate) fn add_tool_error(&self) {
144        self.inner.total_tool_errors.fetch_add(1, Ordering::Relaxed);
145    }
146
147    pub(crate) fn add_iteration(&self) {
148        self.inner.total_iterations.fetch_add(1, Ordering::Relaxed);
149    }
150
151    pub(crate) fn add_compaction(&self) {
152        self.inner.compaction_count.fetch_add(1, Ordering::Relaxed);
153    }
154
155    pub(crate) fn add_turn(&self) {
156        self.inner.turn_count.fetch_add(1, Ordering::Relaxed);
157    }
158
159    pub(crate) fn add_retry(&self) {
160        self.inner.retry_count.fetch_add(1, Ordering::Relaxed);
161    }
162
163    // -- Checkpoint / restore (used by the runtime to roll back failed turns) --
164
165    /// Snapshot current counter values for later restore.
166    pub(crate) fn checkpoint(&self) -> MetricsCheckpoint {
167        MetricsCheckpoint {
168            total_input_tokens: self.inner.total_input_tokens.load(Ordering::Relaxed),
169            total_output_tokens: self.inner.total_output_tokens.load(Ordering::Relaxed),
170            total_prompt_cache_hit_tokens: self
171                .inner
172                .total_prompt_cache_hit_tokens
173                .load(Ordering::Relaxed),
174            total_prompt_cache_miss_tokens: self
175                .inner
176                .total_prompt_cache_miss_tokens
177                .load(Ordering::Relaxed),
178            total_tool_calls: self.inner.total_tool_calls.load(Ordering::Relaxed),
179            total_tool_errors: self.inner.total_tool_errors.load(Ordering::Relaxed),
180            total_iterations: self.inner.total_iterations.load(Ordering::Relaxed),
181            compaction_count: self.inner.compaction_count.load(Ordering::Relaxed),
182            turn_count: self.inner.turn_count.load(Ordering::Relaxed),
183            retry_count: self.inner.retry_count.load(Ordering::Relaxed),
184        }
185    }
186
187    /// Restore counters to previously snapshotted values.
188    pub(crate) fn restore(&self, cp: &MetricsCheckpoint) {
189        self.inner.total_input_tokens.store(cp.total_input_tokens, Ordering::Relaxed);
190        self.inner.total_output_tokens.store(cp.total_output_tokens, Ordering::Relaxed);
191        self.inner
192            .total_prompt_cache_hit_tokens
193            .store(cp.total_prompt_cache_hit_tokens, Ordering::Relaxed);
194        self.inner
195            .total_prompt_cache_miss_tokens
196            .store(cp.total_prompt_cache_miss_tokens, Ordering::Relaxed);
197        self.inner.total_tool_calls.store(cp.total_tool_calls, Ordering::Relaxed);
198        self.inner.total_tool_errors.store(cp.total_tool_errors, Ordering::Relaxed);
199        self.inner.total_iterations.store(cp.total_iterations, Ordering::Relaxed);
200        self.inner.compaction_count.store(cp.compaction_count, Ordering::Relaxed);
201        self.inner.turn_count.store(cp.turn_count, Ordering::Relaxed);
202        self.inner.retry_count.store(cp.retry_count, Ordering::Relaxed);
203    }
204}
205
206/// Opaque snapshot of [`SessionMetrics`] counter values.
207pub(crate) struct MetricsCheckpoint {
208    total_input_tokens: usize,
209    total_output_tokens: usize,
210    total_prompt_cache_hit_tokens: usize,
211    total_prompt_cache_miss_tokens: usize,
212    total_tool_calls: usize,
213    total_tool_errors: usize,
214    total_iterations: usize,
215    compaction_count: usize,
216    turn_count: usize,
217    retry_count: usize,
218}