use crate::types::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionFormation {
Explicit { timestamp: DateTime<Utc> },
FirstLoop { timestamp: DateTime<Utc> },
InactivityTimeout {
threshold_secs: u64,
previous_session_id: Option<String>,
timestamp: DateTime<Utc>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LoopStatus {
Pending,
Running,
Completed,
Rejected,
Aborted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopConfigSnapshot {
pub model: String,
pub provider: String,
pub config_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<crate::provider::ApiProtocol>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<crate::types::ThinkingLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildLoopRef {
pub tool_call_id: String,
pub tool_name: String,
pub child_loop_id: String,
pub child_session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpawnRef {
pub parent_session_id: String,
pub parent_loop_id: String,
pub tool_call_id: String,
pub tool_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelGroupRecord {
pub all_loop_ids: Vec<String>,
pub selected_loop_id: String,
pub selected_config_index: usize,
pub evaluation_usage: Usage,
pub is_selected: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopEvent {
pub sequence: u64,
pub event: AgentEvent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Turn {
pub turn_id: TurnId,
pub triggered_by: TurnTrigger,
pub usage: Usage,
pub input_messages: Vec<AgentMessage>,
pub output_message: AgentMessage,
pub tool_results: Vec<AgentMessage>,
pub started_at: DateTime<Utc>,
pub ended_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_payload: Option<AnnotatedRequestPayload>,
}
impl Turn {
pub fn index(&self) -> u32 {
self.turn_id.turn_index
}
pub fn duration(&self) -> chrono::Duration {
self.ended_at - self.started_at
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_results.is_empty()
}
pub fn all_messages(&self) -> Vec<&AgentMessage> {
let mut msgs: Vec<&AgentMessage> = self.input_messages.iter().collect();
msgs.push(&self.output_message);
msgs.extend(self.tool_results.iter());
msgs
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopRecord {
pub loop_id: String,
pub session_id: String,
pub agent_id: String,
pub parent_loop_id: Option<String>,
#[serde(default, deserialize_with = "deserialize_null_default")]
pub continuation_kind: ContinuationKind,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub status: LoopStatus,
pub rejection: Option<String>,
pub config: Option<LoopConfigSnapshot>,
pub messages: Vec<AgentMessage>,
#[serde(default)]
pub turns: Vec<Turn>,
pub usage: Usage,
pub metadata: Option<serde_json::Value>,
pub events: Vec<LoopEvent>,
pub children_loop_ids: Vec<String>,
pub child_loop_refs: Vec<ChildLoopRef>,
pub parallel_group: Option<ParallelGroupRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction_block: Option<crate::context::CompactionBlock>,
}
impl LoopRecord {
pub fn get_turn(&self, turn_index: u32) -> Option<&Turn> {
self.turns.get(turn_index as usize)
}
pub fn turn_count(&self) -> usize {
self.turns.len()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum SessionScope {
#[default]
Ephemeral,
Persistent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub session_id: String,
pub agent_id: String,
pub created_at: DateTime<Utc>,
pub last_active_at: DateTime<Utc>,
pub formation: SessionFormation,
pub parent_spawn_ref: Option<SpawnRef>,
#[serde(default)]
pub scope: SessionScope,
pub loops: Vec<LoopRecord>,
}
impl Session {
pub fn root_loops(&self) -> impl Iterator<Item = &LoopRecord> {
let loop_ids: std::collections::HashSet<&str> =
self.loops.iter().map(|l| l.loop_id.as_str()).collect();
self.loops.iter().filter(move |l| {
l.parent_loop_id
.as_deref()
.map(|pid| !loop_ids.contains(pid))
.unwrap_or(true)
})
}
pub fn children_of<'a>(&'a self, loop_id: &str) -> impl Iterator<Item = &'a LoopRecord> {
let record = self.loops.iter().find(|l| l.loop_id == loop_id);
let ids: Vec<&str> = record
.map(|r| r.children_loop_ids.iter().map(|s| s.as_str()).collect())
.unwrap_or_default();
self.loops
.iter()
.filter(move |l| ids.contains(&l.loop_id.as_str()))
}
pub fn parallel_siblings<'a>(&'a self, loop_id: &str) -> impl Iterator<Item = &'a LoopRecord> {
let all_ids: Option<Vec<String>> = self
.loops
.iter()
.find(|l| l.loop_id == loop_id)
.and_then(|l| l.parallel_group.as_ref())
.map(|pg| pg.all_loop_ids.clone());
self.loops.iter().filter(move |l| {
all_ids
.as_ref()
.map(|ids| ids.contains(&l.loop_id))
.unwrap_or(false)
})
}
pub fn get_loop(&self, loop_id: &str) -> Option<&LoopRecord> {
self.loops.iter().find(|l| l.loop_id == loop_id)
}
pub fn get_loop_mut(&mut self, loop_id: &str) -> Option<&mut LoopRecord> {
self.loops.iter_mut().find(|l| l.loop_id == loop_id)
}
pub fn loop_chain_to(&self, target_loop_id: &str) -> Vec<String> {
let mut chain = Vec::new();
let mut current = target_loop_id.to_string();
loop {
chain.push(current.clone());
match self
.get_loop(¤t)
.and_then(|r| r.parent_loop_id.as_ref())
{
Some(parent) => current = parent.clone(),
None => break,
}
}
chain.reverse();
chain
}
pub fn total_usage(&self) -> Usage {
self.loops.iter().fold(Usage::default(), |mut acc, l| {
acc.input += l.usage.input;
acc.output += l.usage.output;
acc.reasoning += l.usage.reasoning;
acc.cache_read += l.usage.cache_read;
acc.cache_write += l.usage.cache_write;
acc.total_tokens += l.usage.total_tokens;
acc
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialize(#[from] serde_json::Error),
#[error("Session not found: {session_id}")]
NotFound { session_id: String },
#[error("Session {session_id} is locked by another writer")]
Locked { session_id: String },
#[error("Background task error: {0}")]
Task(String),
}
pub(crate) struct OpenLoop {
pub(crate) record: LoopRecord,
pub(crate) next_seq: u64,
}