ralph_workflow/phases/commit_logging/message_generation/
attempt_log.rs1#[derive(Debug, Clone)]
6pub struct CommitAttemptLog {
7 pub attempt_number: usize,
9 pub agent: String,
11 pub strategy: String,
13 pub timestamp: DateTime<Local>,
15 pub prompt_size_bytes: usize,
17 pub diff_size_bytes: usize,
19 pub diff_was_truncated: bool,
21 pub raw_output: Option<String>,
23 pub extraction_attempts: Vec<ExtractionAttempt>,
25 pub validation_checks: Vec<ValidationCheck>,
27 pub outcome: Option<AttemptOutcome>,
29}
30
31impl CommitAttemptLog {
32 #[must_use]
34 pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
35 Self {
36 attempt_number,
37 agent: agent.to_string(),
38 strategy: strategy.to_string(),
39 timestamp: Local::now(),
40 prompt_size_bytes: 0,
41 diff_size_bytes: 0,
42 diff_was_truncated: false,
43 raw_output: None,
44 extraction_attempts: Vec::new(),
45 validation_checks: Vec::new(),
46 outcome: None,
47 }
48 }
49
50 pub const fn set_prompt_size(&mut self, size: usize) {
52 self.prompt_size_bytes = size;
53 }
54
55 pub const fn set_diff_info(&mut self, size: usize, was_truncated: bool) {
57 self.diff_size_bytes = size;
58 self.diff_was_truncated = was_truncated;
59 }
60
61 pub fn set_raw_output(&mut self, output: &str) {
65 const MAX_OUTPUT_SIZE: usize = 50_000;
66 if output.len() > MAX_OUTPUT_SIZE {
67 self.raw_output = Some(format!(
68 "{}\n\n[... truncated {} bytes ...]\n\n{}",
69 &output[..MAX_OUTPUT_SIZE / 2],
70 output.len() - MAX_OUTPUT_SIZE,
71 &output[output.len() - MAX_OUTPUT_SIZE / 2..]
72 ));
73 } else {
74 self.raw_output = Some(output.to_string());
75 }
76 }
77
78 pub fn add_extraction_attempt(&mut self, attempt: ExtractionAttempt) {
80 self.extraction_attempts.push(attempt);
81 }
82
83 #[cfg(test)]
85 pub fn set_validation_checks(&mut self, checks: Vec<ValidationCheck>) {
86 self.validation_checks = checks;
87 }
88
89 pub fn set_outcome(&mut self, outcome: AttemptOutcome) {
91 self.outcome = Some(outcome);
92 }
93
94 pub fn write_to_workspace(
112 &self,
113 log_dir: &Path,
114 workspace: &dyn Workspace,
115 ) -> std::io::Result<PathBuf> {
116 workspace.create_dir_all(log_dir)?;
118
119 let filename = format!(
121 "attempt_{:03}_{}_{}_{}.log",
122 self.attempt_number,
123 sanitize_agent_name(&self.agent),
124 self.strategy.replace(' ', "_"),
125 self.timestamp.format("%Y%m%dT%H%M%S")
126 );
127 let log_path = log_dir.join(filename);
128
129 let mut content = String::new();
131 self.write_header_to_string(&mut content);
132 self.write_context_to_string(&mut content);
133 self.write_raw_output_to_string(&mut content);
134 self.write_extraction_attempts_to_string(&mut content);
135 self.write_validation_to_string(&mut content);
136 self.write_outcome_to_string(&mut content);
137
138 workspace.write(&log_path, &content)?;
140 Ok(log_path)
141 }
142
143 fn write_header_to_string(&self, s: &mut String) {
145 use std::fmt::Write;
146 let _ = writeln!(
147 s,
148 "========================================================================"
149 );
150 let _ = writeln!(s, "COMMIT GENERATION ATTEMPT LOG");
151 let _ = writeln!(
152 s,
153 "========================================================================"
154 );
155 let _ = writeln!(s);
156 let _ = writeln!(s, "Attempt: #{}", self.attempt_number);
157 let _ = writeln!(s, "Agent: {}", self.agent);
158 let _ = writeln!(s, "Strategy: {}", self.strategy);
159 let _ = writeln!(
160 s,
161 "Timestamp: {}",
162 self.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
163 );
164 let _ = writeln!(s);
165 }
166
167 fn write_context_to_string(&self, s: &mut String) {
168 use std::fmt::Write;
169 let _ = writeln!(
170 s,
171 "------------------------------------------------------------------------"
172 );
173 let _ = writeln!(s, "CONTEXT");
174 let _ = writeln!(
175 s,
176 "------------------------------------------------------------------------"
177 );
178 let _ = writeln!(s);
179 let _ = writeln!(
180 s,
181 "Prompt size: {} bytes ({} KB)",
182 self.prompt_size_bytes,
183 self.prompt_size_bytes / 1024
184 );
185 let _ = writeln!(
186 s,
187 "Diff size: {} bytes ({} KB)",
188 self.diff_size_bytes,
189 self.diff_size_bytes / 1024
190 );
191 let _ = writeln!(
192 s,
193 "Diff truncated: {}",
194 if self.diff_was_truncated { "YES" } else { "NO" }
195 );
196 let _ = writeln!(s);
197 }
198
199 fn write_raw_output_to_string(&self, s: &mut String) {
200 use std::fmt::Write;
201 let _ = writeln!(
202 s,
203 "------------------------------------------------------------------------"
204 );
205 let _ = writeln!(s, "RAW AGENT OUTPUT");
206 let _ = writeln!(
207 s,
208 "------------------------------------------------------------------------"
209 );
210 let _ = writeln!(s);
211 match &self.raw_output {
212 Some(output) => {
213 let _ = writeln!(s, "{output}");
214 }
215 None => {
216 let _ = writeln!(s, "[No output captured]");
217 }
218 }
219 let _ = writeln!(s);
220 }
221
222 fn write_extraction_attempts_to_string(&self, s: &mut String) {
223 use std::fmt::Write;
224 let _ = writeln!(
225 s,
226 "------------------------------------------------------------------------"
227 );
228 let _ = writeln!(s, "EXTRACTION ATTEMPTS");
229 let _ = writeln!(
230 s,
231 "------------------------------------------------------------------------"
232 );
233 let _ = writeln!(s);
234
235 if self.extraction_attempts.is_empty() {
236 let _ = writeln!(s, "[No extraction attempts recorded]");
237 } else {
238 for (i, attempt) in self.extraction_attempts.iter().enumerate() {
239 let status = if attempt.success {
240 "✓ SUCCESS"
241 } else {
242 "✗ FAILED"
243 };
244 let _ = writeln!(s, "{}. {} [{}]", i + 1, attempt.method, status);
245 let _ = writeln!(s, " Detail: {}", attempt.detail);
246 let _ = writeln!(s);
247 }
248 }
249 let _ = writeln!(s);
250 }
251
252 fn write_validation_to_string(&self, s: &mut String) {
253 use std::fmt::Write;
254 let _ = writeln!(
255 s,
256 "------------------------------------------------------------------------"
257 );
258 let _ = writeln!(s, "VALIDATION RESULTS");
259 let _ = writeln!(
260 s,
261 "------------------------------------------------------------------------"
262 );
263 let _ = writeln!(s);
264
265 if self.validation_checks.is_empty() {
266 let _ = writeln!(s, "[No validation checks recorded]");
267 } else {
268 for check in &self.validation_checks {
269 let status = if check.passed { "✓ PASS" } else { "✗ FAIL" };
270 let _ = write!(s, " [{status}] {}", check.name);
271 if let Some(error) = &check.error {
272 let _ = writeln!(s, ": {error}");
273 } else {
274 let _ = writeln!(s);
275 }
276 }
277 }
278 let _ = writeln!(s);
279 }
280
281 fn write_outcome_to_string(&self, s: &mut String) {
282 use std::fmt::Write;
283 let _ = writeln!(
284 s,
285 "------------------------------------------------------------------------"
286 );
287 let _ = writeln!(s, "OUTCOME");
288 let _ = writeln!(
289 s,
290 "------------------------------------------------------------------------"
291 );
292 let _ = writeln!(s);
293 match &self.outcome {
294 Some(outcome) => {
295 let _ = writeln!(s, "{outcome}");
296 }
297 None => {
298 let _ = writeln!(s, "[Outcome not recorded]");
299 }
300 }
301 let _ = writeln!(s);
302 let _ = writeln!(
303 s,
304 "========================================================================"
305 );
306 }
307}
308
309fn sanitize_agent_name(agent: &str) -> String {
311 agent
312 .chars()
313 .map(|c| if c.is_alphanumeric() { c } else { '_' })
314 .collect::<String>()
315 .chars()
316 .take(MAX_AGENT_NAME_LENGTH)
317 .collect()
318}