pub mod compression;
use crate::checkpoint::timestamp;
use crate::workspace::Workspace;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::path::Path;
use std::sync::Arc;
fn deserialize_option_boxed_string_slice_none_if_empty<'de, D>(
deserializer: D,
) -> Result<Option<Box<[String]>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<Vec<String>>::deserialize(deserializer)?;
Ok(match opt {
None => None,
Some(v) if v.is_empty() => None,
Some(v) => Some(v.into_boxed_slice()),
})
}
fn serialize_option_boxed_string_slice_empty_if_none_field<S, V>(
value: V,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
V: std::ops::Deref<Target = Option<Box<[String]>>>,
{
let values = (*value).as_deref();
serialize_option_boxed_string_slice_empty_if_none(values, serializer)
}
fn serialize_option_boxed_string_slice_empty_if_none<S>(
value: Option<&[String]>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
if let Some(values) = value {
values.serialize(serializer)
} else {
let seq = serializer.serialize_seq(Some(0))?;
seq.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepOutcome {
Success {
output: Option<Box<str>>,
#[serde(
default,
deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty",
serialize_with = "serialize_option_boxed_string_slice_empty_if_none_field"
)]
files_modified: Option<Box<[String]>>,
#[serde(default)]
exit_code: Option<i32>,
},
Failure {
error: Box<str>,
recoverable: bool,
#[serde(default)]
exit_code: Option<i32>,
#[serde(
default,
deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty",
serialize_with = "serialize_option_boxed_string_slice_empty_if_none_field"
)]
signals: Option<Box<[String]>>,
},
Partial {
completed: Box<str>,
remaining: Box<str>,
#[serde(default)]
exit_code: Option<i32>,
},
Skipped { reason: Box<str> },
}
impl StepOutcome {
pub fn success(output: Option<String>, files_modified: Vec<String>) -> Self {
Self::Success {
output: output.map(String::into_boxed_str),
files_modified: if files_modified.is_empty() {
None
} else {
Some(files_modified.into_boxed_slice())
},
exit_code: Some(0),
}
}
#[must_use]
pub fn failure(error: String, recoverable: bool) -> Self {
Self::Failure {
error: error.into_boxed_str(),
recoverable,
exit_code: None,
signals: None,
}
}
#[must_use]
pub fn partial(completed: String, remaining: String) -> Self {
Self::Partial {
completed: completed.into_boxed_str(),
remaining: remaining.into_boxed_str(),
exit_code: None,
}
}
#[must_use]
pub fn skipped(reason: String) -> Self {
Self::Skipped {
reason: reason.into_boxed_str(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ModifiedFilesDetail {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
)]
pub added: Option<Box<[String]>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
)]
pub modified: Option<Box<[String]>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
)]
pub deleted: Option<Box<[String]>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct IssuesSummary {
#[serde(default)]
pub found: u32,
#[serde(default)]
pub fixed: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExecutionStep {
pub phase: Arc<str>,
pub iteration: u32,
pub step_type: Box<str>,
pub timestamp: String,
pub outcome: StepOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<Arc<str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_saved_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_commit_oid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified_files_detail: Option<ModifiedFilesDetail>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_used: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issues_summary: Option<IssuesSummary>,
}
impl ExecutionStep {
#[must_use]
pub fn new(phase: &str, iteration: u32, step_type: &str, outcome: StepOutcome) -> Self {
Self {
phase: Arc::from(phase),
iteration,
step_type: Box::from(step_type),
timestamp: timestamp(),
outcome,
agent: None,
duration_secs: None,
checkpoint_saved_at: None,
git_commit_oid: None,
modified_files_detail: None,
prompt_used: None,
issues_summary: None,
}
}
pub fn new_with_pool(
phase: &str,
iteration: u32,
step_type: &str,
outcome: StepOutcome,
pool: crate::checkpoint::StringPool,
) -> (Self, crate::checkpoint::StringPool) {
let (pool, phase_arc) = pool.intern_str(phase);
(
Self {
phase: phase_arc,
iteration,
step_type: Box::from(step_type),
timestamp: timestamp(),
outcome,
agent: None,
duration_secs: None,
checkpoint_saved_at: None,
git_commit_oid: None,
modified_files_detail: None,
prompt_used: None,
issues_summary: None,
},
pool,
)
}
#[must_use]
pub fn with_agent(mut self, agent: &str) -> Self {
self.agent = Some(Arc::from(agent));
self
}
#[must_use]
pub fn with_agent_pooled(
mut self,
agent: &str,
pool: crate::checkpoint::StringPool,
) -> (Self, crate::checkpoint::StringPool) {
let (pool, agent_arc) = pool.intern_str(agent);
self.agent = Some(agent_arc);
(self, pool)
}
#[must_use]
pub const fn with_duration(mut self, duration_secs: u64) -> Self {
self.duration_secs = Some(duration_secs);
self
}
#[must_use]
pub fn with_git_commit_oid(mut self, oid: &str) -> Self {
self.git_commit_oid = Some(oid.to_string());
self
}
}
include!("execution_history/file_snapshot.rs");
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ExecutionHistory {
pub steps: VecDeque<ExecutionStep>,
pub file_snapshots: HashMap<String, FileSnapshot>,
}
impl ExecutionHistory {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add_step_bounded(&mut self, step: ExecutionStep, limit: usize) -> &mut Self {
let drop_count = self.steps.len().saturating_sub(limit.saturating_sub(1));
self.steps = self
.steps
.iter()
.skip(drop_count)
.chain(std::iter::once(&step))
.cloned()
.collect();
self
}
#[must_use]
pub fn clone_bounded(&self, limit: usize) -> Self {
if limit == 0 {
return Self {
steps: VecDeque::new(),
file_snapshots: self.file_snapshots.clone(),
};
}
let len = self.steps.len();
if len <= limit {
return self.clone();
}
let keep_from = len.saturating_sub(limit);
let steps: VecDeque<_> = self.steps.iter().skip(keep_from).cloned().collect();
Self {
steps,
file_snapshots: self.file_snapshots.clone(),
}
}
}
#[cfg(test)]
mod tests;