ralph_workflow/phases/commit/
runner.rs1pub struct CommitMessageResult {
3 pub message: String,
5 pub success: bool,
7 pub _log_path: String,
9 pub generated_prompts: HashMap<String, String>,
11}
12
13pub struct CommitAttemptResult {
15 pub had_error: bool,
16 pub output_valid: bool,
17 pub message: Option<String>,
18 pub validation_detail: String,
19 pub auth_failure: bool,
20}
21
22pub fn run_commit_attempt(
33 ctx: &mut PhaseContext<'_>,
34 attempt: u32,
35 model_safe_diff: &str,
36 commit_agent: &str,
37) -> anyhow::Result<CommitAttemptResult> {
38 let prompt_key = format!("commit_message_attempt_{attempt}");
43 let (prompt, was_replayed) = build_commit_prompt(
44 &prompt_key,
45 ctx.template_context,
46 model_safe_diff,
47 ctx.workspace,
48 &ctx.prompt_history,
49 );
50
51 if let Err(err) = crate::prompts::validate_no_unresolved_placeholders_with_ignored_content(
54 &prompt,
55 &[model_safe_diff],
56 ) {
57 return Err(crate::prompts::TemplateVariablesInvalidError {
58 template_name: "commit_message_xml".to_string(),
59 missing_variables: Vec::new(),
60 unresolved_placeholders: err.unresolved_placeholders,
61 }
62 .into());
63 }
64
65 if !was_replayed {
66 ctx.capture_prompt(&prompt_key, &prompt);
67 }
68
69 let mut runtime = PipelineRuntime {
70 timer: ctx.timer,
71 logger: ctx.logger,
72 colors: ctx.colors,
73 config: ctx.config,
74 executor: ctx.executor,
75 executor_arc: std::sync::Arc::clone(&ctx.executor_arc),
76 workspace: ctx.workspace,
77 };
78
79 let log_dir = Path::new(".agent/logs/commit_generation");
80 let mut session = CommitLogSession::new(log_dir.to_str().unwrap(), ctx.workspace)
81 .unwrap_or_else(|_| CommitLogSession::noop());
82 let mut attempt_log = session.new_attempt(commit_agent, "single");
83 attempt_log.set_prompt_size(prompt.len());
84 let diff_was_truncated =
88 model_safe_diff.contains("[Truncated:") || model_safe_diff.contains("[truncated...]");
89 attempt_log.set_diff_info(model_safe_diff.len(), diff_was_truncated);
90
91 let agent_config = ctx
92 .registry
93 .resolve_config(commit_agent)
94 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
95 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
96
97 let log_prefix = ".agent/logs/commit_generation/commit_generation";
98 let model_index = 0usize;
99 let agent_for_log = commit_agent.to_lowercase();
100 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
101 log_prefix,
102 &agent_for_log,
103 model_index,
104 attempt,
105 );
106 let prompt_cmd = PromptCommand {
107 label: commit_agent,
108 display_name: commit_agent,
109 cmd_str: &cmd_str,
110 prompt: &prompt,
111 log_prefix,
112 model_index: Some(model_index),
113 attempt: Some(attempt),
114 logfile: &logfile,
115 parser_type: agent_config.json_parser,
116 env_vars: &agent_config.env_vars,
117 };
118
119 let result = run_with_prompt(&prompt_cmd, &mut runtime)?;
120 let had_error = result.exit_code != 0;
121 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
122 attempt_log.set_raw_output(&result.stderr);
123
124 if auth_failure {
125 attempt_log.set_outcome(AttemptOutcome::ExtractionFailed(
126 "Authentication error detected".to_string(),
127 ));
128 if !session.is_noop() {
129 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
130 let _ = session.write_summary(1, "AUTHENTICATION_FAILURE", ctx.workspace);
131 }
132 return Ok(CommitAttemptResult {
133 had_error,
134 output_valid: false,
135 message: None,
136 validation_detail: "Authentication error detected".to_string(),
137 auth_failure: true,
138 });
139 }
140
141 let extraction = extract_commit_message_from_file_with_workspace(ctx.workspace);
142 let (outcome, detail, extraction_result) = match extraction {
143 CommitExtractionOutcome::Valid(result) => (
144 AttemptOutcome::Success(result.clone().into_message()),
145 "Valid commit message extracted".to_string(),
146 Some(result),
147 ),
148 CommitExtractionOutcome::InvalidXml(detail) => (
149 AttemptOutcome::XsdValidationFailed(detail.clone()),
150 detail,
151 None,
152 ),
153 CommitExtractionOutcome::MissingFile(detail) => (
154 AttemptOutcome::ExtractionFailed(detail.clone()),
155 detail,
156 None,
157 ),
158 };
159 attempt_log.add_extraction_attempt(match &extraction_result {
160 Some(_) => ExtractionAttempt::success("XML", detail.clone()),
161 None => ExtractionAttempt::failure("XML", detail.clone()),
162 });
163 attempt_log.set_outcome(outcome.clone());
164
165 if !session.is_noop() {
166 let _ = attempt_log.write_to_workspace(session.run_dir(), ctx.workspace);
167 let final_outcome = format!("{outcome}");
168 let _ = session.write_summary(1, &final_outcome, ctx.workspace);
169 }
170
171 if let Some(result) = extraction_result {
172 let message = result.into_message();
173 return Ok(CommitAttemptResult {
174 had_error,
175 output_valid: true,
176 message: Some(message),
177 validation_detail: detail,
178 auth_failure: false,
179 });
180 }
181
182 Ok(CommitAttemptResult {
183 had_error,
184 output_valid: false,
185 message: None,
186 validation_detail: detail,
187 auth_failure: false,
188 })
189}
190
191pub fn generate_commit_message(
215 diff: &str,
216 registry: &AgentRegistry,
217 runtime: &mut PipelineRuntime,
218 commit_agent: &str,
219 template_context: &TemplateContext,
220 workspace: &dyn Workspace,
221 prompt_history: &HashMap<String, String>,
222) -> anyhow::Result<CommitMessageResult> {
223 let model_budget = model_budget_bytes_for_agent_name(commit_agent);
226 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
227 if truncated {
228 runtime.logger.warn(&format!(
229 "Diff size ({} KB) exceeds agent limit ({} KB). Truncated to {} KB.",
230 diff.len() / 1024,
231 model_budget / 1024,
232 model_safe_diff.len() / 1024
233 ));
234 }
235
236 let prompt_key = "commit_message_attempt_1";
237 let (prompt, was_replayed) = build_commit_prompt(
238 prompt_key,
239 template_context,
240 &model_safe_diff,
241 workspace,
242 prompt_history,
243 );
244
245 let mut generated_prompts = HashMap::new();
246 if !was_replayed {
247 generated_prompts.insert(prompt_key.to_string(), prompt.clone());
248 }
249
250 let agent_config = registry
251 .resolve_config(commit_agent)
252 .ok_or_else(|| anyhow::anyhow!("Agent not found: {}", commit_agent))?;
253 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
254
255 let log_prefix = ".agent/logs/commit_generation/commit_generation";
256 let model_index = 0usize;
257 let attempt = 1u32;
258 let agent_for_log = commit_agent.to_lowercase();
259 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
260 log_prefix,
261 &agent_for_log,
262 model_index,
263 attempt,
264 );
265 let prompt_cmd = PromptCommand {
266 label: commit_agent,
267 display_name: commit_agent,
268 cmd_str: &cmd_str,
269 prompt: &prompt,
270 log_prefix,
271 model_index: Some(model_index),
272 attempt: Some(attempt),
273 logfile: &logfile,
274 parser_type: agent_config.json_parser,
275 env_vars: &agent_config.env_vars,
276 };
277
278 let result = run_with_prompt(&prompt_cmd, runtime)?;
279 let had_error = result.exit_code != 0;
280 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
281 if auth_failure {
282 anyhow::bail!("Authentication error detected");
283 }
284
285 let extraction = extract_commit_message_from_file_with_workspace(workspace);
286 let result = match extraction {
287 CommitExtractionOutcome::Valid(result) => result,
288 CommitExtractionOutcome::InvalidXml(detail)
289 | CommitExtractionOutcome::MissingFile(detail) => anyhow::bail!(detail),
290 };
291
292 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
293
294 Ok(CommitMessageResult {
295 message: result.into_message(),
296 success: true,
297 _log_path: String::new(),
298 generated_prompts,
299 })
300}