Skip to content

Commit badab88

Browse files
committed
WIP
1 parent 09124c0 commit badab88

9 files changed

Lines changed: 793 additions & 229 deletions

File tree

crates/chat-cli/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ fn main() {
122122

123123
quote::quote!(
124124
#[doc = #description]
125-
#[derive(Debug, Clone, PartialEq)]
125+
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
126126
#[non_exhaustive]
127127
pub enum #name {
128128
#(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub struct CompactArgs {
4949
}
5050

5151
impl CompactArgs {
52-
pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
52+
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
5353
let default = CompactStrategy::default();
5454
let prompt = if self.prompt.is_empty() {
5555
None

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

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use super::message::{
3333
ToolUseResult,
3434
UserMessage,
3535
};
36+
use super::parser::RequestMetadata;
3637
use super::token_counter::{
3738
CharCount,
3839
CharCounter,
@@ -65,6 +66,8 @@ use crate::os::Os;
6566
const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
6667
const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
6768

69+
pub type HistoryEntry = (UserMessage, AssistantMessage, Option<RequestMetadata>);
70+
6871
/// Tracks state related to an ongoing conversation.
6972
#[derive(Debug, Clone, Serialize, Deserialize)]
7073
pub struct ConversationState {
@@ -73,7 +76,7 @@ pub struct ConversationState {
7376
/// The next user message to be sent as part of the conversation. Required to be [Some] before
7477
/// calling [Self::as_sendable_conversation_state].
7578
next_message: Option<UserMessage>,
76-
history: VecDeque<(UserMessage, AssistantMessage)>,
79+
history: VecDeque<HistoryEntry>,
7780
/// The range in the history sendable to the backend (start inclusive, end exclusive).
7881
valid_history_range: (usize, usize),
7982
/// Similar to history in that stores user and assistant responses, except that it is not used
@@ -90,7 +93,7 @@ pub struct ConversationState {
9093
/// Cached value representing the length of the user context message.
9194
context_message_length: Option<usize>,
9295
/// Stores the latest conversation summary created by /compact
93-
latest_summary: Option<String>,
96+
latest_summary: Option<(String, RequestMetadata)>,
9497
/// Model explicitly selected by the user in this conversation state via `/model`.
9598
#[serde(default, skip_serializing_if = "Option::is_none")]
9699
pub model: Option<String>,
@@ -180,10 +183,10 @@ impl ConversationState {
180183
}
181184

182185
pub fn latest_summary(&self) -> Option<&str> {
183-
self.latest_summary.as_deref()
186+
self.latest_summary.as_ref().map(|(s, _)| s.as_str())
184187
}
185188

186-
pub fn history(&self) -> &VecDeque<(UserMessage, AssistantMessage)> {
189+
pub fn history(&self) -> &VecDeque<HistoryEntry> {
187190
&self.history
188191
}
189192

@@ -219,7 +222,7 @@ impl ConversationState {
219222
let asst = candidate_asst.take().unwrap();
220223
let user = candidate_user.take().unwrap();
221224
self.append_assistant_transcript(&asst);
222-
self.history.push_back((user, asst));
225+
self.history.push_back((user, asst, None));
223226
}
224227
}
225228
Some(last_msg.content.to_string())
@@ -251,12 +254,17 @@ impl ConversationState {
251254
}
252255

253256
/// Sets the response message according to the currently set [Self::next_message].
254-
pub fn push_assistant_message(&mut self, os: &mut Os, message: AssistantMessage) {
257+
pub fn push_assistant_message(
258+
&mut self,
259+
os: &mut Os,
260+
message: AssistantMessage,
261+
request_metadata: Option<RequestMetadata>,
262+
) {
255263
debug_assert!(self.next_message.is_some(), "next_message should exist");
256264
let next_user_message = self.next_message.take().expect("next user message should exist");
257265

258266
self.append_assistant_transcript(&message);
259-
self.history.push_back((next_user_message, message));
267+
self.history.push_back((next_user_message, message, request_metadata));
260268

261269
if let Ok(cwd) = std::env::current_dir() {
262270
os.database.set_conversation_by_path(cwd, self).ok();
@@ -272,7 +280,21 @@ impl ConversationState {
272280
///
273281
/// This is equivalent to `utterance_id` in the Q API.
274282
pub fn message_id(&self) -> Option<&str> {
275-
self.history.back().and_then(|(_, msg)| msg.message_id())
283+
self.history.back().and_then(|(_, msg, _)| msg.message_id())
284+
}
285+
286+
pub fn latest_tool_use_ids(&self) -> Option<String> {
287+
self.history
288+
.back()
289+
.and_then(|(_, assistant, _)| assistant.tool_uses())
290+
.map(|tools| (tools.iter().map(|t| t.id.as_str()).collect::<Vec<_>>().join(",")))
291+
}
292+
293+
pub fn latest_tool_use_names(&self) -> Option<String> {
294+
self.history
295+
.back()
296+
.and_then(|(_, assistant, _)| assistant.tool_uses())
297+
.map(|tools| (tools.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(",")))
276298
}
277299

278300
/// Updates the history so that, when non-empty, the following invariants are in place:
@@ -476,7 +498,7 @@ impl ConversationState {
476498
FILTER OUT CHAT CONVENTIONS (greetings, offers to help, etc).".to_string()
477499
},
478500
};
479-
if let Some(summary) = &self.latest_summary {
501+
if let Some((summary, _)) = &self.latest_summary {
480502
summary_content.push_str("\n\n");
481503
summary_content.push_str(CONTEXT_ENTRY_START_HEADER);
482504
summary_content.push_str("This summary contains ALL relevant information from our previous conversation including tool uses, results, code analysis, and file operations. YOU MUST be sure to include this information when creating your summarization document.\n\n");
@@ -493,7 +515,7 @@ impl ConversationState {
493515
let mut history = conv_state.history.cloned().collect::<VecDeque<_>>();
494516
history.drain((history.len().saturating_sub(strategy.messages_to_exclude))..);
495517
if strategy.truncate_large_messages {
496-
for (user_message, _) in &mut history {
518+
for (user_message, _, _) in &mut history {
497519
user_message.truncate_safe(strategy.max_message_length);
498520
}
499521
}
@@ -524,10 +546,15 @@ impl ConversationState {
524546

525547
/// `strategy` - The [CompactStrategy] used for the corresponding
526548
/// [ConversationState::create_summary_request].
527-
pub fn replace_history_with_summary(&mut self, summary: String, strategy: CompactStrategy) {
549+
pub fn replace_history_with_summary(
550+
&mut self,
551+
summary: String,
552+
strategy: CompactStrategy,
553+
request_metadata: RequestMetadata,
554+
) {
528555
self.history
529556
.drain(..(self.history.len().saturating_sub(strategy.messages_to_exclude)));
530-
self.latest_summary = Some(summary);
557+
self.latest_summary = Some((summary, request_metadata));
531558
}
532559

533560
pub fn current_profile(&self) -> Option<&str> {
@@ -550,10 +577,10 @@ impl ConversationState {
550577
&mut self,
551578
os: &Os,
552579
conversation_start_context: Option<String>,
553-
) -> (Option<Vec<(UserMessage, AssistantMessage)>>, Vec<(String, String)>) {
580+
) -> (Option<Vec<HistoryEntry>>, Vec<(String, String)>) {
554581
let mut context_content = String::new();
555582
let mut dropped_context_files = Vec::new();
556-
if let Some(summary) = &self.latest_summary {
583+
if let Some((summary, _)) = &self.latest_summary {
557584
context_content.push_str(CONTEXT_ENTRY_START_HEADER);
558585
context_content.push_str("This summary contains ALL relevant information from our previous conversation including tool uses, results, code analysis, and file operations. YOU MUST reference this information when answering questions and explicitly acknowledge specific details from the summary when they're relevant to the current question.\n\n");
559586
context_content.push_str("SUMMARY CONTENT:\n");
@@ -592,7 +619,7 @@ impl ConversationState {
592619
self.context_message_length = Some(context_content.len());
593620
let user_msg = UserMessage::new_prompt(context_content);
594621
let assistant_msg = AssistantMessage::new_response(None, "I will fully incorporate this information when generating my responses, and explicitly acknowledge relevant parts of the summary when answering questions.".into());
595-
(Some(vec![(user_msg, assistant_msg)]), dropped_context_files)
622+
(Some(vec![(user_msg, assistant_msg, None)]), dropped_context_files)
596623
} else {
597624
(None, dropped_context_files)
598625
}
@@ -647,11 +674,8 @@ impl ConversationState {
647674
///
648675
/// This is intended to provide us ways to accurately assess the exact state that is sent to the
649676
/// model without having to needlessly clone and mutate [ConversationState] in strange ways.
650-
pub type BackendConversationState<'a> = BackendConversationStateImpl<
651-
'a,
652-
std::collections::vec_deque::Iter<'a, (UserMessage, AssistantMessage)>,
653-
Option<Vec<(UserMessage, AssistantMessage)>>,
654-
>;
677+
pub type BackendConversationState<'a> =
678+
BackendConversationStateImpl<'a, std::collections::vec_deque::Iter<'a, HistoryEntry>, Option<Vec<HistoryEntry>>>;
655679

656680
/// See [BackendConversationState]
657681
#[derive(Debug, Clone)]
@@ -665,13 +689,7 @@ pub struct BackendConversationStateImpl<'a, T, U> {
665689
pub model_id: Option<&'a str>,
666690
}
667691

668-
impl
669-
BackendConversationStateImpl<
670-
'_,
671-
std::collections::vec_deque::Iter<'_, (UserMessage, AssistantMessage)>,
672-
Option<Vec<(UserMessage, AssistantMessage)>>,
673-
>
674-
{
692+
impl BackendConversationStateImpl<'_, std::collections::vec_deque::Iter<'_, HistoryEntry>, Option<Vec<HistoryEntry>>> {
675693
fn into_fig_conversation_state(self) -> eyre::Result<FigConversationState> {
676694
let history = flatten_history(self.context_messages.unwrap_or_default().iter().chain(self.history));
677695
let user_input_message: UserInputMessage = self
@@ -695,7 +713,7 @@ impl
695713
// Count the chars used by the messages in the history.
696714
// this clone is cheap
697715
let history = self.history.clone();
698-
for (user, assistant) in history {
716+
for (user, assistant, _) in history {
699717
user_chars += *user.char_count();
700718
assistant_chars += *assistant.char_count();
701719
}
@@ -705,7 +723,7 @@ impl
705723
.context_messages
706724
.as_ref()
707725
.map(|v| {
708-
v.iter().fold(0, |acc, (user, assistant)| {
726+
v.iter().fold(0, |acc, (user, assistant, _)| {
709727
acc + *user.char_count() + *assistant.char_count()
710728
})
711729
})
@@ -730,9 +748,9 @@ pub struct ConversationSize {
730748
/// Converts a list of user/assistant message pairs into a flattened list of ChatMessage.
731749
fn flatten_history<'a, T>(history: T) -> Vec<ChatMessage>
732750
where
733-
T: Iterator<Item = &'a (UserMessage, AssistantMessage)>,
751+
T: Iterator<Item = &'a HistoryEntry>,
734752
{
735-
history.fold(Vec::new(), |mut acc, (user, assistant)| {
753+
history.fold(Vec::new(), |mut acc, (user, assistant, _)| {
736754
acc.push(ChatMessage::UserInputMessage(user.clone().into_history_entry()));
737755
acc.push(ChatMessage::AssistantResponseMessage(assistant.clone().into()));
738756
acc
@@ -774,7 +792,7 @@ fn format_hook_context<'a>(hook_results: impl IntoIterator<Item = &'a (Hook, Str
774792
}
775793

776794
fn enforce_conversation_invariants(
777-
history: &mut VecDeque<(UserMessage, AssistantMessage)>,
795+
history: &mut VecDeque<HistoryEntry>,
778796
next_message: &mut Option<UserMessage>,
779797
tools: &HashMap<ToolOrigin, Vec<Tool>>,
780798
) -> (usize, usize) {
@@ -791,7 +809,7 @@ fn enforce_conversation_invariants(
791809
.iter()
792810
.enumerate()
793811
.skip(1)
794-
.find(|(_, (m, _))| -> bool { !m.has_tool_use_results() })
812+
.find(|(_, (m, _, _))| -> bool { !m.has_tool_use_results() })
795813
.map(|v| v.0)
796814
{
797815
Some(i) => {
@@ -815,7 +833,7 @@ fn enforce_conversation_invariants(
815833

816834
// If the first message contains tool results, then we add the results to the content field
817835
// instead. This is required to avoid validation errors.
818-
if let Some((user, _)) = history.front_mut() {
836+
if let Some((user, _, _)) = history.front_mut() {
819837
if user.has_tool_use_results() {
820838
user.replace_content_with_tool_use_results();
821839
}
@@ -828,7 +846,7 @@ fn enforce_conversation_invariants(
828846
history.range(valid_history_range.0..valid_history_range.1).last(),
829847
) {
830848
(Some(next_message), prev_msg) if next_message.has_tool_use_results() => match prev_msg {
831-
None | Some((_, AssistantMessage::Response { .. })) => {
849+
None | Some((_, AssistantMessage::Response { .. }, _)) => {
832850
next_message.replace_content_with_tool_use_results();
833851
},
834852
_ => (),
@@ -838,7 +856,7 @@ fn enforce_conversation_invariants(
838856

839857
// If the last message from the assistant contains tool uses AND next_message is set, we need to
840858
// ensure that next_message contains tool results.
841-
if let (Some((_, AssistantMessage::ToolUse { tool_uses, .. })), Some(user_msg)) = (
859+
if let (Some((_, AssistantMessage::ToolUse { tool_uses, .. }, _)), Some(user_msg)) = (
842860
history.range(valid_history_range.0..valid_history_range.1).last(),
843861
next_message,
844862
) {
@@ -859,7 +877,7 @@ fn enforce_conversation_invariants(
859877
}
860878

861879
fn enforce_tool_use_history_invariants(
862-
history: &mut VecDeque<(UserMessage, AssistantMessage)>,
880+
history: &mut VecDeque<HistoryEntry>,
863881
tools: &HashMap<ToolOrigin, Vec<Tool>>,
864882
) {
865883
let tool_names: HashSet<_> = tools
@@ -872,7 +890,7 @@ fn enforce_tool_use_history_invariants(
872890
.filter(|name| *name != DUMMY_TOOL_NAME)
873891
.collect();
874892

875-
for (_, assistant) in history {
893+
for (_, assistant, _) in history {
876894
if let AssistantMessage::ToolUse { tool_uses, .. } = assistant {
877895
for tool_use in tool_uses {
878896
if tool_names.contains(tool_use.name.as_str()) {
@@ -1028,7 +1046,7 @@ mod tests {
10281046
.await
10291047
.unwrap();
10301048
assert_conversation_state_invariants(s, i);
1031-
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()));
1049+
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()), None);
10321050
conversation.set_next_user_message(i.to_string()).await;
10331051
}
10341052
}
@@ -1065,6 +1083,7 @@ mod tests {
10651083
args: serde_json::Value::Null,
10661084
..Default::default()
10671085
}]),
1086+
None,
10681087
);
10691088
conversation.add_tool_results(vec![ToolUseResult {
10701089
tool_use_id: "tool_id".to_string(),
@@ -1099,14 +1118,15 @@ mod tests {
10991118
args: serde_json::Value::Null,
11001119
..Default::default()
11011120
}]),
1121+
None,
11021122
);
11031123
conversation.add_tool_results(vec![ToolUseResult {
11041124
tool_use_id: "tool_id".to_string(),
11051125
content: vec![],
11061126
status: ToolResultStatus::Success,
11071127
}]);
11081128
} else {
1109-
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()));
1129+
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()), None);
11101130
conversation.set_next_user_message(i.to_string()).await;
11111131
}
11121132
}
@@ -1147,7 +1167,7 @@ mod tests {
11471167

11481168
assert_conversation_state_invariants(s, i);
11491169

1150-
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()));
1170+
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()), None);
11511171
conversation.set_next_user_message(i.to_string()).await;
11521172
}
11531173
}
@@ -1206,7 +1226,7 @@ mod tests {
12061226
s.user_input_message.content
12071227
);
12081228

1209-
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()));
1229+
conversation.push_assistant_message(&mut os, AssistantMessage::new_response(None, i.to_string()), None);
12101230
conversation.set_next_user_message(i.to_string()).await;
12111231
}
12121232
}

0 commit comments

Comments
 (0)