1use std::collections::HashMap;
8use std::fmt;
9
10pub use crate::prompts::io::extract_metadata;
11pub use crate::prompts::io::extract_partials;
12pub use crate::prompts::io::extract_variables;
13pub use crate::prompts::io::validate_syntax;
14pub use crate::prompts::template_registry::TemplateError;
15pub use crate::prompts::template_validator::{
16 RenderedTemplate, SubstitutionEntry, SubstitutionLog, SubstitutionSource,
17};
18
19pub struct Template {
21 pub content: String,
22}
23
24impl Template {
25 #[must_use]
26 pub fn new(content: &str) -> Self {
27 Self {
28 content: content.to_string(),
29 }
30 }
31}
32
33impl fmt::Debug for Template {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.debug_struct("Template")
36 .field(
37 "content",
38 &self.content.chars().take(50).collect::<String>(),
39 )
40 .finish()
41 }
42}
43
44struct LiteralSegment {
49 token: String,
50 content: String,
51}
52
53struct PartialExpandState<'a> {
54 result: &'a mut String,
55 literal_segments: &'a mut Vec<LiteralSegment>,
56 log: &'a mut SubstitutionLog,
57}
58
59struct LoopRenderLog {
60 token: String,
61 substituted: Vec<SubstitutionEntry>,
62 unsubstituted: Vec<String>,
63}
64
65fn parse_loop_header(full_match: &str) -> Option<(&str, &str)> {
66 crate::prompts::template_parsing::parse_loop_header_impl(full_match)
67}
68
69fn split_loop_items(values: &str) -> Vec<&str> {
70 crate::prompts::template_parsing::split_loop_items_impl(values)
71}
72
73fn render_loop_item(
74 body: &str,
75 item: &str,
76 var_name: &str,
77 variables: &HashMap<&str, String>,
78) -> String {
79 let after_vars = variables
81 .iter()
82 .fold(body.to_string(), |content, (key, val)| {
83 content.replace(&format!("{{{{{}}}}}", key), val)
84 });
85 after_vars.replace(&format!("{{{}}}", var_name), item)
86}
87
88fn find_unsubstituted_vars(item_content: &str, variables: &HashMap<&str, String>) -> Vec<String> {
89 extract_variables(item_content)
90 .iter()
91 .filter(|v| {
92 !variables.contains_key(v.name.as_str())
93 && !item_content.contains(&format!("{{{}}}", v.name))
94 })
95 .map(|v| v.name.clone())
96 .collect()
97}
98
99fn eval_conditional(condition: &str, variables: &HashMap<&str, String>) -> bool {
100 crate::prompts::template_parsing::eval_conditional_impl(condition, variables)
101}
102
103struct LoopMatchResult {
104 full_match: String,
105 var_name: String,
106 body: String,
107}
108
109fn try_parse_loop_for_tag(result: &str, for_start: usize) -> Option<LoopMatchResult> {
110 let for_end = result[for_start..].find("%}")?;
111 let for_end = for_start + for_end;
112 let full_match = result[for_start..for_end + 2].to_string();
113 let full_match_clone = full_match.clone();
114 let (header, body) = parse_loop_header(&full_match_clone)?;
115 let var_name = header.split_whitespace().next()?.to_string();
116 Some(LoopMatchResult {
117 full_match,
118 var_name,
119 body: body.to_string(),
120 })
121}
122
123fn render_loop_items(
124 body: &str,
125 var_name: &str,
126 values: &str,
127 variables: &HashMap<&str, String>,
128) -> (Vec<String>, Vec<String>) {
129 let items = split_loop_items(values);
130 let (rendered_items, unsubstituted_blocks): (Vec<String>, Vec<Vec<String>>) = items
131 .iter()
132 .map(|item| {
133 let item_content = render_loop_item(body, item, var_name, variables);
134 let unsubstituted = find_unsubstituted_vars(&item_content, variables);
135 (item_content, unsubstituted)
136 })
137 .unzip();
138 let unsubstituted = unsubstituted_blocks.into_iter().flatten().collect();
139 (rendered_items, unsubstituted)
140}
141
142fn find_conditional_block(result: &str, if_start: usize) -> Option<(usize, usize, String)> {
143 let tag_end_offset = result[if_start..].find("%}")?;
144 let tag_close = if_start + tag_end_offset + 2;
145 let rest_from_if = &result[if_start..];
146 let endif_offset = rest_from_if.find("{% endif %}")?;
147 let endif_abs = if_start + endif_offset;
148 let full_end = endif_abs + 11;
149 let full_match = result[if_start..full_end].to_string();
150 Some((tag_close, endif_abs, full_match))
151}
152
153fn eval_conditional_body(
154 condition: &str,
155 body: &str,
156 else_body: Option<&str>,
157 variables: &HashMap<&str, String>,
158) -> String {
159 if eval_conditional(condition, variables) {
160 body.trim().to_string()
161 } else {
162 else_body.map(str::trim).unwrap_or("").to_string()
163 }
164}
165
166fn eval_conditional_replacement(
167 condition: &str,
168 body_and_maybe_else: &str,
169 variables: &HashMap<&str, String>,
170) -> String {
171 if let Some(else_offset) = body_and_maybe_else.find("{% else %}") {
172 eval_conditional_body(
173 condition,
174 &body_and_maybe_else[..else_offset],
175 Some(&body_and_maybe_else[else_offset + 10..]),
176 variables,
177 )
178 } else {
179 eval_conditional_body(condition, body_and_maybe_else, None, variables)
180 }
181}
182
183fn process_one_conditional(result: &str, variables: &HashMap<&str, String>) -> Option<String> {
184 let if_start = result.find("{% if ")?;
185 let tag = {
186 let tag_end_offset = result[if_start..].find("%}")?;
187 let tag_close = if_start + tag_end_offset + 2;
188 result[if_start..tag_close].to_string()
189 };
190 let cond_end = tag.find("%}")?;
191 let condition = tag[6..cond_end].trim().to_string();
192 let (tag_close, endif_abs, full_match) = find_conditional_block(result, if_start)?;
193 let body_and_maybe_else = result[tag_close..endif_abs].to_string();
194 let replacement = eval_conditional_replacement(&condition, &body_and_maybe_else, variables);
195 Some(result.replacen(&full_match, &replacement, 1))
196}
197
198struct VarSubResult {
199 new_result: String,
200 entry: Option<SubstitutionEntry>,
201 unsubstituted: Option<String>,
202}
203
204fn determine_substitution_source(value: &str, has_default: bool) -> SubstitutionSource {
205 if value.is_empty() {
206 SubstitutionSource::EmptyWithDefault
207 } else if has_default {
208 SubstitutionSource::Default
209 } else {
210 SubstitutionSource::Value
211 }
212}
213
214fn sub_result_with_value(
215 result: &str,
216 token: &str,
217 name: &str,
218 value: &str,
219 source: SubstitutionSource,
220) -> VarSubResult {
221 VarSubResult {
222 new_result: result.replace(token, value),
223 entry: Some(SubstitutionEntry {
224 name: name.to_string(),
225 source,
226 }),
227 unsubstituted: None,
228 }
229}
230
231fn sub_result_with_default(
232 result: &str,
233 token: &str,
234 name: &str,
235 default_val: String,
236) -> VarSubResult {
237 VarSubResult {
238 new_result: result.replace(token, &default_val),
239 entry: Some(SubstitutionEntry {
240 name: name.to_string(),
241 source: SubstitutionSource::Default,
242 }),
243 unsubstituted: None,
244 }
245}
246
247fn sub_result_unresolved(result: &str, name: &str) -> VarSubResult {
248 VarSubResult {
249 new_result: result.to_string(),
250 entry: None,
251 unsubstituted: Some(name.to_string()),
252 }
253}
254
255fn sub_result_unresolved_none(result: &str) -> VarSubResult {
256 VarSubResult {
257 new_result: result.to_string(),
258 entry: None,
259 unsubstituted: None,
260 }
261}
262
263fn resolve_variable_substitution(
264 result: &str,
265 var: &crate::prompts::template_validator::VariableInfo,
266 variables: &HashMap<&str, String>,
267 token: &str,
268) -> VarSubResult {
269 if let Some(value) = variables.get(var.name.as_str()) {
270 let source = determine_substitution_source(value, var.has_default);
271 sub_result_with_value(result, token, &var.name, value, source)
272 } else if var.has_default {
273 let default_val = var.default_value.clone().unwrap_or_default();
274 sub_result_with_default(result, token, &var.name, default_val)
275 } else {
276 sub_result_unresolved(result, &var.name)
277 }
278}
279
280fn find_variable_token(result: &str, placeholder: &str, name: &str) -> Option<String> {
281 let raw_token = format!("{{{{{}}}}}", placeholder);
282 let clean_token = format!("{{{{{}}}}}", name);
283 if result.contains(&raw_token) {
284 Some(raw_token)
285 } else if result.contains(&clean_token) {
286 Some(clean_token)
287 } else {
288 None
289 }
290}
291
292fn substitute_one_variable(
293 result: &str,
294 var: &crate::prompts::template_validator::VariableInfo,
295 variables: &HashMap<&str, String>,
296) -> VarSubResult {
297 find_variable_token(result, &var.placeholder, &var.name).map_or_else(
298 || sub_result_unresolved_none(result),
299 |token| resolve_variable_substitution(result, var, variables, &token),
300 )
301}
302
303fn build_circular_reference_chain(visited: &[String]) -> String {
304 visited
305 .iter()
306 .rev()
307 .cloned()
308 .collect::<Vec<_>>()
309 .join(" -> ")
310}
311
312fn collect_missing_from_loop_logs(
313 loop_logs: &[LoopRenderLog],
314 result: &str,
315 unsubstituted: Vec<String>,
316) -> Vec<String> {
317 loop_logs
318 .iter()
319 .filter(|ll| result.contains(&ll.token))
320 .flat_map(|ll| ll.unsubstituted.clone())
321 .chain(unsubstituted)
322 .collect::<std::collections::HashSet<_>>()
323 .into_iter()
324 .collect()
325}
326
327fn extend_log_with_loop_logs(
328 log: &mut SubstitutionLog,
329 loop_logs: Vec<LoopRenderLog>,
330 result: &str,
331) {
332 for loop_log in loop_logs {
333 if !result.contains(&loop_log.token) {
334 continue;
335 }
336 log.substituted.extend(loop_log.substituted);
337 let new_unsub: Vec<String> = loop_log
338 .unsubstituted
339 .into_iter()
340 .filter(|name| !log.unsubstituted.contains(name))
341 .collect();
342 log.unsubstituted.extend(new_unsub);
343 }
344}
345
346fn extend_log_dedup(
347 log: &mut SubstitutionLog,
348 substituted: Vec<SubstitutionEntry>,
349 unsubstituted: Vec<String>,
350) {
351 log.substituted.extend(substituted);
352 let new_unsub: Vec<String> = unsubstituted
353 .into_iter()
354 .filter(|name| !log.unsubstituted.contains(name))
355 .collect();
356 log.unsubstituted.extend(new_unsub);
357}
358
359impl Template {
360 pub fn render(&self, variables: &HashMap<&str, String>) -> Result<String, TemplateError> {
362 let mut literal_segments = Vec::new();
363 let (result, loop_logs) =
364 Self::process_loops_with_log(&self.content, variables, &mut literal_segments);
365
366 let result = Self::process_conditionals(&result, variables);
367
368 let (result_after_sub, _substituted, unsubstituted) =
369 Self::substitute_variables_allow_empty(&result, variables);
370
371 let missing: Vec<String> = loop_logs
372 .iter()
373 .filter(|loop_log| result.contains(&loop_log.token))
374 .flat_map(|loop_log| loop_log.unsubstituted.clone())
375 .chain(unsubstituted)
376 .collect::<std::collections::HashSet<_>>()
377 .into_iter()
378 .collect();
379
380 if let Some(first_missing) = missing.first() {
381 return Err(TemplateError::MissingVariable(first_missing.clone()));
382 }
383
384 Ok(Self::restore_literal_segments(
385 &result_after_sub,
386 &literal_segments,
387 ))
388 }
389
390 pub fn render_with_partials(
392 &self,
393 variables: &HashMap<&str, String>,
394 partials: &HashMap<String, String>,
395 ) -> Result<String, TemplateError> {
396 self.render_with_partials_recursive(variables, partials, &mut Vec::new())
397 }
398
399 pub fn render_with_log(
401 &self,
402 template_name: &str,
403 variables: &HashMap<&str, String>,
404 partials: &HashMap<String, String>,
405 ) -> Result<RenderedTemplate, TemplateError> {
406 self.render_with_log_recursive(template_name, variables, partials, &mut Vec::new())
407 }
408
409 fn expand_one_partial(
410 &self,
411 partial_name: &str,
412 result: &mut String,
413 literal_segments: &mut Vec<LiteralSegment>,
414 variables: &HashMap<&str, String>,
415 partials: &HashMap<String, String>,
416 visited: &mut Vec<String>,
417 ) -> Result<(), TemplateError> {
418 if visited.contains(&partial_name.to_string()) {
419 return Err(TemplateError::CircularReference(
420 build_circular_reference_chain(visited),
421 ));
422 }
423 let partial_content = partials
424 .get(partial_name)
425 .ok_or_else(|| TemplateError::PartialNotFound(partial_name.to_string()))?;
426 let partial_template = Self::new(partial_content);
427 visited.push(partial_name.to_string());
428 let rendered =
429 partial_template.render_with_partials_recursive(variables, partials, visited)?;
430 visited.pop();
431 let full_match = format!("{{{{> {}}}}}", partial_name);
432 let token = Self::next_literal_token(result, &rendered, literal_segments);
433 literal_segments.push(LiteralSegment {
434 token: token.clone(),
435 content: rendered,
436 });
437 *result = result.replace(&full_match, &token);
438 Ok(())
439 }
440
441 fn process_rendered_content(
442 result: &str,
443 variables: &HashMap<&str, String>,
444 literal_segments: &mut Vec<LiteralSegment>,
445 ) -> Result<String, TemplateError> {
446 let (loop_result, loop_logs) =
447 Self::process_loops_with_log(result, variables, literal_segments);
448 let after_cond = Self::process_conditionals(&loop_result, variables);
449 let (result_after_sub, _substituted, unsubstituted) =
450 Self::substitute_variables_allow_empty(&after_cond, variables);
451 let missing = collect_missing_from_loop_logs(&loop_logs, &after_cond, unsubstituted);
452 if let Some(first_missing) = missing.first() {
453 return Err(TemplateError::MissingVariable(first_missing.clone()));
454 }
455 Ok(Self::restore_literal_segments(
456 &result_after_sub,
457 literal_segments,
458 ))
459 }
460
461 fn render_with_partials_recursive(
462 &self,
463 variables: &HashMap<&str, String>,
464 partials: &HashMap<String, String>,
465 visited: &mut Vec<String>,
466 ) -> Result<String, TemplateError> {
467 let mut literal_segments = Vec::new();
468 let mut result = self.content.clone();
469 for partial_name in extract_partials(&result).into_iter().rev() {
470 self.expand_one_partial(
471 &partial_name,
472 &mut result,
473 &mut literal_segments,
474 variables,
475 partials,
476 visited,
477 )?;
478 }
479 Self::process_rendered_content(&result, variables, &mut literal_segments)
480 }
481
482 fn expand_one_partial_with_log(
483 &self,
484 partial_name: &str,
485 template_name: &str,
486 state: &mut PartialExpandState<'_>,
487 variables: &HashMap<&str, String>,
488 partials: &HashMap<String, String>,
489 visited: &mut Vec<String>,
490 ) -> Result<(), TemplateError> {
491 if visited.contains(&partial_name.to_string()) {
492 return Err(TemplateError::CircularReference(
493 build_circular_reference_chain(visited),
494 ));
495 }
496 let partial_content = partials
497 .get(partial_name)
498 .ok_or_else(|| TemplateError::PartialNotFound(partial_name.to_string()))?;
499 let partial_template = Self::new(partial_content);
500 visited.push(partial_name.to_string());
501 let rendered = partial_template.render_with_log_recursive(
502 template_name,
503 variables,
504 partials,
505 visited,
506 )?;
507 visited.pop();
508 let full_match = format!("{{{{> {}}}}}", partial_name);
509 let token =
510 Self::next_literal_token(state.result, &rendered.content, state.literal_segments);
511 state.literal_segments.push(LiteralSegment {
512 token: token.clone(),
513 content: rendered.content,
514 });
515 *state.result = state.result.replace(&full_match, &token);
516 extend_log_dedup(
517 state.log,
518 rendered.log.substituted,
519 rendered.log.unsubstituted,
520 );
521 Ok(())
522 }
523
524 fn render_with_log_recursive(
525 &self,
526 template_name: &str,
527 variables: &HashMap<&str, String>,
528 partials: &HashMap<String, String>,
529 visited: &mut Vec<String>,
530 ) -> Result<RenderedTemplate, TemplateError> {
531 let mut log = SubstitutionLog {
532 template_name: template_name.to_string(),
533 substituted: Vec::new(),
534 unsubstituted: Vec::new(),
535 };
536 let mut literal_segments = Vec::new();
537 let mut result = self.content.clone();
538 for partial_name in extract_partials(&result).into_iter().rev() {
539 let mut state = PartialExpandState {
540 result: &mut result,
541 literal_segments: &mut literal_segments,
542 log: &mut log,
543 };
544 self.expand_one_partial_with_log(
545 &partial_name,
546 template_name,
547 &mut state,
548 variables,
549 partials,
550 visited,
551 )?;
552 }
553 let (loop_result, loop_logs) =
554 Self::process_loops_with_log(&result, variables, &mut literal_segments);
555 let result = Self::process_conditionals(&loop_result, variables);
556 extend_log_with_loop_logs(&mut log, loop_logs, &result);
557 let (result_after_sub, substituted, unsubstituted) =
558 Self::substitute_variables(&result, variables);
559 extend_log_dedup(&mut log, substituted, unsubstituted);
560 Ok(RenderedTemplate {
561 content: Self::restore_literal_segments(&result_after_sub, &literal_segments),
562 log,
563 })
564 }
565
566 fn process_one_loop(
567 result: &str,
568 variables: &HashMap<&str, String>,
569 literal_segments: &mut Vec<LiteralSegment>,
570 token_counter: &mut usize,
571 ) -> Option<(String, LoopRenderLog)> {
572 let for_start = result.find("{% for ")?;
573 let parsed = try_parse_loop_for_tag(result, for_start)?;
574 let values = variables.get(parsed.var_name.as_str())?;
575 let (rendered_items, unsubstituted) =
576 render_loop_items(&parsed.body, &parsed.var_name, values, variables);
577 let loop_token = format!("__LOOP_TOKEN_{}__", token_counter);
578 *token_counter += 1;
579 literal_segments.push(LiteralSegment {
580 token: loop_token.clone(),
581 content: rendered_items.join("\n"),
582 });
583 let log = LoopRenderLog {
584 token: loop_token.clone(),
585 substituted: vec![SubstitutionEntry {
586 name: parsed.var_name.clone(),
587 source: crate::prompts::template_validator::SubstitutionSource::Value,
588 }],
589 unsubstituted,
590 };
591 Some((result.replace(&parsed.full_match, &loop_token), log))
592 }
593
594 fn process_loops_with_log(
595 content: &str,
596 variables: &HashMap<&str, String>,
597 literal_segments: &mut Vec<LiteralSegment>,
598 ) -> (String, Vec<LoopRenderLog>) {
599 let mut result = content.to_string();
600 let mut loop_logs = Vec::new();
601 let mut token_counter = 0;
602 while let Some((new_result, log)) =
603 Self::process_one_loop(&result, variables, literal_segments, &mut token_counter)
604 {
605 result = new_result;
606 loop_logs.push(log);
607 }
608 (result, loop_logs)
609 }
610
611 fn process_conditionals(content: &str, variables: &HashMap<&str, String>) -> String {
612 let mut result = content.to_string();
613 while let Some(new_result) = process_one_conditional(&result, variables) {
614 result = new_result;
615 }
616 result
617 }
618
619 fn substitute_variables_allow_empty(
620 content: &str,
621 variables: &HashMap<&str, String>,
622 ) -> (String, Vec<SubstitutionEntry>, Vec<String>) {
623 let vars = extract_variables(content);
624 vars.iter().fold(
625 (content.to_string(), Vec::new(), Vec::new()),
626 |(result, mut substituted, mut unsubstituted), var| {
627 let sub = substitute_one_variable(&result, var, variables);
628 if let Some(entry) = sub.entry {
629 substituted.push(entry);
630 }
631 if let Some(name) = sub.unsubstituted {
632 unsubstituted.push(name);
633 }
634 (sub.new_result, substituted, unsubstituted)
635 },
636 )
637 }
638
639 fn substitute_variables(
640 content: &str,
641 variables: &HashMap<&str, String>,
642 ) -> (String, Vec<SubstitutionEntry>, Vec<String>) {
643 Self::substitute_variables_allow_empty(content, variables)
644 }
645
646 fn next_literal_token(
647 content: &str,
648 replacement: &str,
649 literal_segments: &[LiteralSegment],
650 ) -> String {
651 let mut token = format!("__LITERAL_{}__", literal_segments.len());
652 while content.contains(&token) || literal_segments.iter().any(|s| s.token == token) {
653 token = format!(
654 "__LITERAL_{}__{}",
655 literal_segments.len(),
656 replacement.len()
657 );
658 }
659 token
660 }
661
662 fn restore_literal_segments(content: &str, literal_segments: &[LiteralSegment]) -> String {
663 let mut result = content.to_string();
664 for segment in literal_segments {
665 result = result.replace(&segment.token, &segment.content);
666 }
667 result
668 }
669}
670
671use crate::checkpoint::execution_history::StepOutcome;
676use crate::checkpoint::restore::ResumeContext;
677use crate::checkpoint::state::PipelinePhase;
678use std::fmt::Write as FmtWrite;
679
680fn format_resume_state(resume_count: u32, rebase_state: &str) -> String {
681 crate::prompts::template_parsing::format_resume_state_impl(resume_count, rebase_state)
682}
683
684fn format_modified_files_summary(
685 detail: &crate::checkpoint::execution_history::ModifiedFilesDetail,
686) -> String {
687 crate::prompts::template_parsing::format_files_summary_impl(detail).unwrap_or_default()
688}
689
690fn format_issues_summary(issues: &crate::checkpoint::execution_history::IssuesSummary) -> String {
691 crate::prompts::template_parsing::format_issues_summary_impl(issues).unwrap_or_default()
692}
693
694fn optional_files_summary(
695 detail: &Option<crate::checkpoint::execution_history::ModifiedFilesDetail>,
696) -> String {
697 detail
698 .as_ref()
699 .map_or(String::new(), format_modified_files_summary)
700}
701
702fn optional_issues_summary(
703 issues: &Option<crate::checkpoint::execution_history::IssuesSummary>,
704) -> String {
705 issues.as_ref().map_or(String::new(), format_issues_summary)
706}
707
708fn optional_commit_line(oid: &Option<String>) -> String {
709 oid.as_deref()
710 .map_or(String::new(), |o| format!(" Commit: {o}\n"))
711}
712
713fn format_recent_step(step: &crate::checkpoint::execution_history::ExecutionStep) -> String {
714 format!(
715 "- [{}] {} (iteration {}): {}\n{}{}{}",
716 step.step_type,
717 step.phase,
718 step.iteration,
719 step.outcome.brief_description(),
720 optional_files_summary(&step.modified_files_detail),
721 optional_issues_summary(&step.issues_summary),
722 optional_commit_line(&step.git_commit_oid),
723 )
724}
725
726fn format_recent_activity(
727 history: &crate::checkpoint::execution_history::ExecutionHistory,
728) -> String {
729 let recent_steps: Vec<_> = history
730 .steps
731 .iter()
732 .rev()
733 .take(5)
734 .collect::<Vec<_>>()
735 .into_iter()
736 .rev()
737 .collect();
738
739 let steps_str: String = recent_steps.iter().map(|s| format_recent_step(s)).collect();
740 format!("RECENT ACTIVITY:\n----------------\n{steps_str}\n")
741}
742
743fn append_resume_and_rebase_state(note: &mut String, context: &ResumeContext) {
744 let rebase_str = format!("{:?}", context.rebase_state);
745 let formatted = format_resume_state(context.resume_count, &rebase_str);
746 note.push_str(&formatted);
747 note.push('\n');
748}
749
750fn append_recent_activity(note: &mut String, context: &ResumeContext) {
751 let Some(ref history) = context.execution_history else {
752 return;
753 };
754 if history.steps.is_empty() {
755 return;
756 }
757 note.push_str(&format_recent_activity(history));
758 note.push('\n');
759}
760
761fn append_guidance(note: &mut String, phase: PipelinePhase) {
762 note.push_str("\nGUIDANCE:\n");
763 note.push_str("--------\n");
764 match phase {
765 PipelinePhase::Development => {
766 note.push_str("Continue working on the implementation tasks from your plan.\n");
767 }
768 PipelinePhase::Review => {
769 note.push_str("Review the code changes and provide feedback.\n");
770 }
771 _ => {}
772 }
773 note.push('\n');
774}
775
776fn append_phase_header(note: &mut String, context: &ResumeContext) {
777 match context.phase {
778 PipelinePhase::Development => {
779 let _ = writeln!(
780 note,
781 "Resuming DEVELOPMENT phase (iteration {} of {})",
782 context.iteration + 1,
783 context.total_iterations
784 );
785 }
786 PipelinePhase::Review => {
787 let _ = writeln!(
788 note,
789 "Resuming REVIEW phase (pass {} of {})",
790 context.reviewer_pass + 1,
791 context.total_reviewer_passes
792 );
793 }
794 _ => {
795 let _ = writeln!(note, "Resuming from phase: {}", context.phase_name());
796 }
797 }
798}
799
800#[must_use]
811pub fn generate_resume_note(context: &ResumeContext) -> String {
812 let mut note = String::from("SESSION RESUME CONTEXT\n");
813 note.push_str("====================\n\n");
814 append_phase_header(&mut note, context);
815 append_resume_and_rebase_state(&mut note, context);
816 append_recent_activity(&mut note, context);
817 note.push_str("Previous progress is preserved in git history.\n");
818 append_guidance(&mut note, context.phase);
819 note
820}
821
822fn brief_description_success(
823 files_modified: &Option<Box<[String]>>,
824 output: &Option<Box<str>>,
825) -> String {
826 use crate::prompts::template_parsing::OutcomeDescription;
827 let files: Option<Vec<String>> = files_modified.as_ref().map(|b| b.to_vec());
828 let output_str: Option<String> = output.as_ref().map(|b| b.to_string());
829 OutcomeDescription::from_outcome(&files, &output_str, &None, &None, &None, &None, &None)
830 .as_string()
831}
832
833fn brief_description_failure(error: &str, recoverable: bool) -> String {
834 use crate::prompts::template_parsing::OutcomeDescription;
835 let error_str: Option<String> = Some(error.to_string());
836 let recoverable_val = Some(recoverable);
837 let desc = OutcomeDescription::from_outcome(
838 &None,
839 &None,
840 &error_str,
841 &recoverable_val,
842 &None,
843 &None,
844 &None,
845 );
846 desc.failure_recoverable
847 .or(desc.failure_fatal)
848 .unwrap_or_default()
849}
850
851fn brief_description_partial(completed: &str, remaining: &str) -> String {
852 use crate::prompts::template_parsing::OutcomeDescription;
853 let completed_str: Option<String> = Some(completed.to_string());
854 let remaining_str: Option<String> = Some(remaining.to_string());
855 OutcomeDescription::from_outcome(
856 &None,
857 &None,
858 &None,
859 &None,
860 &completed_str,
861 &remaining_str,
862 &None,
863 )
864 .partial
865 .unwrap_or_default()
866}
867
868fn brief_description_skipped(reason: &str) -> String {
869 use crate::prompts::template_parsing::OutcomeDescription;
870 let reason_str: Option<String> = Some(reason.to_string());
871 OutcomeDescription::from_outcome(&None, &None, &None, &None, &None, &None, &reason_str)
872 .skipped
873 .unwrap_or_default()
874}
875
876pub trait BriefDescription {
878 fn brief_description(&self) -> String;
879}
880
881impl BriefDescription for StepOutcome {
882 fn brief_description(&self) -> String {
883 match self {
884 Self::Success {
885 files_modified,
886 output,
887 ..
888 } => brief_description_success(files_modified, output),
889 Self::Failure {
890 error, recoverable, ..
891 } => brief_description_failure(error, *recoverable),
892 Self::Partial {
893 completed,
894 remaining,
895 ..
896 } => brief_description_partial(completed, remaining),
897 Self::Skipped { reason } => brief_description_skipped(reason),
898 }
899 }
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905
906 #[test]
907 fn test_render_loop_item_substitutes_variables_and_item() {
908 let body = "Name: {item}, Age: {{age}}, City: {{city}}";
909 let item = "Alice";
910 let var_name = "item";
911 let mut variables = HashMap::new();
912 variables.insert("age", "30".to_string());
913 variables.insert("city", "NYC".to_string());
914
915 let result = render_loop_item(body, item, var_name, &variables);
916
917 assert_eq!(result, "Name: Alice, Age: 30, City: NYC");
918 }
919
920 #[test]
921 fn test_render_loop_item_no_variables() {
922 let body = "Item: {item}";
923 let item = "value";
924 let var_name = "item";
925 let variables = HashMap::new();
926
927 let result = render_loop_item(body, item, var_name, &variables);
928
929 assert_eq!(result, "Item: value");
930 }
931
932 #[test]
933 fn test_render_loop_item_no_item_placeholder() {
934 let body = "Age: {{age}}";
935 let item = "unused";
936 let var_name = "item";
937 let mut variables = HashMap::new();
938 variables.insert("age", "25".to_string());
939
940 let result = render_loop_item(body, item, var_name, &variables);
941
942 assert_eq!(result, "Age: 25");
943 }
944}