use std::path::PathBuf;
use super::support::{ratio_of, scope_depth};
use crate::context::{
ContextBudget, ContextDiagnostic, ContextItem, ContextItemKind, ContextLedger, ContextVisibility,
DiagnosticSeverity, ModelContextLimits, estimate_tokens, item_id_for_path, item_id_for_session_range,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TranscriptCandidate {
pub seq: u64,
pub session_id: String,
pub label: String,
pub bytes: usize,
pub artifact_handle: Option<String>,
pub ui_only: bool,
pub streaming: bool,
}
impl TranscriptCandidate {
pub fn new(session_id: impl Into<String>, seq: u64, label: impl Into<String>, bytes: usize) -> Self {
TranscriptCandidate {
seq,
session_id: session_id.into(),
label: label.into(),
bytes,
artifact_handle: None,
ui_only: false,
streaming: false,
}
}
pub fn ui_only(mut self) -> Self {
self.ui_only = true;
self
}
pub fn streaming(mut self) -> Self {
self.streaming = true;
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PinnedCandidate {
pub id: String,
pub kind: ContextItemKind,
pub label: String,
pub source_path: Option<PathBuf>,
pub scope: String,
pub content_hash: Option<u64>,
pub artifact_handle: Option<String>,
pub bytes: usize,
}
impl PinnedCandidate {
pub fn file(kind: ContextItemKind, path: PathBuf, scope: impl Into<String>, bytes: usize) -> Self {
let id = item_id_for_path(&kind, &path);
let label = path.display().to_string();
PinnedCandidate {
id,
kind,
label,
source_path: Some(path),
scope: scope.into(),
content_hash: None,
artifact_handle: None,
bytes,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompactionSummaryCandidate {
pub id: String,
pub start_seq: u64,
pub end_seq: u64,
pub label: String,
pub content: Option<String>,
pub bytes: usize,
pub latest: bool,
}
impl CompactionSummaryCandidate {
pub fn new(session_id: &str, start_seq: u64, end_seq: u64, bytes: usize, latest: bool) -> Self {
let id = item_id_for_session_range(&ContextItemKind::Summary, session_id, start_seq, end_seq);
CompactionSummaryCandidate {
id,
start_seq,
end_seq,
label: format!("summary {start_seq}..{end_seq}"),
content: None,
bytes,
latest,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SkillCandidate {
pub name: String,
pub path: PathBuf,
pub content_hash: u64,
pub bytes: usize,
pub loaded: bool,
}
impl SkillCandidate {
pub fn discovered(name: impl Into<String>, path: PathBuf, content_hash: u64, bytes: usize) -> Self {
SkillCandidate { name: name.into(), path, content_hash, bytes, loaded: false }
}
pub fn loaded(name: impl Into<String>, path: PathBuf, rendered_bytes: usize, rendered_hash: u64) -> Self {
SkillCandidate { name: name.into(), path, content_hash: rendered_hash, bytes: rendered_bytes, loaded: true }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InstructionCandidate {
pub path: PathBuf,
pub scope: String,
pub content_hash: u64,
pub byte_count: usize,
pub content: Option<String>,
pub truncated: bool,
pub applicable: bool,
}
impl InstructionCandidate {
pub fn scope_depth(&self) -> usize {
scope_depth(&self.scope)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HarnessCandidate {
pub id: String,
pub label: String,
pub bytes: usize,
}
impl HarnessCandidate {
pub fn new(label: impl Into<String>, bytes: usize) -> Self {
let label = label.into();
let id = format!("ctx_harness_{}", slug(&label));
HarnessCandidate { id, label, bytes }
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SelectionInput {
pub harness: Vec<HarnessCandidate>,
pub user_turn: Option<UserTurnCandidate>,
pub instructions: Vec<InstructionCandidate>,
pub pins: Vec<PinnedCandidate>,
pub compaction_summaries: Vec<CompactionSummaryCandidate>,
pub transcript: Vec<TranscriptCandidate>,
pub skills: Vec<SkillCandidate>,
pub dropped_ids: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserTurnCandidate {
pub id: String,
pub bytes: usize,
}
impl UserTurnCandidate {
pub fn new(session_id: &str, seq: u64, bytes: usize) -> Self {
UserTurnCandidate { id: item_id_for_session_range(&ContextItemKind::Transcript, session_id, seq, seq), bytes }
}
}
#[expect(
clippy::cognitive_complexity,
reason = "The selection order is intentionally explicit so every candidate receives an auditable state and reason."
)]
pub fn select_context(input: &SelectionInput, limits: ModelContextLimits) -> ContextLedger {
let mut items: Vec<ContextItem> = Vec::new();
let mut diagnostics = Vec::new();
let available_input = limits.available_input_budget();
for harness in &input.harness {
items.push(ContextItem {
id: harness.id.clone(),
kind: ContextItemKind::Harness,
label: harness.label.clone(),
source_path: None,
scope: ".".to_string(),
content_hash: None,
artifact_handle: None,
byte_count: harness.bytes,
content: None,
token_estimate: estimate_tokens(harness.bytes),
visibility: ContextVisibility::Visible,
reason_code: "harness_always_loaded".to_string(),
reason: "always-loaded harness context".to_string(),
});
}
if let Some(turn) = &input.user_turn {
items.push(ContextItem {
id: turn.id.clone(),
kind: ContextItemKind::Transcript,
label: "current user turn".to_string(),
source_path: None,
scope: ".".to_string(),
content_hash: None,
artifact_handle: None,
byte_count: turn.bytes,
content: None,
token_estimate: estimate_tokens(turn.bytes),
visibility: ContextVisibility::Visible,
reason_code: "current_user_turn".to_string(),
reason: "current user turn, outside ordinary budget eviction".to_string(),
});
}
for pin in &input.pins {
let visibility = if input.dropped_ids.iter().any(|id| id == &pin.id) {
ContextVisibility::Dropped
} else if estimate_tokens(pin.bytes) as u64 > available_input {
diagnostics.push(blocked_oversized(&pin.id, "pinned item"));
ContextVisibility::Blocked
} else {
ContextVisibility::Pinned
};
let reason = visibility.reason("user pin");
let reason_code = match visibility {
ContextVisibility::Dropped => "explicit_drop",
ContextVisibility::Blocked => "pinned_item_over_budget",
ContextVisibility::Pinned => "user_pin",
_ => "user_pin",
};
items.push(ContextItem {
id: pin.id.clone(),
kind: pin.kind.clone(),
label: pin.label.clone(),
source_path: pin.source_path.clone(),
scope: pin.scope.clone(),
content_hash: pin.content_hash,
artifact_handle: pin.artifact_handle.clone(),
byte_count: pin.bytes,
content: None,
token_estimate: estimate_tokens(pin.bytes),
visibility,
reason_code: reason_code.to_string(),
reason,
});
}
let mut ordered_instructions: Vec<&InstructionCandidate> = input.instructions.iter().collect();
ordered_instructions.sort_by_key(|b| std::cmp::Reverse(b.scope_depth()));
for instruction in ordered_instructions {
let id = item_id_for_path(&ContextItemKind::ProjectInstruction, &instruction.path);
let visibility = if input.dropped_ids.iter().any(|d| d == &id) {
ContextVisibility::Dropped
} else if !instruction.applicable {
ContextVisibility::Candidate
} else if estimate_tokens(instruction.byte_count) as u64 > available_input {
diagnostics.push(blocked_oversized(&id, "project instruction"));
ContextVisibility::Blocked
} else {
ContextVisibility::Visible
};
let reason =
visibility.reason(if instruction.applicable { "applicable AGENTS.md" } else { "discovered AGENTS.md" });
let reason_code = match visibility {
ContextVisibility::Dropped => "explicit_drop",
ContextVisibility::Blocked => "project_instruction_over_budget",
ContextVisibility::Candidate => "instruction_not_applicable",
_ => "applicable_project_instruction",
};
items.push(ContextItem {
id,
kind: ContextItemKind::ProjectInstruction,
label: instruction.path.display().to_string(),
source_path: Some(instruction.path.clone()),
scope: instruction.scope.clone(),
content_hash: Some(instruction.content_hash),
artifact_handle: None,
byte_count: instruction.byte_count,
content: if visibility.is_rendered() { instruction.content.clone() } else { None },
token_estimate: estimate_tokens(instruction.byte_count),
visibility,
reason_code: reason_code.to_string(),
reason,
});
}
let mut ordered_skills: Vec<&SkillCandidate> = input.skills.iter().collect();
ordered_skills.sort_by_key(|s| !s.loaded);
for skill in ordered_skills {
let id = item_id_for_path(&ContextItemKind::Skill, &skill.path);
let visibility = if input.dropped_ids.iter().any(|d| d == &id) {
ContextVisibility::Dropped
} else if estimate_tokens(skill.bytes) as u64 > available_input {
diagnostics.push(blocked_oversized(&id, "skill"));
ContextVisibility::Blocked
} else if skill.loaded {
ContextVisibility::Visible
} else {
ContextVisibility::Candidate
};
let reason = visibility.reason(if skill.loaded { "loaded skill" } else { "discovered skill metadata" });
let reason_code = match visibility {
ContextVisibility::Dropped => "explicit_drop",
ContextVisibility::Blocked => "skill_over_budget",
ContextVisibility::Candidate => "skill_metadata_only",
_ => "skill_loaded",
};
items.push(ContextItem {
id,
kind: ContextItemKind::Skill,
label: skill.name.clone(),
source_path: Some(skill.path.clone()),
scope: ".".to_string(),
content_hash: Some(skill.content_hash),
artifact_handle: None,
byte_count: skill.bytes,
content: None,
token_estimate: estimate_tokens(skill.bytes),
visibility,
reason_code: reason_code.to_string(),
reason,
});
}
let omitting_older = should_omit_older_transcript(input, available_input);
for summary in &input.compaction_summaries {
let visibility = if input.dropped_ids.iter().any(|d| d == &summary.id) {
ContextVisibility::Dropped
} else if !omitting_older {
ContextVisibility::Archived
} else if summary.latest {
if estimate_tokens(summary.bytes) as u64 > available_input {
diagnostics.push(blocked_oversized(&summary.id, "compaction summary"));
ContextVisibility::Blocked
} else {
ContextVisibility::Visible
}
} else {
ContextVisibility::Archived
};
let reason = visibility.reason("compaction summary");
let reason_code = match visibility {
ContextVisibility::Dropped => "explicit_drop",
ContextVisibility::Blocked => "summary_over_budget",
ContextVisibility::Archived => "summary_not_current",
_ => "latest_compaction_summary",
};
items.push(ContextItem {
id: summary.id.clone(),
kind: ContextItemKind::Summary,
label: summary.label.clone(),
source_path: None,
scope: ".".to_string(),
content_hash: None,
artifact_handle: None,
byte_count: summary.bytes,
content: if visibility.is_rendered() { summary.content.clone() } else { None },
token_estimate: estimate_tokens(summary.bytes),
visibility,
reason_code: reason_code.to_string(),
reason,
});
}
push_transcript(&mut items, input, available_input);
let budget = ContextBudget::from_limits(limits, &items);
ContextLedger { items, budget, diagnostics }
}
fn push_transcript(items: &mut Vec<ContextItem>, input: &SelectionInput, available_input: u64) {
let target = ratio_of(available_input, super::control::TARGET_BUDGET_RATIO);
let consumed: u64 = items
.iter()
.filter(|item| item.visibility.is_rendered())
.map(|item| item.token_estimate as u64)
.sum();
let transcript: Vec<&TranscriptCandidate> = input.transcript.iter().collect();
let mut selected: Vec<&TranscriptCandidate> = Vec::new();
let mut running = consumed;
for candidate in transcript.iter().rev() {
if candidate.ui_only || candidate.streaming {
continue;
}
let tokens = estimate_tokens(candidate.bytes) as u64;
if running + tokens > target {
break;
}
running += tokens;
selected.push(candidate);
}
let selected_seq: std::collections::HashSet<u64> = selected.iter().map(|c| c.seq).collect();
for candidate in &transcript {
let id = item_id_for_session_range(
&ContextItemKind::Transcript,
&candidate.session_id,
candidate.seq,
candidate.seq,
);
let (visibility, reason_code, reason) = if candidate.ui_only {
(
ContextVisibility::Candidate,
"ui_only_transcript",
"omitted: ui-only transcript entry",
)
} else if candidate.streaming {
(
ContextVisibility::Candidate,
"live_only_transcript",
"omitted: live-only streaming entry",
)
} else if input.dropped_ids.iter().any(|d| d == &id) {
(ContextVisibility::Dropped, "explicit_drop", "explicit drop")
} else if selected_seq.contains(&candidate.seq) {
(
ContextVisibility::Visible,
"recent_transcript",
"recent transcript entry",
)
} else {
(
ContextVisibility::Archived,
"evicted_under_budget",
"archived: evicted under budget pressure",
)
};
items.push(ContextItem {
id,
kind: ContextItemKind::Transcript,
label: candidate.label.clone(),
source_path: None,
scope: ".".to_string(),
content_hash: None,
artifact_handle: candidate.artifact_handle.clone(),
byte_count: candidate.bytes,
content: None,
token_estimate: estimate_tokens(candidate.bytes),
visibility,
reason_code: reason_code.to_string(),
reason: reason.to_string(),
});
}
}
fn should_omit_older_transcript(input: &SelectionInput, available_input: u64) -> bool {
if input.compaction_summaries.iter().all(|s| !s.latest) {
return false;
}
let target = ratio_of(available_input, super::control::TARGET_BUDGET_RATIO);
let transcript_tokens: u64 = input
.transcript
.iter()
.filter(|c| !c.ui_only && !c.streaming)
.map(|c| estimate_tokens(c.bytes) as u64)
.sum();
transcript_tokens > target
}
fn slug(label: &str) -> String {
label
.chars()
.map(|c| if c.is_alphanumeric() { c.to_ascii_lowercase() } else { '_' })
.collect::<String>()
.trim_matches('_')
.to_string()
}
fn blocked_oversized(id: &str, kind: &str) -> ContextDiagnostic {
ContextDiagnostic {
severity: DiagnosticSeverity::Warning,
code: "blocked_oversized".to_string(),
message: format!("{kind} {id} exceeds available input budget; marked blocked instead of truncating"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{ModelLimitConfidence, ModelLimitSource};
fn limits(context_window: u64) -> ModelContextLimits {
ModelContextLimits {
provider: "test".to_string(),
model: "test".to_string(),
context_window,
max_completion_tokens: 1_024,
recommended_completion_tokens: 512,
source: ModelLimitSource::LiveMetadata,
confidence: ModelLimitConfidence::Exact,
}
}
#[test]
fn pin_is_selected_and_can_be_dropped() {
let pin = PinnedCandidate::file(
ContextItemKind::PinnedFile,
PathBuf::from("/repo/src/lib.rs"),
"src",
120,
);
let input = SelectionInput { pins: vec![pin.clone()], ..Default::default() };
let visible = select_context(&input, limits(200_000));
assert!(
visible
.items
.iter()
.any(|item| item.id == pin.id && item.visibility == ContextVisibility::Pinned)
);
let dropped = SelectionInput { dropped_ids: vec![pin.id], ..input };
let ledger = select_context(&dropped, limits(200_000));
assert!(
ledger
.items
.iter()
.any(|item| item.visibility == ContextVisibility::Dropped)
);
}
#[test]
fn latest_summary_replaces_older_transcript_under_pressure() {
let input = SelectionInput {
harness: vec![HarnessCandidate::new("base", 1_000)],
user_turn: Some(UserTurnCandidate::new("session", 0, 100)),
compaction_summaries: vec![CompactionSummaryCandidate::new("session", 1, 10, 300, true)],
transcript: vec![
TranscriptCandidate::new("session", 1, "old", 5_000),
TranscriptCandidate::new("session", 2, "recent", 100),
],
..Default::default()
};
let ledger = select_context(&input, limits(4_000));
assert!(
ledger
.items
.iter()
.any(|item| item.kind == ContextItemKind::Summary && item.visibility == ContextVisibility::Visible)
);
assert!(
ledger
.items
.iter()
.any(|item| item.kind == ContextItemKind::Transcript && item.visibility == ContextVisibility::Archived)
);
}
}