Skip to main content

stynx_code_compact/
full_compact.rs

1use 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}