stynx_code_compact/
full_compact.rs1use stynx_code_errors::AppResult;
2use stynx_code_types::{ContentBlock, Conversation, Message, Provider, Role, StreamEvent};
3use futures::StreamExt;
4
5use crate::prompt;
6
7pub struct FullCompactor;
8
9impl Default for FullCompactor {
10 fn default() -> Self {
11 Self
12 }
13}
14
15impl FullCompactor {
16 pub fn new() -> Self {
17 Self
18 }
19
20 pub async fn compact(
21 &self,
22 conversation: &Conversation,
23 provider: &dyn Provider,
24 ) -> AppResult<Conversation> {
25
26 let conversation_text = self.build_conversation_text(conversation);
27
28 let conversation_text = if conversation_text.len() > 50_000 {
29 format!("{}...\n(truncated)", &conversation_text[..50_000])
30 } else {
31 conversation_text
32 };
33
34 let mut summary_conv = Conversation {
35 system: Some(prompt::compaction_system_prompt()),
36 ..Default::default()
37 };
38 summary_conv.push(Message::user(prompt::compaction_user_prompt(
39 &conversation_text,
40 )));
41
42 let tools: Vec<serde_json::Value> = vec![];
43 let mut summary_text = String::new();
44
45 match provider.stream(&summary_conv, &tools).await {
46 Ok(mut stream) => {
47 while let Some(event) = stream.next().await {
48 if let StreamEvent::ContentDelta { text } = event {
49 summary_text.push_str(&text);
50 }
51 }
52 }
53 Err(e) => {
54 tracing::error!("Full compaction failed: {e}");
55
56 return Ok(self.fallback_compact(conversation));
57 }
58 }
59
60 if summary_text.is_empty() {
61 summary_text = "Previous conversation context was compacted.".into();
62 }
63
64 let mut compacted = Conversation {
65 system: conversation.system.clone(),
66 ..Default::default()
67 };
68
69 compacted.push(Message::user(format!(
70 "[Context from previous conversation]\n{summary_text}"
71 )));
72 compacted.push(Message::assistant(vec![ContentBlock::Text {
73 text: "I understand. I have the context from our previous conversation. How can I help you next?".into(),
74 }]));
75
76 let keep = conversation.messages.len().min(2);
77 let start = conversation.messages.len() - keep;
78 for msg in &conversation.messages[start..] {
79 compacted.push(msg.clone());
80 }
81
82 Ok(compacted)
83 }
84
85 fn build_conversation_text(&self, conversation: &Conversation) -> String {
86 let mut parts = Vec::new();
87
88 for msg in &conversation.messages {
89 let role = match msg.role {
90 Role::User => "User",
91 Role::Assistant => "Assistant",
92 };
93 for block in &msg.content {
94 match block {
95 ContentBlock::Text { text } => {
96 parts.push(format!("{role}: {text}"));
97 }
98 ContentBlock::ToolUse { name, .. } => {
99 parts.push(format!("{role}: [used tool: {name}]"));
100 }
101 ContentBlock::ToolResult { content, .. } => {
102 let preview = if content.len() > 200 {
103 format!("{}...", &content[..200])
104 } else {
105 content.clone()
106 };
107 parts.push(format!("{role}: [tool result: {preview}]"));
108 }
109 ContentBlock::Thinking { thinking } => {
110 let preview = if thinking.len() > 200 {
111 format!("{}...", &thinking[..200])
112 } else {
113 thinking.clone()
114 };
115 parts.push(format!("{role}: [thinking: {preview}]"));
116 }
117 ContentBlock::Image { .. } => {
118 parts.push(format!("{role}: [image]"));
119 }
120 }
121 }
122 }
123
124 parts.join("\n")
125 }
126
127 fn fallback_compact(&self, conversation: &Conversation) -> Conversation {
128 let mut compacted = Conversation {
129 system: conversation.system.clone(),
130 ..Default::default()
131 };
132 let keep = conversation.messages.len().min(4);
133 let start = conversation.messages.len() - keep;
134 for msg in &conversation.messages[start..] {
135 compacted.push(msg.clone());
136 }
137 compacted
138 }
139}