Skip to content

Commit 8bc93ab

Browse files
fix: issues with /compact failing (#402)
1 parent 65aa928 commit 8bc93ab

File tree

6 files changed

+226
-87
lines changed

6 files changed

+226
-87
lines changed

crates/chat-cli/src/cli/chat/cli/compact.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use clap::Args;
22

3+
use crate::cli::chat::consts::MAX_USER_MESSAGE_SIZE;
4+
use crate::cli::chat::message::UserMessageContent;
35
use crate::cli::chat::{
46
ChatError,
57
ChatSession,
@@ -24,17 +26,60 @@ How it works
2426
• Creates an AI-generated summary of your conversation
2527
• Retains key information, code, and tool executions in the summary
2628
• Clears the conversation history to free up space
27-
• The assistant will reference the summary context in future responses"
29+
• The assistant will reference the summary context in future responses
30+
31+
Compaction will be automatically performed whenever the context window overflows.
32+
To disable this behavior, run: `q settings chat.disableAutoCompaction true`"
2833
)]
2934
pub struct CompactArgs {
3035
/// The prompt to use when generating the summary
3136
prompt: Option<String>,
3237
#[arg(long)]
3338
show_summary: bool,
39+
/// The number of user and assistant message pairs to exclude from the summarization.
40+
#[arg(long)]
41+
messages_to_exclude: Option<usize>,
42+
/// Whether or not large messages should be truncated.
43+
#[arg(long)]
44+
truncate_large_messages: Option<bool>,
45+
/// Maximum allowed size of messages in the conversation history. Requires
46+
/// truncate_large_messages to be set.
47+
#[arg(long, requires = "truncate_large_messages")]
48+
max_message_length: Option<usize>,
3449
}
3550

3651
impl CompactArgs {
3752
pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
38-
session.compact_history(os, self.prompt, self.show_summary, true).await
53+
let default = CompactStrategy::default();
54+
session
55+
.compact_history(os, self.prompt, self.show_summary, CompactStrategy {
56+
messages_to_exclude: self.messages_to_exclude.unwrap_or(default.messages_to_exclude),
57+
truncate_large_messages: self.truncate_large_messages.unwrap_or(default.truncate_large_messages),
58+
max_message_length: self.max_message_length.map_or(default.max_message_length, |v| {
59+
v.clamp(UserMessageContent::TRUNCATED_SUFFIX.len(), MAX_USER_MESSAGE_SIZE)
60+
}),
61+
})
62+
.await
63+
}
64+
}
65+
66+
/// Parameters for performing the history compaction request.
67+
#[derive(Debug, Copy, Clone)]
68+
pub struct CompactStrategy {
69+
/// Number of user/assistant pairs to exclude from the history as part of compaction.
70+
pub messages_to_exclude: usize,
71+
/// Whether or not to truncate large messages in the history.
72+
pub truncate_large_messages: bool,
73+
/// Maximum allowed size of messages in the conversation history.
74+
pub max_message_length: usize,
75+
}
76+
77+
impl Default for CompactStrategy {
78+
fn default() -> Self {
79+
Self {
80+
messages_to_exclude: Default::default(),
81+
truncate_large_messages: Default::default(),
82+
max_message_length: MAX_USER_MESSAGE_SIZE,
83+
}
3984
}
4085
}

crates/chat-cli/src/cli/chat/conversation.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use tracing::{
2121
warn,
2222
};
2323

24+
use super::cli::compact::CompactStrategy;
2425
use super::consts::{
2526
DUMMY_TOOL_NAME,
2627
MAX_CHARS,
@@ -534,18 +535,13 @@ impl ConversationState {
534535
})
535536
}
536537

537-
pub async fn truncate_large_user_messages(&mut self) {
538-
for (user_message, _) in &mut self.history {
539-
user_message.truncate_safe(25_000);
540-
}
541-
}
542-
543538
/// Returns a [FigConversationState] capable of replacing the history of the current
544539
/// conversation with a summary generated by the model.
545540
pub async fn create_summary_request(
546541
&mut self,
547542
os: &Os,
548543
custom_prompt: Option<impl AsRef<str>>,
544+
strategy: CompactStrategy,
549545
) -> Result<FigConversationState, ChatError> {
550546
let summary_content = match custom_prompt {
551547
Some(custom_prompt) => {
@@ -591,7 +587,19 @@ impl ConversationState {
591587
};
592588

593589
let conv_state = self.backend_conversation_state(os, false, &mut vec![]).await?;
594-
let history = flatten_history(conv_state.history);
590+
let mut history = conv_state.history.cloned().collect::<Vec<_>>();
591+
592+
if strategy.truncate_large_messages {
593+
for (user_message, _) in &mut history {
594+
user_message.truncate_safe(strategy.max_message_length);
595+
}
596+
}
597+
598+
let history = flatten_history(
599+
history
600+
.iter()
601+
.take(history.len().saturating_sub(strategy.messages_to_exclude)),
602+
);
595603

596604
let user_input_message_context = UserInputMessageContext {
597605
env_state: Some(build_env_state()),

crates/chat-cli/src/cli/chat/message.rs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,30 @@ pub enum UserMessageContent {
6060
}
6161

6262
impl UserMessageContent {
63+
pub const TRUNCATED_SUFFIX: &str = "...content truncated due to length";
64+
6365
fn truncate_safe(&mut self, max_bytes: usize) {
6466
match self {
6567
UserMessageContent::Prompt { prompt } => {
66-
truncate_safe_in_place(prompt, max_bytes);
68+
truncate_safe_in_place(prompt, max_bytes, Self::TRUNCATED_SUFFIX);
6769
},
6870
UserMessageContent::CancelledToolUses {
6971
prompt,
7072
tool_use_results,
7173
} => {
7274
if let Some(prompt) = prompt {
73-
truncate_safe_in_place(prompt, max_bytes / 2);
74-
truncate_safe_tool_use_results(tool_use_results.as_mut_slice(), max_bytes / 2);
75+
truncate_safe_in_place(prompt, max_bytes / 2, Self::TRUNCATED_SUFFIX);
76+
truncate_safe_tool_use_results(
77+
tool_use_results.as_mut_slice(),
78+
max_bytes / 2,
79+
Self::TRUNCATED_SUFFIX,
80+
);
7581
} else {
76-
truncate_safe_tool_use_results(tool_use_results.as_mut_slice(), max_bytes);
82+
truncate_safe_tool_use_results(tool_use_results.as_mut_slice(), max_bytes, Self::TRUNCATED_SUFFIX);
7783
}
7884
},
7985
UserMessageContent::ToolUseResults { tool_use_results } => {
80-
truncate_safe_tool_use_results(tool_use_results.as_mut_slice(), max_bytes);
86+
truncate_safe_tool_use_results(tool_use_results.as_mut_slice(), max_bytes, Self::TRUNCATED_SUFFIX);
8187
},
8288
}
8389
}
@@ -223,9 +229,6 @@ impl UserMessage {
223229
}
224230

225231
/// Truncates the content contained in this user message to a maximum length of `max_bytes`.
226-
///
227-
/// This isn't a perfect truncation - JSON tool use results are ignored, and only the content
228-
/// of the user message is truncated, ignoring extra context fields.
229232
pub fn truncate_safe(&mut self, max_bytes: usize) {
230233
self.content.truncate_safe(max_bytes);
231234
}
@@ -261,15 +264,26 @@ impl From<ToolUseResult> for ToolResult {
261264
}
262265
}
263266

264-
fn truncate_safe_tool_use_results(tool_use_results: &mut [ToolUseResult], max_bytes: usize) {
267+
fn truncate_safe_tool_use_results(tool_use_results: &mut [ToolUseResult], max_bytes: usize, truncated_suffix: &str) {
265268
let max_bytes = max_bytes / tool_use_results.len();
266269
for result in tool_use_results {
267270
for content in &mut result.content {
268271
match content {
269-
ToolUseResultBlock::Json(_) => {
270-
warn!("Unable to truncate JSON safely");
272+
ToolUseResultBlock::Json(value) => match serde_json::to_string(value) {
273+
Ok(mut value_str) => {
274+
if value_str.len() > max_bytes {
275+
truncate_safe_in_place(&mut value_str, max_bytes, truncated_suffix);
276+
*content = ToolUseResultBlock::Text(value_str);
277+
return;
278+
}
279+
},
280+
Err(err) => {
281+
warn!(?err, "Unable to truncate JSON");
282+
},
283+
},
284+
ToolUseResultBlock::Text(t) => {
285+
truncate_safe_in_place(t, max_bytes, truncated_suffix);
271286
},
272-
ToolUseResultBlock::Text(t) => truncate_safe_in_place(t, max_bytes),
273287
}
274288
}
275289
}

0 commit comments

Comments
 (0)