use std::sync::Arc;
use anyhow::Result;
use tokio::sync::mpsc;
use crate::provider::{Message, Role, ToolDefinition};
use crate::session::helper::token::{estimate_request_tokens, estimate_tokens_for_messages};
use crate::session::index::Granularity;
use crate::session::index_produce::produce_summary;
use crate::session::relevance::{RelevanceMeta, extract};
use crate::session::{ResidencyLevel, Session, SessionEvent};
use super::helpers::DerivedContext;
use super::incremental_clamp::clamp_and_recompute;
use super::incremental_insert::{interleave, range_from_tuple};
use super::incremental_observability::DerivationObservability;
use super::incremental_repair::repair_with_origins;
use super::incremental_types::SummaryGap;
const DEFAULT_RECENT_WINDOW: usize = 8;
const FILE_OVERLAP_WEIGHT: f64 = 4.0;
const TOOL_OVERLAP_WEIGHT: f64 = 2.5;
const ERROR_BOUNDARY_WEIGHT: f64 = 3.0;
const RECENCY_DECAY_WEIGHT: f64 = 0.05;
pub(super) async fn derive_incremental(
session: &Session,
provider: Arc<dyn crate::provider::Provider>,
model: &str,
system_prompt: &str,
tools: &[ToolDefinition],
budget_tokens: usize,
event_tx: Option<&mpsc::Sender<SessionEvent>>,
) -> Result<DerivedContext> {
let origin_len = session.messages.len();
let mut clone = session.messages.clone();
if let Some(ctx) = super::incremental_below_budget::try_pass_through(
&mut clone,
system_prompt,
tools,
budget_tokens,
origin_len,
) {
return Ok(ctx);
}
let task = task_signature(&clone);
let scores = score_messages(&clone, &task);
let recent_window = std::cmp::min(DEFAULT_RECENT_WINDOW, clone.len());
let recent_start = clone.len() - recent_window;
let header_cost = budget_tokens
.saturating_sub(estimate_request_tokens(system_prompt, &[], tools))
.max(1);
let mut budget_for_messages = header_cost;
let per_msg = clone
.iter()
.map(|m| message_tokens(m))
.collect::<Vec<usize>>();
let mut keep = vec![false; clone.len()];
for (i, slot) in keep
.iter_mut()
.enumerate()
.take(clone.len())
.skip(recent_start)
{
*slot = true;
budget_for_messages = budget_for_messages.saturating_sub(per_msg[i]);
}
let mut order: Vec<usize> = (0..recent_start).collect();
order.sort_by(|a, b| {
scores[*b]
.partial_cmp(&scores[*a])
.unwrap_or(std::cmp::Ordering::Equal)
});
for idx in order {
let cost = per_msg[idx];
if cost <= budget_for_messages {
keep[idx] = true;
budget_for_messages = budget_for_messages.saturating_sub(cost);
}
}
let dropped_ranges = collect_dropped_ranges(&keep);
let mut gaps = Vec::new();
let rlm_config = session.metadata.rlm.clone();
let mut summary_index = session.summary_index.clone();
let observability = DerivationObservability::new(event_tx);
for tuple in &dropped_ranges {
let Some(range) = range_from_tuple(*tuple) else {
continue;
};
let node = summary_index
.summary_for(range, |r| {
produce_summary(
&clone,
r,
512,
Granularity::Phase,
session.summary_index.generation(),
Arc::clone(&provider),
model,
&rlm_config,
&session.id,
session.metadata.subcall_provider.clone(),
session.metadata.subcall_model_name.clone(),
observability.template(),
)
})
.await?;
gaps.push(SummaryGap {
range,
content: node.content,
});
}
let (mut messages, _, mut origins) = interleave(&clone, &keep, &gaps);
repair_with_origins(&mut messages, &mut origins);
let mut resolutions: Vec<ResidencyLevel> = messages.iter().map(residency_for_message).collect();
let mut provenance = vec!["incremental".to_string()];
let (final_dropped, tags) = clamp_and_recompute(
&mut messages,
&mut resolutions,
&mut origins,
system_prompt,
tools,
budget_tokens,
clone.len(),
&dropped_ranges,
);
provenance.extend(tags.into_iter().map(str::to_string));
Ok(DerivedContext {
resolutions,
dropped_ranges: final_dropped,
provenance,
messages,
origin_len,
compressed: true,
})
}
fn task_signature(messages: &[Message]) -> RelevanceMeta {
messages
.iter()
.rev()
.find(|m| matches!(m.role, Role::User))
.map(extract)
.unwrap_or_default()
}
fn score_messages(messages: &[Message], task: &RelevanceMeta) -> Vec<f64> {
let n = messages.len();
messages
.iter()
.enumerate()
.map(|(i, msg)| {
let meta = extract(msg);
let mut score = 0.0;
score += FILE_OVERLAP_WEIGHT * overlap_count(&meta.files, &task.files) as f64;
score += TOOL_OVERLAP_WEIGHT * overlap_count(&meta.tools, &task.tools) as f64;
score += ERROR_BOUNDARY_WEIGHT * meta.error_classes.len() as f64;
let distance_from_tail = (n - 1).saturating_sub(i) as f64;
score -= RECENCY_DECAY_WEIGHT * distance_from_tail;
score
})
.collect()
}
fn overlap_count(left: &[String], right: &[String]) -> usize {
if left.is_empty() || right.is_empty() {
return 0;
}
let right_set: std::collections::HashSet<_> = right.iter().collect();
left.iter().filter(|item| right_set.contains(item)).count()
}
fn message_tokens(msg: &Message) -> usize {
estimate_tokens_for_messages(std::slice::from_ref(msg))
}
fn residency_for_message(msg: &Message) -> ResidencyLevel {
let text = msg
.content
.iter()
.filter_map(|part| match part {
crate::provider::ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.next()
.unwrap_or_default();
if text.starts_with("[SUMMARY of turns ") {
ResidencyLevel::Compressed
} else {
ResidencyLevel::Full
}
}
fn collect_dropped_ranges(keep: &[bool]) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut i = 0;
while i < keep.len() {
if !keep[i] {
let start = i;
while i < keep.len() && !keep[i] {
i += 1;
}
ranges.push((start, i));
} else {
i += 1;
}
}
ranges
}
pub(super) const DEFAULT_INCREMENTAL_BUDGET: usize = 16_000;
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::{ContentPart, Role};
fn user(text: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentPart::Text {
text: text.to_string(),
}],
}
}
fn assistant(text: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentPart::Text {
text: text.to_string(),
}],
}
}
#[test]
fn task_signature_picks_up_files_from_last_user_turn() {
let msgs = vec![
assistant("noise"),
user("Please edit src/lib.rs and crates/foo/main.rs"),
assistant("ok"),
];
let task = task_signature(&msgs);
assert!(task.files.iter().any(|f| f == "src/lib.rs"));
assert!(task.files.iter().any(|f| f == "crates/foo/main.rs"));
}
#[test]
fn score_messages_rewards_file_overlap() {
let msgs = vec![
assistant("looking at src/lib.rs"), assistant("totally unrelated noise"),
user("touch src/lib.rs"),
];
let task = task_signature(&msgs);
let scores = score_messages(&msgs, &task);
assert!(scores[0] > scores[1]);
}
#[test]
fn collect_dropped_ranges_groups_consecutive_drops() {
let keep = vec![true, false, false, true, false, true];
assert_eq!(collect_dropped_ranges(&keep), vec![(1, 3), (4, 5)]);
}
#[test]
fn collect_dropped_ranges_empty_when_all_kept() {
let keep = vec![true, true, true];
assert_eq!(collect_dropped_ranges(&keep), Vec::<(usize, usize)>::new());
}
#[test]
fn overlap_count_short_circuits_on_empty() {
let left: Vec<String> = vec!["a".into()];
let right: Vec<String> = Vec::new();
assert_eq!(overlap_count(&left, &right), 0);
}
}