Skip to main content

stynx_code_engine/application/
compactor.rs

1use std::sync::Arc;
2
3use stynx_code_errors::AppResult;
4use stynx_code_types::{ContentBlock, Conversation, Message, Provider, Role, StreamEvent};
5use futures::StreamExt;
6
7use crate::domain::EngineEvent;
8
9pub async fn compact<F>(
10    provider: &Arc<dyn Provider>,
11    conversation: Conversation,
12    on_event: &mut F,
13) -> AppResult<Conversation>
14where
15    F: FnMut(EngineEvent) + Send,
16{
17    let mut summary_parts = Vec::new();
18    for msg in &conversation.messages {
19        let role = match msg.role {
20            Role::User => "User",
21            Role::Assistant => "Assistant",
22        };
23        for block in &msg.content {
24            match block {
25                ContentBlock::Text { text } => {
26                    summary_parts.push(format!("{role}: {text}"));
27                }
28                ContentBlock::ToolUse { name, .. } => {
29                    summary_parts.push(format!("{role}: [used tool: {name}]"));
30                }
31                ContentBlock::ToolResult { content, .. } => {
32                    let preview = if content.len() > 200 {
33                        format!("{}...", &content[..200])
34                    } else {
35                        content.clone()
36                    };
37                    summary_parts.push(format!("{role}: [tool result: {preview}]"));
38                }
39                ContentBlock::Thinking { thinking } => {
40                    let preview = if thinking.len() > 200 {
41                        format!("{}...", &thinking[..200])
42                    } else {
43                        thinking.clone()
44                    };
45                    summary_parts.push(format!("{role}: [thinking: {preview}]"));
46                }
47                ContentBlock::Image { .. } => {
48                    summary_parts.push(format!("{role}: [image]"));
49                }
50            }
51        }
52    }
53
54    let summary_request = format!(
55        "Please provide a concise summary of the following conversation so far. \
56         Focus on key decisions, facts, and context that would be needed to continue the conversation:\n\n{}",
57        summary_parts.join("\n")
58    );
59
60    let summary_request = if summary_request.len() > 50_000 {
61        format!("{}...\n(truncated)", &summary_request[..50_000])
62    } else {
63        summary_request
64    };
65
66    let mut summary_conv = Conversation {
67        system: Some("You are a summarization assistant. Provide a concise summary of the conversation.".into()),
68        ..Default::default()
69    };
70    summary_conv.push(Message::user(&summary_request));
71
72    let tools: Vec<serde_json::Value> = vec![];
73    let mut summary_text = String::new();
74
75    match provider.stream(&summary_conv, &tools).await {
76        Ok(mut stream) => {
77            while let Some(event) = stream.next().await {
78                if let StreamEvent::ContentDelta { text } = event {
79                    summary_text.push_str(&text);
80                }
81            }
82        }
83        Err(e) => {
84            on_event(EngineEvent::Error(format!("compact failed: {e}")));
85            return Ok(fallback_compact(conversation));
86        }
87    }
88
89    if summary_text.is_empty() {
90        summary_text = "Previous conversation context was compacted.".into();
91    }
92
93    let mut compacted = Conversation {
94        system: conversation.system,
95        ..Default::default()
96    };
97    compacted.push(Message::user(format!(
98        "[Context from previous conversation]\n{summary_text}"
99    )));
100
101    Ok(compacted)
102}
103
104fn fallback_compact(conversation: Conversation) -> Conversation {
105    let mut compacted = Conversation {
106        system: conversation.system,
107        ..Default::default()
108    };
109    let msgs = &conversation.messages;
110    let start = msgs.len().saturating_sub(6);
111    let first_user = msgs[start..]
112        .iter()
113        .position(|m| matches!(m.role, Role::User))
114        .map(|i| start + i)
115        .unwrap_or(start);
116    for msg in &msgs[first_user..] {
117        compacted.push(msg.clone());
118    }
119    compacted
120}