Skip to main content

ag_agent/model/
session.rs

1//! Session execution settings and usage statistics shared by agent transports.
2
3use std::fmt;
4use std::str::FromStr;
5
6/// Response-speed preference applied to one agent session.
7#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
8pub enum SpeedMode {
9    /// Use the provider's standard routing and pricing.
10    #[default]
11    Normal,
12    /// Request the provider's higher-cost low-latency routing.
13    Fast,
14}
15
16impl SpeedMode {
17    /// Stable selector ordering.
18    pub const ALL: [Self; 2] = [Self::Normal, Self::Fast];
19
20    /// Stable persisted identifier for this speed mode.
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::Normal => "normal",
24            Self::Fast => "fast",
25        }
26    }
27
28    /// User-visible selector label for this speed mode.
29    pub const fn name(self) -> &'static str {
30        match self {
31            Self::Normal => "Normal",
32            Self::Fast => "Fast",
33        }
34    }
35
36    /// User-visible explanation of this speed mode.
37    pub const fn description(self) -> &'static str {
38        match self {
39            Self::Normal => "Standard response speed and provider pricing.",
40            Self::Fast => "Faster responses at a higher provider cost.",
41        }
42    }
43
44    /// Codex app-server service-tier value for this speed mode.
45    pub const fn codex_service_tier(self) -> &'static str {
46        match self {
47            Self::Normal => "default",
48            Self::Fast => "fast",
49        }
50    }
51
52    /// Whether Claude Code should enable its `fastMode` setting.
53    pub const fn claude_fast_mode(self) -> bool {
54        matches!(self, Self::Fast)
55    }
56}
57
58impl fmt::Display for SpeedMode {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        formatter.write_str(self.as_str())
61    }
62}
63
64impl FromStr for SpeedMode {
65    type Err = String;
66
67    fn from_str(value: &str) -> Result<Self, Self::Err> {
68        match value {
69            "normal" => Ok(Self::Normal),
70            "fast" => Ok(Self::Fast),
71            _ => Err(format!("Unknown speed mode: {value}")),
72        }
73    }
74}
75
76/// Known availability of a session worktree diff.
77#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
78pub enum SessionDiffState {
79    /// Diff availability could not be determined, so callers should preserve
80    /// access to diagnostic diff output.
81    #[default]
82    Unknown,
83    /// The latest successful diff refresh returned no content.
84    Empty,
85    /// The latest successful diff refresh returned content.
86    Present,
87}
88
89/// Token and diff usage statistics associated with one agent session or
90/// isolated prompt.
91#[derive(Clone, PartialEq, Eq, Debug, Default)]
92pub struct SessionStats {
93    /// Added diff lines currently attributed to the session worktree.
94    pub added_lines: u64,
95    /// Deleted diff lines currently attributed to the session worktree.
96    pub deleted_lines: u64,
97    /// Availability derived from the latest worktree diff refresh.
98    pub diff_state: SessionDiffState,
99    /// Input/prompt tokens consumed by this session.
100    pub input_tokens: u64,
101    /// Output/response tokens produced by this session.
102    pub output_tokens: u64,
103}
104
105impl SessionStats {
106    /// Returns whether the UI should advertise access to the session diff.
107    ///
108    /// Unknown state retains the shortcut so a subsequent diff attempt can
109    /// surface the underlying Git diagnostic instead of hiding it.
110    pub fn should_show_diff(&self) -> bool {
111        self.diff_state != SessionDiffState::Empty
112    }
113
114    /// Counts added and deleted lines in one git patch while ignoring file
115    /// header markers such as `+++` and `---`.
116    pub fn line_change_counts(diff: &str) -> (u64, u64) {
117        diff.lines()
118            .fold((0_u64, 0_u64), |(added_lines, deleted_lines), line| {
119                if line.starts_with('+') && !line.starts_with("+++") {
120                    return (added_lines.saturating_add(1), deleted_lines);
121                }
122
123                if line.starts_with('-') && !line.starts_with("---") {
124                    return (added_lines, deleted_lines.saturating_add(1));
125                }
126
127                (added_lines, deleted_lines)
128            })
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn speed_mode_round_trips_persisted_values() {
138        // Arrange, Act, Assert
139        for speed_mode in SpeedMode::ALL {
140            assert_eq!(speed_mode.as_str().parse::<SpeedMode>(), Ok(speed_mode));
141            assert_eq!(speed_mode.to_string(), speed_mode.as_str());
142        }
143    }
144
145    #[test]
146    fn speed_mode_maps_provider_settings() {
147        // Arrange, Act, Assert
148        assert_eq!(SpeedMode::Normal.codex_service_tier(), "default");
149        assert!(!SpeedMode::Normal.claude_fast_mode());
150        assert_eq!(SpeedMode::Fast.codex_service_tier(), "fast");
151        assert!(SpeedMode::Fast.claude_fast_mode());
152    }
153
154    #[test]
155    fn speed_mode_rejects_unknown_persisted_value() {
156        // Arrange, Act
157        let result = "turbo".parse::<SpeedMode>();
158
159        // Assert
160        assert_eq!(result, Err("Unknown speed mode: turbo".to_string()));
161    }
162
163    #[test]
164    fn should_show_diff_hides_only_known_empty_diffs() {
165        // Arrange
166        let unknown = SessionStats::default();
167        let empty = SessionStats {
168            diff_state: SessionDiffState::Empty,
169            ..SessionStats::default()
170        };
171        let present = SessionStats {
172            diff_state: SessionDiffState::Present,
173            ..SessionStats::default()
174        };
175
176        // Act, Assert
177        assert!(unknown.should_show_diff());
178        assert!(!empty.should_show_diff());
179        assert!(present.should_show_diff());
180    }
181}