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