use std::collections::HashSet;
use std::time::Instant;
use anyhow::{Result, ensure};
use crate::engine::model::Similarity;
pub(super) const BRIEF_MAX_LINES: usize = 120;
pub(super) const BRIEF_HEAD_LINES: usize = 24;
pub(super) const BRIEF_TAIL_LINES: usize = 48;
pub(super) const BRIEF_FORMAT_LINES: usize = 3;
pub(super) const BRIEF_RANKED_FORMAT_LINES: usize = 4;
pub(super) const BRIEF_RELEVANT_LINES: usize =
BRIEF_MAX_LINES - BRIEF_HEAD_LINES - BRIEF_TAIL_LINES - BRIEF_RANKED_FORMAT_LINES - 1;
pub(super) const TEXT_LINE_MAX_CHARS: usize = 240;
pub(super) const ASSISTANT_LINE_MAX_CHARS: usize = 200;
pub(super) const BASH_LINE_MAX_CHARS: usize = 120;
pub(super) const TOOL_CALLS_PER_TURN: usize = 8;
pub(super) const GOAL_LIMIT: usize = 8;
pub(super) const COMMIT_LIMIT: usize = 8;
pub(super) const COMMIT_SUBJECT_MAX_CHARS: usize = 120;
pub(super) const PREFERENCE_LINE_MAX_CHARS: usize = 180;
pub(super) const OUTSTANDING_LINE_MAX_CHARS: usize = 150;
pub(super) const DIRECTIVE_PROMPT_MAX_CHARS: usize = 80;
pub(super) const DIRECTIVE_SEMANTIC_SIMILARITY: f32 = 0.84;
pub(super) const DIRECTIVE_CONTENT_SIMILARITY: f32 = 0.80;
pub(super) const PREFERENCE_LIMIT: usize = 15;
pub(super) const SUMMARY_BRIEF_HEAD_LINES: usize = 4;
pub(super) const SUMMARY_BRIEF_TAIL_LINES: usize = 8;
pub(super) const SECTION_GOAL: &str = "## Goal";
pub(super) const SECTION_CONSTRAINTS: &str = "## Constraints & Preferences";
pub(super) const SECTION_PROGRESS: &str = "## Progress";
pub(super) const SECTION_DONE: &str = "### Done";
pub(super) const SECTION_IN_PROGRESS: &str = "### In Progress";
pub(super) const SECTION_CRITICAL_CONTEXT: &str = "## Critical Context";
pub(super) const TAG_READ_FILES: &str = "<read-files>";
pub(super) const TAG_MODIFIED_FILES: &str = "<modified-files>";
#[derive(Debug, Default)]
pub(super) struct ExtractedContext {
pub(super) goals: Vec<String>,
pub(super) file_activity: FileActivity,
pub(super) commits: Vec<String>,
pub(super) outstanding: Vec<String>,
pub(super) preferences: Vec<String>,
pub(super) brief: String,
}
#[derive(Debug, Default)]
pub(super) struct OrderedPaths {
pub(super) values: Vec<String>,
pub(super) seen: HashSet<String>,
}
impl OrderedPaths {
pub(super) fn contains(&self, path: &str) -> bool {
self.seen.contains(path)
}
pub(super) fn insert(&mut self, path: String) {
if self.seen.insert(path.clone()) {
self.values.push(path);
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &String> {
self.values.iter()
}
pub(super) fn map_paths(&mut self, prefix: &str) {
let mut next = Self::default();
for path in &self.values {
next.insert(path.strip_prefix(prefix).unwrap_or(path).to_string());
}
*self = next;
}
}
#[derive(Debug, Default)]
pub(super) struct FileActivity {
pub(super) read: OrderedPaths,
pub(super) modified: OrderedPaths,
pub(super) created: OrderedPaths,
}
#[derive(Debug)]
pub(super) struct BriefSection {
pub(super) header: &'static str,
pub(super) lines: Vec<String>,
}
pub struct Summary {
pub goals: Vec<String>,
pub read: Vec<String>,
pub modified: Vec<String>,
pub created: Vec<String>,
pub commits: Vec<String>,
pub outstanding: Vec<String>,
pub preferences: Vec<String>,
pub brief: String,
pub tokens_before: u64,
}
#[derive(Debug, Default)]
pub(super) struct SummaryRelevance {
pub(super) read: Vec<Similarity>,
pub(super) modified: Vec<Similarity>,
pub(super) created: Vec<Similarity>,
pub(super) brief: Vec<Similarity>,
}
impl Summary {
pub(super) fn split_relevance(&self, scores: Vec<Similarity>) -> Result<SummaryRelevance> {
let lengths = [
self.read.len(),
self.modified.len(),
self.created.len(),
self.brief.lines().count(),
];
let expected = lengths.into_iter().try_fold(0_usize, |total, length| {
total
.checked_add(length)
.ok_or(CompactError::RelevanceCountOverflow)
})?;
ensure!(
scores.len() == expected,
"summary item and relevance counts differ"
);
let mut scores = scores.into_iter();
Ok(SummaryRelevance {
read: scores.by_ref().take(lengths[0]).collect(),
modified: scores.by_ref().take(lengths[1]).collect(),
created: scores.by_ref().take(lengths[2]).collect(),
brief: scores.collect(),
})
}
}
pub(super) struct CompactProfiler {
pub(super) print: bool,
}
impl CompactProfiler {
pub(super) fn new(print: bool) -> Self {
Self { print }
}
pub(super) fn record(&mut self, name: &'static str, started: Instant) {
let milliseconds = started.elapsed().as_secs_f64() * 1000.0;
if self.print {
eprintln!("goosedump: profile: compact: {name}: {milliseconds:.1} ms");
}
}
}
pub(super) fn compact_profile_enabled() -> bool {
std::env::var_os("GOOSEDUMP_PROFILE_COMPACT").is_some()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompactError {
EmbedderUnavailable,
RelevanceCountOverflow,
RelevanceScoresEmpty,
InvalidBriefRange(&'static str),
}
impl std::fmt::Display for CompactError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmbedderUnavailable => write!(f, "BGE embedder is unavailable after loading"),
Self::RelevanceCountOverflow => write!(f, "summary relevance count overflow"),
Self::RelevanceScoresEmpty => write!(f, "summary relevance scores are empty"),
Self::InvalidBriefRange(detail) => write!(f, "{detail}"),
}
}
}
impl std::error::Error for CompactError {}