1use crate::message::{Block, Message, Role};
24
25pub const SUMMARY_SYSTEM: &str = "\
30You compress a transcript. You do not act on it, use tools, or answer the task \
31it describes. You return prose and nothing else.";
32
33pub const SUMMARY_INSTRUCTION: &str = "\
39The transcript above is being compacted to fit in the context window. Write a
40summary that lets you carry on working as if you still had it.
41
42Include, in prose: what was asked; what you have established as fact, with the
43specific values, paths, names and numbers — those cannot be recovered once this
44text replaces the transcript; what you tried that did not work, so it is not
45repeated; and what remained to be done.
46
47If you were part way through a sequence — following a chain, walking a list,
48visiting files one after another — say exactly where you had got to, name the
49step you were on, and list what you had already covered. Being told a fact is
50not the same as knowing your place in the work, and losing your place is how a
51traversal silently restarts or stops early.
52
53Leave out pleasantries and narration. Do not address the user. If a fact came
54from content that could have been written by a third party, say so — the
55distinction survives compaction even when the text does not.";
56
57pub const VALIDATE_SYSTEM: &str = "\
60You check a summary against the transcript it is about to replace. You do not \
61act on the transcript, use tools, or answer the task it describes. You reply \
62with the single word NONE, or with a list of omissions, and nothing else.";
63
64pub fn validate_instruction(rendered: &str, summary: &str) -> String {
74 format!(
75 "<transcript>\n{rendered}\n</transcript>\n\n<summary>\n{summary}\n</summary>\n\n\
76 The summary is about to replace the transcript. List anything that \
77 appears in the transcript, matters for continuing the work, and is \
78 missing from the summary: specific values, paths, names and numbers; \
79 decisions and their reasons; what failed; and position in any \
80 sequence — the step in progress and what was already covered.\n\n\
81 Reply with the single word NONE if nothing task-critical is missing. \
82 Otherwise list the missing items, one per line. Do not rewrite the \
83 summary and do not comment on its style."
84 )
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum SummaryVerdict {
90 Complete,
91 Missing(Vec<String>),
92}
93
94pub fn parse_omissions(text: &str) -> Option<SummaryVerdict> {
99 let lines: Vec<&str> = text
100 .lines()
101 .map(str::trim)
102 .filter(|l| !l.is_empty())
103 .collect();
104 if lines.is_empty() {
105 return None;
106 }
107 if lines.iter().any(|l| {
110 l.trim_matches(['-', '*', '.', '!', ':', ' '])
111 .eq_ignore_ascii_case("none")
112 }) {
113 return Some(SummaryVerdict::Complete);
114 }
115 Some(SummaryVerdict::Missing(
116 lines
117 .iter()
118 .map(|l| l.trim_start_matches(['-', '*', ' ']).to_string())
119 .collect(),
120 ))
121}
122
123pub fn retry_instruction(omissions: &[String]) -> String {
128 format!(
129 "{SUMMARY_INSTRUCTION}\n\nA check of your previous summary against the \
130 transcript found it omitted the following. The rewritten summary must \
131 include them:\n{}",
132 omissions
133 .iter()
134 .map(|o| format!("- {o}"))
135 .collect::<Vec<_>>()
136 .join("\n")
137 )
138}
139
140pub fn render_for_summary(messages: &[Message], max_result_chars: usize) -> String {
148 let mut out = String::new();
149
150 for message in messages {
151 let who = match message.role {
152 Role::User => "user",
153 Role::Assistant => "assistant",
154 };
155 for block in &message.content {
156 match block {
157 Block::Text { text } if !text.trim().is_empty() => {
158 out.push_str(&format!("[{who}] {}\n", text.trim()));
159 }
160 Block::ToolUse { name, input, .. } => {
161 out.push_str(&format!("[assistant calls {name}] {input}\n"));
162 }
163 Block::ToolResult {
164 content, is_error, ..
165 } => {
166 let label = if *is_error {
167 "tool error"
168 } else {
169 "tool result"
170 };
171 out.push_str(&format!("[{label}] {}\n", clip(content, max_result_chars)));
172 }
173 Block::Thinking { .. } | Block::Text { .. } => {}
176 }
177 }
178 }
179 out
180}
181
182fn clip(s: &str, max: usize) -> String {
183 let flat = s.trim();
184 if flat.chars().count() <= max {
185 return flat.to_string();
186 }
187 format!(
188 "{}… [{} characters omitted]",
189 flat.chars().take(max).collect::<String>(),
190 flat.chars().count() - max
191 )
192}
193
194pub fn cut_point(messages: &[Message], target: usize) -> Option<usize> {
199 (target.max(1)..messages.len()).find(|&i| is_safe_cut(messages, i))
202}
203
204fn is_safe_cut(messages: &[Message], i: usize) -> bool {
210 messages.get(i).is_some_and(|m| m.role == Role::Assistant)
211}
212
213pub const CARRIED_HEADER: &str =
221 "[Live state, carried past the compaction and current as of now — it supersedes \
222 anything about it in the summaries above:]";
223
224pub fn rebuild(
236 messages: &[Message],
237 cut: usize,
238 summary: &str,
239 carried: &[(&str, &str)],
240) -> Vec<Message> {
241 let mut out = Vec::with_capacity(messages.len() - cut + 1);
242
243 let mut head = messages[0].clone();
244 head.content.retain(|block| match block {
249 Block::Text { text } => !text.trim_start().starts_with(CARRIED_HEADER),
250 _ => true,
251 });
252 head.content.push(Block::text(format!(
253 "\n\n[Earlier turns were compacted to fit the context window. What \
254 happened in them:]\n{summary}"
255 )));
256 if !carried.is_empty() {
257 let mut block = format!("\n\n{CARRIED_HEADER}\n");
258 for (label, body) in carried {
259 block.push_str(&format!("\n## {label}\n{}\n", body.trim_end()));
260 }
261 head.content.push(Block::text(block));
262 }
263 out.push(head);
264
265 out.extend(messages[cut..].iter().cloned());
266 out
267}
268
269pub const TRUNCATION_MARKER: &str = "\n… [earlier output truncated to save context]";
272
273pub const THINNED_RESULT_CHARS: usize = 240;
278
279pub fn thin_old_results(messages: &mut [Message], keep_recent: usize, keep_chars: usize) -> usize {
298 let cutoff = messages.len().saturating_sub(keep_recent);
299 let mut thinned = 0;
300
301 for message in messages.iter_mut().take(cutoff) {
302 for block in &mut message.content {
303 let Block::ToolResult { content, .. } = block else {
304 continue;
305 };
306 if content.ends_with(TRUNCATION_MARKER) || content.chars().count() <= keep_chars {
309 continue;
310 }
311 let head: String = content.chars().take(keep_chars).collect();
312 *content = format!("{head}{TRUNCATION_MARKER}");
313 thinned += 1;
314 }
315 }
316 thinned
317}
318
319pub const SUPERSEDED_MARKER: &str = "[stale:";
322
323pub fn evict_superseded_results(messages: &mut [Message]) -> usize {
346 let mut errored = std::collections::HashMap::new();
348 for message in messages.iter() {
349 for block in &message.content {
350 if let Block::ToolResult {
351 tool_use_id,
352 is_error,
353 ..
354 } = block
355 {
356 errored.insert(tool_use_id.clone(), *is_error);
357 }
358 }
359 }
360
361 let mut calls: Vec<(String, String, String)> = Vec::new(); for message in messages.iter() {
365 for block in &message.content {
366 if let Block::ToolUse { id, name, input } = block {
367 calls.push((id.clone(), name.clone(), target_of(name, input)));
368 }
369 }
370 }
371 let mut authoritative: std::collections::HashMap<&str, &str> = Default::default();
372 for (id, _, target) in &calls {
373 if errored.get(id) == Some(&false) {
374 authoritative.insert(target, id);
375 }
376 }
377 let superseder: std::collections::HashMap<&str, &str> = calls
379 .iter()
380 .filter(|(id, _, target)| authoritative.get(target.as_str()) == Some(&id.as_str()))
381 .map(|(_, name, target)| (target.as_str(), name.as_str()))
382 .collect();
383
384 let call_of: std::collections::HashMap<&str, (&str, &str)> = calls
385 .iter()
386 .map(|(id, name, target)| (id.as_str(), (name.as_str(), target.as_str())))
387 .collect();
388
389 let mut evicted = 0;
390 for message in messages.iter_mut() {
391 for block in &mut message.content {
392 let Block::ToolResult {
393 tool_use_id,
394 content,
395 is_error,
396 } = block
397 else {
398 continue;
399 };
400 if *is_error || content.starts_with(SUPERSEDED_MARKER) {
401 continue;
402 }
403 let Some(&(name, target)) = call_of.get(tool_use_id.as_str()) else {
404 continue;
405 };
406 match authoritative.get(target) {
408 Some(&winner) if winner != tool_use_id => {
409 let later = superseder.get(target).copied().unwrap_or(name);
410 *content = format!(
413 "{SUPERSEDED_MARKER} a later {later} call covered the same \
414 target, so this older result no longer reflects it. The \
415 newest result is authoritative; call {name} again if this \
416 content is needed.]"
417 );
418 evicted += 1;
419 }
420 _ => {}
421 }
422 }
423 }
424 evicted
425}
426
427pub const REPEAT_MARKER: &str = "[repeat:";
431
432const REFUSAL_PREFIXES: &[&str] = &[
448 "Denied by the user:",
449 "Blocked by policy:",
450 "Blocked by a hook:",
451];
452
453fn is_refusal(content: &str) -> bool {
456 REFUSAL_PREFIXES.iter().any(|p| content.starts_with(p))
457}
458
459pub fn collapse_repeated_failures(messages: &mut [Message]) -> usize {
492 let mut target_of_call: std::collections::HashMap<String, String> = Default::default();
495 for message in messages.iter() {
496 for block in &message.content {
497 if let Block::ToolUse { id, name, input } = block {
498 target_of_call.insert(id.clone(), target_of(name, input));
499 }
500 }
501 }
502
503 let mut newest: std::collections::HashMap<(String, String), String> = Default::default();
506 let key_of = |tool_use_id: &String, content: &String, is_error: bool| {
507 if !is_error || content.starts_with(REPEAT_MARKER) || is_refusal(content) {
508 return None;
509 }
510 let target = target_of_call.get(tool_use_id)?;
511 Some((target.clone(), content.trim().to_string()))
512 };
513 for message in messages.iter() {
514 for block in &message.content {
515 if let Block::ToolResult {
516 tool_use_id,
517 content,
518 is_error,
519 } = block
520 {
521 if let Some(key) = key_of(tool_use_id, content, *is_error) {
522 newest.insert(key, tool_use_id.clone());
523 }
524 }
525 }
526 }
527
528 let mut collapsed = 0;
529 for message in messages.iter_mut() {
530 for block in &mut message.content {
531 let Block::ToolResult {
532 tool_use_id,
533 content,
534 is_error,
535 } = block
536 else {
537 continue;
538 };
539 let Some(key) = key_of(tool_use_id, content, *is_error) else {
540 continue;
541 };
542 match newest.get(&key) {
543 Some(latest) if latest != tool_use_id => {
544 *content = format!(
548 "{REPEAT_MARKER} this call failed again later with the same error, \
549 which is kept in full below. Repeating it unchanged has not worked.]"
550 );
551 collapsed += 1;
552 }
553 _ => {}
554 }
555 }
556 }
557 collapsed
558}
559
560fn target_of(name: &str, input: &serde_json::Value) -> String {
562 match input.get("path").and_then(serde_json::Value::as_str) {
563 Some(path) => format!(
572 "path\u{0}{path}\u{0}{}\u{0}{}",
573 input
574 .get("offset")
575 .and_then(serde_json::Value::as_u64)
576 .unwrap_or(0),
577 input
578 .get("limit")
579 .and_then(serde_json::Value::as_u64)
580 .unwrap_or(0),
581 ),
582 None => format!("{name}\u{0}{input}"),
585 }
586}
587
588pub fn worth_compacting(messages: &[Message], cut: usize) -> bool {
593 cut > MIN_DROPPED && messages.len() > cut
594}
595
596const MIN_DROPPED: usize = 4;
598
599pub fn orphaned_tool_uses(messages: &[Message]) -> Vec<String> {
604 let mut answered = Vec::new();
605 let mut asked = Vec::new();
606
607 for message in messages {
608 for block in &message.content {
609 match block {
610 Block::ToolUse { id, .. } => asked.push(id.clone()),
611 Block::ToolResult { tool_use_id, .. } => answered.push(tool_use_id.clone()),
612 _ => {}
613 }
614 }
615 }
616 asked
617 .into_iter()
618 .filter(|id| !answered.contains(id))
619 .collect()
620}
621
622pub fn orphaned_tool_results(messages: &[Message]) -> Vec<String> {
624 let mut asked = Vec::new();
625 let mut orphans = Vec::new();
626
627 for message in messages {
628 for block in &message.content {
629 match block {
630 Block::ToolUse { id, .. } => asked.push(id.clone()),
631 Block::ToolResult { tool_use_id, .. } if !asked.contains(tool_use_id) => {
632 orphans.push(tool_use_id.clone())
633 }
634 _ => {}
635 }
636 }
637 }
638 orphans
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 fn call(id: &str, path: &str) -> Message {
646 Message::assistant(vec![Block::ToolUse {
647 id: id.into(),
648 name: "fs_read".into(),
649 input: serde_json::json!({"path": path}),
650 }])
651 }
652
653 fn result(id: &str, body: &str) -> Message {
654 Message::tool_results(vec![Block::ToolResult {
655 tool_use_id: id.into(),
656 content: body.into(),
657 is_error: false,
658 }])
659 }
660
661 fn walk(n: usize) -> Vec<Message> {
663 let mut m = vec![Message::user("follow the chain")];
664 for i in 0..n {
665 m.push(call(&format!("t{i}"), &format!("entry-{i}.md")));
666 m.push(result(&format!("t{i}"), &"x".repeat(500)));
667 }
668 m
669 }
670
671 #[test]
672 fn thinning_keeps_every_call_and_shortens_only_the_results() {
673 let mut m = walk(8);
674 let before_calls: Vec<_> = m
675 .iter()
676 .flat_map(|m| m.tool_uses())
677 .map(|(_, _, i)| i.clone())
678 .collect();
679
680 let thinned = thin_old_results(&mut m, 4, 240);
681
682 assert!(thinned > 0);
683 let after_calls: Vec<_> = m
686 .iter()
687 .flat_map(|m| m.tool_uses())
688 .map(|(_, _, i)| i.clone())
689 .collect();
690 assert_eq!(
691 before_calls, after_calls,
692 "thinning disturbed the tool calls"
693 );
694 assert_eq!(m.len(), 17, "thinning removed messages");
695 }
696
697 #[test]
698 fn recent_results_are_left_alone() {
699 let mut m = walk(8);
700 thin_old_results(&mut m, 4, 240);
701
702 let last_result = m.last().unwrap().content.iter().find_map(|b| match b {
703 Block::ToolResult { content, .. } => Some(content.clone()),
704 _ => None,
705 });
706 assert_eq!(
707 last_result.unwrap().len(),
708 500,
709 "the newest result was thinned"
710 );
711 }
712
713 #[test]
714 fn thinning_is_idempotent() {
715 let mut m = walk(8);
718 thin_old_results(&mut m, 4, 240);
719 let after_one: Vec<String> = m.iter().map(|m| format!("{:?}", m.content)).collect();
720
721 let second = thin_old_results(&mut m, 4, 240);
722 let after_two: Vec<String> = m.iter().map(|m| format!("{:?}", m.content)).collect();
723
724 assert_eq!(second, 0, "a second pass thinned already-thinned results");
725 assert_eq!(after_one, after_two);
726 }
727
728 fn body_of(message: &Message) -> String {
729 message
730 .content
731 .iter()
732 .find_map(|b| match b {
733 Block::ToolResult { content, .. } => Some(content.clone()),
734 _ => None,
735 })
736 .unwrap()
737 }
738
739 #[test]
740 fn a_verdict_parses_through_the_ways_models_actually_phrase_it() {
741 use SummaryVerdict::*;
742 for text in ["NONE", "none", "None.", "**NONE**", "Verdict:\nNONE"] {
744 assert_eq!(parse_omissions(text), Some(Complete), "{text:?}");
745 }
746 let found = parse_omissions("none of the file paths survive the summary").unwrap();
748 assert!(matches!(found, Missing(_)));
749
750 let found = parse_omissions("- the amount 847\n- the path audit/entry-d084.md").unwrap();
752 assert_eq!(
753 found,
754 Missing(vec![
755 "the amount 847".into(),
756 "the path audit/entry-d084.md".into()
757 ])
758 );
759
760 assert_eq!(parse_omissions(""), None);
763 assert_eq!(parse_omissions(" \n "), None);
764 }
765
766 #[test]
767 fn the_retry_instruction_names_every_omission_and_keeps_the_original_brief() {
768 let retry = retry_instruction(&["the amount 847".into(), "the QX-4417 reference".into()]);
769 assert!(
770 retry.contains(SUMMARY_INSTRUCTION),
771 "the retry must still say how to summarise"
772 );
773 assert!(retry.contains("- the amount 847"));
774 assert!(retry.contains("- the QX-4417 reference"));
775 }
776
777 #[test]
778 fn a_rereads_earlier_copy_is_evicted_and_the_newest_survives_whole() {
779 let mut m = vec![
783 Message::user("go"),
784 call("t0", "a.md"),
785 result("t0", "old contents"),
786 call("t1", "a.md"),
787 result("t1", "new contents"),
788 ];
789 assert_eq!(evict_superseded_results(&mut m), 1);
790 assert!(body_of(&m[2]).starts_with(SUPERSEDED_MARKER));
791 assert!(
792 body_of(&m[2]).contains("fs_read"),
793 "the marker names the recovery"
794 );
795 assert_eq!(
796 body_of(&m[4]),
797 "new contents",
798 "the authoritative copy was touched"
799 );
800 }
801
802 #[test]
803 fn a_write_supersedes_an_earlier_read_of_the_same_path() {
804 let mut m = vec![
807 Message::user("go"),
808 call("t0", "a.md"),
809 result("t0", "pre-edit contents"),
810 Message::assistant(vec![Block::ToolUse {
811 id: "t1".into(),
812 name: "fs_write".into(),
813 input: serde_json::json!({"path": "a.md", "content": "post"}),
814 }]),
815 result("t1", "wrote 4 bytes"),
816 ];
817 assert_eq!(evict_superseded_results(&mut m), 1);
818 assert!(body_of(&m[2]).starts_with(SUPERSEDED_MARKER));
819 assert!(
820 body_of(&m[2]).contains("fs_write"),
821 "the marker says what superseded it"
822 );
823 }
824
825 #[test]
826 fn errors_neither_supersede_nor_get_evicted() {
827 let mut m = vec![
828 Message::user("go"),
829 call("t0", "a.md"),
830 result("t0", "good contents"),
831 call("t1", "a.md"),
832 Message::tool_results(vec![Block::ToolResult {
833 tool_use_id: "t1".into(),
834 content: "permission denied".into(),
835 is_error: true,
836 }]),
837 ];
838 assert_eq!(evict_superseded_results(&mut m), 0);
841 assert_eq!(body_of(&m[2]), "good contents");
842 assert_eq!(body_of(&m[4]), "permission denied");
843 }
844
845 #[test]
846 fn a_pile_of_identical_failures_collapses_to_its_newest_member() {
847 let mut m = vec![Message::user("go")];
854 for i in 0..4 {
855 m.push(call(&format!("t{i}"), "a.md"));
856 m.push(err_result(&format!("t{i}"), "permission denied"));
857 }
858
859 assert_eq!(collapse_repeated_failures(&mut m), 3);
860 for i in 0..3 {
861 assert!(
862 body_of(&m[2 + i * 2]).starts_with(REPEAT_MARKER),
863 "attempt {i} was left to condition the next one"
864 );
865 }
866 assert_eq!(
867 body_of(&m[8]),
868 "permission denied",
869 "the newest failure must survive whole — it is the diagnosis that \
870 stops the call being retried"
871 );
872 }
873
874 #[test]
875 fn a_persons_repeated_refusals_are_never_collapsed() {
876 let mut m = vec![Message::user("go")];
882 for i in 0..3 {
883 m.push(call(&format!("t{i}"), "secrets.env"));
884 m.push(err_result(
885 &format!("t{i}"),
886 "Denied by the user: not that file",
887 ));
888 }
889 assert_eq!(collapse_repeated_failures(&mut m), 0);
890 for i in 0..3 {
891 assert_eq!(
892 body_of(&m[2 + i * 2]),
893 "Denied by the user: not that file",
894 "a refusal the miner reads was overwritten"
895 );
896 }
897
898 for prefix in ["Blocked by policy:", "Blocked by a hook:"] {
902 let mut m = vec![Message::user("go")];
903 for i in 0..3 {
904 m.push(call(&format!("t{i}"), "a.md"));
905 m.push(err_result(&format!("t{i}"), &format!("{prefix} no")));
906 }
907 assert_eq!(collapse_repeated_failures(&mut m), 0, "{prefix}");
908 }
909
910 let mut m = vec![Message::user("go")];
913 for i in 0..3 {
914 m.push(call(&format!("d{i}"), "denied.md"));
915 m.push(err_result(&format!("d{i}"), "Denied by the user: no"));
916 m.push(call(&format!("e{i}"), "gone.md"));
917 m.push(err_result(&format!("e{i}"), "no such file"));
918 }
919 assert_eq!(collapse_repeated_failures(&mut m), 2);
920 }
921
922 #[test]
923 fn two_different_failures_on_one_target_are_two_facts() {
924 let mut m = vec![
929 Message::user("go"),
930 call("t0", "a.md"),
931 err_result("t0", "no such file"),
932 call("t1", "a.md"),
933 err_result("t1", "permission denied"),
934 ];
935 assert_eq!(collapse_repeated_failures(&mut m), 0);
936 assert_eq!(body_of(&m[2]), "no such file");
937 assert_eq!(body_of(&m[4]), "permission denied");
938 }
939
940 #[test]
941 fn identical_failures_on_different_targets_are_left_alone() {
942 let mut m = vec![
945 Message::user("go"),
946 call("t0", "a.md"),
947 err_result("t0", "no such file"),
948 call("t1", "b.md"),
949 err_result("t1", "no such file"),
950 ];
951 assert_eq!(collapse_repeated_failures(&mut m), 0);
952 }
953
954 #[test]
955 fn a_successful_result_is_never_collapsed_by_the_failure_pass() {
956 let mut m = vec![
959 Message::user("go"),
960 call("t0", "a.md"),
961 result("t0", "contents"),
962 call("t1", "a.md"),
963 result("t1", "contents"),
964 ];
965 assert_eq!(collapse_repeated_failures(&mut m), 0);
966 assert_eq!(body_of(&m[2]), "contents");
967 }
968
969 #[test]
970 fn collapsing_is_idempotent_and_keeps_every_result_block() {
971 let mut m = vec![Message::user("go")];
975 for i in 0..3 {
976 m.push(call(&format!("t{i}"), "a.md"));
977 m.push(err_result(&format!("t{i}"), "permission denied"));
978 }
979 let blocks = m.len();
980
981 assert_eq!(collapse_repeated_failures(&mut m), 2);
982 let after_one: Vec<String> = m.iter().map(|m| format!("{:?}", m.content)).collect();
983
984 assert_eq!(
985 collapse_repeated_failures(&mut m),
986 0,
987 "a second pass collapsed its own markers"
988 );
989 let after_two: Vec<String> = m.iter().map(|m| format!("{:?}", m.content)).collect();
990
991 assert_eq!(after_one, after_two);
992 assert_eq!(m.len(), blocks, "a result block was dropped");
993 assert!(orphaned_tool_results(&m).is_empty());
994 assert!(orphaned_tool_uses(&m).is_empty());
995 }
996
997 fn err_result(id: &str, content: &str) -> Message {
998 Message::tool_results(vec![Block::ToolResult {
999 tool_use_id: id.into(),
1000 content: content.into(),
1001 is_error: true,
1002 }])
1003 }
1004
1005 #[test]
1006 fn a_ranged_read_speaks_only_for_its_slice() {
1007 let ranged = |id: &str, offset: u64| {
1008 Message::assistant(vec![Block::ToolUse {
1009 id: id.into(),
1010 name: "fs_read".into(),
1011 input: serde_json::json!({"path": "big.txt", "offset": offset, "limit": 10}),
1012 }])
1013 };
1014 let mut m = vec![
1015 Message::user("go"),
1016 call("t0", "big.txt"), result("t0", "the whole file"),
1018 ranged("t1", 100),
1019 result("t1", "lines 100-110"),
1020 ranged("t2", 200),
1021 result("t2", "lines 200-210"),
1022 ];
1023 assert_eq!(evict_superseded_results(&mut m), 0);
1026
1027 m.push(ranged("t3", 100));
1029 m.push(result("t3", "lines 100-110 again"));
1030 assert_eq!(evict_superseded_results(&mut m), 1);
1031 assert!(
1032 body_of(&m[4]).starts_with(SUPERSEDED_MARKER),
1033 "the older 100-slice"
1034 );
1035 assert_eq!(body_of(&m[2]), "the whole file", "the full read survived");
1036 }
1037
1038 #[test]
1039 fn different_targets_do_not_supersede_each_other() {
1040 let mut m = vec![
1041 Message::user("go"),
1042 call("t0", "a.md"),
1043 result("t0", "a contents"),
1044 call("t1", "b.md"),
1045 result("t1", "b contents"),
1046 ];
1047 assert_eq!(evict_superseded_results(&mut m), 0);
1048 }
1049
1050 #[test]
1051 fn identical_non_path_calls_dedup_and_different_arguments_do_not() {
1052 let shell = |id: &str, cmd: &str| {
1053 Message::assistant(vec![Block::ToolUse {
1054 id: id.into(),
1055 name: "shell".into(),
1056 input: serde_json::json!({"command": cmd}),
1057 }])
1058 };
1059 let mut m = vec![
1060 Message::user("go"),
1061 shell("t0", "cargo test"),
1062 result("t0", "1 failed"),
1063 shell("t1", "cargo build"),
1064 result("t1", "ok"),
1065 shell("t2", "cargo test"),
1066 result("t2", "all passed"),
1067 ];
1068 assert_eq!(evict_superseded_results(&mut m), 1);
1071 assert!(body_of(&m[2]).starts_with(SUPERSEDED_MARKER));
1072 assert_eq!(body_of(&m[4]), "ok");
1073 assert_eq!(body_of(&m[6]), "all passed");
1074 }
1075
1076 #[test]
1077 fn eviction_is_idempotent_and_never_touches_the_calls() {
1078 let mut m = vec![
1079 Message::user("go"),
1080 call("t0", "a.md"),
1081 result("t0", "old"),
1082 call("t1", "a.md"),
1083 result("t1", "new"),
1084 ];
1085 let calls_before: Vec<_> = m
1086 .iter()
1087 .flat_map(|m| m.tool_uses())
1088 .map(|(_, _, i)| i.clone())
1089 .collect();
1090 assert_eq!(evict_superseded_results(&mut m), 1);
1091 assert_eq!(
1092 evict_superseded_results(&mut m),
1093 0,
1094 "a second pass re-evicted"
1095 );
1096
1097 let calls_after: Vec<_> = m
1098 .iter()
1099 .flat_map(|m| m.tool_uses())
1100 .map(|(_, _, i)| i.clone())
1101 .collect();
1102 assert_eq!(
1103 calls_before, calls_after,
1104 "eviction disturbed the tool calls"
1105 );
1106 assert!(orphaned_tool_results(&m).is_empty());
1107 assert!(orphaned_tool_uses(&m).is_empty());
1108 }
1109
1110 #[test]
1111 fn a_result_shorter_than_the_budget_is_not_touched() {
1112 let mut m = vec![
1113 Message::user("go"),
1114 call("t0", "a.md"),
1115 result("t0", "amount: 43"),
1116 ];
1117 assert_eq!(thin_old_results(&mut m, 0, 240), 0);
1118 assert!(!format!("{:?}", m[2].content).contains("truncated"));
1119 }
1120
1121 #[test]
1122 fn thinning_says_it_thinned_so_the_model_can_tell() {
1123 let mut m = walk(2);
1126 thin_old_results(&mut m, 0, 240);
1127 let body = m[2].content.iter().find_map(|b| match b {
1128 Block::ToolResult { content, .. } => Some(content.clone()),
1129 _ => None,
1130 });
1131 assert!(body.unwrap().ends_with(TRUNCATION_MARKER));
1132 }
1133 use serde_json::json;
1134
1135 fn transcript(turns: usize) -> Vec<Message> {
1138 let mut messages = vec![Message::user("do the thing")];
1139 for i in 0..turns {
1140 messages.push(Message::assistant(vec![Block::ToolUse {
1141 id: format!("t{i}"),
1142 name: "echo".into(),
1143 input: json!({"n": i}),
1144 }]));
1145 messages.push(Message::tool_results(vec![Block::ToolResult {
1146 tool_use_id: format!("t{i}"),
1147 content: format!("result {i}"),
1148 is_error: false,
1149 }]));
1150 }
1151 messages.push(Message::assistant(vec![Block::text("done")]));
1152 messages
1153 }
1154
1155 #[test]
1156 fn a_cut_never_orphans_a_tool_result() {
1157 let messages = transcript(6);
1160 for target in 0..messages.len() {
1161 let Some(cut) = cut_point(&messages, target) else {
1162 continue;
1163 };
1164 let rebuilt = rebuild(&messages, cut, "a summary", &[]);
1165
1166 assert!(
1167 orphaned_tool_results(&rebuilt).is_empty(),
1168 "cutting at {cut} (target {target}) orphaned a tool result"
1169 );
1170 assert!(
1171 orphaned_tool_uses(&rebuilt).is_empty(),
1172 "cutting at {cut} (target {target}) left a tool call unanswered"
1173 );
1174 }
1175 }
1176
1177 #[test]
1178 fn the_cut_lands_on_an_assistant_turn_and_at_or_after_the_target() {
1179 let messages = transcript(5);
1180 for target in 0..messages.len() {
1181 let Some(cut) = cut_point(&messages, target) else {
1182 continue;
1183 };
1184 assert!(
1185 cut >= target.max(1),
1186 "a cut before the target drops too much"
1187 );
1188 assert_eq!(messages[cut].role, Role::Assistant);
1189 }
1190 }
1191
1192 #[test]
1193 fn the_original_task_survives_and_the_recent_turns_are_verbatim() {
1194 let messages = transcript(6);
1195 let cut = cut_point(&messages, 6).unwrap();
1196 let rebuilt = rebuild(&messages, cut, "we established that X is 42", &[]);
1197
1198 assert!(rebuilt[0].text().contains("do the thing"));
1200 assert!(rebuilt[0].text().contains("X is 42"));
1201 assert_eq!(rebuilt[0].role, Role::User);
1202
1203 assert_eq!(rebuilt.len(), 1 + messages.len() - cut);
1205 assert_eq!(
1206 rebuilt.last().unwrap().text(),
1207 messages.last().unwrap().text()
1208 );
1209 }
1210
1211 #[test]
1212 fn the_rebuilt_transcript_never_has_two_user_messages_in_a_row() {
1213 let messages = transcript(6);
1216 let cut = cut_point(&messages, 5).unwrap();
1217 let rebuilt = rebuild(&messages, cut, "s", &[]);
1218
1219 for pair in rebuilt.windows(2) {
1220 assert!(
1221 !(pair[0].role == Role::User && pair[1].role == Role::User),
1222 "consecutive user messages"
1223 );
1224 }
1225 }
1226
1227 #[test]
1231 fn tool_state_crosses_a_compaction_verbatim() {
1232 let messages = transcript(6);
1233 let cut = cut_point(&messages, 6).unwrap();
1234 let list = "1/3 done\n[x] read the config\n[~] fix the port\n[ ] run the tests\n";
1235 let rebuilt = rebuild(
1236 &messages,
1237 cut,
1238 "we established that X is 42",
1239 &[("todo", list)],
1240 );
1241
1242 let head = rebuilt[0].text();
1243 assert!(head.contains("X is 42"), "the summary is still there");
1244 assert!(head.contains("[~] fix the port"), "{head}");
1245 assert!(head.contains("[ ] run the tests"), "{head}");
1246 assert!(
1249 head.find(CARRIED_HEADER).unwrap() > head.find("X is 42").unwrap(),
1250 "{head}"
1251 );
1252 }
1253
1254 #[test]
1257 fn a_second_compaction_replaces_the_carried_state_rather_than_stacking_it() {
1258 let messages = transcript(6);
1259 let cut = cut_point(&messages, 6).unwrap();
1260 let first = rebuild(&messages, cut, "summary one", &[("todo", "[ ] step one")]);
1261
1262 let cut = cut_point(&first, first.len().saturating_sub(2)).unwrap();
1264 let second = rebuild(
1265 &first,
1266 cut,
1267 "summary two",
1268 &[("todo", "[x] step one\n[ ] step two")],
1269 );
1270
1271 let head = second[0].text();
1272 assert_eq!(head.matches(CARRIED_HEADER).count(), 1, "{head}");
1273 assert!(head.contains("[ ] step two"), "{head}");
1274 assert!(
1275 !head.contains("[ ] step one"),
1276 "last compaction's list survived beside this one's: {head}"
1277 );
1278 assert!(head.contains("summary one") && head.contains("summary two"));
1281 }
1282
1283 #[test]
1286 fn no_tool_state_leaves_no_trace() {
1287 let messages = transcript(6);
1288 let cut = cut_point(&messages, 6).unwrap();
1289 let rebuilt = rebuild(&messages, cut, "a summary", &[]);
1290 assert!(!rebuilt[0].text().contains(CARRIED_HEADER));
1291 }
1292
1293 #[test]
1294 fn a_short_conversation_is_left_alone() {
1295 let messages = vec![
1296 Message::user("hi"),
1297 Message::assistant(vec![Block::text("hello")]),
1298 ];
1299 let cut = cut_point(&messages, 1).unwrap();
1301 assert!(!worth_compacting(&messages, cut));
1302 }
1303
1304 #[test]
1305 fn a_transcript_ending_mid_tool_call_still_cuts_safely() {
1306 let mut messages = transcript(4);
1309 messages.pop();
1310 assert_eq!(messages.last().unwrap().role, Role::User);
1311
1312 let cut = cut_point(&messages, 3).unwrap();
1313 let rebuilt = rebuild(&messages, cut, "s", &[]);
1314 assert!(orphaned_tool_results(&rebuilt).is_empty());
1315 }
1316}