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