ralph_workflow/phases/commit/runner/
chain.rs1pub fn generate_commit_message(
29 diff: &str,
30 registry: &AgentRegistry,
31 runtime: &mut PipelineRuntime<'_>,
32 commit_agent: &str,
33 template_context: &TemplateContext,
34 workspace: &dyn Workspace,
35) -> anyhow::Result<CommitMessageResult> {
36 let model_budget = model_budget_bytes_for_agent_name(commit_agent);
39 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
40 if truncated {
41 runtime.logger.warn(&format!(
42 "Diff size ({} KB) exceeds agent limit ({} KB). Truncated to {} KB.",
43 diff.len() / 1024,
44 model_budget / 1024,
45 model_safe_diff.len() / 1024
46 ));
47 }
48
49 let (prompt, substitution_log) =
50 build_commit_prompt(template_context, &model_safe_diff, workspace);
51 if !substitution_log.is_complete() {
52 return Err(anyhow::anyhow!(
53 "Commit prompt has unresolved placeholders: {:?}",
54 substitution_log.unsubstituted
55 ));
56 }
57
58 let agent_config = registry
59 .resolve_config(commit_agent)
60 .ok_or_else(|| anyhow::anyhow!("Agent not found: {commit_agent}"))?;
61 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
62
63 let log_prefix = ".agent/logs/commit_generation/commit_generation";
64 let model_index = 0usize;
65 let attempt = 1u32;
66 let agent_for_log = commit_agent.to_lowercase();
67 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
68 log_prefix,
69 &agent_for_log,
70 model_index,
71 attempt,
72 );
73 let prompt_cmd = PromptCommand {
74 label: commit_agent,
75 display_name: commit_agent,
76 cmd_str: &cmd_str,
77 prompt: &prompt,
78 log_prefix,
79 model_index: Some(model_index),
80 attempt: Some(attempt),
81 logfile: &logfile,
82 parser_type: agent_config.json_parser,
83 env_vars: &agent_config.env_vars,
84 completion_output_path: Some(Path::new(xml_paths::COMMIT_MESSAGE_XML)),
85 };
86
87 let result = run_with_prompt(&prompt_cmd, runtime)?;
88 let had_error = result.exit_code != 0;
89 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
90 if auth_failure {
91 anyhow::bail!("Authentication error detected");
92 }
93
94 let extraction = extract_commit_message_from_file_with_workspace(workspace);
95 let result = match extraction {
96 CommitExtractionOutcome::Valid {
97 extracted: result,
98 files: _,
99 ..
100 } => result,
101 CommitExtractionOutcome::InvalidXml(detail)
102 | CommitExtractionOutcome::MissingFile(detail) => anyhow::bail!(detail),
103 CommitExtractionOutcome::Skipped(reason) => {
104 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
105 return Ok(CommitMessageResult {
106 outcome: CommitMessageOutcome::Skipped { reason },
107 });
108 }
109 };
110
111 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
112
113 Ok(CommitMessageResult {
114 outcome: CommitMessageOutcome::Message(result.into_message()),
115 })
116}
117
118enum TryAgentResult {
120 Success(CommitMessageResult),
121 Skip(Option<anyhow::Error>),
122}
123
124fn try_single_commit_agent(
125 agent_index: usize,
126 commit_agent: &str,
127 template_context: &TemplateContext,
128 model_safe_diff: &str,
129 registry: &AgentRegistry,
130 runtime: &mut PipelineRuntime<'_>,
131 workspace: &dyn Workspace,
132) -> TryAgentResult {
133 let (prompt, substitution_log) =
134 build_commit_prompt(template_context, model_safe_diff, workspace);
135 if !substitution_log.is_complete() {
136 return TryAgentResult::Skip(Some(anyhow::anyhow!(
137 "Commit prompt has unresolved placeholders: {:?}",
138 substitution_log.unsubstituted
139 )));
140 }
141
142 let Some(agent_config) = registry.resolve_config(commit_agent) else {
143 return TryAgentResult::Skip(Some(anyhow::anyhow!("Agent not found: {commit_agent}")));
144 };
145 let cmd_str = agent_config.build_cmd_with_model(true, true, true, None);
146
147 let log_prefix = ".agent/logs/commit_generation/commit_generation";
148 let model_index = agent_index;
149 let attempt = 1u32;
150 let agent_for_log = commit_agent.to_lowercase();
151 let logfile = crate::pipeline::logfile::build_logfile_path_with_attempt(
152 log_prefix,
153 &agent_for_log,
154 model_index,
155 attempt,
156 );
157 let prompt_cmd = PromptCommand {
158 label: commit_agent,
159 display_name: commit_agent,
160 cmd_str: &cmd_str,
161 prompt: &prompt,
162 log_prefix,
163 model_index: Some(model_index),
164 attempt: Some(attempt),
165 logfile: &logfile,
166 parser_type: agent_config.json_parser,
167 env_vars: &agent_config.env_vars,
168 completion_output_path: Some(Path::new(xml_paths::COMMIT_MESSAGE_XML)),
169 };
170
171 let result = match run_with_prompt(&prompt_cmd, runtime) {
172 Ok(r) => r,
173 Err(e) => return TryAgentResult::Skip(Some(e.into())),
174 };
175
176 let had_error = result.exit_code != 0;
177 let auth_failure = had_error && stderr_contains_auth_error(&result.stderr);
178
179 if auth_failure {
180 return TryAgentResult::Skip(Some(anyhow::anyhow!("Authentication error detected")));
181 }
182
183 if had_error
184 && !has_valid_xml_output(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML))
185 {
186 return TryAgentResult::Skip(Some(anyhow::anyhow!(
187 "Agent {} failed with exit code {}",
188 commit_agent,
189 result.exit_code
190 )));
191 }
192
193 let extraction = extract_commit_message_from_file_with_workspace(workspace);
194 match extraction {
195 CommitExtractionOutcome::Valid {
196 extracted,
197 files: _,
198 ..
199 } => {
200 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
201 TryAgentResult::Success(CommitMessageResult {
202 outcome: CommitMessageOutcome::Message(extracted.into_message()),
203 })
204 }
205 CommitExtractionOutcome::Skipped(reason) => {
206 archive_xml_file_with_workspace(workspace, Path::new(xml_paths::COMMIT_MESSAGE_XML));
207 TryAgentResult::Success(CommitMessageResult {
208 outcome: CommitMessageOutcome::Skipped { reason },
209 })
210 }
211 CommitExtractionOutcome::InvalidXml(detail)
212 | CommitExtractionOutcome::MissingFile(detail) => {
213 TryAgentResult::Skip(Some(anyhow::anyhow!(detail)))
214 }
215 }
216}
217
218pub fn generate_commit_message_with_chain(
239 diff: &str,
240 registry: &AgentRegistry,
241 runtime: &mut PipelineRuntime<'_>,
242 agents: &[String],
243 template_context: &TemplateContext,
244 workspace: &dyn Workspace,
245) -> anyhow::Result<CommitMessageResult> {
246 if agents.is_empty() {
247 anyhow::bail!("No agents provided in commit chain");
248 }
249
250 let model_budget = effective_model_budget_bytes(agents);
252 let (model_safe_diff, truncated) = truncate_diff_to_model_budget(diff, model_budget);
253 if truncated {
254 runtime.logger.warn(&format!(
255 "Diff size ({} KB) exceeds chain limit ({} KB). Truncated to {} KB.",
256 diff.len() / 1024,
257 model_budget / 1024,
258 model_safe_diff.len() / 1024
259 ));
260 }
261
262 let last_error =
263 agents
264 .iter()
265 .enumerate()
266 .try_fold(
267 None,
268 |last_err, (agent_index, commit_agent)| match try_single_commit_agent(
269 agent_index,
270 commit_agent,
271 template_context,
272 &model_safe_diff,
273 registry,
274 runtime,
275 workspace,
276 ) {
277 TryAgentResult::Success(result) => Err(result),
278 TryAgentResult::Skip(opt_err) => Ok(opt_err.or(last_err)),
279 },
280 );
281
282 match last_error {
283 Ok(last_err) => {
284 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("All agents in commit chain failed")))
285 }
286 Err(result) => Ok(result),
287 }
288}