pub const FREE_TEXT_COMPACT_PROMPT: &str = "\
Your task is to create a detailed summary of the conversation so far for a \
coding agent. This summary will be placed back into the agent's context, so \
it must be comprehensive enough that the agent can continue working without \
loss of important context. Do not attempt to call any tools — produce text only.
Before providing your final summary, wrap your analysis in <analysis> tags. \
This analysis is scratchpad thinking and will be discarded — only the content \
between <summary> tags will be preserved.
Your summary should include the following sections:
1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections (file paths modified, full code snippets where applicable)
4. Errors and fixes
5. Problem Solving
6. All user messages (non-tool-result)
7. Pending Tasks
8. Current Work (what was being worked on immediately before this summary)
9. Optional Next Step (directly in line with the most recent request; include verbatim quotes)
Wrap the final summary in <summary>…</summary>.";
pub fn format_compact_summary(raw: &str) -> String {
let mut result = raw.to_string();
if let Some(start) = result.find("<analysis>") {
if let Some(end) = result[start..].find("</analysis>") {
let end_absolute = start + end + "</analysis>".len();
result.replace_range(start..end_absolute, "");
}
}
if let Some(start) = result.find("<summary>") {
if let Some(end) = result[start..].find("</summary>") {
let content_start = start + "<summary>".len();
let content_end = start + end;
let summary_content = result[content_start..content_end].trim().to_string();
let end_absolute = start + end + "</summary>".len();
result.replace_range(start..end_absolute, &format!("Summary:\n{summary_content}"));
}
} else {
return result.trim().to_string();
}
let mut collapsed = String::with_capacity(result.len());
let mut newline_count = 0_usize;
for ch in result.chars() {
if ch == '\n' {
newline_count += 1;
if newline_count <= 2 {
collapsed.push(ch);
}
} else {
newline_count = 0;
collapsed.push(ch);
}
}
collapsed.trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prompt_contains_all_nine_sections() {
let prompt = FREE_TEXT_COMPACT_PROMPT;
let sections = [
"Primary Request and Intent",
"Key Technical Concepts",
"Files and Code Sections",
"Errors and fixes",
"Problem Solving",
"All user messages",
"Pending Tasks",
"Current Work",
"Optional Next Step",
];
for section in §ions {
assert!(
prompt.contains(section),
"prompt must contain section title: '{section}'"
);
}
}
#[test]
fn format_strips_analysis_block() {
let input = "<analysis>this is scratchpad thinking</analysis>\n\
<summary>this is the real summary</summary>";
let output = format_compact_summary(input);
assert!(
!output.contains("scratchpad thinking"),
"analysis content must be stripped"
);
assert!(
output.contains("this is the real summary"),
"summary content must be preserved"
);
}
#[test]
fn format_converts_summary_tag_to_header() {
let input = "<summary>Key decision: added tool X.</summary>";
let output = format_compact_summary(input);
assert!(
output.starts_with("Summary:"),
"output must start with 'Summary:' header, got: {output:?}"
);
assert!(
output.contains("Key decision: added tool X."),
"summary content must be present"
);
}
#[test]
fn format_preserves_content_when_no_tags() {
let input = " plain text without any tags ";
let output = format_compact_summary(input);
assert_eq!(output, "plain text without any tags");
}
#[test]
fn format_collapses_excess_newlines() {
let input = "<summary>line1\n\n\n\nline2</summary>";
let output = format_compact_summary(input);
assert!(
!output.contains("\n\n\n"),
"output must not contain 3+ consecutive newlines, got: {output:?}"
);
assert!(output.contains("line1"));
assert!(output.contains("line2"));
}
#[test]
fn format_handles_tags_embedded_in_other_text() {
let input = "Some text before.\n\
<analysis>scratchpad here</analysis>\n\
<summary>Real summary content</summary>\n\
Some text after.";
let output = format_compact_summary(input);
assert!(!output.contains("scratchpad"), "analysis must be stripped");
assert!(
output.contains("Real summary content"),
"summary content must be preserved"
);
assert!(
output.contains("Summary:"),
"must contain Summary: header, got: {output:?}"
);
assert!(output.contains("Some text before."));
assert!(output.contains("Some text after."));
}
#[test]
fn format_handles_missing_summary_tag() {
let input = " <analysis>thinking</analysis> ";
let output = format_compact_summary(input);
assert_eq!(
output, "",
"without summary tag, should return trimmed post-analysis removal"
);
}
#[test]
fn format_handles_only_summary_tag_with_no_analysis() {
let input = "<summary>Just a summary</summary>";
let output = format_compact_summary(input);
assert!(output.starts_with("Summary:"));
assert!(output.contains("Just a summary"));
}
}