Skip to content

Commit 86a4cca

Browse files
refactor(chat): address auto compaction regressions and minor cleanup (#1312)
1 parent 7ebf3b0 commit 86a4cca

File tree

4 files changed

+72
-53
lines changed

4 files changed

+72
-53
lines changed

crates/q_cli/src/cli/chat/conversation_state.rs

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,10 @@ impl ConversationState {
172172
}
173173

174174
/// Sets the response message according to the currently set [Self::next_message].
175-
// pub fn push_assistant_message(&mut self, message: AssistantResponseMessage) {
176175
pub fn push_assistant_message(&mut self, message: AssistantMessage) {
177176
debug_assert!(self.next_message.is_some(), "next_message should exist");
178177
let next_user_message = self.next_message.take().expect("next user message should exist");
179178

180-
// // Don't include the tool spec in all user messages in the history.
181-
// if let Some(ctx) = next_user_message.user_input_message_context.as_mut() {
182-
// ctx.tools.take();
183-
// }
184-
185179
self.append_assistant_transcript(&message);
186180
self.history.push_back((next_user_message, message));
187181
}
@@ -276,26 +270,29 @@ impl ConversationState {
276270
}
277271

278272
/// Returns a [FigConversationState] capable of being sent by [fig_api_client::StreamingClient].
279-
pub async fn as_sendable_conversation_state(&mut self) -> FigConversationState {
273+
///
274+
/// Params:
275+
/// - `run_hooks` - whether hooks should be executed and included as context
276+
pub async fn as_sendable_conversation_state(&mut self, run_hooks: bool) -> FigConversationState {
280277
debug_assert!(self.next_message.is_some());
281278
self.enforce_conversation_invariants();
282279
self.history.drain(self.valid_history_range.1..);
283280
self.history.drain(..self.valid_history_range.0);
284281

285-
self.backend_conversation_state(false)
282+
self.backend_conversation_state(run_hooks, false)
286283
.await
287284
.into_fig_conversation_state()
288285
.expect("unable to construct conversation state")
289286
}
290287

291288
/// Returns a conversation state representation which reflects the exact conversation to send
292289
/// back to the model.
293-
pub async fn backend_conversation_state(&mut self, quiet: bool) -> BackendConversationState<'_> {
290+
pub async fn backend_conversation_state(&mut self, run_hooks: bool, quiet: bool) -> BackendConversationState<'_> {
294291
self.enforce_conversation_invariants();
295292

296293
// Run hooks and add to conversation start and next user message.
297294
let mut conversation_start_context = None;
298-
if let Some(cm) = self.context_manager.as_mut() {
295+
if let (true, Some(cm)) = (run_hooks, self.context_manager.as_mut()) {
299296
let mut null_writer = SharedWriter::null();
300297
let updates = if quiet {
301298
None
@@ -371,7 +368,7 @@ impl ConversationState {
371368
},
372369
};
373370

374-
let conv_state = self.backend_conversation_state(true).await;
371+
let conv_state = self.backend_conversation_state(false, true).await;
375372

376373
// Include everything but the last message in the history.
377374
let history_len = conv_state.history.len();
@@ -505,7 +502,7 @@ impl ConversationState {
505502

506503
/// Calculate the total character count in the conversation
507504
pub async fn calculate_char_count(&mut self) -> CharCount {
508-
self.backend_conversation_state(true).await.char_count()
505+
self.backend_conversation_state(false, true).await.char_count()
509506
}
510507

511508
/// Get the current token warning level
@@ -625,7 +622,7 @@ impl
625622
let mut user_input_message: UserInputMessage = self
626623
.next_user_message
627624
.cloned()
628-
.map(Into::into)
625+
.map(UserMessage::into_user_input_message)
629626
.ok_or(eyre::eyre!("next user message is not set"))?;
630627
if let Some(ctx) = user_input_message.user_input_message_context.as_mut() {
631628
ctx.tools = Some(self.tools.to_vec());
@@ -638,7 +635,7 @@ impl
638635
})
639636
}
640637

641-
pub fn get_utilization(&self) -> ConversationSize {
638+
pub fn calculate_conversation_size(&self) -> ConversationSize {
642639
let mut user_chars = 0;
643640
let mut assistant_chars = 0;
644641
let mut context_chars = 0;
@@ -684,7 +681,7 @@ where
684681
T: Iterator<Item = &'a (UserMessage, AssistantMessage)>,
685682
{
686683
history.fold(Vec::new(), |mut acc, (user, assistant)| {
687-
acc.push(ChatMessage::UserInputMessage(user.clone().into()));
684+
acc.push(ChatMessage::UserInputMessage(user.clone().into_history_entry()));
688685
acc.push(ChatMessage::AssistantResponseMessage(assistant.clone().into()));
689686
acc
690687
})
@@ -839,7 +836,7 @@ mod tests {
839836
// User -> Assistant -> User -> Assistant ...and so on.
840837
conversation_state.set_next_user_message("start".to_string()).await;
841838
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
842-
let s = conversation_state.as_sendable_conversation_state().await;
839+
let s = conversation_state.as_sendable_conversation_state(true).await;
843840
assert_conversation_state_invariants(s, i);
844841
conversation_state.push_assistant_message(AssistantMessage::new_response(None, i.to_string()));
845842
conversation_state.set_next_user_message(i.to_string()).await;
@@ -853,7 +850,7 @@ mod tests {
853850
ConversationState::new(Context::new_fake(), load_tools().unwrap(), None, None).await;
854851
conversation_state.set_next_user_message("start".to_string()).await;
855852
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
856-
let s = conversation_state.as_sendable_conversation_state().await;
853+
let s = conversation_state.as_sendable_conversation_state(true).await;
857854
assert_conversation_state_invariants(s, i);
858855

859856
conversation_state.push_assistant_message(AssistantMessage::new_tool_use(None, i.to_string(), vec![
@@ -875,7 +872,7 @@ mod tests {
875872
ConversationState::new(Context::new_fake(), load_tools().unwrap(), None, None).await;
876873
conversation_state.set_next_user_message("start".to_string()).await;
877874
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
878-
let s = conversation_state.as_sendable_conversation_state().await;
875+
let s = conversation_state.as_sendable_conversation_state(true).await;
879876
assert_conversation_state_invariants(s, i);
880877
if i % 3 == 0 {
881878
conversation_state.push_assistant_message(AssistantMessage::new_tool_use(None, i.to_string(), vec![
@@ -908,7 +905,7 @@ mod tests {
908905
// User -> Assistant -> User -> Assistant ...and so on.
909906
conversation_state.set_next_user_message("start".to_string()).await;
910907
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
911-
let s = conversation_state.as_sendable_conversation_state().await;
908+
let s = conversation_state.as_sendable_conversation_state(true).await;
912909

913910
// Ensure that the first two messages are the fake context messages.
914911
let hist = s.history.as_ref().unwrap();
@@ -965,7 +962,7 @@ mod tests {
965962
// Simulate conversation flow
966963
conversation_state.set_next_user_message("start".to_string()).await;
967964
for i in 0..=5 {
968-
let s = conversation_state.as_sendable_conversation_state().await;
965+
let s = conversation_state.as_sendable_conversation_state(true).await;
969966
let hist = s.history.as_ref().unwrap();
970967
#[allow(clippy::match_wildcard_for_single_variants)]
971968
match &hist[0] {

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

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,50 @@ impl UserMessage {
9292
}
9393
}
9494

95+
/// Converts this message into a [UserInputMessage] to be stored in the history of
96+
/// [fig_api_client::model::ConversationState].
97+
pub fn into_history_entry(self) -> UserInputMessage {
98+
UserInputMessage {
99+
content: self.prompt().unwrap_or_default().to_string(),
100+
user_input_message_context: Some(UserInputMessageContext {
101+
shell_state: self.env_context.shell_state,
102+
env_state: self.env_context.env_state,
103+
tool_results: match self.content {
104+
UserMessageContent::CancelledToolUses { tool_use_results, .. }
105+
| UserMessageContent::ToolUseResults { tool_use_results } => {
106+
Some(tool_use_results.into_iter().map(Into::into).collect())
107+
},
108+
UserMessageContent::Prompt { .. } => None,
109+
},
110+
tools: None,
111+
..Default::default()
112+
}),
113+
user_intent: None,
114+
}
115+
}
116+
117+
/// Converts this message into a [UserInputMessage] to be sent as
118+
/// [FigConversationState::user_input_message].
119+
pub fn into_user_input_message(self) -> UserInputMessage {
120+
UserInputMessage {
121+
content: format!("{} {}", self.additional_context, self.prompt().unwrap_or_default()),
122+
user_input_message_context: Some(UserInputMessageContext {
123+
shell_state: self.env_context.shell_state,
124+
env_state: self.env_context.env_state,
125+
tool_results: match self.content {
126+
UserMessageContent::CancelledToolUses { tool_use_results, .. }
127+
| UserMessageContent::ToolUseResults { tool_use_results } => {
128+
Some(tool_use_results.into_iter().map(Into::into).collect())
129+
},
130+
UserMessageContent::Prompt { .. } => None,
131+
},
132+
tools: None,
133+
..Default::default()
134+
}),
135+
user_intent: None,
136+
}
137+
}
138+
95139
pub fn has_tool_use_results(&self) -> bool {
96140
match self.content() {
97141
UserMessageContent::CancelledToolUses { .. } | UserMessageContent::ToolUseResults { .. } => true,
@@ -124,28 +168,6 @@ impl UserMessage {
124168
}
125169
}
126170

127-
impl From<UserMessage> for UserInputMessage {
128-
fn from(value: UserMessage) -> Self {
129-
Self {
130-
content: format!("{} {}", value.additional_context, value.prompt().unwrap_or_default()),
131-
user_input_message_context: Some(UserInputMessageContext {
132-
shell_state: value.env_context.shell_state,
133-
env_state: value.env_context.env_state,
134-
tool_results: match value.content {
135-
UserMessageContent::CancelledToolUses { tool_use_results, .. }
136-
| UserMessageContent::ToolUseResults { tool_use_results } => {
137-
Some(tool_use_results.into_iter().map(Into::into).collect())
138-
},
139-
UserMessageContent::Prompt { .. } => None,
140-
},
141-
tools: None,
142-
..Default::default()
143-
}),
144-
user_intent: None,
145-
}
146-
}
147-
}
148-
149171
#[derive(Debug, Clone, Serialize, Deserialize)]
150172
pub struct ToolUseResult {
151173
/// The ID for the tool request.

crates/q_cli/src/cli/chat/mod.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ impl ChatContext {
877877
tool_uses,
878878
"The user interrupted the tool execution.".to_string(),
879879
);
880-
let _ = self.conversation_state.as_sendable_conversation_state().await;
880+
let _ = self.conversation_state.as_sendable_conversation_state(false).await;
881881
self.conversation_state
882882
.push_assistant_message(AssistantMessage::new_response(
883883
None,
@@ -893,7 +893,7 @@ impl ChatContext {
893893
fig_api_client::Error::ContextWindowOverflow => {
894894
let history_too_small = self
895895
.conversation_state
896-
.backend_conversation_state(true)
896+
.backend_conversation_state(false, true)
897897
.await
898898
.history
899899
.len()
@@ -1106,7 +1106,7 @@ impl ChatContext {
11061106
if self.conversation_state.next_user_message().is_some() {
11071107
Ok(ChatState::HandleResponseStream(
11081108
self.client
1109-
.send_message(self.conversation_state.as_sendable_conversation_state().await)
1109+
.send_message(self.conversation_state.as_sendable_conversation_state(false).await)
11101110
.await?,
11111111
))
11121112
} else {
@@ -1237,7 +1237,7 @@ impl ChatContext {
12371237
self.conversation_state.set_next_user_message(user_input).await;
12381238
}
12391239

1240-
let conv_state = self.conversation_state.as_sendable_conversation_state().await;
1240+
let conv_state = self.conversation_state.as_sendable_conversation_state(true).await;
12411241

12421242
if self.interactive {
12431243
queue!(self.output, style::SetForegroundColor(Color::Magenta))?;
@@ -2224,8 +2224,8 @@ impl ChatContext {
22242224
}
22252225
},
22262226
Command::Usage => {
2227-
let state = self.conversation_state.backend_conversation_state(true).await;
2228-
let data = state.get_utilization();
2227+
let state = self.conversation_state.backend_conversation_state(true, true).await;
2228+
let data = state.calculate_conversation_size();
22292229

22302230
let context_token_count: TokenCount = data.context_messages.into();
22312231
let assistant_token_count: TokenCount = data.assistant_messages.into();
@@ -2472,7 +2472,7 @@ impl ChatContext {
24722472
self.send_tool_use_telemetry().await;
24732473
return Ok(ChatState::HandleResponseStream(
24742474
self.client
2475-
.send_message(self.conversation_state.as_sendable_conversation_state().await)
2475+
.send_message(self.conversation_state.as_sendable_conversation_state(false).await)
24762476
.await?,
24772477
));
24782478
}
@@ -2560,7 +2560,7 @@ impl ChatContext {
25602560
self.send_tool_use_telemetry().await;
25612561
return Ok(ChatState::HandleResponseStream(
25622562
self.client
2563-
.send_message(self.conversation_state.as_sendable_conversation_state().await)
2563+
.send_message(self.conversation_state.as_sendable_conversation_state(false).await)
25642564
.await?,
25652565
));
25662566
},
@@ -2593,7 +2593,7 @@ impl ChatContext {
25932593
self.send_tool_use_telemetry().await;
25942594
return Ok(ChatState::HandleResponseStream(
25952595
self.client
2596-
.send_message(self.conversation_state.as_sendable_conversation_state().await)
2596+
.send_message(self.conversation_state.as_sendable_conversation_state(false).await)
25972597
.await?,
25982598
));
25992599
},
@@ -2788,7 +2788,7 @@ impl ChatContext {
27882788

27892789
let response = self
27902790
.client
2791-
.send_message(self.conversation_state.as_sendable_conversation_state().await)
2791+
.send_message(self.conversation_state.as_sendable_conversation_state(false).await)
27922792
.await?;
27932793
return Ok(ChatState::HandleResponseStream(response));
27942794
}

crates/q_cli/src/cli/chat/token_counter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub trait CharCounter {
105105

106106
impl CharCounter for BackendConversationState<'_> {
107107
fn char_count(&self) -> CharCount {
108-
self.get_utilization().char_count()
108+
self.calculate_conversation_size().char_count()
109109
}
110110
}
111111

0 commit comments

Comments
 (0)