#![allow(
dead_code,
clippy::pedantic,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::str_to_string,
clippy::unreadable_literal
)]
use loopctl::compact::ContextCompactor;
use loopctl::compact::HeuristicTokenCounter;
use loopctl::compact::TokenSplitter;
use loopctl::compact::TruncatingCompactor;
use loopctl::compact::types::{CompactReason, CompactionContext};
use loopctl::message::{Message, MessagePart, Role, ToolContent};
use std::sync::Arc;
const HISTORIES_PER_PROPERTY: u64 = 10_000;
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0 >> 16
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn gen_messages(rng: &mut Lcg) -> Vec<Message> {
let len = 4 + rng.below(6) as usize;
(0..len)
.map(|i| {
let kind = rng.below(10);
if kind < 3 {
let id = ["a", "b", "c"][rng.below(3) as usize];
Message::new(
Role::Assistant,
vec![MessagePart::tool_call(
id,
"Read",
serde_json::json!({ "i": i }),
)],
)
} else if kind < 6 {
let id = ["a", "b", "c"][rng.below(3) as usize];
Message::new(
Role::User,
vec![MessagePart::tool_result(
id,
"Read",
ToolContent::from_string(format!("r{i}")),
false,
)],
)
} else if kind < 8 {
Message::user("q")
} else {
Message::assistant("a")
}
})
.collect()
}
fn input_pairs(messages: &[Message]) -> (Vec<(usize, usize)>, Vec<usize>) {
let mut pending: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
let mut pairs = Vec::new();
for (i, msg) in messages.iter().enumerate() {
for part in &msg.parts {
match part {
MessagePart::ToolCall { id, .. } => {
pending.entry(id.clone()).or_default().push(i);
}
MessagePart::ToolResult { call_id, .. } => {
if let Some(cm) = pending.get_mut(call_id).and_then(Vec::pop) {
pairs.push((cm, i));
}
}
_ => {}
}
}
}
let paired = pairs.iter().map(|(_, r)| *r).collect();
(pairs, paired)
}
fn part_id(messages: &[Message], idx: usize) -> String {
messages
.get(idx)
.and_then(|m| m.parts.first())
.map_or_else(String::new, |p| match p {
MessagePart::ToolCall { id, .. } | MessagePart::ToolResult { call_id: id, .. } => {
id.clone()
}
_ => String::new(),
})
}
fn ctx() -> CompactionContext {
CompactionContext {
tokens_before: 10_000,
reason: CompactReason::ThresholdExceeded,
context_window: 8_000,
turn: 3,
counter: Arc::new(HeuristicTokenCounter),
instructions: None,
additional_context: Vec::new(),
}
}
#[tokio::test]
async fn compaction_invariants_hold_under_random_histories() {
let mut rng = Lcg(0x5EED_600D);
for iter in 0..HISTORIES_PER_PROPERTY {
let messages = gen_messages(&mut rng);
let compactor = TruncatingCompactor::new()
.with_min_messages(2 + rng.below(3) as usize)
.with_preserve_recent(1 + rng.below(5) as usize);
let outcome = compactor.compact(messages.clone(), 1, ctx()).await;
assert!(outcome.success, "iter {iter}");
let (pairs, paired_results) = input_pairs(&messages);
let out_calls: std::collections::HashSet<(String, String)> = outcome
.messages
.iter()
.flat_map(|m| m.parts.iter())
.filter_map(|p| match p {
MessagePart::ToolCall { id, input, .. } => {
Some((id.clone(), input.get("i")?.to_string()))
}
_ => None,
})
.collect();
let out_results: std::collections::HashSet<(String, String)> = outcome
.messages
.iter()
.flat_map(|m| m.parts.iter())
.filter_map(|p| match p {
MessagePart::ToolResult {
call_id, output, ..
} => Some((call_id.clone(), output.to_string())),
_ => None,
})
.collect();
for (cm, rm) in &pairs {
let call_in = out_calls.contains(&(part_id(&messages, *cm), cm.to_string()));
let result_in = out_results.contains(&(part_id(&messages, *rm), format!("r{rm}")));
assert_eq!(
call_in, result_in,
"iter {iter}: pair ({cm},{rm}) split — call kept {call_in}, result kept {result_in}"
);
}
let input_all_lone_results = paired_results.is_empty()
&& messages.iter().all(|m| {
m.parts
.iter()
.all(|p| matches!(p, MessagePart::ToolResult { .. }))
});
if !input_all_lone_results {
for (id, content) in &out_results {
let idx: usize = content
.trim_start_matches('r')
.parse()
.unwrap_or(usize::MAX);
assert!(
paired_results.contains(&idx),
"iter {iter}: orphaned result {id}/{content} carried into the output"
);
}
}
assert!(
outcome.messages.iter().all(|m| !m.parts.is_empty()),
"iter {iter}: empty message in the output"
);
assert!(
!outcome.messages.is_empty(),
"iter {iter}: compaction emptied the conversation"
);
}
}
#[test]
fn splitter_preserves_pair_closure_under_random_histories() {
let mut rng = Lcg(0xC0FF_EE00);
for iter in 0..HISTORIES_PER_PROPERTY {
let messages = gen_messages(&mut rng);
let splitter = TokenSplitter::new()
.with_min_messages(2)
.with_preserve_recent(1 + rng.below(5) as usize);
let split = splitter.split(&messages);
let (pairs, _) = input_pairs(&messages);
for (cm, rm) in &pairs {
assert_eq!(
cm < &split.split_index,
rm < &split.split_index,
"iter {iter}: pair ({cm},{rm}) split at {}",
split.split_index
);
}
}
}