use anyhow::{Context, Result}; use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use crate::agent::context::AstNode; pub struct ConversationLog { path: PathBuf, } impl ConversationLog { pub fn new(path: PathBuf) -> Result { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("creating log dir {}", parent.display()))?; } Ok(Self { path }) } pub fn append_node(&self, node: &AstNode) -> Result<()> { let mut file = OpenOptions::new() .create(true) .append(true) .open(&self.path) .with_context(|| format!("opening log {}", self.path.display()))?; let line = serde_json::to_string(node) .context("serializing node for log")?; writeln!(file, "{}", line) .context("writing to conversation log")?; file.sync_all() .context("syncing conversation log")?; Ok(()) } pub fn read_nodes(&self, max_bytes: u64) -> Result> { if !self.path.exists() { return Ok(Vec::new()); } let file = File::open(&self.path) .with_context(|| format!("opening log {}", self.path.display()))?; let file_len = file.metadata()?.len(); let mut reader = BufReader::new(file); if file_len > max_bytes { reader.seek(SeekFrom::Start(file_len - max_bytes))?; let mut discard = String::new(); reader.read_line(&mut discard)?; } let mut nodes = Vec::new(); for line in reader.lines() { let line = line.context("reading log tail")?; let line = line.trim(); if line.is_empty() { continue; } if let Ok(node) = serde_json::from_str::(line) { nodes.push(node); } } Ok(nodes) } pub fn path(&self) -> &Path { &self.path } pub fn oldest_timestamp(&self) -> Option> { let file = File::open(&self.path).ok()?; let reader = BufReader::new(file); for line in reader.lines().flatten() { let line = line.trim().to_string(); if line.is_empty() { continue; } if let Ok(node) = serde_json::from_str::(&line) { if let Some(leaf) = node.leaf() { if let Some(ts) = leaf.timestamp() { return Some(ts); } } } } None } }