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(log_dir.to_str().unwrap(), ctx.workspace)
82 .unwrap_or_else(|_| CommitLogSession::noop());
83 let mut attempt_log = session.new_attempt(commit_agent, "single");
84 attempt_log.set_prompt_size(prompt.len());
85 let diff_was_truncated =
89 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
90 attempt_log.set_diff_info(model_safe_diff.len(), diff_was_truncated);
91
92 let agent_config = ctx
93 .registry
94 .resolve_config(commit_agent)
95 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
96 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
97
98 let base_log_path = ctx.run_log_context.agent_log("commit", attempt, None);
100 let log_attempt = crate::pipeline::logfile::next_simplified_logfile_attempt_index(
101 &base_log_path,
102 ctx.workspace,
103 );
104 let logfile = if log_attempt == 0 {
105 base_log_path.to_str().unwrap().to_string()
106 } else {
107 ctx.run_log_context
108 .agent_log("commit", attempt, Some(log_attempt))
109 .to_str()
110 .unwrap()
111 .to_string()
112 };
113
114 let log_header = format!(
117 "# Ralph Agent Invocation Log\n\
118 # Role: Commit\n\
119 # Agent: {}\n\
120 # Model Index: 0\n\
121 # Attempt: {}\n\
122 # Phase: CommitMessage\n\
123 # Timestamp: {}\n\n",
124 commit_agent,
125 log_attempt,
126 chrono::Utc::now().to_rfc3339()
127 );
128 ctx.workspace
129 .append_bytes(std::path::Path::new(&logfile), log_header.as_bytes())
130 .context("Failed to write agent log header - log would be incomplete without metadata")?;
131
132 let log_prefix = format!("commit_{attempt}"); let model_index = 0usize; let prompt_cmd = PromptCommand {
135 label: commit_agent,
136 display_name: commit_agent,
137 cmd_str: &cmd_str,
138 prompt: &prompt,
139 log_prefix: &log_prefix,
140 model_index: Some(model_index),
141 attempt: Some(log_attempt),
142 logfile: &logfile,
143 parser_type: agent_config.json_parser,
144 env_vars: &agent_config.env_vars,
145 };
146
147 let result = run_with_prompt(&prompt_cmd, &mut runtime)?;
148 let had_error = result.exit_code != 0;
149 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
150 attempt_log.set_raw_output(&result.stderr);
151
152 if auth_failure {
153 attempt_log.set_outcome(AttemptOutcome::ExtractionFailed(
154 "Authentication error detected".to_string(),
155 ));
156 if !session.is_noop() {
157 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
158 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
159 }
160 return Ok(CommitAttemptResult {
161 had_error,
162 output_valid: false,
163 message: None,
164 validation_detail: "Authentication error detected".to_string(),
165 auth_failure: true,
166 });
167 }
168
169 let extraction = extract_commit_message_from_file_with_workspace(ctx.workspace);
170 let (outcome, detail, extraction_result) = match extraction {
171 CommitExtractionOutcome::Valid(result) => (
172 AttemptOutcome::Success(result.clone().into_message()),
173 "Valid commit message extracted".to_string(),
174 Some(result),
175 ),
176 CommitExtractionOutcome::InvalidXml(detail) => (
177 AttemptOutcome::XsdValidationFailed(detail.clone()),
178 detail,
179 None,
180 ),
181 CommitExtractionOutcome::MissingFile(detail) => (
182 AttemptOutcome::ExtractionFailed(detail.clone()),
183 detail,
184 None,
185 ),
186 };
187 attempt_log.add_extraction_attempt(match &extraction_result {
188 Some(_) => ExtractionAttempt::success("XML", detail.clone()),
189 None => ExtractionAttempt::failure("XML", detail.clone()),
190 });
191 attempt_log.set_outcome(outcome.clone());
192
193 if !session.is_noop() {
194 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
195 let final_outcome = format!("{outcome}");
196 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
197 }
198
199 if let Some(result) = extraction_result {
200 let message = result.into_message();
201 return Ok(CommitAttemptResult {
202 had_error,
203 output_valid: true,
204 message: Some(message),
205 validation_detail: detail,
206 auth_failure: false,
207 });
208 }
209
210 Ok(CommitAttemptResult {
211 had_error,
212 output_valid: false,
213 message: None,
214 validation_detail: detail,
215 auth_failure: false,
216 })
217}
218
219pub fn generate_commit_message(
243 diff: &str,
244 registry: &AgentRegistry,
245 runtime: &mut PipelineRuntime,
246 commit_agent: &str,
247 template_context: &TemplateContext,
248 workspace: &dyn Workspace,
249 prompt_history: &HashMap<String, String>,
250) -> anyhow::Result<CommitMessageResult> {
251 let model_budget = model_budget_bytes_for_agent_name(commit_agent);
254 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
255 if truncated {
256 runtime.logger.warn(&format!(
257 "Diff size ({} KB) exceeds agent limit ({} KB). Truncated to {} KB.",
258 diff.len() / 1024,
259 model_budget / 1024,
260 model_safe_diff.len() / 1024
261 ));
262 }
263
264 let prompt_key = "commit_message_attempt_1";
265 let (prompt, was_replayed) = build_commit_prompt(
266 prompt_key,
267 template_context,
268 &model_safe_diff,
269 workspace,
270 prompt_history,
271 );
272
273 let mut generated_prompts = HashMap::new();
274 if !was_replayed {
275 generated_prompts.insert(prompt_key.to_string(), prompt.clone());
276 }
277
278 let agent_config = registry
279 .resolve_config(commit_agent)
280 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
281 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
282
283 let log_prefix = ".agent/logs/commit_generation/commit_generation";
284 let model_index = 0usize;
285 let attempt = 1u32;
286 let agent_for_log = commit_agent.to_lowercase();
287 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
288 log_prefix,
289 &agent_for_log,
290 model_index,
291 attempt,
292 );
293 let prompt_cmd = PromptCommand {
294 label: commit_agent,
295 display_name: commit_agent,
296 cmd_str: &cmd_str,
297 prompt: &prompt,
298 log_prefix,
299 model_index: Some(model_index),
300 attempt: Some(attempt),
301 logfile: &logfile,
302 parser_type: agent_config.json_parser,
303 env_vars: &agent_config.env_vars,
304 };
305
306 let result = run_with_prompt(&prompt_cmd, runtime)?;
307 let had_error = result.exit_code != 0;
308 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
309 if auth_failure {
310 anyhow::bail!("Authentication error detected");
311 }
312
313 let extraction = extract_commit_message_from_file_with_workspace(workspace);
314 let result = match extraction {
315 CommitExtractionOutcome::Valid(result) => result,
316 CommitExtractionOutcome::InvalidXml(detail)
317 | CommitExtractionOutcome::MissingFile(detail) => anyhow::bail!(detail),
318 };
319
320 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
321
322 Ok(CommitMessageResult {
323 message: result.into_message(),
324 success: true,
325 _log_path: String::new(),
326 generated_prompts,
327 })
328}
329
330pub fn generate_commit_message_with_chain(
349 diff: &str,
350 registry: &AgentRegistry,
351 runtime: &mut PipelineRuntime,
352 agents: &[String],
353 template_context: &TemplateContext,
354 workspace: &dyn Workspace,
355 prompt_history: &HashMap<String, String>,
356) -> anyhow::Result<CommitMessageResult> {
357 if agents.is_empty() {
358 anyhow::bail!("No agents provided in commit chain");
359 }
360
361 let model_budget = effective_model_budget_bytes(agents);
363 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
364 if truncated {
365 runtime.logger.warn(&format!(
366 "Diff size ({} KB) exceeds chain limit ({} KB). Truncated to {} KB.",
367 diff.len() / 1024,
368 model_budget / 1024,
369 model_safe_diff.len() / 1024
370 ));
371 }
372
373 let mut last_error: Option<anyhow::Error> = None;
374
375 for (agent_index, commit_agent) in agents.iter().enumerate() {
376 let prompt_key = format!("commit_message_chain_attempt_{}", agent_index + 1);
377 let (prompt, was_replayed) = build_commit_prompt(
378 &prompt_key,
379 template_context,
380 &model_safe_diff,
381 workspace,
382 prompt_history,
383 );
384
385 let mut generated_prompts = HashMap::new();
386 if !was_replayed {
387 generated_prompts.insert(prompt_key.clone(), prompt.clone());
388 }
389
390 let agent_config = match registry.resolve_config(commit_agent) {
391 Some(config) => config,
392 None => {
393 last_error = Some(anyhow::anyhow!("Agent not found: {}", commit_agent));
394 continue;
395 }
396 };
397 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
398
399 let log_prefix = ".agent/logs/commit_generation/commit_generation";
400 let model_index = agent_index;
401 let attempt = 1u32;
402 let agent_for_log = commit_agent.to_lowercase();
403 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
404 log_prefix,
405 &agent_for_log,
406 model_index,
407 attempt,
408 );
409 let prompt_cmd = PromptCommand {
410 label: commit_agent,
411 display_name: commit_agent,
412 cmd_str: &cmd_str,
413 prompt: &prompt,
414 log_prefix,
415 model_index: Some(model_index),
416 attempt: Some(attempt),
417 logfile: &logfile,
418 parser_type: agent_config.json_parser,
419 env_vars: &agent_config.env_vars,
420 };
421
422 let result = match run_with_prompt(&prompt_cmd, runtime) {
423 Ok(r) => r,
424 Err(e) => {
425 last_error = Some(e.into());
426 continue;
427 }
428 };
429
430 let had_error = result.exit_code != 0;
431 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
432
433 if auth_failure {
434 last_error = Some(anyhow::anyhow!("Authentication error detected"));
435 continue;
436 }
437
438 if had_error {
439 last_error = Some(anyhow::anyhow!(
440 "Agent {} failed with exit code {}",
441 commit_agent,
442 result.exit_code
443 ));
444 continue;
445 }
446
447 let extraction = extract_commit_message_from_file_with_workspace(workspace);
448 match extraction {
449 CommitExtractionOutcome::Valid(extracted) => {
450 archive_xml_file_with_workspace(
451 workspace,
452 Path::new(xml_paths::COMMIT_MESSAGE_XML),
453 );
454 return Ok(CommitMessageResult {
455 message: extracted.into_message(),
456 success: true,
457 _log_path: String::new(),
458 generated_prompts,
459 });
460 }
461 CommitExtractionOutcome::InvalidXml(detail)
462 | CommitExtractionOutcome::MissingFile(detail) => {
463 last_error = Some(anyhow::anyhow!(detail));
464 continue;
465 }
466 }
467 }
468
469 Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All agents in commit chain failed")))
471}