agent_framework_core/
history.rs1use std::path::PathBuf;
18use std::sync::{Arc, Mutex};
19
20use async_trait::async_trait;
21use serde_json::Value;
22
23use crate::error::{Error, Result};
24use crate::memory::{ContextProvider, SessionContext};
25use crate::session::AgentSession;
26use crate::types::Message;
27
28pub trait HistoryProvider: ContextProvider {}
36
37pub fn ensure_history_provider(session: &mut AgentSession) {
43 if session.service_session_id().is_none()
44 && !session
45 .context_providers
46 .iter()
47 .any(|p| p.is_history_provider())
48 {
49 session
50 .context_providers
51 .insert(0, Arc::new(InMemoryHistoryProvider::new()));
52 }
53}
54
55#[derive(Default, Clone)]
58pub struct InMemoryHistoryProvider {
59 messages: Arc<Mutex<Vec<Message>>>,
60}
61
62impl InMemoryHistoryProvider {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn with_messages(messages: Vec<Message>) -> Self {
70 Self {
71 messages: Arc::new(Mutex::new(messages)),
72 }
73 }
74
75 pub fn list_messages(&self) -> Vec<Message> {
77 self.messages.lock().unwrap().clone()
78 }
79
80 pub fn to_dict(&self) -> Value {
82 serde_json::json!({ "messages": self.list_messages() })
83 }
84
85 pub fn from_dict(state: &Value) -> Result<Self> {
87 let messages = match state.get("messages") {
88 Some(v) if !v.is_null() => serde_json::from_value(v.clone()).map_err(|e| {
89 Error::Serialization(format!("failed to restore history provider: {e}"))
90 })?,
91 _ => Vec::new(),
92 };
93 Ok(Self::with_messages(messages))
94 }
95}
96
97#[async_trait]
98impl ContextProvider for InMemoryHistoryProvider {
99 async fn before_run(&self, ctx: &mut SessionContext) -> Result<()> {
100 let stored = self.messages.lock().unwrap().clone();
101 let existing = std::mem::take(&mut ctx.messages);
102 ctx.messages = stored.into_iter().chain(existing).collect();
103 Ok(())
104 }
105
106 async fn after_run(
107 &self,
108 request_messages: &[Message],
109 response_messages: &[Message],
110 error: Option<&Error>,
111 ) -> Result<()> {
112 if error.is_none() {
113 let mut guard = self.messages.lock().unwrap();
114 guard.extend(request_messages.iter().cloned());
115 guard.extend(response_messages.iter().cloned());
116 }
117 Ok(())
118 }
119
120 fn is_history_provider(&self) -> bool {
121 true
122 }
123}
124
125impl HistoryProvider for InMemoryHistoryProvider {}
126
127#[derive(Clone)]
139pub struct FileHistoryProvider {
140 path: PathBuf,
141 messages: Arc<Mutex<Vec<Message>>>,
142 write_lock: Arc<tokio::sync::Mutex<()>>,
147}
148
149impl FileHistoryProvider {
150 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
155 let path = path.into();
156 let messages = if path.exists() {
157 let data = std::fs::read_to_string(&path)
158 .map_err(|e| Error::other(format!("failed to read history file {path:?}: {e}")))?;
159 if data.trim().is_empty() {
160 Vec::new()
161 } else {
162 let value: Value = serde_json::from_str(&data).map_err(|e| {
163 Error::Serialization(format!("failed to parse history file {path:?}: {e}"))
164 })?;
165 match value.get("messages") {
166 Some(v) if !v.is_null() => serde_json::from_value(v.clone()).map_err(|e| {
167 Error::Serialization(format!("failed to parse history file {path:?}: {e}"))
168 })?,
169 _ => Vec::new(),
170 }
171 }
172 } else {
173 Vec::new()
174 };
175 Ok(Self {
176 path,
177 messages: Arc::new(Mutex::new(messages)),
178 write_lock: Arc::new(tokio::sync::Mutex::new(())),
179 })
180 }
181
182 pub fn path(&self) -> &std::path::Path {
184 &self.path
185 }
186
187 pub fn list_messages(&self) -> Vec<Message> {
189 self.messages.lock().unwrap().clone()
190 }
191
192 pub fn to_dict(&self) -> Value {
194 serde_json::json!({ "messages": self.list_messages() })
195 }
196
197 async fn persist(&self, messages: &[Message]) -> Result<()> {
210 let dict = serde_json::json!({ "messages": messages });
211 let json = serde_json::to_string_pretty(&dict)
212 .map_err(|e| Error::Serialization(format!("failed to serialize history: {e}")))?;
213 let file_name = self
217 .path
218 .file_name()
219 .and_then(|f| f.to_str())
220 .unwrap_or("history.json");
221 let tmp = self
222 .path
223 .with_file_name(format!("{file_name}.tmp.{}", uuid::Uuid::new_v4()));
224 if let Err(e) = tokio::fs::write(&tmp, &json).await {
225 let _ = tokio::fs::remove_file(&tmp).await;
227 return Err(Error::other(format!(
228 "failed to write history temp file {tmp:?}: {e}"
229 )));
230 }
231 tokio::fs::rename(&tmp, &self.path).await.map_err(|e| {
232 let tmp = tmp.clone();
234 tokio::spawn(async move {
235 let _ = tokio::fs::remove_file(&tmp).await;
236 });
237 Error::other(format!(
238 "failed to finalize history file {:?}: {e}",
239 self.path
240 ))
241 })
242 }
243}
244
245#[async_trait]
246impl ContextProvider for FileHistoryProvider {
247 async fn before_run(&self, ctx: &mut SessionContext) -> Result<()> {
248 let stored = self.messages.lock().unwrap().clone();
249 let existing = std::mem::take(&mut ctx.messages);
250 ctx.messages = stored.into_iter().chain(existing).collect();
251 Ok(())
252 }
253
254 async fn after_run(
255 &self,
256 request_messages: &[Message],
257 response_messages: &[Message],
258 error: Option<&Error>,
259 ) -> Result<()> {
260 if error.is_some() {
261 return Ok(());
262 }
263 let _write = self.write_lock.lock().await;
267
268 let snapshot = {
274 let guard = self.messages.lock().unwrap();
275 let mut next = guard.clone();
276 next.extend(request_messages.iter().cloned());
277 next.extend(response_messages.iter().cloned());
278 next
279 };
280 self.persist(&snapshot).await?;
281 *self.messages.lock().unwrap() = snapshot;
282 Ok(())
283 }
284
285 fn is_history_provider(&self) -> bool {
286 true
287 }
288}
289
290impl HistoryProvider for FileHistoryProvider {}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::types::Message;
296
297 #[tokio::test]
298 async fn before_run_prepends_stored_messages_ahead_of_existing_context_messages() {
299 let provider = InMemoryHistoryProvider::with_messages(vec![
300 Message::user("q1"),
301 Message::assistant("a1"),
302 ]);
303 let mut ctx = SessionContext::new(vec![Message::user("q2")]);
304 ctx.messages
305 .push(Message::system("injected by another provider"));
306 provider.before_run(&mut ctx).await.unwrap();
307 let texts: Vec<String> = ctx.messages.iter().map(|m| m.text()).collect();
308 assert_eq!(
309 texts,
310 vec![
311 "q1".to_string(),
312 "a1".to_string(),
313 "injected by another provider".to_string(),
314 ]
315 );
316 }
317
318 #[tokio::test]
319 async fn after_run_appends_only_on_success() {
320 let provider = InMemoryHistoryProvider::new();
321 provider
322 .after_run(&[Message::user("hi")], &[Message::assistant("hello")], None)
323 .await
324 .unwrap();
325 assert_eq!(provider.list_messages().len(), 2);
326
327 provider
329 .after_run(
330 &[Message::user("again")],
331 &[],
332 Some(&Error::service("boom")),
333 )
334 .await
335 .unwrap();
336 assert_eq!(provider.list_messages().len(), 2);
337 }
338
339 #[test]
340 fn to_dict_from_dict_round_trips_messages() {
341 let provider = InMemoryHistoryProvider::with_messages(vec![
342 Message::user("q1"),
343 Message::assistant("a1"),
344 ]);
345 let state = provider.to_dict();
346 let restored = InMemoryHistoryProvider::from_dict(&state).unwrap();
347 let msgs = restored.list_messages();
348 assert_eq!(msgs.len(), 2);
349 assert_eq!(msgs[0].text(), "q1");
350 assert_eq!(msgs[1].text(), "a1");
351 }
352
353 #[test]
354 fn from_dict_tolerates_a_missing_messages_key() {
355 let restored = InMemoryHistoryProvider::from_dict(&serde_json::json!({})).unwrap();
356 assert!(restored.list_messages().is_empty());
357 }
358
359 #[test]
360 fn ensure_history_provider_attaches_once_and_skips_service_managed() {
361 let mut local = AgentSession::new();
362 ensure_history_provider(&mut local);
363 assert_eq!(local.context_providers.len(), 1);
364 assert!(local.context_providers[0].is_history_provider());
365 ensure_history_provider(&mut local);
367 assert_eq!(local.context_providers.len(), 1);
368
369 let mut service = AgentSession::service("svc-1");
370 ensure_history_provider(&mut service);
371 assert!(service.context_providers.is_empty());
372 }
373
374 #[tokio::test]
375 async fn file_history_provider_persists_and_reloads() {
376 let dir = std::env::temp_dir().join(format!("afr-history-test-{}", uuid::Uuid::new_v4()));
377 std::fs::create_dir_all(&dir).unwrap();
378 let path = dir.join("history.json");
379
380 let provider = FileHistoryProvider::new(&path).unwrap();
381 assert!(provider.list_messages().is_empty());
382 provider
383 .after_run(&[Message::user("hi")], &[Message::assistant("hello")], None)
384 .await
385 .unwrap();
386 assert_eq!(provider.list_messages().len(), 2);
387
388 let reloaded = FileHistoryProvider::new(&path).unwrap();
391 let msgs = reloaded.list_messages();
392 assert_eq!(msgs.len(), 2);
393 assert_eq!(msgs[0].text(), "hi");
394 assert_eq!(msgs[1].text(), "hello");
395
396 std::fs::remove_dir_all(&dir).ok();
397 }
398
399 #[tokio::test]
400 async fn file_history_provider_concurrent_runs_do_not_lose_messages() {
401 let dir = std::env::temp_dir().join(format!("afr-history-conc-{}", uuid::Uuid::new_v4()));
405 std::fs::create_dir_all(&dir).unwrap();
406 let path = dir.join("history.json");
407
408 let provider = FileHistoryProvider::new(&path).unwrap();
409 const N: usize = 50;
410 let mut handles = Vec::new();
411 for i in 0..N {
412 let p = provider.clone();
413 handles.push(tokio::spawn(async move {
414 p.after_run(
415 &[Message::user(format!("q{i}"))],
416 &[Message::assistant(format!("a{i}"))],
417 None,
418 )
419 .await
420 .unwrap();
421 }));
422 }
423 for h in handles {
424 h.await.unwrap();
425 }
426
427 assert_eq!(provider.list_messages().len(), N * 2);
429
430 let reloaded = FileHistoryProvider::new(&path).unwrap();
433 assert_eq!(reloaded.list_messages().len(), N * 2);
434
435 let leftover: Vec<_> = std::fs::read_dir(&dir)
437 .unwrap()
438 .filter_map(|e| e.ok())
439 .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
440 .collect();
441 assert!(leftover.is_empty(), "temp files leaked: {leftover:?}");
442
443 std::fs::remove_dir_all(&dir).ok();
444 }
445}