1pub mod compression;
7
8use crate::checkpoint::timestamp;
9use crate::workspace::Workspace;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, VecDeque};
12use std::path::Path;
13use std::sync::Arc;
14
15fn deserialize_option_boxed_string_slice_none_if_empty<'de, D>(
16 deserializer: D,
17) -> Result<Option<Box<[String]>>, D::Error>
18where
19 D: serde::Deserializer<'de>,
20{
21 let opt = Option::<Vec<String>>::deserialize(deserializer)?;
22 Ok(match opt {
23 None => None,
24 Some(v) if v.is_empty() => None,
25 Some(v) => Some(v.into_boxed_slice()),
26 })
27}
28
29fn serialize_option_boxed_string_slice_empty_if_none_field<S, V>(
30 value: V,
31 serializer: S,
32) -> Result<S::Ok, S::Error>
33where
34 S: serde::Serializer,
35 V: std::ops::Deref<Target = Option<Box<[String]>>>,
36{
37 let values = (*value).as_deref();
38 serialize_option_boxed_string_slice_empty_if_none(values, serializer)
39}
40
41fn serialize_option_boxed_string_slice_empty_if_none<S>(
42 value: Option<&[String]>,
43 serializer: S,
44) -> Result<S::Ok, S::Error>
45where
46 S: serde::Serializer,
47{
48 use serde::ser::SerializeSeq;
49
50 if let Some(values) = value {
51 values.serialize(serializer)
52 } else {
53 let seq = serializer.serialize_seq(Some(0))?;
54 seq.end()
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub enum StepOutcome {
67 Success {
69 output: Option<Box<str>>,
70 #[serde(
71 default,
72 deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty",
73 serialize_with = "serialize_option_boxed_string_slice_empty_if_none_field"
74 )]
75 files_modified: Option<Box<[String]>>,
76 #[serde(default)]
77 exit_code: Option<i32>,
78 },
79 Failure {
81 error: Box<str>,
82 recoverable: bool,
83 #[serde(default)]
84 exit_code: Option<i32>,
85 #[serde(
86 default,
87 deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty",
88 serialize_with = "serialize_option_boxed_string_slice_empty_if_none_field"
89 )]
90 signals: Option<Box<[String]>>,
91 },
92 Partial {
94 completed: Box<str>,
95 remaining: Box<str>,
96 #[serde(default)]
97 exit_code: Option<i32>,
98 },
99 Skipped { reason: Box<str> },
101}
102
103impl StepOutcome {
104 pub fn success(output: Option<String>, files_modified: Vec<String>) -> Self {
106 Self::Success {
107 output: output.map(String::into_boxed_str),
108 files_modified: if files_modified.is_empty() {
109 None
110 } else {
111 Some(files_modified.into_boxed_slice())
112 },
113 exit_code: Some(0),
114 }
115 }
116
117 #[must_use]
119 pub fn failure(error: String, recoverable: bool) -> Self {
120 Self::Failure {
121 error: error.into_boxed_str(),
122 recoverable,
123 exit_code: None,
124 signals: None,
125 }
126 }
127
128 #[must_use]
130 pub fn partial(completed: String, remaining: String) -> Self {
131 Self::Partial {
132 completed: completed.into_boxed_str(),
133 remaining: remaining.into_boxed_str(),
134 exit_code: None,
135 }
136 }
137
138 #[must_use]
140 pub fn skipped(reason: String) -> Self {
141 Self::Skipped {
142 reason: reason.into_boxed_str(),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
156pub struct ModifiedFilesDetail {
157 #[serde(
158 default,
159 skip_serializing_if = "Option::is_none",
160 deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
161 )]
162 pub added: Option<Box<[String]>>,
163 #[serde(
164 default,
165 skip_serializing_if = "Option::is_none",
166 deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
167 )]
168 pub modified: Option<Box<[String]>>,
169 #[serde(
170 default,
171 skip_serializing_if = "Option::is_none",
172 deserialize_with = "deserialize_option_boxed_string_slice_none_if_empty"
173 )]
174 pub deleted: Option<Box<[String]>>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
179pub struct IssuesSummary {
180 #[serde(default)]
182 pub found: u32,
183 #[serde(default)]
185 pub fixed: u32,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub description: Option<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204pub struct ExecutionStep {
205 pub phase: Arc<str>,
207 pub iteration: u32,
209 pub step_type: Box<str>,
211 pub timestamp: String,
213 pub outcome: StepOutcome,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub agent: Option<Arc<str>>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub duration_secs: Option<u64>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub checkpoint_saved_at: Option<String>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub git_commit_oid: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub modified_files_detail: Option<ModifiedFilesDetail>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub prompt_used: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub issues_summary: Option<IssuesSummary>,
236}
237
238impl ExecutionStep {
239 #[must_use]
247 pub fn new(phase: &str, iteration: u32, step_type: &str, outcome: StepOutcome) -> Self {
248 Self {
249 phase: Arc::from(phase),
250 iteration,
251 step_type: Box::from(step_type),
252 timestamp: timestamp(),
253 outcome,
254 agent: None,
255 duration_secs: None,
256 checkpoint_saved_at: None,
257 git_commit_oid: None,
258 modified_files_detail: None,
259 prompt_used: None,
260 issues_summary: None,
261 }
262 }
263
264 pub fn new_with_pool(
270 phase: &str,
271 iteration: u32,
272 step_type: &str,
273 outcome: StepOutcome,
274 pool: crate::checkpoint::StringPool,
275 ) -> (Self, crate::checkpoint::StringPool) {
276 let (pool, phase_arc) = pool.intern_str(phase);
277 (
278 Self {
279 phase: phase_arc,
280 iteration,
281 step_type: Box::from(step_type),
282 timestamp: timestamp(),
283 outcome,
284 agent: None,
285 duration_secs: None,
286 checkpoint_saved_at: None,
287 git_commit_oid: None,
288 modified_files_detail: None,
289 prompt_used: None,
290 issues_summary: None,
291 },
292 pool,
293 )
294 }
295
296 #[must_use]
298 pub fn with_agent(mut self, agent: &str) -> Self {
299 self.agent = Some(Arc::from(agent));
300 self
301 }
302
303 #[must_use]
305 pub fn with_agent_pooled(
306 mut self,
307 agent: &str,
308 pool: crate::checkpoint::StringPool,
309 ) -> (Self, crate::checkpoint::StringPool) {
310 let (pool, agent_arc) = pool.intern_str(agent);
311 self.agent = Some(agent_arc);
312 (self, pool)
313 }
314
315 #[must_use]
317 pub const fn with_duration(mut self, duration_secs: u64) -> Self {
318 self.duration_secs = Some(duration_secs);
319 self
320 }
321
322 #[must_use]
324 pub fn with_git_commit_oid(mut self, oid: &str) -> Self {
325 self.git_commit_oid = Some(oid.to_string());
326 self
327 }
328}
329
330include!("execution_history/file_snapshot.rs");
331
332#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
334pub struct ExecutionHistory {
335 pub steps: VecDeque<ExecutionStep>,
337 pub file_snapshots: HashMap<String, FileSnapshot>,
339}
340
341impl ExecutionHistory {
342 #[must_use]
359 pub fn new() -> Self {
360 Self::default()
361 }
362
363 #[must_use]
368 pub fn add_step_bounded(&mut self, step: ExecutionStep, limit: usize) -> &mut Self {
369 let drop_count = self.steps.len().saturating_sub(limit.saturating_sub(1));
370 self.steps = self
371 .steps
372 .iter()
373 .skip(drop_count)
374 .chain(std::iter::once(&step))
375 .cloned()
376 .collect();
377 self
378 }
379
380 #[must_use]
386 pub fn clone_bounded(&self, limit: usize) -> Self {
387 if limit == 0 {
388 return Self {
389 steps: VecDeque::new(),
390 file_snapshots: self.file_snapshots.clone(),
391 };
392 }
393
394 let len = self.steps.len();
395 if len <= limit {
396 return self.clone();
397 }
398
399 let keep_from = len.saturating_sub(limit);
400 let steps: VecDeque<_> = self.steps.iter().skip(keep_from).cloned().collect();
401 Self {
402 steps,
403 file_snapshots: self.file_snapshots.clone(),
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests;