recursive/compact/
prompt.rs1pub const FREE_TEXT_COMPACT_PROMPT: &str = "\
12Your task is to create a detailed summary of the conversation so far for a \
13coding agent. This summary will be placed back into the agent's context, so \
14it must be comprehensive enough that the agent can continue working without \
15loss of important context. Do not attempt to call any tools — produce text only.
16
17Before providing your final summary, wrap your analysis in <analysis> tags. \
18This analysis is scratchpad thinking and will be discarded — only the content \
19between <summary> tags will be preserved.
20
21Your summary should include the following sections:
221. Primary Request and Intent
232. Key Technical Concepts
243. Files and Code Sections (file paths modified, full code snippets where applicable)
254. Errors and fixes
265. Problem Solving
276. All user messages (non-tool-result)
287. Pending Tasks
298. Current Work (what was being worked on immediately before this summary)
309. Optional Next Step (directly in line with the most recent request; include verbatim quotes)
31
32Wrap the final summary in <summary>…</summary>.";
33
34pub fn format_compact_summary(raw: &str) -> String {
47 let mut result = raw.to_string();
48
49 if let Some(start) = result.find("<analysis>") {
51 if let Some(end) = result[start..].find("</analysis>") {
52 let end_absolute = start + end + "</analysis>".len();
53 result.replace_range(start..end_absolute, "");
54 }
55 }
56
57 if let Some(start) = result.find("<summary>") {
59 if let Some(end) = result[start..].find("</summary>") {
60 let content_start = start + "<summary>".len();
61 let content_end = start + end;
62 let summary_content = result[content_start..content_end].trim().to_string();
63 let end_absolute = start + end + "</summary>".len();
64 result.replace_range(start..end_absolute, &format!("Summary:\n{summary_content}"));
65 }
66 } else {
67 return result.trim().to_string();
69 }
70
71 let mut collapsed = String::with_capacity(result.len());
74 let mut newline_count = 0_usize;
75 for ch in result.chars() {
76 if ch == '\n' {
77 newline_count += 1;
78 if newline_count <= 2 {
79 collapsed.push(ch);
80 }
81 } else {
82 newline_count = 0;
83 collapsed.push(ch);
84 }
85 }
86
87 collapsed.trim().to_string()
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn prompt_contains_all_nine_sections() {
97 let prompt = FREE_TEXT_COMPACT_PROMPT;
98 let sections = [
99 "Primary Request and Intent",
100 "Key Technical Concepts",
101 "Files and Code Sections",
102 "Errors and fixes",
103 "Problem Solving",
104 "All user messages",
105 "Pending Tasks",
106 "Current Work",
107 "Optional Next Step",
108 ];
109 for section in §ions {
110 assert!(
111 prompt.contains(section),
112 "prompt must contain section title: '{section}'"
113 );
114 }
115 }
116
117 #[test]
118 fn format_strips_analysis_block() {
119 let input = "<analysis>this is scratchpad thinking</analysis>\n\
120 <summary>this is the real summary</summary>";
121 let output = format_compact_summary(input);
122 assert!(
123 !output.contains("scratchpad thinking"),
124 "analysis content must be stripped"
125 );
126 assert!(
127 output.contains("this is the real summary"),
128 "summary content must be preserved"
129 );
130 }
131
132 #[test]
133 fn format_converts_summary_tag_to_header() {
134 let input = "<summary>Key decision: added tool X.</summary>";
135 let output = format_compact_summary(input);
136 assert!(
137 output.starts_with("Summary:"),
138 "output must start with 'Summary:' header, got: {output:?}"
139 );
140 assert!(
141 output.contains("Key decision: added tool X."),
142 "summary content must be present"
143 );
144 }
145
146 #[test]
147 fn format_preserves_content_when_no_tags() {
148 let input = " plain text without any tags ";
149 let output = format_compact_summary(input);
150 assert_eq!(output, "plain text without any tags");
151 }
152
153 #[test]
154 fn format_collapses_excess_newlines() {
155 let input = "<summary>line1\n\n\n\nline2</summary>";
157 let output = format_compact_summary(input);
158 assert!(
162 !output.contains("\n\n\n"),
163 "output must not contain 3+ consecutive newlines, got: {output:?}"
164 );
165 assert!(output.contains("line1"));
166 assert!(output.contains("line2"));
167 }
168
169 #[test]
170 fn format_handles_tags_embedded_in_other_text() {
171 let input = "Some text before.\n\
172 <analysis>scratchpad here</analysis>\n\
173 <summary>Real summary content</summary>\n\
174 Some text after.";
175 let output = format_compact_summary(input);
176 assert!(!output.contains("scratchpad"), "analysis must be stripped");
177 assert!(
178 output.contains("Real summary content"),
179 "summary content must be preserved"
180 );
181 assert!(
182 output.contains("Summary:"),
183 "must contain Summary: header, got: {output:?}"
184 );
185 assert!(output.contains("Some text before."));
187 assert!(output.contains("Some text after."));
188 }
189
190 #[test]
191 fn format_handles_missing_summary_tag() {
192 let input = " <analysis>thinking</analysis> ";
194 let output = format_compact_summary(input);
195 assert_eq!(
200 output, "",
201 "without summary tag, should return trimmed post-analysis removal"
202 );
203 }
204
205 #[test]
206 fn format_handles_only_summary_tag_with_no_analysis() {
207 let input = "<summary>Just a summary</summary>";
208 let output = format_compact_summary(input);
209 assert!(output.starts_with("Summary:"));
210 assert!(output.contains("Just a summary"));
211 }
212}