1use anyhow::anyhow;
4use llimorse::Agent;
5use llimorse::line_format::{
6 AssistantMessage, ChatMessage, ToolCallParams, ToolResult, UserMessage,
7};
8use std::collections::HashMap;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum HistoryEntryType {
13 Empty,
15
16 User,
18
19 Content,
21
22 Reasoning,
24
25 ToolCall,
27
28 ToolResultOk,
30
31 ToolResultErr,
33}
34
35#[derive(Debug, Default)]
37pub struct ChatHistory {
38 lines: Vec<(String, HistoryEntryType)>,
40
41 open_tool_calls: HashMap<String, ToolCallParams>,
43}
44
45impl ChatHistory {
46 pub fn push_raw(&mut self, agent: &Agent, msg: &ChatMessage) {
48 match msg {
49 ChatMessage::System(_) => (),
51
52 ChatMessage::User(UserMessage { content }) => {
53 self.push_lines(content, HistoryEntryType::User, true);
54 }
55
56 ChatMessage::Assistant(AssistantMessage {
57 reasoning_content,
58 content,
59 tool_calls,
60 }) => {
61 if let Some(reasoning) = reasoning_content {
62 self.push_lines(reasoning, HistoryEntryType::Reasoning, true);
63 }
64 if let Some(content) = content {
65 self.push_lines(content, HistoryEntryType::Content, true);
66 }
67 if let Some(tool_calls) = tool_calls {
68 for call in tool_calls {
69 self.open_tool_calls
70 .insert(call.id.clone(), call.call.clone());
71
72 self.push_lines(
73 &format!("[{}] {}\n", call.id, agent.display_call(&call.call)),
74 HistoryEntryType::ToolCall,
75 true,
76 );
77 }
78 }
79 }
80
81 ChatMessage::Tool(ToolResult {
82 tool_call_id,
83 content,
84 }) => {
85 let call = self.open_tool_calls.remove(tool_call_id);
86 let name = match &call {
87 Some(ToolCallParams::Function { function }) => &function.name,
88 Some(ToolCallParams::Custom { custom }) => &custom.name,
89 _ => "(unmatched call)",
90 };
91
92 match content
93 .strip_prefix("TOOL CALL FAILED: ")
94 .or_else(|| content.strip_prefix("TOOL CALL REJECTED: "))
95 {
96 Some(error) => self.push_lines(
97 &format!("=[{name}/{tool_call_id}]=> {error}\n"),
98 HistoryEntryType::ToolResultErr,
99 true,
100 ),
101 None => {
102 let line = if let Some(call) = &call {
103 format!(
104 "=[{name}/{tool_call_id}]=> {}\n",
105 agent.display_call_result(call, content)
106 )
107 } else {
108 format!("=[{name}/{tool_call_id}]=> {content}\n")
109 };
110
111 self.push_lines(&line, HistoryEntryType::ToolResultOk, true);
112 }
113 }
114 }
115 }
116 }
117
118 pub fn lines(&self) -> &[(String, HistoryEntryType)] {
120 &self.lines
121 }
122
123 pub fn push(&mut self, string: &str, kind: HistoryEntryType, force_new_line: bool) {
128 if let Some(last) = self.lines.last_mut() {
129 if last.1 == kind && !force_new_line {
130 last.0.push_str(string);
131 return;
132 } else if last.1 != kind {
133 if last.0.is_empty() {
135 last.1 = HistoryEntryType::Empty;
136 } else {
137 self.lines.push((String::new(), HistoryEntryType::Empty));
138 }
139 }
140 }
141
142 self.lines.push((string.to_string(), kind));
143 }
144
145 pub fn push_lines(&mut self, string: &str, kind: HistoryEntryType, mut force_new_line: bool) {
153 for line in string.split('\n') {
154 self.push(line, kind, force_new_line);
155 force_new_line = true; }
157 }
158
159 pub fn force_resolve_unresolved_tool_calls(&mut self, agent: &mut Agent) {
165 while let Some(id) = self.open_tool_calls.keys().next() {
166 let result = ToolResult::new(
167 id.clone(),
168 Err(anyhow!(
169 "Tool call aborted due to incomplete session state, please retry"
170 )),
171 );
172 let message: ChatMessage = result.into();
173 self.push_raw(agent, &message);
174 agent.push(message);
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use llimorse::line_format::{FunctionCall, SystemMessage, ToolCall};
183
184 impl ChatHistory {
189 fn push_raw_test(&mut self, msg: &ChatMessage) {
190 match msg {
191 ChatMessage::System(_) => (),
192
193 ChatMessage::User(UserMessage { content }) => {
194 self.push_lines(content, HistoryEntryType::User, true);
195 }
196
197 ChatMessage::Assistant(AssistantMessage {
198 reasoning_content,
199 content,
200 tool_calls,
201 }) => {
202 if let Some(reasoning) = reasoning_content {
203 self.push_lines(reasoning, HistoryEntryType::Reasoning, true);
204 }
205 if let Some(content) = content {
206 self.push_lines(content, HistoryEntryType::Content, true);
207 }
208 if let Some(tool_calls) = tool_calls {
209 for call in tool_calls {
210 self.open_tool_calls
211 .insert(call.id.clone(), call.call.clone());
212
213 let display = match &call.call {
214 ToolCallParams::Function { function } => {
215 format!("[unknown function {}]", function.name)
216 }
217 ToolCallParams::Custom { custom } => {
218 format!("[unknown tool {}]", custom.name)
219 }
220 };
221 self.push_lines(
222 &format!("[{}] {}\n", call.id, display),
223 HistoryEntryType::ToolCall,
224 true,
225 );
226 }
227 }
228 }
229
230 ChatMessage::Tool(ToolResult {
231 tool_call_id,
232 content,
233 }) => {
234 let call = self.open_tool_calls.remove(tool_call_id);
235 let name = match &call {
236 Some(ToolCallParams::Function { function }) => &function.name,
237 Some(ToolCallParams::Custom { custom }) => &custom.name,
238 _ => "(unmatched call)",
239 };
240
241 match content
242 .strip_prefix("TOOL CALL FAILED: ")
243 .or_else(|| content.strip_prefix("TOOL CALL REJECTED: "))
244 {
245 Some(error) => self.push_lines(
246 &format!("[{name}/{tool_call_id}]=> {error}\n"),
247 HistoryEntryType::ToolResultErr,
248 true,
249 ),
250 None => {
251 let display = match &call {
252 Some(ToolCallParams::Function { function }) => {
253 format!("[unknown function {}]", function.name)
254 }
255 Some(ToolCallParams::Custom { custom }) => {
256 format!("[unknown tool {}]", custom.name)
257 }
258 None => String::new(),
259 };
260 let line = if display.is_empty() {
261 format!("[{name}/{tool_call_id}]=> {}\n", content)
262 } else {
263 format!("=[{name}/{tool_call_id}]=> {}\n", display)
264 };
265 self.push_lines(&line, HistoryEntryType::ToolResultOk, true);
266 }
267 }
268 }
269 }
270 }
271 }
272
273 fn assert_single_line_entries(history: &ChatHistory) {
275 assert!(
276 !history.lines().iter().any(|(line, _)| line.contains('\n')),
277 "history entry spans multiple lines: {:#?}",
278 history.lines()
279 );
280 }
281
282 #[test]
283 fn push_raw_splits_multiline_messages() {
284 let mut history = ChatHistory::default();
285
286 history.push_raw_test(&ChatMessage::User(UserMessage {
287 content: "line one\nline two".into(),
288 }));
289 history.push_raw_test(&ChatMessage::Assistant(AssistantMessage {
290 reasoning_content: Some("think one\nthink two".into()),
291 content: Some("reply one\nreply two".into()),
292 tool_calls: None,
293 }));
294
295 assert_eq!(
296 history.lines(),
297 &[
298 ("line one".to_owned(), HistoryEntryType::User),
299 ("line two".to_owned(), HistoryEntryType::User),
300 (String::new(), HistoryEntryType::Empty),
301 ("think one".to_owned(), HistoryEntryType::Reasoning),
302 ("think two".to_owned(), HistoryEntryType::Reasoning),
303 (String::new(), HistoryEntryType::Empty),
304 ("reply one".to_owned(), HistoryEntryType::Content),
305 ("reply two".to_owned(), HistoryEntryType::Content),
306 ]
307 );
308 assert_single_line_entries(&history);
309 }
310
311 #[test]
312 fn push_raw_splits_tool_calls_and_results() {
313 let mut history = ChatHistory::default();
314
315 let call = ToolCall {
316 id: "call_1".into(),
317 call: ToolCallParams::Function {
318 function: FunctionCall {
319 name: "some_tool".into(),
320 arguments: "{}".into(),
321 },
322 },
323 };
324 history.push_raw_test(&ChatMessage::Assistant(AssistantMessage {
325 reasoning_content: None,
326 content: None,
327 tool_calls: Some(vec![call]),
328 }));
329 history.push_raw_test(&ChatMessage::Tool(ToolResult {
330 tool_call_id: "call_1".into(),
331 content: "result one\nresult two\n".into(),
332 }));
333
334 assert_eq!(
337 history.lines(),
338 &[
339 (
340 "[call_1] [unknown function some_tool]".to_owned(),
341 HistoryEntryType::ToolCall,
342 ),
343 (String::new(), HistoryEntryType::Empty),
344 (
345 "=[some_tool/call_1]=> [unknown function some_tool]".to_owned(),
346 HistoryEntryType::ToolResultOk,
347 ),
348 (String::new(), HistoryEntryType::ToolResultOk),
349 ]
350 );
351 assert_single_line_entries(&history);
352 }
353
354 #[test]
355 fn push_raw_does_not_merge_consecutive_messages() {
356 let mut history = ChatHistory::default();
357
358 history.push_raw_test(&ChatMessage::User(UserMessage {
359 content: "first".into(),
360 }));
361 history.push_raw_test(&ChatMessage::System(SystemMessage {
362 content: "in between".into(),
363 }));
364 history.push_raw_test(&ChatMessage::User(UserMessage {
365 content: "second".into(),
366 }));
367
368 assert_eq!(
371 history.lines(),
372 &[
373 ("first".to_owned(), HistoryEntryType::User),
374 ("second".to_owned(), HistoryEntryType::User),
375 ]
376 );
377 assert_single_line_entries(&history);
378 }
379}