goosedump 0.12.43

Browse, search, compact, and learn from coding-agent sessions
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

//! Compaction summary types and constants.

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>,
}

/// The extracted compaction summary, exposed field-by-field so the per-client
/// emitters can map it into each client's own summary text and record shape.
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,
    /// Estimated token count of the messages the summary replaces, for
    /// clients whose compaction records carry one (e.g. Pi's `tokensBefore`).
    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 {}