ralph_workflow/prompts/
resume_note.rs1use std::fmt::Write;
7
8use crate::checkpoint::execution_history::StepOutcome;
9use crate::checkpoint::restore::ResumeContext;
10use crate::checkpoint::state::PipelinePhase;
11
12fn append_phase_header(note: &mut String, context: &ResumeContext) {
13 match context.phase {
14 PipelinePhase::Development => {
15 writeln!(
16 note,
17 "Resuming DEVELOPMENT phase (iteration {} of {})",
18 context.iteration + 1,
19 context.total_iterations
20 )
21 .unwrap();
22 }
23 PipelinePhase::Review => {
24 writeln!(
25 note,
26 "Resuming REVIEW phase (pass {} of {})",
27 context.reviewer_pass + 1,
28 context.total_reviewer_passes
29 )
30 .unwrap();
31 }
32 _ => {
33 writeln!(note, "Resuming from phase: {}", context.phase_name()).unwrap();
34 }
35 }
36}
37
38fn append_resume_and_rebase_state(note: &mut String, context: &ResumeContext) {
39 if context.resume_count > 0 {
40 writeln!(
41 note,
42 "This session has been resumed {} time(s)",
43 context.resume_count
44 )
45 .unwrap();
46 }
47
48 if !matches!(
49 context.rebase_state,
50 crate::checkpoint::state::RebaseState::NotStarted
51 ) {
52 writeln!(note, "Rebase state: {:?}", context.rebase_state).unwrap();
53 }
54
55 note.push('\n');
56}
57
58fn append_modified_files_summary(
59 note: &mut String,
60 detail: &crate::checkpoint::execution_history::ModifiedFilesDetail,
61) {
62 let added_count = detail.added.as_ref().map_or(0, |v| v.len());
63 let modified_count = detail.modified.as_ref().map_or(0, |v| v.len());
64 let deleted_count = detail.deleted.as_ref().map_or(0, |v| v.len());
65 let total_files = added_count + modified_count + deleted_count;
66 if total_files == 0 {
67 return;
68 }
69
70 write!(note, " Files: {total_files} changed").unwrap();
71 if added_count > 0 {
72 write!(note, " ({added_count} added)").unwrap();
73 }
74 if modified_count > 0 {
75 write!(note, " ({modified_count} modified)").unwrap();
76 }
77 if deleted_count > 0 {
78 write!(note, " ({deleted_count} deleted)").unwrap();
79 }
80 note.push('\n');
81}
82
83fn append_issues_summary(
84 note: &mut String,
85 issues: &crate::checkpoint::execution_history::IssuesSummary,
86) {
87 if issues.found == 0 && issues.fixed == 0 {
88 return;
89 }
90
91 write!(
92 note,
93 " Issues: {} found, {} fixed",
94 issues.found, issues.fixed
95 )
96 .unwrap();
97 if let Some(ref desc) = issues.description {
98 write!(note, " ({desc})").unwrap();
99 }
100 note.push('\n');
101}
102
103fn append_recent_step(
104 note: &mut String,
105 step: &crate::checkpoint::execution_history::ExecutionStep,
106) {
107 writeln!(
108 note,
109 "- [{}] {} (iteration {}): {}",
110 step.step_type,
111 step.phase,
112 step.iteration,
113 step.outcome.brief_description()
114 )
115 .unwrap();
116
117 if let Some(ref detail) = step.modified_files_detail {
118 append_modified_files_summary(note, detail);
119 }
120
121 if let Some(ref issues) = step.issues_summary {
122 append_issues_summary(note, issues);
123 }
124
125 if let Some(ref oid) = step.git_commit_oid {
126 writeln!(note, " Commit: {oid}").unwrap();
127 }
128}
129
130fn append_recent_activity(note: &mut String, context: &ResumeContext) {
131 let Some(ref history) = context.execution_history else {
132 return;
133 };
134 if history.steps.is_empty() {
135 return;
136 }
137
138 note.push_str("RECENT ACTIVITY:\n");
139 note.push_str("----------------\n");
140
141 let recent_steps: Vec<_> = history
142 .steps
143 .iter()
144 .rev()
145 .take(5)
146 .collect::<Vec<_>>()
147 .into_iter()
148 .rev()
149 .collect();
150
151 for step in &recent_steps {
152 append_recent_step(note, step);
153 }
154
155 note.push('\n');
156}
157
158fn append_guidance(note: &mut String, phase: PipelinePhase) {
159 note.push_str("\nGUIDANCE:\n");
160 note.push_str("--------\n");
161 match phase {
162 PipelinePhase::Development => {
163 note.push_str("Continue working on the implementation tasks from your plan.\n");
164 }
165 PipelinePhase::Review => {
166 note.push_str("Review the code changes and provide feedback.\n");
167 }
168 _ => {}
169 }
170 note.push('\n');
171}
172
173#[must_use]
184pub fn generate_resume_note(context: &ResumeContext) -> String {
185 let mut note = String::from("SESSION RESUME CONTEXT\n");
186 note.push_str("====================\n\n");
187
188 append_phase_header(&mut note, context);
189 append_resume_and_rebase_state(&mut note, context);
190 append_recent_activity(&mut note, context);
191
192 note.push_str("Previous progress is preserved in git history.\n");
193 append_guidance(&mut note, context.phase);
194 note
195}
196
197pub trait BriefDescription {
199 fn brief_description(&self) -> String;
200}
201
202const PARTIAL_FIELD_MAX_CHARS: usize = 120;
203
204fn one_line_truncated(input: &str, max_chars: usize) -> String {
205 let first_line = input.lines().next().unwrap_or("").trim();
206 let mut out: String = first_line.chars().take(max_chars).collect();
207 if first_line.chars().count() > max_chars {
208 out.push_str("...(truncated)");
209 }
210 out
211}
212
213impl BriefDescription for StepOutcome {
214 fn brief_description(&self) -> String {
215 match self {
216 Self::Success {
217 files_modified,
218 output,
219 ..
220 } => output
221 .as_ref()
222 .and_then(|out| {
223 if out.is_empty() {
224 None
225 } else {
226 Some(format!("Success - {}", out.lines().next().unwrap_or("")))
227 }
228 })
229 .or_else(|| {
230 files_modified.as_ref().and_then(|files| {
231 if files.is_empty() {
232 None
233 } else {
234 Some(format!("Success - {} files modified", files.len()))
235 }
236 })
237 })
238 .unwrap_or_else(|| "Success".to_string()),
239 Self::Failure {
240 error, recoverable, ..
241 } => {
242 if *recoverable {
243 format!("Recoverable error - {}", error.lines().next().unwrap_or(""))
244 } else {
245 format!("Failed - {}", error.lines().next().unwrap_or(""))
246 }
247 }
248 Self::Partial {
249 completed,
250 remaining,
251 ..
252 } => {
253 let completed = one_line_truncated(completed, PARTIAL_FIELD_MAX_CHARS);
254 let remaining = one_line_truncated(remaining, PARTIAL_FIELD_MAX_CHARS);
255 format!("Partial - {completed} done, {remaining}")
256 }
257 Self::Skipped { reason } => {
258 format!("Skipped - {reason}")
259 }
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::BriefDescription;
267 use crate::checkpoint::execution_history::StepOutcome;
268
269 #[test]
270 fn test_partial_brief_description_is_single_line_and_truncated() {
271 let outcome =
272 StepOutcome::partial("done line 1\ndone line 2".to_string(), "x".repeat(1000));
273
274 let desc = outcome.brief_description();
275 assert!(
276 !desc.contains('\n'),
277 "description must be single-line: {desc}"
278 );
279 assert!(
280 desc.contains("truncated"),
281 "expected truncation marker for oversized fields: {desc}"
282 );
283 assert!(desc.len() < 300, "expected bounded output size: {desc}");
284 }
285}