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)]
131pub struct FileHistoryProvider {
132 path: PathBuf,
133 messages: Arc<Mutex<Vec<Message>>>,
134}
135
136impl FileHistoryProvider {
137 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
142 let path = path.into();
143 let messages = if path.exists() {
144 let data = std::fs::read_to_string(&path)
145 .map_err(|e| Error::other(format!("failed to read history file {path:?}: {e}")))?;
146 if data.trim().is_empty() {
147 Vec::new()
148 } else {
149 let value: Value = serde_json::from_str(&data).map_err(|e| {
150 Error::Serialization(format!("failed to parse history file {path:?}: {e}"))
151 })?;
152 match value.get("messages") {
153 Some(v) if !v.is_null() => serde_json::from_value(v.clone()).map_err(|e| {
154 Error::Serialization(format!("failed to parse history file {path:?}: {e}"))
155 })?,
156 _ => Vec::new(),
157 }
158 }
159 } else {
160 Vec::new()
161 };
162 Ok(Self {
163 path,
164 messages: Arc::new(Mutex::new(messages)),
165 })
166 }
167
168 pub fn path(&self) -> &std::path::Path {
170 &self.path
171 }
172
173 pub fn list_messages(&self) -> Vec<Message> {
175 self.messages.lock().unwrap().clone()
176 }
177
178 pub fn to_dict(&self) -> Value {
180 serde_json::json!({ "messages": self.list_messages() })
181 }
182
183 fn persist(&self) -> Result<()> {
184 let json = serde_json::to_string_pretty(&self.to_dict())
185 .map_err(|e| Error::Serialization(format!("failed to serialize history: {e}")))?;
186 std::fs::write(&self.path, json)
187 .map_err(|e| Error::other(format!("failed to write history file {:?}: {e}", self.path)))
188 }
189}
190
191#[async_trait]
192impl ContextProvider for FileHistoryProvider {
193 async fn before_run(&self, ctx: &mut SessionContext) -> Result<()> {
194 let stored = self.messages.lock().unwrap().clone();
195 let existing = std::mem::take(&mut ctx.messages);
196 ctx.messages = stored.into_iter().chain(existing).collect();
197 Ok(())
198 }
199
200 async fn after_run(
201 &self,
202 request_messages: &[Message],
203 response_messages: &[Message],
204 error: Option<&Error>,
205 ) -> Result<()> {
206 if error.is_none() {
207 {
208 let mut guard = self.messages.lock().unwrap();
209 guard.extend(request_messages.iter().cloned());
210 guard.extend(response_messages.iter().cloned());
211 }
212 self.persist()?;
213 }
214 Ok(())
215 }
216
217 fn is_history_provider(&self) -> bool {
218 true
219 }
220}
221
222impl HistoryProvider for FileHistoryProvider {}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::types::Message;
228
229 #[tokio::test]
230 async fn before_run_prepends_stored_messages_ahead_of_existing_context_messages() {
231 let provider = InMemoryHistoryProvider::with_messages(vec![
232 Message::user("q1"),
233 Message::assistant("a1"),
234 ]);
235 let mut ctx = SessionContext::new(vec![Message::user("q2")]);
236 ctx.messages
237 .push(Message::system("injected by another provider"));
238 provider.before_run(&mut ctx).await.unwrap();
239 let texts: Vec<String> = ctx.messages.iter().map(|m| m.text()).collect();
240 assert_eq!(
241 texts,
242 vec![
243 "q1".to_string(),
244 "a1".to_string(),
245 "injected by another provider".to_string(),
246 ]
247 );
248 }
249
250 #[tokio::test]
251 async fn after_run_appends_only_on_success() {
252 let provider = InMemoryHistoryProvider::new();
253 provider
254 .after_run(&[Message::user("hi")], &[Message::assistant("hello")], None)
255 .await
256 .unwrap();
257 assert_eq!(provider.list_messages().len(), 2);
258
259 provider
261 .after_run(
262 &[Message::user("again")],
263 &[],
264 Some(&Error::service("boom")),
265 )
266 .await
267 .unwrap();
268 assert_eq!(provider.list_messages().len(), 2);
269 }
270
271 #[test]
272 fn to_dict_from_dict_round_trips_messages() {
273 let provider = InMemoryHistoryProvider::with_messages(vec![
274 Message::user("q1"),
275 Message::assistant("a1"),
276 ]);
277 let state = provider.to_dict();
278 let restored = InMemoryHistoryProvider::from_dict(&state).unwrap();
279 let msgs = restored.list_messages();
280 assert_eq!(msgs.len(), 2);
281 assert_eq!(msgs[0].text(), "q1");
282 assert_eq!(msgs[1].text(), "a1");
283 }
284
285 #[test]
286 fn from_dict_tolerates_a_missing_messages_key() {
287 let restored = InMemoryHistoryProvider::from_dict(&serde_json::json!({})).unwrap();
288 assert!(restored.list_messages().is_empty());
289 }
290
291 #[test]
292 fn ensure_history_provider_attaches_once_and_skips_service_managed() {
293 let mut local = AgentSession::new();
294 ensure_history_provider(&mut local);
295 assert_eq!(local.context_providers.len(), 1);
296 assert!(local.context_providers[0].is_history_provider());
297 ensure_history_provider(&mut local);
299 assert_eq!(local.context_providers.len(), 1);
300
301 let mut service = AgentSession::service("svc-1");
302 ensure_history_provider(&mut service);
303 assert!(service.context_providers.is_empty());
304 }
305
306 #[tokio::test]
307 async fn file_history_provider_persists_and_reloads() {
308 let dir = std::env::temp_dir().join(format!("afr-history-test-{}", uuid::Uuid::new_v4()));
309 std::fs::create_dir_all(&dir).unwrap();
310 let path = dir.join("history.json");
311
312 let provider = FileHistoryProvider::new(&path).unwrap();
313 assert!(provider.list_messages().is_empty());
314 provider
315 .after_run(&[Message::user("hi")], &[Message::assistant("hello")], None)
316 .await
317 .unwrap();
318 assert_eq!(provider.list_messages().len(), 2);
319
320 let reloaded = FileHistoryProvider::new(&path).unwrap();
323 let msgs = reloaded.list_messages();
324 assert_eq!(msgs.len(), 2);
325 assert_eq!(msgs[0].text(), "hi");
326 assert_eq!(msgs[1].text(), "hello");
327
328 std::fs::remove_dir_all(&dir).ok();
329 }
330}