1use dashmap::DashMap;
2use std::hash::{DefaultHasher, Hash, Hasher};
3use std::sync::Arc;
4use uuid::Uuid;
5
6use crate::types::ChatMessage;
7
8#[derive(Clone)]
21pub struct SessionStore {
22 inner: Arc<DashMap<String, Vec<ChatMessage>>>,
23 reasoning: Arc<DashMap<String, String>>,
24 turn_reasoning: Arc<DashMap<u64, String>>,
26}
27
28impl SessionStore {
29 pub fn new() -> Self {
30 Self {
31 inner: Arc::new(DashMap::new()),
32 reasoning: Arc::new(DashMap::new()),
33 turn_reasoning: Arc::new(DashMap::new()),
34 }
35 }
36
37 pub fn store_reasoning(&self, call_id: String, reasoning: String) {
40 if !reasoning.is_empty() {
41 self.reasoning.insert(call_id, reasoning);
42 }
43 }
44
45 pub fn get_reasoning(&self, call_id: &str) -> Option<String> {
47 self.reasoning.get(call_id).map(|v| v.clone())
48 }
49
50 pub fn store_turn_reasoning(&self, _prior: &[ChatMessage], assistant: &ChatMessage, reasoning: String) {
53 if !reasoning.is_empty() {
54 let content = assistant.content.as_deref().unwrap_or("");
57 if !content.is_empty() {
58 let key = Self::content_key(content);
59 self.turn_reasoning.insert(key, reasoning.clone());
60 }
61 if let Some(tcs) = &assistant.tool_calls {
63 for tc in tcs {
64 if let Some(id) = tc.get("id").and_then(|v| v.as_str()) {
65 if !id.is_empty() {
66 self.store_reasoning(id.to_string(), reasoning.clone());
67 }
68 }
69 }
70 }
71 }
72 }
73
74 pub fn get_turn_reasoning(&self, _prior: &[ChatMessage], assistant: &ChatMessage) -> Option<String> {
76 let content = assistant.content.as_deref().unwrap_or("");
77 if content.is_empty() {
78 return None;
79 }
80 let key = Self::content_key(content);
81 self.turn_reasoning.get(&key).map(|v| v.clone())
82 }
83
84 fn content_key(content: &str) -> u64 {
86 let mut hasher = DefaultHasher::new();
87 content.hash(&mut hasher);
88 hasher.finish()
89 }
90
91 pub fn get_history(&self, response_id: &str) -> Vec<ChatMessage> {
93 self.inner
94 .get(response_id)
95 .map(|v| v.clone())
96 .unwrap_or_default()
97 }
98
99 pub fn new_id(&self) -> String {
102 format!("resp_{}", Uuid::new_v4().simple())
103 }
104
105 pub fn save_with_id(&self, id: String, messages: Vec<ChatMessage>) {
107 self.inner.insert(id, messages);
108 }
109
110 pub fn save(&self, messages: Vec<ChatMessage>) -> String {
112 let id = self.new_id();
113 self.inner.insert(id.clone(), messages);
114 id
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::types::ChatMessage;
122
123 fn msg(role: &str, content: Option<&str>) -> ChatMessage {
124 ChatMessage {
125 role: role.into(),
126 content: content.map(Into::into),
127 reasoning_content: None,
128 tool_calls: None,
129 tool_call_id: None,
130 name: None,
131 }
132 }
133
134 #[test]
135 fn test_store_and_get_reasoning() {
136 let store = SessionStore::new();
137 store.store_reasoning("call_1".into(), "think".into());
138 assert_eq!(store.get_reasoning("call_1"), Some("think".into()));
139 }
140
141 #[test]
142 fn test_get_reasoning_missing() {
143 let store = SessionStore::new();
144 assert_eq!(store.get_reasoning("nonexistent"), None);
145 }
146
147 #[test]
148 fn test_empty_reasoning_not_stored() {
149 let store = SessionStore::new();
150 store.store_reasoning("call_e".into(), "".into());
151 assert_eq!(store.get_reasoning("call_e"), None);
152 }
153
154 #[test]
155 fn test_turn_reasoning_by_content() {
156 let store = SessionStore::new();
157 let assistant = msg("assistant", Some("hello world"));
158 store.store_turn_reasoning(&[], &assistant, "deep thought".into());
159 assert_eq!(
160 store.get_turn_reasoning(&[], &assistant),
161 Some("deep thought".into())
162 );
163 }
164
165 #[test]
166 fn test_turn_reasoning_empty_content() {
167 let store = SessionStore::new();
168 let assistant = msg("assistant", Some(""));
169 store.store_turn_reasoning(&[], &assistant, "reason".into());
170 assert_eq!(store.get_turn_reasoning(&[], &assistant), None);
171 }
172
173 #[test]
174 fn test_turn_reasoning_also_stores_call_ids() {
175 let store = SessionStore::new();
176 let mut assistant = msg("assistant", Some("hi"));
177 assistant.tool_calls = Some(vec![serde_json::json!({
178 "id": "call_123",
179 "type": "function",
180 "function": {"name": "exec", "arguments": "{}"}
181 })]);
182 store.store_turn_reasoning(&[], &assistant, "reason_tc".into());
183 assert_eq!(store.get_reasoning("call_123"), Some("reason_tc".into()));
184 }
185
186 #[test]
187 fn test_history_save_and_get() {
188 let store = SessionStore::new();
189 let msgs = vec![msg("user", Some("hi")), msg("assistant", Some("hey"))];
190 let id = store.save(msgs.clone());
191 let got = store.get_history(&id);
192 assert_eq!(got.len(), 2);
193 assert_eq!(got[0].content.as_deref(), Some("hi"));
194
195 let id2 = store.new_id();
197 store.save_with_id(id2.clone(), vec![msg("user", Some("q"))]);
198 assert_eq!(store.get_history(&id2).len(), 1);
199 }
200
201 #[test]
202 fn test_content_key_deterministic() {
203 let a = SessionStore::content_key("same text");
204 let b = SessionStore::content_key("same text");
205 assert_eq!(a, b);
206 let c = SessionStore::content_key("different");
207 assert_ne!(a, c);
208 }
209}