use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SectionBoundaryMode {
BracketOnly,
BracketOrMarkdown,
}
pub(crate) fn find_prompt_section_bounds(
prompt: &str,
section_header: &str,
boundary_mode: SectionBoundaryMode,
) -> Option<(usize, usize)> {
fn is_section_header_line(line: &str, boundary_mode: SectionBoundaryMode) -> bool {
let trimmed = line.trim();
(trimmed.starts_with('[') && trimmed.ends_with(']'))
|| matches!(boundary_mode, SectionBoundaryMode::BracketOrMarkdown)
&& (trimmed.starts_with("# ") || trimmed.starts_with("## "))
}
let mut offset = 0usize;
let mut section_start = None;
for line in prompt.split_inclusive('\n') {
let trimmed = line.trim();
if section_start.is_none() && trimmed == section_header {
section_start = Some(offset);
offset += line.len();
continue;
}
if let Some(start) = section_start
&& is_section_header_line(line, boundary_mode)
{
return Some((start, offset));
}
offset += line.len();
}
section_start.map(|start| (start, prompt.len()))
}
pub(crate) fn append_prompt_block(prompt: &mut String, block: &str) {
if block.is_empty() {
return;
}
if prompt.is_empty() {
prompt.push_str(block);
} else {
let _ = write!(prompt, "\n\n{block}");
}
}
pub(crate) fn remove_prompt_section(prompt: &mut String, section_header: &str, boundary_mode: SectionBoundaryMode) {
while let Some((section_start, section_end)) = find_prompt_section_bounds(prompt, section_header, boundary_mode) {
prompt.replace_range(section_start..section_end, "");
}
}
pub(crate) fn trim_trailing_newlines(prompt: &mut String) {
while prompt.ends_with('\n') {
prompt.pop();
}
}
#[cfg(test)]
mod tests {
use super::{SectionBoundaryMode, append_prompt_block, find_prompt_section_bounds, remove_prompt_section};
#[test]
fn markdown_boundary_mode_stops_before_next_markdown_heading() {
let prompt = "Base\n## Active Tools\n- a\n## Environment\n- b\n";
let bounds = find_prompt_section_bounds(prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown)
.expect("section bounds");
assert_eq!(&prompt[bounds.0..bounds.1], "## Active Tools\n- a\n");
}
#[test]
fn bracket_only_mode_ignores_markdown_headings() {
let prompt = "Base\n[Harness Limits]\n- a\n# Planning\n- b\n## Environment\n- c\n[Next Section]\n- d\n";
let bounds = find_prompt_section_bounds(prompt, "[Harness Limits]", SectionBoundaryMode::BracketOnly)
.expect("section bounds");
assert_eq!(&prompt[bounds.0..bounds.1], "[Harness Limits]\n- a\n# Planning\n- b\n## Environment\n- c\n");
}
#[test]
fn active_tools_boundary_preserves_following_planning_contract() {
let prompt = "Base\n## Active Tools\n- old\n# PLANNING WORKFLOW (READ-ONLY)\nKeep the plan.\n";
let (start, end) =
find_prompt_section_bounds(prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown)
.expect("section bounds");
let mut replaced = prompt.to_string();
replaced.replace_range(start..end, "## Active Tools\n- new\n");
assert_eq!(replaced, "Base\n## Active Tools\n- new\n# PLANNING WORKFLOW (READ-ONLY)\nKeep the plan.\n");
}
#[test]
fn shared_section_helpers_stay_idempotent() {
let mut prompt = String::new();
append_prompt_block(&mut prompt, "");
assert!(prompt.is_empty());
append_prompt_block(&mut prompt, "## Active Tools\n- a");
assert_eq!(prompt, "## Active Tools\n- a");
append_prompt_block(&mut prompt, "[Deferred Tools]\n- b");
assert_eq!(prompt, "## Active Tools\n- a\n\n[Deferred Tools]\n- b");
remove_prompt_section(&mut prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown);
assert_eq!(prompt, "[Deferred Tools]\n- b");
prompt.push_str("\n\n");
super::trim_trailing_newlines(&mut prompt);
assert_eq!(prompt, "[Deferred Tools]\n- b");
}
}