supercode-reduce 0.4.10

Optional lossless, reversible session reduction for Supercode
Documentation
//! TR-7 (T20): the injectable side-call that turns an A10 `TurnsCleared`
//! span into a short LLM-written summary paragraph, instead of leaving it as
//! the deterministic `[turns cleared]` stub — "what incumbents' compaction
//! writes, but with the original retained in the sidecar" (TR-7.md).
//!
//! **Purity boundary.** [`super::project_messages`] must stay pure and
//! I/O-free (its own doc comment: "nothing here touches the filesystem").
//! Exactly like A8's disk probe ([`super::probe_read_freshness`]), the one
//! side-call this feature ever makes lives OUTSIDE projection, in
//! [`super::prepare_cleared_turns_summary`] — called by the driving caller
//! (`Agent::build_request_messages`, or a test) BEFORE
//! `project`/`project_messages` ever runs, with the result threaded through
//! [`super::ReductionPolicy::cleared_turns_summary`] (data, not a config
//! knob — mirrors [`super::ReductionPolicy::read_freshness`]).
//!
//! **Never blocks, never fails the pass.** [`SpanSummarizer::summarize`]
//! returning `Err` (a real implementation's way of modeling a timeout, a
//! provider error, a budget exhaustion — whatever the caller wants) simply
//! means [`super::prepare_cleared_turns_summary`] returns `None`, and
//! `project_messages` falls back to the byte-identical deterministic stub —
//! SPEC.md TR-7 dev/03.
//!
//! **Off by default.** [`super::ReductionPolicy::summarize_cleared_turns`]
//! defaults to `false`; with it off, `project_messages` never even looks at
//! [`super::ReductionPolicy::cleared_turns_summary`], so the A10 stub stays
//! byte-identical to pre-TR-7 behavior (dev/01) regardless of what a caller
//! did or didn't precompute.

use crate::Result;

/// Injectable summarization side-call (SPEC.md TR-7's "explicit, budgeted,
/// injectable side-call"). A real implementation calls out to a cheap model;
/// tests inject a deterministic fake (and, for the dev/03 fault-injection
/// AC, one that always errors) — nothing in this crate's own test suite ever
/// performs a real network/model call.
pub trait SpanSummarizer {
    /// Summarize `span_text` (the rendering `render_span_text` produces)
    /// into a short paragraph. `Err` — for any reason, including a
    /// caller-modeled timeout or budget exhaustion — means the caller must
    /// fall back to the deterministic stub; this call must never block or
    /// fail the surrounding reduce pass.
    ///
    /// **Threading note for implementers.** This method is *synchronous*,
    /// and is invoked synchronously from `Agent::build_request_messages`
    /// (via [`super::prepare_cleared_turns_summary`]), which itself runs
    /// on a tokio worker thread as part of `Agent::run_loop`'s async
    /// machinery. No implementation in this crate performs real network
    /// I/O — only in-memory test stubs implement this trait today — but a
    /// FUTURE real, provider-backed implementation must NOT perform a
    /// blocking network call inline here: doing so would stall that tokio
    /// worker thread on every reduce cycle in which TR-7 fires. Such an
    /// implementation must instead run the call off-thread and block only
    /// on that (e.g. `tokio::task::block_in_place` + `Handle::block_on`,
    /// or a dedicated blocking thread/pool joined synchronously), so the
    /// surrounding async runtime is never starved by this call.
    fn summarize(&self, span_text: &str) -> Result<String>;

    /// Identifier of the model behind this summarizer (e.g.
    /// `"claude-haiku-4-5"`), recorded on [`super::SpanSummary::model_id`]
    /// for the audit trail (SPEC.md TR-7 dev/04).
    fn model_id(&self) -> &str;
}

/// The fixed, in-repo, VERSIONED summarization prompt template (SPEC.md
/// TR-7: "summarization prompt is fixed and versioned in-repo"). Bump this
/// any time [`render_prompt`]'s wording changes — the version rides the
/// audit trail ([`super::SpanSummary::prompt_version`]) precisely so a later
/// reader can tell which wording produced a given summary.
pub const PROMPT_VERSION: &str = "tr7-summary-v1";

/// Render the fixed prompt for summarizing one cleared span's rendered text
/// (see `render_span_text`). Exposed so a real [`SpanSummarizer`]
/// implementation (elsewhere — never in this crate's test-only code) sends
/// exactly the wording [`PROMPT_VERSION`] names.
pub fn render_prompt(span_text: &str) -> String {
    format!(
        "You are compacting an AI coding agent's conversation history. Write a \
         short (2-4 sentence) factual summary of the transcript span below, \
         preserving concrete facts (file names, commands, decisions, results) \
         a later turn might need to reference. Do not editorialize, and do \
         not state anything not present in the span.\n\n\
         --- BEGIN SPAN ---\n\
         {span_text}\n\
         --- END SPAN ---\n"
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_prompt_embeds_the_span_verbatim() {
        let p = render_prompt("user: hello\nassistant: hi\n");
        assert!(p.contains("user: hello"));
        assert!(p.contains("assistant: hi"));
        assert!(p.contains("BEGIN SPAN"));
        assert!(p.contains("END SPAN"));
    }
}