#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use async_trait::async_trait;
use chrono::Utc;
use entelix_core::ir::{ContentPart, ToolResultContent};
use entelix_core::{ExecutionContext, Result};
use entelix_session::{CompactedHistory, Compactor, GraphEvent, Turn};
struct FirstNCompactor {
n: usize,
}
#[async_trait]
impl Compactor for FirstNCompactor {
async fn compact(
&self,
events: &[GraphEvent],
_budget_chars: usize,
_ctx: &ExecutionContext,
) -> Result<CompactedHistory> {
let mut turns = CompactedHistory::group(events)?.turns().to_vec();
turns.truncate(self.n);
Ok(CompactedHistory::from_turns(turns))
}
}
fn user(text: &str) -> GraphEvent {
GraphEvent::UserMessage {
content: vec![ContentPart::text(text)],
timestamp: Utc::now(),
}
}
fn assistant(text: &str) -> GraphEvent {
GraphEvent::AssistantMessage {
content: vec![ContentPart::text(text)],
usage: None,
timestamp: Utc::now(),
}
}
fn assistant_with_tool_use(text: &str, call_id: &str, name: &str) -> GraphEvent {
GraphEvent::AssistantMessage {
content: vec![
ContentPart::text(text),
ContentPart::ToolUse {
id: call_id.to_owned(),
name: name.to_owned(),
input: serde_json::json!({"q": "x"}),
provider_echoes: Vec::new(),
},
],
usage: None,
timestamp: Utc::now(),
}
}
fn tool_call(id: &str, name: &str) -> GraphEvent {
GraphEvent::ToolCall {
id: id.to_owned(),
name: name.to_owned(),
input: serde_json::json!({"q": "x"}),
timestamp: Utc::now(),
}
}
fn tool_result(id: &str, name: &str, text: &str) -> GraphEvent {
GraphEvent::ToolResult {
tool_use_id: id.to_owned(),
name: name.to_owned(),
content: ToolResultContent::Text(text.to_owned()),
is_error: false,
timestamp: Utc::now(),
}
}
#[tokio::test]
async fn external_compactor_can_construct_compacted_history() {
let events = vec![
user("first"),
assistant("first reply"),
user("second"),
assistant("second reply"),
];
let history = FirstNCompactor { n: 2 }
.compact(&events, 0, &ExecutionContext::new())
.await
.unwrap();
assert_eq!(history.len(), 2);
let turns = history.turns();
assert!(matches!(&turns[0], Turn::User { .. }));
assert!(matches!(&turns[1], Turn::Assistant { .. }));
}
#[tokio::test]
async fn external_compactor_passes_tool_pairs_through_unchanged() {
let events = vec![
user("query"),
assistant_with_tool_use("checking", "call_1", "search"),
tool_call("call_1", "search"),
tool_result("call_1", "search", "hits"),
assistant("done"),
];
let history = FirstNCompactor { n: 2 }
.compact(&events, 0, &ExecutionContext::new())
.await
.unwrap();
assert_eq!(history.len(), 2);
let turns = history.turns();
assert!(matches!(&turns[0], Turn::User { .. }));
let Turn::Assistant { tools, .. } = &turns[1] else {
panic!("expected Turn::Assistant at index 1");
};
assert_eq!(tools.len(), 1, "the round-trip must survive the rebuild");
assert_eq!(tools[0].id(), "call_1");
assert_eq!(tools[0].name(), "search");
}