use crate::types::{now_ms, AgentMessage};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SessionEntry {
pub id: String,
pub parent_id: Option<String>,
pub message: AgentMessage,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SessionError {
#[error("unknown session entry: {0}")]
UnknownEntry(String),
#[error("unknown checkpoint label: {0}")]
UnknownCheckpoint(String),
#[error("failed to parse session line {line}: {error}")]
Parse { line: usize, error: String },
#[error("session is empty")]
Empty,
#[error("duplicate session entry id: {0}")]
DuplicateId(String),
#[error("entry {id} references unknown parent {parent}")]
UnknownParent { id: String, parent: String },
#[error(
"history diverged from the session path at index {index}: the agent's \
messages no longer extend this branch (did compaction rewrite them?)"
)]
HistoryDiverged { index: usize },
}
#[derive(Debug, Clone, Default)]
pub struct Session {
entries: Vec<SessionEntry>,
head: Option<String>,
next_seq: u64,
}
impl Session {
pub fn new() -> Self {
Self::default()
}
pub fn from_messages(messages: &[AgentMessage]) -> Self {
let mut s = Self::new();
for m in messages {
s.append(m.clone());
}
s
}
pub fn append(&mut self, message: AgentMessage) -> String {
self.next_seq += 1;
let id = format!("e{}", self.next_seq);
self.entries.push(SessionEntry {
id: id.clone(),
parent_id: self.head.clone(),
message,
timestamp: now_ms(),
label: None,
});
self.head = Some(id.clone());
id
}
pub fn append_new(&mut self, full_history: &[AgentMessage]) -> Result<usize, SessionError> {
let path = self.path_messages();
if full_history.len() < path.len() {
return Err(SessionError::HistoryDiverged {
index: full_history.len(),
});
}
for (i, known) in path.iter().enumerate() {
if &full_history[i] != known {
return Err(SessionError::HistoryDiverged { index: i });
}
}
let mut appended = 0;
for m in full_history.iter().skip(path.len()) {
self.append(m.clone());
appended += 1;
}
Ok(appended)
}
pub fn head(&self) -> Option<&str> {
self.head.as_deref()
}
pub fn seek(&mut self, entry_id: &str) -> Result<(), SessionError> {
if self.entry(entry_id).is_none() {
return Err(SessionError::UnknownEntry(entry_id.to_string()));
}
self.head = Some(entry_id.to_string());
Ok(())
}
pub fn checkpoint(&mut self, label: impl Into<String>) -> Result<(), SessionError> {
let head = self.head.clone().ok_or(SessionError::Empty)?;
let label = label.into();
let entry = self
.entries
.iter_mut()
.find(|e| e.id == head)
.expect("head always exists");
entry.label = Some(label);
Ok(())
}
pub fn seek_checkpoint(&mut self, label: &str) -> Result<(), SessionError> {
let id = self
.entries
.iter()
.rev()
.find(|e| e.label.as_deref() == Some(label))
.map(|e| e.id.clone())
.ok_or_else(|| SessionError::UnknownCheckpoint(label.to_string()))?;
self.head = Some(id);
Ok(())
}
pub fn path_messages(&self) -> Vec<AgentMessage> {
self.path_ids()
.iter()
.map(|id| self.entry(id).expect("path ids exist").message.clone())
.collect()
}
pub fn path_ids(&self) -> Vec<String> {
let mut ids = Vec::new();
let mut cursor = self.head.clone();
while let Some(id) = cursor {
cursor = self.entry(&id).and_then(|e| e.parent_id.clone());
ids.push(id);
}
ids.reverse();
ids
}
pub fn entries(&self) -> &[SessionEntry] {
&self.entries
}
pub fn entry(&self, id: &str) -> Option<&SessionEntry> {
self.entries.iter().find(|e| e.id == id)
}
pub fn branch_tips(&self) -> Vec<&str> {
let mut has_child: HashMap<&str, bool> = HashMap::new();
for e in &self.entries {
has_child.entry(e.id.as_str()).or_insert(false);
if let Some(p) = &e.parent_id {
has_child.insert(p.as_str(), true);
}
}
self.entries
.iter()
.filter(|e| !has_child.get(e.id.as_str()).copied().unwrap_or(false))
.map(|e| e.id.as_str())
.collect()
}
pub fn children(&self, id: &str) -> Vec<&SessionEntry> {
self.entries
.iter()
.filter(|e| e.parent_id.as_deref() == Some(id))
.collect()
}
pub fn to_jsonl(&self) -> String {
self.entries
.iter()
.map(|e| serde_json::to_string(e).expect("session entries serialize"))
.collect::<Vec<_>>()
.join("\n")
}
pub fn from_jsonl(s: &str) -> Result<Self, SessionError> {
let mut session = Self::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for (i, line) in s.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let entry: SessionEntry =
serde_json::from_str(line).map_err(|e| SessionError::Parse {
line: i + 1,
error: e.to_string(),
})?;
if seen.contains(&entry.id) {
return Err(SessionError::DuplicateId(entry.id));
}
if let Some(parent) = &entry.parent_id {
if !seen.contains(parent) {
return Err(SessionError::UnknownParent {
id: entry.id.clone(),
parent: parent.clone(),
});
}
}
seen.insert(entry.id.clone());
if let Some(n) = entry
.id
.strip_prefix('e')
.and_then(|n| n.parse::<u64>().ok())
{
session.next_seq = session.next_seq.max(n);
}
session.head = Some(entry.id.clone());
session.entries.push(entry);
}
Ok(session)
}
}