use std::sync::Arc;
use serde_json::Value;
use crate::core::tool_calls::normalize_message_tool_calls;
use crate::tape::entries::TapeEntry;
use crate::tape::query::TapeQuery;
#[derive(Debug, Clone, Default)]
pub enum AnchorSelector {
#[default]
LastAnchor,
Named(String),
None,
}
pub type SelectFn = Arc<dyn Fn(&[TapeEntry], &TapeContext) -> Vec<Value> + Send + Sync>;
#[derive(Clone)]
pub struct TapeContext {
pub anchor: AnchorSelector,
pub select: Option<SelectFn>,
pub state: Value,
}
impl Default for TapeContext {
fn default() -> Self {
Self {
anchor: AnchorSelector::default(),
select: None,
state: Value::Object(serde_json::Map::new()),
}
}
}
impl std::fmt::Debug for TapeContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TapeContext")
.field("anchor", &self.anchor)
.field("select", &self.select.as_ref().map(|_| "<fn>"))
.field("state", &self.state)
.finish()
}
}
impl TapeContext {
pub fn new() -> Self {
Self::default()
}
pub fn build_query(&self, query: TapeQuery) -> TapeQuery {
match &self.anchor {
AnchorSelector::None => query,
AnchorSelector::LastAnchor => query.last_anchor(),
AnchorSelector::Named(name) => query.after_anchor(name.clone()),
}
}
}
pub fn build_messages(entries: &[TapeEntry], context: &TapeContext) -> Vec<Value> {
if let Some(ref select) = context.select {
return select(entries, context);
}
default_messages(entries)
}
fn default_messages(entries: &[TapeEntry]) -> Vec<Value> {
let mut messages = Vec::new();
for entry in entries {
if entry.kind != "message" {
continue;
}
if !entry.payload.is_object() {
continue;
}
messages.push(normalize_message_tool_calls(&entry.payload));
}
messages
}
const MAX_TOOL_RESULT_CHARS: usize = 16_000;
const MAX_TOTAL_CONTEXT_CHARS: usize = 400_000;
const AGGRESSIVE_TRIM_KEEP_ROUNDS: usize = 2;
pub fn apply_context_budget(messages: &mut Vec<Value>) {
for msg in messages.iter_mut() {
if msg_role(msg) == "tool" {
truncate_tool_result_content(msg, MAX_TOOL_RESULT_CHARS);
}
}
let total_chars: usize = messages.iter().map(content_char_count).sum();
if total_chars > MAX_TOTAL_CONTEXT_CHARS {
aggressive_trim(messages);
}
}
fn truncate_tool_result_content(msg: &mut Value, limit: usize) {
let content = match msg.get("content").and_then(|c| c.as_str()) {
Some(s) => s,
None => return,
};
if content.len() <= limit {
return;
}
let safe_limit = (0..=limit)
.rev()
.find(|&i| content.is_char_boundary(i))
.unwrap_or(0);
let cut = content[..safe_limit].rfind('\n').unwrap_or(safe_limit);
let shown_lines = content[..cut].matches('\n').count() + 1;
let total_lines = content.matches('\n').count() + 1;
let truncated = format!(
"{}\n\n[Truncated: {}/{} lines shown ({}/{} chars). Use tape.search to see full output.]",
&content[..cut],
shown_lines,
total_lines,
cut,
content.len()
);
if let Some(obj) = msg.as_object_mut() {
obj.insert("content".to_owned(), Value::String(truncated));
}
}
fn msg_role(msg: &Value) -> &str {
msg.get("role").and_then(|r| r.as_str()).unwrap_or("")
}
fn content_char_count(msg: &Value) -> usize {
msg.get("content")
.and_then(|c| c.as_str())
.map_or(0, str::len)
}
fn find_trim_boundary(msgs: &[Value], rounds: usize) -> usize {
let mut seen = 0;
for (i, m) in msgs.iter().enumerate().rev() {
if msg_role(m) == "user" {
seen += 1;
if seen >= rounds {
return i;
}
}
}
0
}
const TRIM_NOTICE: &str = "[Earlier tool interactions trimmed to fit context window. Use tape.search to review full history.]";
fn aggressive_trim(messages: &mut Vec<Value>) {
let (system, conversation): (Vec<_>, Vec<_>) =
messages.drain(..).partition(|m| msg_role(m) == "system");
let keep_from = find_trim_boundary(&conversation, AGGRESSIVE_TRIM_KEEP_ROUNDS);
let mut recent: Vec<Value> = conversation.into_iter().skip(keep_from).collect();
messages.extend(system);
if keep_from > 0 {
inject_trim_notice(&mut recent, messages);
}
messages.extend(recent);
}
fn inject_trim_notice(recent: &mut [Value], before: &mut Vec<Value>) {
if msg_role(&recent[0]) == "assistant" {
let existing = recent[0]
.get("content")
.and_then(|c| c.as_str())
.unwrap_or("");
recent[0]["content"] = Value::String(format!("{TRIM_NOTICE}\n\n{existing}"));
} else {
before.push(serde_json::json!({"role": "assistant", "content": TRIM_NOTICE}));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn msg(role: &str, text: &str) -> Value {
json!({"role": role, "content": text})
}
fn assert_no_consecutive_roles(messages: &[Value]) {
for w in messages.windows(2) {
let (a, b) = (msg_role(&w[0]), msg_role(&w[1]));
assert!(
a != b || a == "system",
"consecutive '{a}': {:?} / {:?}",
w[0],
w[1]
);
}
}
#[test]
fn inject_trim_notice_prepends_to_leading_assistant() {
let mut recent = vec![msg("assistant", "hello"), msg("user", "q")];
let mut before = vec![msg("system", "sys")];
inject_trim_notice(&mut recent, &mut before);
assert_eq!(before.len(), 1); let content = recent[0]["content"].as_str().unwrap();
assert!(content.starts_with(TRIM_NOTICE));
assert!(content.contains("hello"));
}
#[test]
fn inject_trim_notice_adds_message_before_user() {
let mut recent = vec![msg("user", "q"), msg("assistant", "a")];
let mut before = vec![msg("system", "sys")];
inject_trim_notice(&mut recent, &mut before);
assert_eq!(before.len(), 2);
assert_eq!(msg_role(&before[1]), "assistant");
assert!(before[1]["content"].as_str().unwrap().contains("trimmed"));
}
#[test]
fn aggressive_trim_injects_notice_before_user() {
let mut msgs = vec![msg("system", "sys")];
for i in 0..4 {
msgs.push(msg("user", &format!("q{i}")));
msgs.push(msg("assistant", &format!("a{i}")));
}
aggressive_trim(&mut msgs);
assert_no_consecutive_roles(&msgs);
assert_eq!(msg_role(&msgs[1]), "assistant");
assert!(msgs[1]["content"].as_str().unwrap().contains("trimmed"));
assert_eq!(msg_role(&msgs[2]), "user");
}
}