use std::collections::{BTreeSet, HashSet};
use std::ops::Range;
use anyhow::{Result, ensure};
use crate::engine::display;
use crate::engine::message::{BashOutput, ConversationMessage, MessageView, ToolCall};
use crate::engine::model::Embedder;
use crate::engine::text;
use super::types::{
ASSISTANT_LINE_MAX_CHARS, BASH_LINE_MAX_CHARS, BRIEF_FORMAT_LINES, BRIEF_HEAD_LINES,
BRIEF_MAX_LINES, BRIEF_RELEVANT_LINES, BRIEF_TAIL_LINES, BriefSection, CompactError,
TEXT_LINE_MAX_CHARS, TOOL_CALLS_PER_TURN,
};
use super::util::{
clean_sentence, entry_ref, is_compact_summary, is_compaction_record, strip_entry_ref,
};
pub(super) fn conversation_brief(messages: &[ConversationMessage]) -> String {
let mut sections: Vec<BriefSection> = Vec::new();
for message in messages {
if is_compaction_record(message) {
continue;
}
match message.view() {
MessageView::Text { role, text } => {
if is_compact_summary(&text) {
continue;
}
if text.trim().is_empty() {
continue;
}
let header = match role {
"user" => "[user]",
"assistant" => "[assistant]",
_ => continue,
};
push_brief(
&mut sections,
header,
format!(
"{} ({})",
clean_sentence(&text, brief_limit(header)),
entry_ref(&message.entry_id)
),
);
}
MessageView::Assistant {
text, tool_calls, ..
} => {
if is_compact_summary(&text) {
continue;
}
if !text.trim().is_empty() {
push_brief(
&mut sections,
"[assistant]",
format!(
"{} ({})",
clean_sentence(&strip_self_talk(&text), ASSISTANT_LINE_MAX_CHARS),
entry_ref(&message.entry_id)
),
);
}
push_tool_calls(&mut sections, &tool_calls, &message.entry_id);
}
MessageView::Bash(output) => {
let cmd = compress_bash(output);
if !cmd.is_empty() {
push_brief(
&mut sections,
"[user]",
format!("$ {} ({})", cmd, entry_ref(&message.entry_id)),
);
}
}
MessageView::ToolResult(_) => {}
}
}
lexical_dedup_sections(&mut sections);
stringify_brief(&mut sections)
}
pub(super) fn lexical_dedup_sections(sections: &mut [BriefSection]) {
let mut seen: HashSet<String> = HashSet::new();
for section in sections.iter_mut() {
section.lines.retain(|line| {
if line.starts_with("* ") {
return true;
}
let normalized = strip_entry_ref(line).to_lowercase();
normalized.trim().is_empty() || seen.insert(normalized)
});
}
}
pub(super) fn push_tool_calls(
sections: &mut Vec<BriefSection>,
tool_calls: &[&ToolCall],
entry_id: &str,
) {
let visible_tool_calls: Vec<&ToolCall> = tool_calls
.iter()
.copied()
.filter(|tool_call| is_visible_tool_call(tool_call))
.collect();
if visible_tool_calls.is_empty() {
return;
}
let omitted = visible_tool_calls.len().saturating_sub(TOOL_CALLS_PER_TURN);
if omitted > 0 {
push_brief(
sections,
"[assistant]",
format!("* ({omitted} earlier tool-call entries omitted)"),
);
}
for tool_call in visible_tool_calls.into_iter().skip(omitted) {
push_brief(
sections,
"[assistant]",
format!("{} ({})", tool_one_liner(tool_call), entry_ref(entry_id)),
);
}
}
pub(super) fn is_visible_tool_call(tool_call: &ToolCall) -> bool {
let name = tool_call.name.trim();
!name.is_empty() && !is_internal_tool_name(name)
}
pub(super) fn is_internal_tool_name(name: &str) -> bool {
[
"todo",
"todowrite",
"task",
"task_status",
"progress",
"update_plan",
]
.iter()
.any(|internal| name.eq_ignore_ascii_case(internal))
}
pub(super) fn push_brief(sections: &mut Vec<BriefSection>, header: &'static str, line: String) {
if line.trim().is_empty() {
return;
}
if sections.last().is_some_and(|last| last.header == header) {
if let Some(last) = sections.last_mut() {
last.lines.push(line);
}
return;
}
sections.push(BriefSection {
header,
lines: vec![line],
});
}
pub(super) fn stringify_brief(sections: &mut [BriefSection]) -> String {
collapse_repeated_tool_lines(sections);
cap_tool_lines(sections);
let mut out = Vec::new();
for (idx, section) in sections.iter().enumerate() {
if idx > 0 {
let previous = §ions[idx - 1];
let previous_tools = previous.lines.iter().all(|line| line.starts_with("* "));
let current_tools = section.lines.iter().all(|line| line.starts_with("* "));
if !(previous.header == "[assistant]"
&& section.header == "[assistant]"
&& previous_tools
&& current_tools)
{
out.push(String::new());
}
}
out.push(section.header.to_string());
out.extend(section.lines.iter().cloned());
}
out.join("\n")
}
pub(super) fn collapse_repeated_tool_lines(sections: &mut [BriefSection]) {
let mut seen: HashSet<String> = HashSet::new();
for section in sections {
if section.header != "[assistant]" {
continue;
}
section
.lines
.retain(|line| match tool_line_signature(line) {
Some(signature) => seen.insert(signature.to_string()),
None => true,
});
}
}
pub(super) fn tool_line_signature(line: &str) -> Option<&str> {
if !line.starts_with("* ") {
return None;
}
line.rsplit_once(" (#").map(|(signature, _)| signature)
}
pub(super) fn cap_tool_lines(sections: &mut [BriefSection]) {
for section in sections {
if section.header != "[assistant]" {
continue;
}
let tool_indexes: Vec<usize> = section
.lines
.iter()
.enumerate()
.filter_map(|(idx, line)| line.starts_with("* ").then_some(idx))
.collect();
if tool_indexes.len() <= TOOL_CALLS_PER_TURN {
continue;
}
let drop_count = tool_indexes.len() - TOOL_CALLS_PER_TURN;
let drop_set: BTreeSet<usize> = tool_indexes.iter().take(drop_count).copied().collect();
let first_kept = tool_indexes[drop_count];
let mut next = Vec::new();
let mut inserted = false;
for (idx, line) in section.lines.iter().enumerate() {
if drop_set.contains(&idx) {
continue;
}
if !inserted && idx == first_kept {
next.push(format!(
"* ({drop_count} earlier tool-call entries omitted)"
));
inserted = true;
}
next.push(line.clone());
}
section.lines = next;
}
}
pub(super) struct BriefCandidates<'a> {
pub(super) lines: &'a [&'a str],
pub(super) ranges: Vec<Range<usize>>,
}
impl<'a> BriefCandidates<'a> {
pub(super) fn new(lines: &'a [&'a str], tail_starts_section: bool) -> Self {
let starts = lines
.iter()
.enumerate()
.filter_map(|(index, line)| is_brief_header(line).then_some(index))
.collect::<Vec<_>>();
let mut ranges = Vec::new();
if let Some(&first) = starts.first()
&& first > 0
{
ranges.push(0..first);
}
ranges.extend(
starts
.windows(2)
.filter_map(|window| window.first().zip(window.get(1)))
.map(|(&start, &end)| start..end),
);
if tail_starts_section && let Some(&start) = starts.last() {
ranges.push(start..lines.len());
}
Self { lines, ranges }
}
pub(super) fn select(
&self,
references: &[&str],
embedder: &Embedder,
) -> Result<BriefSelection> {
let candidates = self
.ranges
.iter()
.map(|range| {
self.lines
.get(range.clone())
.map(|lines| lines.join("\n"))
.ok_or(CompactError::InvalidBriefRange(
"brief candidate range is invalid",
))
.map_err(Into::into)
})
.collect::<Result<Vec<_>>>()?;
let candidate_refs = candidates.iter().map(String::as_str).collect::<Vec<_>>();
let scores = embedder.relevance(references, &candidate_refs)?;
ensure!(
scores.len() == self.ranges.len(),
"brief relevance score count differs"
);
let mut ranked = scores.into_iter().enumerate().collect::<Vec<_>>();
ranked.sort_by(|(left_index, left_score), (right_index, right_score)| {
right_score
.cmp(left_score)
.then_with(|| left_index.cmp(right_index))
});
let mut selection = BriefSelection::default();
for (index, _) in ranked {
let range = self
.ranges
.get(index)
.ok_or(CompactError::InvalidBriefRange(
"brief candidate index is invalid",
))?;
let chunk_lines = range.end.saturating_sub(range.start);
if chunk_lines <= BRIEF_RELEVANT_LINES.saturating_sub(selection.line_count) {
selection.indexes.insert(index);
selection.line_count += chunk_lines;
}
}
Ok(selection)
}
pub(super) fn selected_lines(&self, selection: &BriefSelection) -> Result<Vec<&'a str>> {
let mut lines = Vec::new();
for index in &selection.indexes {
let range = self
.ranges
.get(*index)
.ok_or(CompactError::InvalidBriefRange(
"selected brief index is invalid",
))?;
let chunk = self
.lines
.get(range.clone())
.ok_or(CompactError::InvalidBriefRange(
"selected brief range is invalid",
))?;
lines.extend_from_slice(chunk);
}
Ok(lines)
}
}
#[derive(Default)]
pub(super) struct BriefSelection {
pub(super) indexes: BTreeSet<usize>,
pub(super) line_count: usize,
}
pub(super) fn cap_brief_ranked(
value: &str,
references: &[&str],
embedder: &Embedder,
) -> Result<String> {
let lines: Vec<&str> = value.lines().collect();
if lines.len() <= BRIEF_MAX_LINES {
return Ok(value.to_string());
}
let first_header = lines
.iter()
.position(|line| is_brief_header(line))
.unwrap_or(0);
let effective = lines
.get(first_header..)
.ok_or(CompactError::InvalidBriefRange(
"brief header range is invalid",
))?;
if effective.len() <= BRIEF_MAX_LINES {
return Ok(effective.join("\n"));
}
let head_len = BRIEF_HEAD_LINES.min(effective.len());
let tail_start =
effective
.len()
.checked_sub(BRIEF_TAIL_LINES)
.ok_or(CompactError::InvalidBriefRange(
"brief tail range underflow",
))?;
let middle = effective
.get(head_len..tail_start)
.ok_or(CompactError::InvalidBriefRange(
"brief middle range is invalid",
))?;
let tail_first = effective
.get(tail_start)
.ok_or(CompactError::InvalidBriefRange("brief tail is empty"))?;
let candidates = BriefCandidates::new(middle, is_brief_header(tail_first));
if candidates.ranges.is_empty() {
return Ok(cap_brief(value));
}
let selection = candidates.select(references, embedder)?;
if selection.indexes.is_empty() {
return Ok(cap_brief(value));
}
let sampled = candidates.selected_lines(&selection)?.join("\n");
let tail_header = brief_tail_header(effective, head_len, tail_start);
let omitted = effective
.len()
.saturating_sub(head_len + BRIEF_TAIL_LINES + selection.line_count)
.saturating_sub(usize::from(tail_header.is_some()));
let head = effective
.get(..head_len)
.ok_or(CompactError::InvalidBriefRange(
"brief head range is invalid",
))?
.join("\n");
let tail = effective
.get(tail_start..)
.ok_or(CompactError::InvalidBriefRange(
"brief tail range is invalid",
))?
.join("\n");
let tail_header = tail_header.map_or_else(String::new, |header| format!("{header}\n"));
Ok(format!(
"{head}\n\n...({omitted} earlier lines omitted; selected relevant excerpts retained)\n\n{sampled}\n\n{tail_header}{tail}"
))
}
pub(super) fn cap_brief(value: &str) -> String {
let lines: Vec<&str> = value.lines().collect();
if lines.len() <= BRIEF_MAX_LINES {
return value.to_string();
}
let first_header = lines
.iter()
.position(|line| is_brief_header(line))
.unwrap_or(0);
let effective = &lines[first_header..];
if effective.len() <= BRIEF_MAX_LINES {
return effective.join("\n");
}
let head_len = BRIEF_HEAD_LINES.min(effective.len());
let available_tail = BRIEF_MAX_LINES - head_len - BRIEF_FORMAT_LINES;
let tentative_tail_start = effective.len() - available_tail;
let restore_header = brief_tail_header(effective, head_len, tentative_tail_start).is_some();
let tail_len = available_tail - usize::from(restore_header);
let tail_start = effective.len() - tail_len;
let tail_header = brief_tail_header(effective, head_len, tail_start);
let omitted = (tail_start - head_len).saturating_sub(usize::from(tail_header.is_some()));
let head = effective[..head_len].join("\n");
let tail = effective[tail_start..].join("\n");
let tail_header = tail_header.map_or_else(String::new, |header| format!("{header}\n"));
format!("{head}\n\n...({omitted} earlier lines omitted)\n\n{tail_header}{tail}")
}
pub(super) fn is_brief_header(line: &str) -> bool {
line.starts_with('[') && line.ends_with(']')
}
pub(super) fn brief_tail_header<'a>(
lines: &'a [&'a str],
omitted_start: usize,
tail_start: usize,
) -> Option<&'a str> {
let first = *lines.get(tail_start)?;
if is_brief_header(first) {
return None;
}
lines
.get(omitted_start..tail_start)?
.iter()
.rev()
.copied()
.find(|line| is_brief_header(line))
}
pub(super) fn tool_one_liner(tool_call: &ToolCall) -> String {
if let Some(path) = display::path_argument(&tool_call.arguments) {
return format!("* {} \"{}\"", tool_call.name, text::clip(&path, 90));
}
if let Some(obj) = tool_call.arguments.as_object() {
if (tool_call.name == "bash" || tool_call.name == "Bash")
&& let Some(command) = obj.get("command").and_then(|value| value.as_str())
{
return format!("* {} \"{}\"", tool_call.name, compress_command(command));
}
for key in ["query", "pattern", "description"] {
if let Some(value) = obj.get(key).and_then(|value| value.as_str()) {
return format!("* {} \"{}\"", tool_call.name, text::clip(value, 60));
}
}
}
format!("* {}", tool_call.name)
}
pub(super) fn compress_bash(output: &BashOutput) -> String {
compress_command(&output.command)
}
pub(super) fn compress_command(command: &str) -> String {
let mut cmd = command
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or(command)
.to_string();
if let Some(stripped) = strip_cd_prefix(&cmd) {
cmd = stripped.to_string();
}
for pipe in [
" | head",
" | tail",
" | sort",
" | wc",
" | column",
" | tr",
" | cut",
] {
if let Some(idx) = cmd.rfind(pipe) {
cmd.truncate(idx);
}
}
text::clip(&cmd, BASH_LINE_MAX_CHARS)
}
pub(super) fn strip_cd_prefix(command: &str) -> Option<&str> {
let rest = command.strip_prefix("cd ")?;
let (_, suffix) = rest.split_once(" && ")?;
Some(suffix)
}
pub(super) fn brief_limit(header: &str) -> usize {
if header == "[assistant]" {
ASSISTANT_LINE_MAX_CHARS
} else {
TEXT_LINE_MAX_CHARS
}
}
pub(super) fn strip_self_talk(value: &str) -> String {
let mut text_value = value.trim().to_string();
for _ in 0..2 {
let lower = text_value.to_ascii_lowercase();
let Some(prefix) = ["hmm", "wait", "actually", "oh", "okay", "ok", "well", "so"]
.iter()
.find(|prefix| lower.starts_with(**prefix))
else {
break;
};
let rest = text_value[prefix.len()..].trim_start_matches([',', '.', '!', ' ', '-']);
if rest == text_value {
break;
}
text_value = rest.to_string();
}
text_value
}