1use crate::agent::core::Agent;
2use crate::apis::api_client::Message;
3use crate::app::state::{App, AppState};
4use crate::app::utils::Scrollable;
5use crate::prompts::CONVERSATION_SUMMARY_PROMPT;
6use anyhow::Result;
7use std::time::Instant;
8
9const DEFAULT_SUMMARIZATION_CHAR_THRESHOLD: usize = 1000000;
11const DEFAULT_SUMMARIZATION_COUNT_THRESHOLD: usize = 1000;
13const DEFAULT_KEEP_RECENT_COUNT: usize = 20;
15
16pub struct ConversationSummary {
18 pub content: String,
20 pub created_at: Instant,
22 pub messages_count: usize,
24 pub original_chars: usize,
26}
27
28impl ConversationSummary {
29 pub fn new(content: String, messages_count: usize, original_chars: usize) -> Self {
30 Self {
31 content,
32 created_at: Instant::now(),
33 messages_count,
34 original_chars,
35 }
36 }
37}
38
39pub trait ContextCompressor {
41 fn compress_context(&mut self) -> Result<()>;
43
44 fn should_compress(&self) -> bool;
46
47 fn conversation_char_count(&self) -> usize;
49
50 fn summary_count(&self) -> usize;
52
53 fn clear_history(&mut self);
55
56 fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message>;
58
59 fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String>;
61}
62
63impl ContextCompressor for App {
64 fn compress_context(&mut self) -> Result<()> {
65 if self.messages.is_empty() {
67 return Ok(());
68 }
69
70 let agent = match &self.agent {
72 Some(agent) => agent.clone(),
73 None => return Err(anyhow::anyhow!("No agent available for summarization")),
74 };
75
76 let keep_recent = DEFAULT_KEEP_RECENT_COUNT.min(self.messages.len());
78 let to_summarize = self.messages.len().saturating_sub(keep_recent);
79
80 if to_summarize == 0 {
82 return Ok(());
83 }
84
85 let messages_to_summarize = self.messages[0..to_summarize].join("\n");
87 let messages_chars = messages_to_summarize.len();
88
89 self.messages
91 .push("[wait] ⚪ Summarizing conversation history...".into());
92
93 let summary = self.generate_summary_with_agent(&agent, &messages_to_summarize)?;
95
96 let summary_record =
98 ConversationSummary::new(summary.clone(), to_summarize, messages_chars);
99
100 self.conversation_summaries.push(summary_record);
102
103 self.messages.drain(0..to_summarize);
105
106 self.messages.insert(
108 0,
109 format!("💬 [CONVERSATION SUMMARY]\n{}\n[END SUMMARY]", summary),
110 );
111
112 let messages_to_keep = self.messages[to_summarize..].to_vec();
115
116 let session_messages = self.display_to_session_messages(&messages_to_keep);
118
119 if let Some(session) = &mut self.session_manager {
121 session.replace_with_summary(summary.clone());
123
124 for msg in session_messages {
126 session.add_message(msg.clone());
127 }
128 }
129
130 self.messages.push(format!(
132 "[success] ⏺ Summarized {} messages ({} chars)",
133 to_summarize, messages_chars
134 ));
135
136 self.auto_scroll_to_bottom();
138
139 Ok(())
140 }
141
142 fn should_compress(&self) -> bool {
143 if self.state != AppState::Chat {
145 return false;
146 }
147
148 let message_count = self.messages.len();
150 let char_count = self.conversation_char_count();
151
152 let session_count = self
154 .session_manager
155 .as_ref()
156 .map_or(0, |s| s.message_count());
157
158 message_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
159 || char_count > DEFAULT_SUMMARIZATION_CHAR_THRESHOLD
160 || session_count > DEFAULT_SUMMARIZATION_COUNT_THRESHOLD
161 }
162
163 fn conversation_char_count(&self) -> usize {
164 self.messages.iter().map(|m| m.len()).sum()
165 }
166
167 fn summary_count(&self) -> usize {
168 self.conversation_summaries.len()
169 }
170
171 fn clear_history(&mut self) {
172 self.messages.clear();
173 self.conversation_summaries.clear();
174
175 self.message_scroll.scroll_to_top();
177 self.scroll_position = 0;
178
179 if let Some(agent) = &mut self.agent {
181 agent.clear_history();
182 }
183
184 if let Some(session) = &mut self.session_manager {
186 session.clear();
187 }
188 }
189
190 fn display_to_session_messages(&self, display_messages: &[String]) -> Vec<Message> {
191 let mut session_messages = Vec::new();
192 let mut current_role = "user";
193
194 for msg in display_messages {
195 if msg.starts_with("[user]") || msg.starts_with("User:") {
197 current_role = "user";
198 let content = msg
199 .replace("[user]", "")
200 .replace("User:", "")
201 .trim()
202 .to_string();
203 session_messages.push(Message::user(content));
204 } else if msg.starts_with("[assistant]") || msg.starts_with("Assistant:") {
205 current_role = "assistant";
206 let content = msg
207 .replace("[assistant]", "")
208 .replace("Assistant:", "")
209 .trim()
210 .to_string();
211 session_messages.push(Message::assistant(content));
212 } else if msg.starts_with("[system]") || msg.starts_with("System:") {
213 current_role = "system";
214 let content = msg
215 .replace("[system]", "")
216 .replace("System:", "")
217 .trim()
218 .to_string();
219 session_messages.push(Message::system(content));
220 } else if !msg.starts_with("[wait]")
221 && !msg.starts_with("[success]")
222 && !msg.starts_with("[info]")
223 {
224 match current_role {
226 "user" => session_messages.push(Message::user(msg.clone())),
227 "assistant" => session_messages.push(Message::assistant(msg.clone())),
228 "system" => session_messages.push(Message::system(msg.clone())),
229 _ => session_messages.push(Message::user(msg.clone())),
230 }
231 }
232 }
233
234 session_messages
235 }
236
237 fn session_to_display_messages(&self, session_messages: &[Message]) -> Vec<String> {
238 session_messages
239 .iter()
240 .map(|msg| match msg.role.as_str() {
241 "user" => format!("[user] {}", msg.content),
242 "assistant" => format!("[assistant] {}", msg.content),
243 "system" => format!("[system] {}", msg.content),
244 _ => msg.content.clone(),
245 })
246 .collect()
247 }
248}
249
250impl App {
251 fn generate_summary_with_agent(&mut self, agent: &Agent, content: &str) -> Result<String> {
253 let runtime = match &self.tokio_runtime {
255 Some(rt) => rt,
256 None => return Err(anyhow::anyhow!("Async runtime not available")),
257 };
258
259 let agent_clone = agent.clone();
261
262 let content_to_summarize = content.to_string();
264
265 let prompt = format!("{}{}", CONVERSATION_SUMMARY_PROMPT, content_to_summarize);
267
268 let result = runtime.block_on(async { agent_clone.execute(&prompt).await })?;
270
271 Ok(result)
272 }
273}