stynx_code_engine/application/
compactor.rs1use 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}