Skip to main content

deepseek_rust_cli/agent/
history.rs

1use std::{fs, path::PathBuf};
2
3use crate::api::types::Message;
4
5/// Repair a message list that may have orphaned tool messages or unfulfilled
6/// assistant tool_calls. The API requires every `tool` message to be preceded
7/// by an assistant message with matching `tool_calls`, and every assistant
8/// with `tool_calls` must have its tool responses before the next user message.
9fn repair_history(mut messages: Vec<Message>) -> Vec<Message> {
10    // Strip trailing orphaned tool messages
11    while messages.last().is_some_and(|m| m.role == "tool") {
12        messages.pop();
13    }
14    // Strip trailing assistant message with unfulfilled tool_calls
15    if messages
16        .last()
17        .is_some_and(|m| m.role == "assistant" && m.tool_calls.is_some())
18    {
19        messages.pop();
20    }
21
22    // Walk through and fix mid-sequence corruption:
23    // 1. Tool messages whose tool_call_id doesn't match the preceding assistant
24    // 2. Assistant messages with tool_calls that have no following tool messages
25    let mut i = 0;
26    while i < messages.len() {
27        if messages[i].role == "tool" {
28            let tool_id = messages[i].tool_call_id.as_deref().unwrap_or("");
29            // Look backwards for matching assistant message
30            let mut found = false;
31            for j in (0..i).rev() {
32                if messages[j].role == "assistant" {
33                    if let Some(ref tcs) = messages[j].tool_calls {
34                        if tcs.iter().any(|tc| tc.id == tool_id) {
35                            found = true;
36                        }
37                    }
38                    break; // Only check the immediately preceding assistant
39                }
40            }
41            if !found {
42                // Remove orphaned tool message
43                messages.remove(i);
44                continue;
45            }
46        } else if messages[i].role == "assistant" && messages[i].tool_calls.is_some() {
47            // Check if this assistant has at least one tool response before
48            // the next user or assistant message
49            let mut has_tool_response = false;
50            for msg in messages.iter().skip(i + 1) {
51                match msg.role.as_str() {
52                    "tool" => {
53                        has_tool_response = true;
54                    }
55                    "user" | "assistant" => {
56                        break;
57                    }
58                    _ => {}
59                }
60            }
61            if !has_tool_response {
62                // Remove assistant with no tool responses
63                messages.remove(i);
64                continue;
65            }
66        }
67        i += 1;
68    }
69
70    messages
71}
72
73pub fn load_history(session_id: &str) -> Vec<Message> {
74    let path = get_history_path(session_id);
75    if let Some(msgs) = fs::read_to_string(path)
76        .ok()
77        .and_then(|c| serde_json::from_str::<Vec<Message>>(&c).ok())
78    {
79        let original_len = msgs.len();
80        let repaired = repair_history(msgs);
81        // Save the repaired history back so the corruption doesn't persist
82        if repaired.len() != original_len {
83            save_history(session_id, &repaired);
84        }
85        return repaired;
86    }
87    Vec::new()
88}
89
90pub fn save_history(session_id: &str, messages: &[Message]) {
91    let path = get_history_path(session_id);
92    let _ = fs::create_dir_all(path.parent().unwrap());
93    if let Ok(json) = serde_json::to_string_pretty(messages) {
94        let _ = fs::write(path, json);
95    }
96}
97
98fn get_history_path(session_id: &str) -> PathBuf {
99    let mut path = PathBuf::from(".deep/history");
100    path.push(format!("{}.json", session_id));
101    path
102}