use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
use crate::segment::Segment;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchoredSummarySection {
pub id: String,
pub label: String,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchoredSummary {
pub schema_version: u32,
pub sections: Vec<AnchoredSummarySection>,
pub token_estimate: usize,
pub iteration: u32,
}
impl AnchoredSummary {
pub const SCHEMA_VERSION: u32 = 1;
#[must_use]
pub fn empty() -> Self {
Self {
schema_version: Self::SCHEMA_VERSION,
sections: vec![
AnchoredSummarySection {
id: "intent".into(),
label: "Intent".into(),
content: String::new(),
},
AnchoredSummarySection {
id: "decisions".into(),
label: "Decisions".into(),
content: String::new(),
},
AnchoredSummarySection {
id: "files_touched".into(),
label: "Files touched".into(),
content: String::new(),
},
AnchoredSummarySection {
id: "pending_tasks".into(),
label: "Pending tasks".into(),
content: String::new(),
},
AnchoredSummarySection {
id: "current_state".into(),
label: "Current state".into(),
content: String::new(),
},
],
token_estimate: 0,
iteration: 0,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sections.iter().all(|s| s.content.trim().is_empty())
}
#[must_use]
pub fn to_prompt_text(&self) -> String {
let mut out = String::new();
for section in &self.sections {
if section.content.trim().is_empty() {
continue;
}
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str("## ");
out.push_str(§ion.label);
out.push('\n');
out.push_str(section.content.trim());
}
out
}
}
#[derive(Debug)]
pub enum SummarizerError {
Timeout,
Transport(String),
Parse(String),
Other(String),
}
impl fmt::Display for SummarizerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Timeout => f.write_str("summarizer timed out"),
Self::Transport(m) => write!(f, "summarizer transport: {m}"),
Self::Parse(m) => write!(f, "summarizer parse: {m}"),
Self::Other(m) => write!(f, "summarizer: {m}"),
}
}
}
impl Error for SummarizerError {}
impl SummarizerError {
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
Self::Timeout => "timeout",
Self::Transport(_) => "transport",
Self::Parse(_) => "parse",
Self::Other(_) => "other",
}
}
}
pub trait Summarizer: Send + Sync {
fn summarize(
&self,
segments: &[Segment],
existing_summary: Option<&AnchoredSummary>,
) -> Result<AnchoredSummary, SummarizerError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_summary_renders_nothing() {
let s = AnchoredSummary::empty();
assert!(s.is_empty());
assert_eq!(s.to_prompt_text(), "");
}
#[test]
fn populated_summary_renders_sections() {
let mut s = AnchoredSummary::empty();
s.sections[0].content = "Build the context compiler.".into();
s.sections[1].content = "Reuse ainl-compression for fine pruning.".into();
let text = s.to_prompt_text();
assert!(text.contains("## Intent"));
assert!(text.contains("Build the context compiler."));
assert!(text.contains("## Decisions"));
assert!(!text.contains("## Files touched")); }
#[test]
fn summarizer_error_kinds_unique() {
let kinds = [
SummarizerError::Timeout.kind(),
SummarizerError::Transport("x".into()).kind(),
SummarizerError::Parse("x".into()).kind(),
SummarizerError::Other("x".into()).kind(),
];
for i in 0..kinds.len() {
for j in (i + 1)..kinds.len() {
assert_ne!(kinds[i], kinds[j]);
}
}
}
#[test]
fn json_roundtrip() {
let mut s = AnchoredSummary::empty();
s.sections[0].content = "hello".into();
s.token_estimate = 3;
s.iteration = 1;
let j = serde_json::to_value(&s).unwrap();
let back: AnchoredSummary = serde_json::from_value(j).unwrap();
assert_eq!(s, back);
}
}