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