ainl_context_compiler/
summarizer.rs1use serde::{Deserialize, Serialize};
13use std::error::Error;
14use std::fmt;
15
16use crate::segment::Segment;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct AnchoredSummarySection {
21 pub id: String,
23 pub label: String,
25 pub content: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct AnchoredSummary {
36 pub schema_version: u32,
38 pub sections: Vec<AnchoredSummarySection>,
40 pub token_estimate: usize,
42 pub iteration: u32,
44}
45
46impl AnchoredSummary {
47 pub const SCHEMA_VERSION: u32 = 1;
49
50 #[must_use]
52 pub fn empty() -> Self {
53 Self {
54 schema_version: Self::SCHEMA_VERSION,
55 sections: vec![
56 AnchoredSummarySection {
57 id: "intent".into(),
58 label: "Intent".into(),
59 content: String::new(),
60 },
61 AnchoredSummarySection {
62 id: "decisions".into(),
63 label: "Decisions".into(),
64 content: String::new(),
65 },
66 AnchoredSummarySection {
67 id: "files_touched".into(),
68 label: "Files touched".into(),
69 content: String::new(),
70 },
71 AnchoredSummarySection {
72 id: "pending_tasks".into(),
73 label: "Pending tasks".into(),
74 content: String::new(),
75 },
76 AnchoredSummarySection {
77 id: "current_state".into(),
78 label: "Current state".into(),
79 content: String::new(),
80 },
81 ],
82 token_estimate: 0,
83 iteration: 0,
84 }
85 }
86
87 #[must_use]
89 pub fn is_empty(&self) -> bool {
90 self.sections.iter().all(|s| s.content.trim().is_empty())
91 }
92
93 #[must_use]
95 pub fn to_prompt_text(&self) -> String {
96 let mut out = String::new();
97 for section in &self.sections {
98 if section.content.trim().is_empty() {
99 continue;
100 }
101 if !out.is_empty() {
102 out.push_str("\n\n");
103 }
104 out.push_str("## ");
105 out.push_str(§ion.label);
106 out.push('\n');
107 out.push_str(section.content.trim());
108 }
109 out
110 }
111}
112
113#[derive(Debug)]
115pub enum SummarizerError {
116 Timeout,
118 Transport(String),
120 Parse(String),
122 Other(String),
124}
125
126impl fmt::Display for SummarizerError {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::Timeout => f.write_str("summarizer timed out"),
130 Self::Transport(m) => write!(f, "summarizer transport: {m}"),
131 Self::Parse(m) => write!(f, "summarizer parse: {m}"),
132 Self::Other(m) => write!(f, "summarizer: {m}"),
133 }
134 }
135}
136
137impl Error for SummarizerError {}
138
139impl SummarizerError {
140 #[must_use]
142 pub fn kind(&self) -> &'static str {
143 match self {
144 Self::Timeout => "timeout",
145 Self::Transport(_) => "transport",
146 Self::Parse(_) => "parse",
147 Self::Other(_) => "other",
148 }
149 }
150}
151
152pub trait Summarizer: Send + Sync {
160 fn summarize(
168 &self,
169 segments: &[Segment],
170 existing_summary: Option<&AnchoredSummary>,
171 ) -> Result<AnchoredSummary, SummarizerError>;
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn empty_summary_renders_nothing() {
180 let s = AnchoredSummary::empty();
181 assert!(s.is_empty());
182 assert_eq!(s.to_prompt_text(), "");
183 }
184
185 #[test]
186 fn populated_summary_renders_sections() {
187 let mut s = AnchoredSummary::empty();
188 s.sections[0].content = "Build the context compiler.".into();
189 s.sections[1].content = "Reuse ainl-compression for fine pruning.".into();
190 let text = s.to_prompt_text();
191 assert!(text.contains("## Intent"));
192 assert!(text.contains("Build the context compiler."));
193 assert!(text.contains("## Decisions"));
194 assert!(!text.contains("## Files touched")); }
196
197 #[test]
198 fn summarizer_error_kinds_unique() {
199 let kinds = [
200 SummarizerError::Timeout.kind(),
201 SummarizerError::Transport("x".into()).kind(),
202 SummarizerError::Parse("x".into()).kind(),
203 SummarizerError::Other("x".into()).kind(),
204 ];
205 for i in 0..kinds.len() {
206 for j in (i + 1)..kinds.len() {
207 assert_ne!(kinds[i], kinds[j]);
208 }
209 }
210 }
211
212 #[test]
213 fn json_roundtrip() {
214 let mut s = AnchoredSummary::empty();
215 s.sections[0].content = "hello".into();
216 s.token_estimate = 3;
217 s.iteration = 1;
218 let j = serde_json::to_value(&s).unwrap();
219 let back: AnchoredSummary = serde_json::from_value(j).unwrap();
220 assert_eq!(s, back);
221 }
222}