synaps_cli/engine/
session.rs1use crate::{Session, Runtime};
7use crate::pricing::calculate_cost;
8use serde_json::Value;
9
10pub struct ConversationState {
12 pub session: Session,
13 pub api_messages: Vec<Value>,
14 pub total_input_tokens: u64,
15 pub total_output_tokens: u64,
16 pub total_cache_read_tokens: u64,
17 pub total_cache_creation_tokens: u64,
18 pub session_cost: f64,
19 pub abort_context: Option<String>,
20 pub queued_message: Option<String>,
22 pub pending_events: Vec<String>,
24}
25
26impl ConversationState {
27 pub fn new(session: Session) -> Self {
29 Self {
30 session,
31 api_messages: Vec::new(),
32 total_input_tokens: 0,
33 total_output_tokens: 0,
34 total_cache_read_tokens: 0,
35 total_cache_creation_tokens: 0,
36 session_cost: 0.0,
37 abort_context: None,
38 queued_message: None,
39 pending_events: Vec::new(),
40 }
41 }
42
43 pub fn from_resumed(session: Session) -> Self {
45 Self {
46 api_messages: session.api_messages.clone(),
47 total_input_tokens: session.total_input_tokens,
48 total_output_tokens: session.total_output_tokens,
49 total_cache_read_tokens: 0,
50 total_cache_creation_tokens: 0,
51 session_cost: session.session_cost,
52 abort_context: session.abort_context.clone(),
53 queued_message: None,
54 pending_events: Vec::new(),
55 session,
56 }
57 }
58
59 pub async fn save(&mut self) {
61 if self.api_messages.is_empty() {
62 return;
63 }
64 self.session.api_messages = self.api_messages.clone();
65 self.session.total_input_tokens = self.total_input_tokens;
66 self.session.total_output_tokens = self.total_output_tokens;
67 self.session.session_cost = self.session_cost;
68 self.session.abort_context = self.abort_context.clone();
69 self.session.updated_at = chrono::Utc::now();
70 self.session.auto_title();
71 if let Err(e) = self.session.save().await {
72 tracing::error!("Failed to save session: {}", e);
73 }
74 }
75
76 pub async fn clear(&mut self, runtime: &Runtime) {
78 self.save().await;
79 self.api_messages.clear();
80 self.total_input_tokens = 0;
81 self.total_output_tokens = 0;
82 self.total_cache_read_tokens = 0;
83 self.total_cache_creation_tokens = 0;
84 self.session_cost = 0.0;
85 self.abort_context = None;
86 self.queued_message = None;
87 self.pending_events.clear();
88 self.session = Session::new(
89 runtime.model(),
90 runtime.thinking_level(),
91 runtime.system_prompt(),
92 );
93 }
94
95 pub fn add_usage(
97 &mut self,
98 input_tokens: u64,
99 output_tokens: u64,
100 cache_read: u64,
101 cache_creation: u64,
102 model: &str,
103 ) {
104 self.total_input_tokens += input_tokens;
105 self.total_output_tokens += output_tokens;
106 self.total_cache_read_tokens += cache_read;
107 self.total_cache_creation_tokens += cache_creation;
108
109 self.session_cost += calculate_cost(model, input_tokens, output_tokens, cache_read, cache_creation);
110 }
111
112 pub fn estimate_tokens(&self) -> usize {
114 let mut total_chars = 0usize;
115 for msg in &self.api_messages {
116 if let Some(s) = msg["content"].as_str() {
117 total_chars += s.len();
118 } else if let Some(arr) = msg["content"].as_array() {
119 for block in arr {
120 if let Some(s) = block["text"].as_str() {
121 total_chars += s.len();
122 }
123 if let Some(s) = block["thinking"].as_str() {
124 total_chars += s.len();
125 }
126 if let Some(s) = block["content"].as_str() {
127 total_chars += s.len();
128 } else if let Some(content_arr) = block["content"].as_array() {
129 for inner in content_arr {
130 if let Some(s) = inner["text"].as_str() {
131 total_chars += s.len();
132 }
133 }
134 }
135 if let Some(input) = block.get("input") {
136 total_chars += input.to_string().len();
137 }
138 }
139 }
140 }
141 total_chars / 4
142 }
143}