Skip to main content

bamboo_engine/metrics/
types.rs

1//! Metrics types for tracking agent performance and usage
2//!
3//! This module provides data structures for collecting and aggregating
4//! metrics about agent sessions, token usage, tool calls, and performance.
5
6use std::collections::HashMap;
7
8use chrono::{DateTime, NaiveDate, Utc};
9use serde::{Deserialize, Serialize};
10
11/// Re-exported shared token usage type.
12///
13/// See [`bamboo_domain::TokenUsage`] for the canonical definition.
14pub use bamboo_domain::TokenUsage;
15
16/// Round execution status
17#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "snake_case")]
19pub enum RoundStatus {
20    /// Round is currently running
21    Running,
22    /// Round completed successfully
23    Success,
24    /// Round ended with an error
25    Error,
26    /// Round was cancelled by user
27    Cancelled,
28}
29
30impl RoundStatus {
31    pub fn as_str(self) -> &'static str {
32        match self {
33            Self::Running => "running",
34            Self::Success => "success",
35            Self::Error => "error",
36            Self::Cancelled => "cancelled",
37        }
38    }
39
40    pub fn from_db(value: &str) -> Option<Self> {
41        match value {
42            "running" => Some(Self::Running),
43            "success" => Some(Self::Success),
44            "error" => Some(Self::Error),
45            "cancelled" => Some(Self::Cancelled),
46            _ => None,
47        }
48    }
49}
50
51/// Session execution status
52#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum SessionStatus {
55    /// Session is currently active
56    Running,
57    /// Session execution is paused awaiting an external response or resume trigger
58    AwaitingResponse,
59    /// Session completed successfully
60    Completed,
61    /// Session ended with an error
62    Error,
63    /// Session was cancelled by user
64    Cancelled,
65}
66
67impl SessionStatus {
68    pub fn as_str(self) -> &'static str {
69        match self {
70            Self::Running => "running",
71            Self::AwaitingResponse => "awaiting_response",
72            Self::Completed => "completed",
73            Self::Error => "error",
74            Self::Cancelled => "cancelled",
75        }
76    }
77
78    pub fn from_db(value: &str) -> Option<Self> {
79        match value {
80            "running" => Some(Self::Running),
81            "awaiting_response" => Some(Self::AwaitingResponse),
82            "completed" => Some(Self::Completed),
83            "error" => Some(Self::Error),
84            "cancelled" => Some(Self::Cancelled),
85            _ => None,
86        }
87    }
88}
89
90/// Metrics for a single tool call
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct ToolCallMetrics {
93    /// Unique identifier for the tool call
94    pub tool_call_id: String,
95    /// Name of the tool that was called
96    pub tool_name: String,
97    /// When the tool call started
98    pub started_at: DateTime<Utc>,
99    /// When the tool call completed
100    pub completed_at: Option<DateTime<Utc>>,
101    /// Whether the call succeeded
102    pub success: Option<bool>,
103    /// Error message if the call failed
104    pub error: Option<String>,
105    /// Duration of the call in milliseconds
106    pub duration_ms: Option<u64>,
107}
108
109/// Metrics for a single conversation round
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub struct RoundMetrics {
112    /// Unique round identifier
113    pub round_id: String,
114    /// Session this round belongs to
115    pub session_id: String,
116    /// Model used for this round
117    pub model: String,
118    /// When the round started
119    pub started_at: DateTime<Utc>,
120    /// When the round completed
121    pub completed_at: Option<DateTime<Utc>>,
122    /// Token usage for this round
123    pub token_usage: TokenUsage,
124    /// Tool calls made during this round
125    pub tool_calls: Vec<ToolCallMetrics>,
126    /// Round execution status
127    pub status: RoundStatus,
128    /// Error message if round failed
129    pub error: Option<String>,
130    /// Round duration in milliseconds
131    pub duration_ms: Option<u64>,
132    /// Number of tool outputs compacted into prompt-side cache summaries in this round.
133    #[serde(default)]
134    pub prompt_cached_tool_outputs: u32,
135    /// Number of context compression events applied during this round.
136    #[serde(default)]
137    pub compression_count: u32,
138    /// Tokens saved by context compression during this round.
139    #[serde(default)]
140    pub tokens_saved: u32,
141}
142
143/// Metrics for an entire session
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145pub struct SessionMetrics {
146    /// Unique session identifier
147    pub session_id: String,
148    /// Model used for this session
149    pub model: String,
150    /// When the session started
151    pub started_at: DateTime<Utc>,
152    /// When the session completed
153    pub completed_at: Option<DateTime<Utc>>,
154    /// Total number of rounds in the session
155    pub total_rounds: u32,
156    /// Total token usage across all rounds
157    pub total_token_usage: TokenUsage,
158    /// Total number of tool calls
159    pub tool_call_count: u32,
160    /// Breakdown of tool calls by tool name
161    pub tool_breakdown: HashMap<String, u32>,
162    /// Session execution status
163    pub status: SessionStatus,
164    /// Total number of messages exchanged
165    pub message_count: u32,
166    /// Session duration in milliseconds
167    pub duration_ms: Option<u64>,
168    /// Total number of prompt-side cached tool outputs observed across rounds.
169    #[serde(default)]
170    pub prompt_cached_tool_outputs: u64,
171    /// Total number of context compression events across all rounds.
172    #[serde(default)]
173    pub total_compression_events: u64,
174    /// Total tokens saved by context compression across all rounds.
175    #[serde(default)]
176    pub total_tokens_saved: u64,
177}
178
179/// Detailed session metrics with round information
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
181pub struct SessionDetail {
182    /// Session-level metrics
183    pub session: SessionMetrics,
184    /// Metrics for each round in the session
185    pub rounds: Vec<RoundMetrics>,
186}
187
188/// Aggregated metrics for a single day
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct DailyMetrics {
191    /// Date for these metrics
192    pub date: NaiveDate,
193    /// Total number of sessions
194    pub total_sessions: u32,
195    /// Total number of rounds
196    pub total_rounds: u32,
197    /// Total token usage
198    pub total_token_usage: TokenUsage,
199    /// Total number of tool calls
200    pub total_tool_calls: u32,
201    /// Token usage breakdown by model
202    pub model_breakdown: HashMap<String, TokenUsage>,
203    /// Tool call breakdown by tool name
204    pub tool_breakdown: HashMap<String, u32>,
205    /// Total number of prompt-side cached tool outputs observed on this day.
206    #[serde(default)]
207    pub prompt_cached_tool_outputs: u64,
208}
209
210/// Overall metrics summary
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212pub struct MetricsSummary {
213    /// Total number of sessions
214    pub total_sessions: u64,
215    /// Total token usage
216    pub total_tokens: TokenUsage,
217    /// Total number of tool calls
218    pub total_tool_calls: u64,
219    /// Number of currently active sessions
220    pub active_sessions: u64,
221    /// Total number of prompt-side cached tool outputs.
222    #[serde(default)]
223    pub prompt_cached_tool_outputs: u64,
224    /// Total number of context compression events.
225    #[serde(default)]
226    pub total_compression_events: u64,
227    /// Total tokens saved by context compression.
228    #[serde(default)]
229    pub total_tokens_saved: u64,
230    /// Number of completed sessions in the selected range.
231    #[serde(default)]
232    pub completed_sessions: u64,
233    /// Number of sessions currently paused awaiting an external response.
234    #[serde(default)]
235    pub awaiting_response_sessions: u64,
236    /// Number of sessions that ended with an error.
237    #[serde(default)]
238    pub error_sessions: u64,
239    /// Number of sessions cancelled by the user.
240    #[serde(default)]
241    pub cancelled_sessions: u64,
242    /// Total number of execute sync mismatches observed for the filtered period.
243    #[serde(default)]
244    pub total_sync_mismatches: u64,
245    /// Breakdown of execute sync mismatches by stable reason label.
246    #[serde(default)]
247    pub sync_mismatch_breakdown: HashMap<String, u64>,
248}
249
250/// Metrics aggregated by model
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
252pub struct ModelMetrics {
253    /// Model name
254    pub model: String,
255    /// Number of sessions using this model
256    pub sessions: u64,
257    /// Number of rounds using this model
258    pub rounds: u64,
259    /// Token usage for this model
260    pub tokens: TokenUsage,
261    /// Number of tool calls using this model
262    pub tool_calls: u64,
263    /// Number of prompt-side cached tool outputs for this model.
264    #[serde(default)]
265    pub prompt_cached_tool_outputs: u64,
266}
267
268/// Date filter for metrics queries
269#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
270pub struct MetricsDateFilter {
271    /// Start date (inclusive)
272    pub start_date: Option<NaiveDate>,
273    /// End date (inclusive)
274    pub end_date: Option<NaiveDate>,
275}
276
277/// Filter for session metrics queries
278#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
279pub struct SessionMetricsFilter {
280    /// Start date filter
281    pub start_date: Option<NaiveDate>,
282    /// End date filter
283    pub end_date: Option<NaiveDate>,
284    /// Filter by model name
285    pub model: Option<String>,
286    /// Limit number of results
287    pub limit: Option<u32>,
288}
289
290/// Status of a forwarded request
291#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
292#[serde(rename_all = "snake_case")]
293pub enum ForwardStatus {
294    /// Request has been recorded but not yet completed
295    Pending,
296    /// Request succeeded
297    Success,
298    /// Request failed
299    Error,
300}
301
302impl ForwardStatus {
303    pub fn as_str(self) -> &'static str {
304        match self {
305            Self::Pending => "pending",
306            Self::Success => "success",
307            Self::Error => "error",
308        }
309    }
310
311    pub fn from_db(value: &str) -> Option<Self> {
312        match value {
313            "pending" => Some(Self::Pending),
314            "success" => Some(Self::Success),
315            "error" => Some(Self::Error),
316            _ => None,
317        }
318    }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
322pub struct ForwardRequestMetrics {
323    pub forward_id: String,
324    pub endpoint: String,
325    pub model: String,
326    pub is_stream: bool,
327    pub started_at: DateTime<Utc>,
328    pub completed_at: Option<DateTime<Utc>>,
329    pub status_code: Option<u16>,
330    pub status: Option<ForwardStatus>,
331    pub token_usage: Option<TokenUsage>,
332    pub error: Option<String>,
333    pub duration_ms: Option<u64>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
337pub struct ForwardMetricsSummary {
338    pub total_requests: u64,
339    pub successful_requests: u64,
340    pub failed_requests: u64,
341    pub total_tokens: TokenUsage,
342    pub avg_duration_ms: Option<u64>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
346pub struct ForwardEndpointMetrics {
347    pub endpoint: String,
348    pub requests: u64,
349    pub successful: u64,
350    pub failed: u64,
351    pub tokens: TokenUsage,
352    pub avg_duration_ms: Option<u64>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
356pub struct ForwardMetricsFilter {
357    pub start_date: Option<NaiveDate>,
358    pub end_date: Option<NaiveDate>,
359    pub endpoint: Option<String>,
360    pub model: Option<String>,
361    pub limit: Option<u32>,
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_token_usage_default() {
370        let usage = TokenUsage::default();
371        assert_eq!(usage.prompt_tokens, 0);
372        assert_eq!(usage.completion_tokens, 0);
373        assert_eq!(usage.total_tokens, 0);
374    }
375
376    #[test]
377    fn test_token_usage_add_assign() {
378        let mut usage1 = TokenUsage {
379            prompt_tokens: 100,
380            completion_tokens: 50,
381            total_tokens: 150,
382        };
383        let usage2 = TokenUsage {
384            prompt_tokens: 200,
385            completion_tokens: 100,
386            total_tokens: 300,
387        };
388
389        usage1.add_assign(usage2);
390
391        assert_eq!(usage1.prompt_tokens, 300);
392        assert_eq!(usage1.completion_tokens, 150);
393        assert_eq!(usage1.total_tokens, 450);
394    }
395
396    #[test]
397    fn test_token_usage_serialization() {
398        let usage = TokenUsage {
399            prompt_tokens: 100,
400            completion_tokens: 50,
401            total_tokens: 150,
402        };
403
404        let json = serde_json::to_string(&usage).unwrap();
405        assert!(json.contains("\"prompt_tokens\":100"));
406        assert!(json.contains("\"completion_tokens\":50"));
407    }
408
409    #[test]
410    fn test_round_status_as_str() {
411        assert_eq!(RoundStatus::Running.as_str(), "running");
412        assert_eq!(RoundStatus::Success.as_str(), "success");
413        assert_eq!(RoundStatus::Error.as_str(), "error");
414        assert_eq!(RoundStatus::Cancelled.as_str(), "cancelled");
415    }
416
417    #[test]
418    fn test_round_status_from_db() {
419        assert_eq!(RoundStatus::from_db("running"), Some(RoundStatus::Running));
420        assert_eq!(RoundStatus::from_db("success"), Some(RoundStatus::Success));
421        assert_eq!(RoundStatus::from_db("invalid"), None);
422    }
423
424    #[test]
425    fn test_round_status_serialization() {
426        let status = RoundStatus::Success;
427        let json = serde_json::to_string(&status).unwrap();
428        assert!(json.contains("\"success\""));
429    }
430
431    #[test]
432    fn test_session_status_as_str() {
433        assert_eq!(SessionStatus::Running.as_str(), "running");
434        assert_eq!(SessionStatus::AwaitingResponse.as_str(), "awaiting_response");
435        assert_eq!(SessionStatus::Completed.as_str(), "completed");
436        assert_eq!(SessionStatus::Error.as_str(), "error");
437        assert_eq!(SessionStatus::Cancelled.as_str(), "cancelled");
438    }
439
440    #[test]
441    fn test_session_status_from_db() {
442        assert_eq!(
443            SessionStatus::from_db("running"),
444            Some(SessionStatus::Running)
445        );
446        assert_eq!(
447            SessionStatus::from_db("awaiting_response"),
448            Some(SessionStatus::AwaitingResponse)
449        );
450        assert_eq!(
451            SessionStatus::from_db("completed"),
452            Some(SessionStatus::Completed)
453        );
454        assert_eq!(SessionStatus::from_db("invalid"), None);
455    }
456
457    #[test]
458    fn test_forward_status_as_str() {
459        assert_eq!(ForwardStatus::Pending.as_str(), "pending");
460        assert_eq!(ForwardStatus::Success.as_str(), "success");
461        assert_eq!(ForwardStatus::Error.as_str(), "error");
462    }
463
464    #[test]
465    fn test_forward_status_from_db() {
466        assert_eq!(ForwardStatus::from_db("pending"), Some(ForwardStatus::Pending));
467        assert_eq!(
468            ForwardStatus::from_db("success"),
469            Some(ForwardStatus::Success)
470        );
471        assert_eq!(ForwardStatus::from_db("error"), Some(ForwardStatus::Error));
472        assert_eq!(ForwardStatus::from_db("invalid"), None);
473    }
474
475    #[test]
476    fn test_metrics_date_filter_default() {
477        let filter = MetricsDateFilter::default();
478        assert!(filter.start_date.is_none());
479        assert!(filter.end_date.is_none());
480    }
481
482    #[test]
483    fn test_session_metrics_filter_default() {
484        let filter = SessionMetricsFilter::default();
485        assert!(filter.start_date.is_none());
486        assert!(filter.model.is_none());
487        assert!(filter.limit.is_none());
488    }
489
490    #[test]
491    fn test_forward_metrics_filter_default() {
492        let filter = ForwardMetricsFilter::default();
493        assert!(filter.start_date.is_none());
494        assert!(filter.endpoint.is_none());
495        assert!(filter.limit.is_none());
496    }
497
498    #[test]
499    fn test_tool_call_metrics_serialization() {
500        let metrics = ToolCallMetrics {
501            tool_call_id: "call-123".to_string(),
502            tool_name: "bash".to_string(),
503            started_at: Utc::now(),
504            completed_at: Some(Utc::now()),
505            success: Some(true),
506            error: None,
507            duration_ms: Some(150),
508        };
509
510        let json = serde_json::to_string(&metrics).unwrap();
511        assert!(json.contains("\"tool_call_id\":\"call-123\""));
512        assert!(json.contains("\"tool_name\":\"bash\""));
513    }
514
515    #[test]
516    fn test_round_metrics_serialization() {
517        let metrics = RoundMetrics {
518            round_id: "round-1".to_string(),
519            session_id: "session-1".to_string(),
520            model: "gpt-4".to_string(),
521            started_at: Utc::now(),
522            completed_at: None,
523            token_usage: TokenUsage::default(),
524            tool_calls: vec![],
525            status: RoundStatus::Running,
526            error: None,
527            duration_ms: None,
528            prompt_cached_tool_outputs: 0,
529            compression_count: 0,
530            tokens_saved: 0,
531        };
532
533        let json = serde_json::to_string(&metrics).unwrap();
534        assert!(json.contains("\"model\":\"gpt-4\""));
535    }
536
537    #[test]
538    fn test_session_metrics_serialization() {
539        let metrics = SessionMetrics {
540            session_id: "session-1".to_string(),
541            model: "gpt-4".to_string(),
542            started_at: Utc::now(),
543            completed_at: None,
544            total_rounds: 5,
545            total_token_usage: TokenUsage::default(),
546            tool_call_count: 10,
547            tool_breakdown: HashMap::new(),
548            status: SessionStatus::Running,
549            message_count: 15,
550            duration_ms: None,
551            prompt_cached_tool_outputs: 0,
552            total_compression_events: 0,
553            total_tokens_saved: 0,
554        };
555
556        let json = serde_json::to_string(&metrics).unwrap();
557        assert!(json.contains("\"session_id\":\"session-1\""));
558        assert!(json.contains("\"total_rounds\":5"));
559    }
560
561    #[test]
562    fn test_daily_metrics_serialization() {
563        let metrics = DailyMetrics {
564            date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
565            total_sessions: 10,
566            total_rounds: 50,
567            total_token_usage: TokenUsage::default(),
568            total_tool_calls: 100,
569            prompt_cached_tool_outputs: 0,
570            model_breakdown: HashMap::new(),
571            tool_breakdown: HashMap::new(),
572        };
573
574        let json = serde_json::to_string(&metrics).unwrap();
575        assert!(json.contains("\"total_sessions\":10"));
576        assert!(json.contains("\"total_rounds\":50"));
577    }
578
579    #[test]
580    fn test_metrics_summary_serialization() {
581        let summary = MetricsSummary {
582            total_sessions: 100,
583            total_tokens: TokenUsage::default(),
584            total_tool_calls: 500,
585            active_sessions: 5,
586            prompt_cached_tool_outputs: 0,
587            total_compression_events: 0,
588            total_tokens_saved: 0,
589            completed_sessions: 80,
590            awaiting_response_sessions: 10,
591            error_sessions: 7,
592            cancelled_sessions: 3,
593            total_sync_mismatches: 0,
594            sync_mismatch_breakdown: HashMap::new(),
595        };
596
597        let json = serde_json::to_string(&summary).unwrap();
598        assert!(json.contains("\"total_sessions\":100"));
599        assert!(json.contains("\"active_sessions\":5"));
600    }
601
602    #[test]
603    fn test_model_metrics_serialization() {
604        let metrics = ModelMetrics {
605            model: "gpt-4".to_string(),
606            sessions: 50,
607            rounds: 200,
608            tokens: TokenUsage::default(),
609            tool_calls: 100,
610            prompt_cached_tool_outputs: 0,
611        };
612
613        let json = serde_json::to_string(&metrics).unwrap();
614        assert!(json.contains("\"model\":\"gpt-4\""));
615        assert!(json.contains("\"sessions\":50"));
616    }
617
618    #[test]
619    fn test_forward_request_metrics_serialization() {
620        let metrics = ForwardRequestMetrics {
621            forward_id: "fwd-123".to_string(),
622            endpoint: "/api/chat".to_string(),
623            model: "gpt-4".to_string(),
624            is_stream: true,
625            started_at: Utc::now(),
626            completed_at: Some(Utc::now()),
627            status_code: Some(200),
628            status: Some(ForwardStatus::Success),
629            token_usage: None,
630            error: None,
631            duration_ms: Some(250),
632        };
633
634        let json = serde_json::to_string(&metrics).unwrap();
635        assert!(json.contains("\"forward_id\":\"fwd-123\""));
636        assert!(json.contains("\"endpoint\":\"/api/chat\""));
637    }
638
639    #[test]
640    fn test_forward_metrics_summary_serialization() {
641        let summary = ForwardMetricsSummary {
642            total_requests: 1000,
643            successful_requests: 950,
644            failed_requests: 50,
645            total_tokens: TokenUsage::default(),
646            avg_duration_ms: Some(200),
647        };
648
649        let json = serde_json::to_string(&summary).unwrap();
650        assert!(json.contains("\"total_requests\":1000"));
651        assert!(json.contains("\"successful_requests\":950"));
652    }
653
654    #[test]
655    fn test_token_usage_clone() {
656        let usage = TokenUsage {
657            prompt_tokens: 100,
658            completion_tokens: 50,
659            total_tokens: 150,
660        };
661        let cloned = usage.clone();
662        assert_eq!(usage.prompt_tokens, cloned.prompt_tokens);
663    }
664
665    #[test]
666    fn test_round_status_clone() {
667        let status = RoundStatus::Success;
668        let cloned = status.clone();
669        assert_eq!(status, cloned);
670    }
671
672    #[test]
673    fn test_session_status_clone() {
674        let status = SessionStatus::Completed;
675        let cloned = status.clone();
676        assert_eq!(status, cloned);
677    }
678
679    #[test]
680    fn test_forward_status_clone() {
681        let status = ForwardStatus::Pending;
682        let cloned = status.clone();
683        assert_eq!(status, cloned);
684    }
685
686    #[test]
687    fn test_token_usage_eq() {
688        let usage1 = TokenUsage {
689            prompt_tokens: 100,
690            completion_tokens: 50,
691            total_tokens: 150,
692        };
693        let usage2 = TokenUsage {
694            prompt_tokens: 100,
695            completion_tokens: 50,
696            total_tokens: 150,
697        };
698        assert_eq!(usage1, usage2);
699    }
700
701    #[test]
702    fn test_round_status_eq() {
703        assert_eq!(RoundStatus::Running, RoundStatus::Running);
704        assert_ne!(RoundStatus::Running, RoundStatus::Success);
705    }
706
707    #[test]
708    fn test_session_status_eq() {
709        assert_eq!(SessionStatus::Running, SessionStatus::Running);
710        assert_ne!(SessionStatus::Running, SessionStatus::Completed);
711        assert_ne!(SessionStatus::AwaitingResponse, SessionStatus::Completed);
712    }
713
714    #[test]
715    fn test_round_metrics_compression_fields_deserialize_with_defaults() {
716        let json = r#"{"round_id":"r1","session_id":"s1","model":"m","started_at":"2026-01-01T00:00:00Z","completed_at":null,"token_usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"tool_calls":[],"status":"running","error":null,"duration_ms":null,"prompt_cached_tool_outputs":0}"#;
717        let metrics: RoundMetrics = serde_json::from_str(json).unwrap();
718        assert_eq!(metrics.compression_count, 0);
719        assert_eq!(metrics.tokens_saved, 0);
720    }
721
722    #[test]
723    fn test_session_metrics_compression_fields_deserialize_with_defaults() {
724        let json = r#"{"session_id":"s1","model":"m","started_at":"2026-01-01T00:00:00Z","completed_at":null,"total_rounds":0,"total_token_usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"tool_call_count":0,"tool_breakdown":{},"status":"running","message_count":0,"duration_ms":null,"prompt_cached_tool_outputs":0}"#;
725        let metrics: SessionMetrics = serde_json::from_str(json).unwrap();
726        assert_eq!(metrics.total_compression_events, 0);
727        assert_eq!(metrics.total_tokens_saved, 0);
728    }
729
730    #[test]
731    fn test_metrics_summary_additive_fields_deserialize_with_defaults() {
732        let json = r#"{"total_sessions":1,"total_tokens":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"total_tool_calls":0,"active_sessions":0}"#;
733        let summary: MetricsSummary = serde_json::from_str(json).unwrap();
734        assert_eq!(summary.prompt_cached_tool_outputs, 0);
735        assert_eq!(summary.total_compression_events, 0);
736        assert_eq!(summary.total_tokens_saved, 0);
737        assert_eq!(summary.completed_sessions, 0);
738        assert_eq!(summary.awaiting_response_sessions, 0);
739        assert_eq!(summary.error_sessions, 0);
740        assert_eq!(summary.cancelled_sessions, 0);
741    }
742}