baml_agent/session/
store.rs1use std::fs::OpenOptions;
4use std::io::{BufRead, BufReader, Write};
5use std::path::{Path, PathBuf};
6
7use super::format::{make_persisted, parse_entry};
8use super::traits::{AgentMessage, EntryType, MessageRole};
9
10pub struct Session<M: AgentMessage> {
14 messages: Vec<M>,
15 session_file: PathBuf,
16 session_id: String,
17 last_uuid: Option<String>,
18 max_history: usize,
19}
20
21impl<M: AgentMessage> Session<M> {
22 pub fn new(session_dir: &str, max_history: usize) -> std::io::Result<Self> {
27 std::fs::create_dir_all(session_dir)?;
28 let session_id = uuid::Uuid::now_v7().to_string();
29 let session_file = PathBuf::from(format!("{}/{}.jsonl", session_dir, session_id));
30 Ok(Self {
31 messages: Vec::new(),
32 session_file,
33 session_id,
34 last_uuid: None,
35 max_history,
36 })
37 }
38
39 pub fn resume(path: &Path, _session_dir: &str, max_history: usize) -> Self {
41 let (messages, session_id, last_uuid) = Self::load_file(path);
42 Self {
43 messages,
44 session_file: path.to_path_buf(),
45 session_id,
46 last_uuid,
47 max_history,
48 }
49 }
50
51 pub fn resume_last(session_dir: &str, max_history: usize) -> Option<Self> {
53 let last = Self::find_last_session(session_dir)?;
54 Some(Self::resume(&last, session_dir, max_history))
55 }
56
57 pub fn push(&mut self, role: <M as AgentMessage>::Role, content: String) -> &M {
59 let msg = M::new(role, content);
60 self.messages.push(msg);
61 self.persist_last();
62 self.messages.last().expect("just pushed")
63 }
64
65 pub fn push_msg(&mut self, msg: M) {
67 self.messages.push(msg);
68 self.persist_last();
69 }
70
71 pub fn messages(&self) -> &[M] {
73 &self.messages
74 }
75
76 pub fn messages_mut(&mut self) -> &mut Vec<M> {
78 &mut self.messages
79 }
80
81 pub fn is_empty(&self) -> bool {
82 self.messages.is_empty()
83 }
84
85 pub fn len(&self) -> usize {
86 self.messages.len()
87 }
88
89 pub fn session_file(&self) -> &Path {
90 &self.session_file
91 }
92
93 pub fn session_id(&self) -> &str {
94 &self.session_id
95 }
96
97 pub fn trim(&mut self) -> usize {
103 if self.messages.len() <= self.max_history {
104 return 0;
105 }
106
107 let system_msgs: Vec<M> = self
108 .messages
109 .iter()
110 .filter(|m| m.role().is_system())
111 .cloned()
112 .collect();
113
114 let non_system: Vec<M> = self
115 .messages
116 .iter()
117 .filter(|m| !m.role().is_system())
118 .cloned()
119 .collect();
120
121 let keep = self.max_history.saturating_sub(system_msgs.len());
122 let skip = non_system.len().saturating_sub(keep);
123
124 if skip == 0 {
125 return 0;
126 }
127
128 let mut trimmed = system_msgs;
129 trimmed.push(M::new(
130 <M as AgentMessage>::Role::system(),
131 format!("[{} earlier messages trimmed]", skip),
132 ));
133 trimmed.extend(non_system.into_iter().skip(skip));
134 self.messages = trimmed;
135 skip
136 }
137
138 fn persist_last(&mut self) {
141 let Some(msg) = self.messages.last() else {
142 return;
143 };
144 let Some(entry_type) = EntryType::parse(msg.role().as_str()) else {
145 return;
146 };
147 let persisted = make_persisted(
148 entry_type,
149 msg.content(),
150 &self.session_id,
151 self.last_uuid.as_deref(),
152 );
153 self.last_uuid = Some(persisted.uuid.clone());
154 let Ok(json) = serde_json::to_string(&persisted) else {
155 return;
156 };
157 let Ok(mut f) = OpenOptions::new()
158 .create(true)
159 .append(true)
160 .open(&self.session_file)
161 else {
162 return;
163 };
164 let _ = writeln!(f, "{}", json);
165 }
166
167 fn load_file(path: &Path) -> (Vec<M>, String, Option<String>) {
170 let Ok(file) = std::fs::File::open(path) else {
171 return (vec![], uuid::Uuid::now_v7().to_string(), None);
172 };
173
174 let mut messages = Vec::new();
175 let mut session_id = None;
176 let mut last_uuid = None;
177
178 for line in BufReader::new(file).lines().map_while(Result::ok) {
179 let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) else {
180 continue;
181 };
182
183 if let Some(sid) = value["sessionId"].as_str() {
184 session_id = Some(sid.to_string());
185 }
186 if let Some(uid) = value["uuid"].as_str() {
187 last_uuid = Some(uid.to_string());
188 }
189
190 if let Some((entry_type, content)) = parse_entry(&value) {
191 messages.push(M::new(
192 entry_type.into_role::<<M as AgentMessage>::Role>(),
193 content,
194 ));
195 }
196 }
197
198 let sid = session_id.unwrap_or_else(|| {
199 path.file_stem()
200 .and_then(|s| s.to_str())
201 .map(String::from)
202 .unwrap_or_else(|| uuid::Uuid::now_v7().to_string())
203 });
204
205 (messages, sid, last_uuid)
206 }
207
208 fn find_last_session(dir: &str) -> Option<PathBuf> {
209 let mut entries: Vec<_> = std::fs::read_dir(dir)
210 .ok()?
211 .filter_map(|e| e.ok())
212 .filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
213 .collect();
214 entries.sort_by_key(|e| e.file_name());
215 entries.last().map(|e| e.path())
216 }
217}