agent_code_lib/state/
mod.rs1use std::collections::HashMap;
7
8use crate::config::Config;
9use crate::llm::message::{Message, Usage};
10
11pub struct AppState {
17 pub config: Config,
19 pub messages: Vec<Message>,
21 pub is_query_active: bool,
23 pub total_usage: Usage,
25 pub total_cost_usd: f64,
27 pub turn_count: usize,
29 pub cwd: String,
31 pub model_usage: HashMap<String, Usage>,
33 pub plan_mode: bool,
35 pub task_manager: std::sync::Arc<crate::services::background::TaskManager>,
37 pub session_id: String,
39}
40
41impl AppState {
42 pub fn new(config: Config) -> Self {
43 let cwd = std::env::current_dir()
44 .map(|p| p.display().to_string())
45 .unwrap_or_else(|_| ".".into());
46
47 Self {
48 config,
49 messages: Vec::new(),
50 is_query_active: false,
51 total_usage: Usage::default(),
52 total_cost_usd: 0.0,
53 turn_count: 0,
54 cwd,
55 model_usage: HashMap::new(),
56 plan_mode: false,
57 task_manager: std::sync::Arc::new(crate::services::background::TaskManager::new()),
58 session_id: crate::services::session::new_session_id(),
59 }
60 }
61
62 pub fn record_usage(&mut self, usage: &Usage, model: &str) {
64 self.total_usage.merge(usage);
65 self.model_usage
66 .entry(model.to_string())
67 .or_default()
68 .merge(usage);
69 self.total_cost_usd += estimate_cost(usage, model);
70 }
71
72 pub fn push_message(&mut self, msg: Message) {
74 self.messages.push(msg);
75 }
76
77 pub fn history(&self) -> &[Message] {
79 &self.messages
80 }
81}
82
83fn estimate_cost(usage: &Usage, model: &str) -> f64 {
85 crate::services::pricing::calculate_cost(
86 model,
87 usage.input_tokens,
88 usage.output_tokens,
89 usage.cache_read_input_tokens,
90 usage.cache_creation_input_tokens,
91 )
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_new_state() {
100 let state = AppState::new(crate::config::Config::default());
101 assert!(!state.cwd.is_empty());
102 assert_eq!(state.turn_count, 0);
103 assert_eq!(state.total_cost_usd, 0.0);
104 assert!(state.messages.is_empty());
105 }
106
107 #[test]
108 fn test_push_message() {
109 let mut state = AppState::new(crate::config::Config::default());
110 state.push_message(crate::llm::message::user_message("hello"));
111 assert_eq!(state.messages.len(), 1);
112 assert_eq!(state.history().len(), 1);
113 }
114
115 #[test]
116 fn test_record_usage() {
117 let mut state = AppState::new(crate::config::Config::default());
118 let usage = Usage {
119 input_tokens: 1000,
120 output_tokens: 500,
121 ..Default::default()
122 };
123 state.record_usage(&usage, "claude-sonnet-4");
124 assert_eq!(state.total_usage.input_tokens, 1000);
125 assert_eq!(state.total_usage.output_tokens, 500);
126 assert!(state.total_cost_usd > 0.0);
127 }
128
129 #[test]
130 fn test_record_usage_accumulates() {
131 let mut state = AppState::new(crate::config::Config::default());
132 let u1 = Usage {
133 input_tokens: 100,
134 output_tokens: 50,
135 ..Default::default()
136 };
137 let u2 = Usage {
138 input_tokens: 200,
139 output_tokens: 30,
140 ..Default::default()
141 };
142 state.record_usage(&u1, "claude-sonnet-4");
143 state.record_usage(&u2, "claude-sonnet-4");
144 assert_eq!(state.total_usage.output_tokens, 80); }
146
147 #[test]
148 fn test_model_usage_tracking() {
149 let mut state = AppState::new(crate::config::Config::default());
150 let u1 = Usage {
151 input_tokens: 100,
152 output_tokens: 50,
153 ..Default::default()
154 };
155 state.record_usage(&u1, "model-a");
156 state.record_usage(&u1, "model-b");
157 assert!(state.model_usage.contains_key("model-a"));
158 assert!(state.model_usage.contains_key("model-b"));
159 }
160}