1#[derive(Debug)]
3pub struct CommitMessageResult {
4 pub message: String,
6 pub success: bool,
8 pub _log_path: String,
10 pub generated_prompts: HashMap<String, String>,
12}
13
14pub struct CommitAttemptResult {
16 pub had_error: bool,
17 pub output_valid: bool,
18 pub message: Option<String>,
19 pub validation_detail: String,
20 pub auth_failure: bool,
21}
22
23pub fn run_commit_attempt(
34 ctx: &mut PhaseContext<'_>,
35 attempt: u32,
36 model_safe_diff: &str,
37 commit_agent: &str,
38) -> anyhow::Result<CommitAttemptResult> {
39 let prompt_key = format!("commit_message_attempt_{attempt}");
44 let (prompt, was_replayed) = build_commit_prompt(
45 &prompt_key,
46 ctx.template_context,
47 model_safe_diff,
48 ctx.workspace,
49 &ctx.prompt_history,
50 );
51
52 if let Err(err) = crate::prompts::validate_no_unresolved_placeholders_with_ignored_content(
55 &prompt,
56 &[model_safe_diff],
57 ) {
58 return Err(crate::prompts::TemplateVariablesInvalidError {
59 template_name: "commit_message_xml".to_string(),
60 missing_variables: Vec::new(),
61 unresolved_placeholders: err.unresolved_placeholders,
62 }
63 .into());
64 }
65
66 if !was_replayed {
67 ctx.capture_prompt(&prompt_key, &prompt);
68 }
69
70 let mut runtime = PipelineRuntime {
71 timer: ctx.timer,
72 logger: ctx.logger,
73 colors: ctx.colors,
74 config: ctx.config,
75 executor: ctx.executor,
76 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
77 workspace: ctx.workspace,
78 };
79
80 let log_dir = Path::new(".agent/logs/commit_generation");
81 let mut session = CommitLogSession::new(
82 log_dir
83 .to_str()
84 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8"),
85 ctx.workspace,
86 )
87 .unwrap_or_else(|_| CommitLogSession::noop());
88 let mut attempt_log = session.new_attempt(commit_agent, "single");
89 attempt_log.set_prompt_size(prompt.len());
90 let diff_was_truncated =
94 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
95 attempt_log.set_diff_info(model_safe_diff.len(), diff_was_truncated);
96
97 let agent_config = ctx
98 .registry
99 .resolve_config(commit_agent)
100 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
101 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
102
103 let base_log_path = ctx.run_log_context.agent_log("commit", attempt, None);
105 let log_attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
106 &base_log_path,
107 ctx.workspace,
108 );
109 let logfile = if log_attempt == 0 {
110 base_log_path
111 .to_str()
112 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
113 .to_string()
114 } else {
115 ctx.run_log_context
116 .agent_log("commit", attempt, Some(log_attempt))
117 .to_str()
118 .expect("Path contains invalid UTF-8 - all paths in this codebase should be UTF-8")
119 .to_string()
120 };
121
122 let log_header = format!(
125 "# Ralph Agent Invocation Log\n\
126 # Role: Commit\n\
127 # Agent: {}\n\
128 # Model Index: 0\n\
129 # Attempt: {}\n\
130 # Phase: CommitMessage\n\
131 # Timestamp: {}\n\n",
132 commit_agent,
133 log_attempt,
134 chrono::Utc::now().to_rfc3339()
135 );
136 ctx.workspace
137 .append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
138 .context("Failed to write agent log header - log would be incomplete without metadata")?;
139
140 let log_prefix = format!("commit_{attempt}"); let model_index = 0usize; let prompt_cmd = PromptCommand {
143 label: commit_agent,
144 display_name: commit_agent,
145 cmd_str: &cmd_str,
146 prompt: &prompt,
147 log_prefix: &log_prefix,
148 model_index: Some(model_index),
149 attempt: Some(log_attempt),
150 logfile: &logfile,
151 parser_type: agent_config.json_parser,
152 env_vars: &agent_config.env_vars,
153 };
154
155 let result = run_with_prompt(&prompt_cmd, &mut runtime)?;
156 let had_error = result.exit_code != 0;
157 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
158 attempt_log.set_raw_output(&result.stderr);
159
160 if auth_failure {
161 attempt_log.set_outcome(AttemptOutcome::ExtractionFailed(
162 "Authentication error detected".to_string(),
163 ));
164 if !session.is_noop() {
165 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
166 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
167 }
168 return Ok(CommitAttemptResult {
169 had_error,
170 output_valid: false,
171 message: None,
172 validation_detail: "Authentication error detected".to_string(),
173 auth_failure: true,
174 });
175 }
176
177 let extraction = extract_commit_message_from_file_with_workspace(ctx.workspace);
178 let (outcome, detail, extraction_result) = match extraction {
179 CommitExtractionOutcome::Valid(result) => (
180 AttemptOutcome::Success(result.clone().into_message()),
181 "Valid commit message extracted".to_string(),
182 Some(result),
183 ),
184 CommitExtractionOutcome::InvalidXml(detail) => (
185 AttemptOutcome::XsdValidationFailed(detail.clone()),
186 detail,
187 None,
188 ),
189 CommitExtractionOutcome::MissingFile(detail) => (
190 AttemptOutcome::ExtractionFailed(detail.clone()),
191 detail,
192 None,
193 ),
194 };
195 attempt_log.add_extraction_attempt(match &extraction_result {
196 Some(_) => ExtractionAttempt::success("XML", detail.clone()),
197 None => ExtractionAttempt::failure("XML", detail.clone()),
198 });
199 attempt_log.set_outcome(outcome.clone());
200
201 if !session.is_noop() {
202 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
203 let final_outcome = format!("{outcome}");
204 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
205 }
206
207 if let Some(result) = extraction_result {
208 let message = result.into_message();
209 return Ok(CommitAttemptResult {
210 had_error,
211 output_valid: true,
212 message: Some(message),
213 validation_detail: detail,
214 auth_failure: false,
215 });
216 }
217
218 Ok(CommitAttemptResult {
219 had_error,
220 output_valid: false,
221 message: None,
222 validation_detail: detail,
223 auth_failure: false,
224 })
225}
226
227pub fn generate_commit_message(
251 diff: &str,
252 registry: &AgentRegistry,
253 runtime: &mut PipelineRuntime,
254 commit_agent: &str,
255 template_context: &TemplateContext,
256 workspace: &dyn Workspace,
257 prompt_history: &HashMap<String, String>,
258) -> anyhow::Result<CommitMessageResult> {
259 let model_budget = model_budget_bytes_for_agent_name(commit_agent);
262 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
263 if truncated {
264 runtime.logger.warn(&format!(
265 "Diff size ({} KB) exceeds agent limit ({} KB). Truncated to {} KB.",
266 diff.len() / 1024,
267 model_budget / 1024,
268 model_safe_diff.len() / 1024
269 ));
270 }
271
272 let prompt_key = "commit_message_attempt_1";
273 let (prompt, was_replayed) = build_commit_prompt(
274 prompt_key,
275 template_context,
276 &model_safe_diff,
277 workspace,
278 prompt_history,
279 );
280
281 let mut generated_prompts = HashMap::new();
282 if !was_replayed {
283 generated_prompts.insert(prompt_key.to_string(), prompt.clone());
284 }
285
286 let agent_config = registry
287 .resolve_config(commit_agent)
288 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
289 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
290
291 let log_prefix = ".agent/logs/commit_generation/commit_generation";
292 let model_index = 0usize;
293 let attempt = 1u32;
294 let agent_for_log = commit_agent.to_lowercase();
295 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
296 log_prefix,
297 &agent_for_log,
298 model_index,
299 attempt,
300 );
301 let prompt_cmd = PromptCommand {
302 label: commit_agent,
303 display_name: commit_agent,
304 cmd_str: &cmd_str,
305 prompt: &prompt,
306 log_prefix,
307 model_index: Some(model_index),
308 attempt: Some(attempt),
309 logfile: &logfile,
310 parser_type: agent_config.json_parser,
311 env_vars: &agent_config.env_vars,
312 };
313
314 let result = run_with_prompt(&prompt_cmd, runtime)?;
315 let had_error = result.exit_code != 0;
316 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
317 if auth_failure {
318 anyhow::bail!("Authentication error detected");
319 }
320
321 let extraction = extract_commit_message_from_file_with_workspace(workspace);
322 let result = match extraction {
323 CommitExtractionOutcome::Valid(result) => result,
324 CommitExtractionOutcome::InvalidXml(detail)
325 | CommitExtractionOutcome::MissingFile(detail) => anyhow::bail!(detail),
326 };
327
328 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
329
330 Ok(CommitMessageResult {
331 message: result.into_message(),
332 success: true,
333 _log_path: String::new(),
334 generated_prompts,
335 })
336}
337
338pub fn generate_commit_message_with_chain(
357 diff: &str,
358 registry: &AgentRegistry,
359 runtime: &mut PipelineRuntime,
360 agents: &[String],
361 template_context: &TemplateContext,
362 workspace: &dyn Workspace,
363 prompt_history: &HashMap<String, String>,
364) -> anyhow::Result<CommitMessageResult> {
365 if agents.is_empty() {
366 anyhow::bail!("No agents provided in commit chain");
367 }
368
369 let model_budget = effective_model_budget_bytes(agents);
371 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
372 if truncated {
373 runtime.logger.warn(&format!(
374 "Diff size ({} KB) exceeds chain limit ({} KB). Truncated to {} KB.",
375 diff.len() / 1024,
376 model_budget / 1024,
377 model_safe_diff.len() / 1024
378 ));
379 }
380
381 let mut last_error: Option<anyhow::Error> = None;
382 let mut generated_prompts = HashMap::new();
383
384 for (agent_index, commit_agent) in agents.iter().enumerate() {
385 let prompt_key = format!("commit_message_chain_attempt_{}", agent_index + 1);
386 let (prompt, was_replayed) = build_commit_prompt(
387 &prompt_key,
388 template_context,
389 &model_safe_diff,
390 workspace,
391 prompt_history,
392 );
393
394 if !was_replayed {
395 generated_prompts.insert(prompt_key.clone(), prompt.clone());
396 }
397
398 let agent_config = match registry.resolve_config(commit_agent) {
399 Some(config) => config,
400 None => {
401 last_error = Some(anyhow::anyhow!("Agent not found: {}", commit_agent));
402 continue;
403 }
404 };
405 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
406
407 let log_prefix = ".agent/logs/commit_generation/commit_generation";
408 let model_index = agent_index;
409 let attempt = 1u32;
410 let agent_for_log = commit_agent.to_lowercase();
411 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
412 log_prefix,
413 &agent_for_log,
414 model_index,
415 attempt,
416 );
417 let prompt_cmd = PromptCommand {
418 label: commit_agent,
419 display_name: commit_agent,
420 cmd_str: &cmd_str,
421 prompt: &prompt,
422 log_prefix,
423 model_index: Some(model_index),
424 attempt: Some(attempt),
425 logfile: &logfile,
426 parser_type: agent_config.json_parser,
427 env_vars: &agent_config.env_vars,
428 };
429
430 let result = match run_with_prompt(&prompt_cmd, runtime) {
431 Ok(r) => r,
432 Err(e) => {
433 last_error = Some(e.into());
434 continue;
435 }
436 };
437
438 let had_error = result.exit_code != 0;
439 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
440
441 if auth_failure {
442 last_error = Some(anyhow::anyhow!("Authentication error detected"));
443 continue;
444 }
445
446 if had_error {
447 last_error = Some(anyhow::anyhow!(
448 "Agent {} failed with exit code {}",
449 commit_agent,
450 result.exit_code
451 ));
452 continue;
453 }
454
455 let extraction = extract_commit_message_from_file_with_workspace(workspace);
456 match extraction {
457 CommitExtractionOutcome::Valid(extracted) => {
458 archive_xml_file_with_workspace(
459 workspace,
460 Path::new(xml_paths::COMMIT_MESSAGE_XML),
461 );
462 return Ok(CommitMessageResult {
463 message: extracted.into_message(),
464 success: true,
465 _log_path: String::new(),
466 generated_prompts,
467 });
468 }
469 CommitExtractionOutcome::InvalidXml(detail)
470 | CommitExtractionOutcome::MissingFile(detail) => {
471 last_error = Some(anyhow::anyhow!(detail));
472 continue;
473 }
474 }
475 }
476
477 Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All agents in commit chain failed")))
479}