use lean_ctx::core::context_field::{ContextItemId, ContextState};
use lean_ctx::core::context_ledger::{ContextLedger, PressureAction};
use lean_ctx::core::context_overlay::{OverlayOp, OverlayStore};
pub(super) fn protected_path_requested(
args: Option<&serde_json::Map<String, serde_json::Value>>,
) -> bool {
let Some(args) = args else {
return false;
};
let config = lean_ctx::core::config::Config::load();
if config.protection.file_patterns.is_empty() {
return false;
}
["path", "file_path", "filePath"].iter().any(|key| {
args.get(*key)
.and_then(serde_json::Value::as_str)
.is_some_and(|path| config.protection.path_is_protected(path))
})
}
fn is_precise_pinned_mode(requested_mode: &str) -> bool {
requested_mode
.parse::<crate::tools::ctx_read::ReadMode>()
.is_ok_and(|m| m.is_precise_pinned_read())
}
#[derive(Debug, Clone)]
pub struct PreDispatchResult {
pub overridden_mode: Option<String>,
pub reason: Option<&'static str>,
pub budget_blocked: bool,
pub budget_warning: Option<String>,
pub triage_filter_level: u8,
}
#[derive(Debug, Clone)]
pub struct PostDispatchResult {
pub eviction_hint: Option<String>,
pub elicitation_hint: Option<String>,
pub resource_changed: bool,
}
pub fn knowledge_advice(
query: &str,
) -> lean_ctx::core::knowledge_router::gate_integration::KnowledgeAdvice {
let current = lean_ctx::core::context_kernel::ContextState::new(
Vec::new(),
lean_ctx::core::knowledge::KnowledgeQuery::default(),
);
lean_ctx::core::knowledge_router::gate_integration::KnowledgeGateAdvisor::advise(
query, ¤t,
)
}
pub fn pre_dispatch_read(
path: &str,
requested_mode: &str,
task: Option<&str>,
project_root: Option<&str>,
) -> PreDispatchResult {
pre_dispatch_read_for_agent(path, requested_mode, task, project_root, None, false)
}
enum AgentBudgetDecision {
Allowed,
Warning(String),
Exceeded(PreDispatchResult),
}
fn no_change_result() -> PreDispatchResult {
PreDispatchResult {
overridden_mode: None,
reason: None,
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
}
}
fn check_agent_budget(
path: &str,
requested_mode: &str,
agent_id: Option<&str>,
) -> AgentBudgetDecision {
let Some(aid) = agent_id else {
return AgentBudgetDecision::Allowed;
};
let estimated_tokens = estimate_read_tokens(path, requested_mode);
match lean_ctx::core::agent_budget::check_budget(aid, estimated_tokens) {
lean_ctx::core::agent_budget::BudgetCheckResult::Exceeded { limit, consumed } => {
let mut result = no_change_result();
result.reason = Some("agent-budget-exceeded");
result.budget_blocked = true;
result.budget_warning = Some(format!(
"Agent budget exceeded: {consumed}/{limit} tokens consumed. Reset via ctx_session or set a higher limit."
));
AgentBudgetDecision::Exceeded(result)
}
lean_ctx::core::agent_budget::BudgetCheckResult::Warning {
remaining,
percent_used,
} => AgentBudgetDecision::Warning(format!(
"[BUDGET WARNING] Agent '{aid}' at {:.0}% budget ({remaining} tokens remaining)",
percent_used * 100.0
)),
lean_ctx::core::agent_budget::BudgetCheckResult::Allowed { .. } => {
AgentBudgetDecision::Allowed
}
}
}
pub fn pre_dispatch_pinned_read_for_agent(
path: &str,
requested_mode: &str,
agent_id: Option<&str>,
) -> PreDispatchResult {
let no_change = no_change_result();
match check_agent_budget(path, requested_mode, agent_id) {
AgentBudgetDecision::Exceeded(result) => result,
AgentBudgetDecision::Warning(warning) => PreDispatchResult {
budget_warning: Some(warning),
..no_change
},
AgentBudgetDecision::Allowed => no_change,
}
}
pub fn pre_dispatch_read_for_agent(
path: &str,
requested_mode: &str,
task: Option<&str>,
project_root: Option<&str>,
agent_id: Option<&str>,
fresh: bool,
) -> PreDispatchResult {
let no_change = no_change_result();
match check_agent_budget(path, requested_mode, agent_id) {
AgentBudgetDecision::Exceeded(result) => return result,
AgentBudgetDecision::Warning(warning) => {
let mut result = no_change.clone();
result.budget_warning = Some(warning);
if is_precise_pinned_mode(requested_mode) {
return result;
}
let rest = pre_dispatch_inner(path, requested_mode, task, project_root, fresh);
return PreDispatchResult {
budget_warning: result.budget_warning,
..rest
};
}
AgentBudgetDecision::Allowed => {}
}
pre_dispatch_inner(path, requested_mode, task, project_root, fresh)
}
fn path_matches_target(norm: &str, target: &str) -> bool {
const MIN_TARGET_LEN: usize = 4;
let target = target.trim().trim_matches('/');
if target.len() < MIN_TARGET_LEN {
return false;
}
if norm == target || norm.ends_with(&format!("/{target}")) {
return true;
}
norm.split('/').any(|component| {
component == target
|| component
.rsplit_once('.')
.is_some_and(|(stem, _)| stem == target)
})
}
fn is_explicit_compressing_mode(mode: &str) -> bool {
use crate::tools::ctx_read::ReadMode;
mode.parse::<ReadMode>().is_ok_and(|parsed| {
parsed.is_lossy_summary() || matches!(parsed, ReadMode::Reference | ReadMode::Density(_))
})
}
fn pre_dispatch_inner(
path: &str,
requested_mode: &str,
task: Option<&str>,
project_root: Option<&str>,
fresh: bool,
) -> PreDispatchResult {
let no_change = PreDispatchResult {
overridden_mode: None,
reason: None,
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
if is_precise_pinned_mode(requested_mode) {
return no_change;
}
if let Some(root) = project_root {
let overlay = OverlayStore::load_project(&std::path::PathBuf::from(root));
if let Some(result) = check_overlay_mode_override(path, requested_mode, &overlay) {
return result;
}
}
if requested_mode == "full" {
return no_change;
}
if !fresh
&& let Ok(bt) = lean_ctx::core::bounce_tracker::global().lock()
&& bt.should_force_full(path)
{
return PreDispatchResult {
overridden_mode: Some("full".to_string()),
reason: Some("bounce-prevention"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
if !fresh
&& is_explicit_compressing_mode(requested_mode)
&& lean_ctx::core::path_mode_memory::should_force_full(path)
{
return PreDispatchResult {
overridden_mode: Some("full".to_string()),
reason: Some("learned-full"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
if let Some(task_str) = task
&& !fresh
{
let intent = lean_ctx::core::intent_engine::StructuredIntent::from_query(task_str);
let norm = lean_ctx::core::pathutil::normalize_tool_path(path);
let is_target = intent.targets.iter().any(|t| path_matches_target(&norm, t));
if is_target {
return PreDispatchResult {
overridden_mode: Some("full".to_string()),
reason: Some("intent-target"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
}
if let Some(root) = project_root
&& let Some(open) = try_load_graph(root)
{
let gp = &open.provider;
let related = gp.related(path, 1);
if let Some(task_str) = task {
let intent = lean_ctx::core::intent_engine::StructuredIntent::from_query(task_str);
for target in &intent.targets {
let target_related = gp.related(target, 1);
let norm = lean_ctx::core::pathutil::normalize_tool_path(path);
if target_related
.iter()
.any(|r| r.contains(&norm) || norm.contains(r))
{
return PreDispatchResult {
overridden_mode: Some("map".to_string()),
reason: Some("graph-direct-import"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
}
}
if !related.is_empty() && requested_mode == "auto" {
let reverse_deps = gp.dependents(path);
if reverse_deps.len() > 3 {
return PreDispatchResult {
overridden_mode: Some("map".to_string()),
reason: Some("graph-hub-file"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
}
}
if let Some(root) = project_root
&& let Some(knowledge) = lean_ctx::core::knowledge::ProjectKnowledge::load(root)
{
let norm = lean_ctx::core::pathutil::normalize_tool_path(path);
let mentions = knowledge
.facts
.iter()
.filter(|f| f.value.contains(&norm) || f.key.contains(&norm))
.count();
if mentions >= 3 {
return PreDispatchResult {
overridden_mode: Some("map".to_string()),
reason: Some("knowledge-high-relevance"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
};
}
}
no_change
}
fn estimate_read_tokens(path: &str, mode: &str) -> usize {
let file_size = std::fs::metadata(path).map_or(4000, |m| m.len() as usize);
let char_estimate = file_size;
let full_tokens = char_estimate / 4;
match mode {
"signatures" => full_tokens / 5,
"map" => full_tokens / 3,
"aggressive" | "entropy" => full_tokens / 4,
"diff" => full_tokens / 10,
_ if mode.starts_with("lines:") => {
if let Some(range) = mode.strip_prefix("lines:") {
let mut selected = 0usize;
for part in range.split(',') {
let part = part.trim();
selected += if let Some((s, e)) = part.split_once('-') {
let start = s.trim().parse::<usize>().unwrap_or(1);
let end = e.trim().parse::<usize>().unwrap_or(start + 100);
end.saturating_sub(start) + 1
} else {
1
};
}
if selected == 0 {
full_tokens / 10
} else {
selected * 10
}
} else {
full_tokens / 10
}
}
_ => full_tokens,
}
}
fn check_overlay_mode_override(
path: &str,
requested_mode: &str,
overlay: &OverlayStore,
) -> Option<PreDispatchResult> {
let item_id = ContextItemId::from_file(path);
let overlays = overlay.for_item(&item_id);
for ov in overlays.iter().rev() {
match &ov.operation {
OverlayOp::SetView(view) => {
let mode_str = view.as_str();
if mode_str != requested_mode {
return Some(PreDispatchResult {
overridden_mode: Some(mode_str.to_string()),
reason: Some("overlay-set-view"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
});
}
}
OverlayOp::Pin { .. } if requested_mode != "full" => {
return Some(PreDispatchResult {
overridden_mode: Some("full".to_string()),
reason: Some("pinned"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
});
}
OverlayOp::Exclude { .. } if requested_mode != "signatures" => {
return Some(PreDispatchResult {
overridden_mode: Some("signatures".to_string()),
reason: Some("excluded"),
budget_blocked: false,
budget_warning: None,
triage_filter_level: 0,
});
}
_ => {}
}
}
None
}
pub fn post_dispatch_record(
path: &str,
mode: &str,
original_tokens: usize,
sent_tokens: usize,
ledger: &mut ContextLedger,
overlay: &OverlayStore,
) -> PostDispatchResult {
post_dispatch_record_with_task(
path,
mode,
original_tokens,
sent_tokens,
ledger,
overlay,
None,
None,
)
}
pub fn post_dispatch_record_with_task(
path: &str,
mode: &str,
original_tokens: usize,
sent_tokens: usize,
ledger: &mut ContextLedger,
overlay: &OverlayStore,
task: Option<&str>,
project_root: Option<&str>,
) -> PostDispatchResult {
let prev_count = ledger.entries.len();
let prev_pressure = ledger.pressure().recommendation;
ledger.record_with_task(path, mode, original_tokens, sent_tokens, task);
let item_id = ContextItemId::from_file(path);
let state = overlay.apply_to_state(&item_id, ContextState::Included);
if state == ContextState::Excluded {
return PostDispatchResult {
eviction_hint: Some(format!("File '{path}' is excluded by overlay.")),
elicitation_hint: None,
resource_changed: true,
};
}
let elicitation =
super::elicitation::check_elicitation_needed(ledger, Some(path), Some(sent_tokens))
.map(|s| s.format_fallback_hint());
let pressure = ledger.pressure();
let ignited = ledger.ignite_high_salience();
apply_reinjection_plan(ledger, &pressure.recommendation);
let new_entry = ledger.entries.len() != prev_count;
let pressure_shifted = pressure.recommendation != prev_pressure;
let resource_changed = new_entry || pressure_shifted || !ignited.is_empty();
if pressure.utilization > 0.9 {
let candidates = ledger.eviction_candidates_by_phi(3);
if !candidates.is_empty() {
let names: Vec<_> = candidates
.iter()
.take(3)
.map(|p| eviction_target_display(p, project_root))
.collect();
return PostDispatchResult {
eviction_hint: Some(format!(
"Context pressure {:.0}%. Evict: ctx_control(action=\"evict\", targets=\"{}\")",
pressure.utilization * 100.0,
names.join(", ")
)),
elicitation_hint: elicitation,
resource_changed,
};
}
}
PostDispatchResult {
eviction_hint: None,
elicitation_hint: elicitation,
resource_changed,
}
}
fn eviction_target_display(path: &str, project_root: Option<&str>) -> String {
if let Some(root) = project_root.filter(|r| !r.is_empty()) {
let root_prefix = format!("{}/", root.trim_end_matches(['/', '\\']).replace('\\', "/"));
if let Some(rel) = path.strip_prefix(&root_prefix)
&& !rel.is_empty()
{
return rel.to_string();
}
}
path.to_string()
}
fn apply_reinjection_plan(ledger: &mut ContextLedger, action: &PressureAction) {
if *action != PressureAction::ForceCompression && *action != PressureAction::EvictLeastRelevant
{
return;
}
for entry in &mut ledger.entries {
if entry.state == Some(ContextState::Pinned) {
continue;
}
if entry.mode == "full" {
entry.mode = "map".to_string();
}
}
}
fn try_load_graph(project_root: &str) -> Option<lean_ctx::core::graph_provider::OpenGraphProvider> {
lean_ctx::core::graph_provider::open_ready(project_root)
}
pub fn triage_filter_level(profile: &lean_ctx::core::triage::profile::TaskProfileLocal) -> u8 {
use lean_ctx::core::triage::confidence::ACTIONABLE_FLOOR_MILLI;
if profile.confidence_milli < ACTIONABLE_FLOOR_MILLI || profile.context_need_milli == 0 {
return 0;
}
u8::from(profile.context_need_milli < 600)
}
pub fn apply_triage_filter(
output: &str,
profile: &lean_ctx::core::triage::profile::TaskProfileLocal,
level: u8,
) -> (String, usize) {
if level == 0 || output.chars().count() < 500 {
return (output.to_string(), 0);
}
let trimmed = output.trim_start();
if (trimmed.starts_with('{') || trimmed.starts_with('['))
&& serde_json::from_str::<serde_json::Value>(trimmed).is_ok()
{
return (output.to_string(), 0);
}
let lines: Vec<&str> = output.lines().collect();
if let Some(json_start) = lines.iter().position(|l| {
let t = l.trim_start();
t.starts_with('{') || t.starts_with('[')
}) {
let json_portion = lines[json_start..].join("\n");
if serde_json::from_str::<serde_json::Value>(&json_portion).is_ok() {
return (output.to_string(), 0);
}
}
if output.contains("<protect>") && lean_ctx::core::config::Config::load().protection.tags {
return (output.to_string(), 0);
}
let keep: std::collections::HashSet<usize> = match level {
1 => lines
.iter()
.enumerate()
.filter(|(_, line)| !is_boilerplate_line(line.trim_start()))
.map(|(i, _)| i)
.collect(),
2 => {
let keywords = extract_task_keywords(&profile.task_class, &profile.intent);
if lean_ctx::core::triage::markdown::looks_like_markdown(&lines) {
lean_ctx::core::triage::markdown::keep_indices(&lines, &keywords)
} else {
lines
.iter()
.enumerate()
.filter(|(_, line)| !is_boilerplate_line(line.trim_start()))
.map(|(i, _)| i)
.collect()
}
}
_ => return (output.to_string(), 0),
};
let removed = lines.len().saturating_sub(keep.len());
if removed == 0 {
return (output.to_string(), 0);
}
if keep.is_empty() {
return (output.to_string(), 0);
}
if lean_ctx::core::triage::markdown::is_heading_only_collapse(&lines, &keep) {
return (output.to_string(), 0);
}
let mut result = String::with_capacity(output.len());
let mut consecutive_omitted: usize = 0;
for (i, line) in lines.iter().enumerate() {
if keep.contains(&i) {
if consecutive_omitted > 0 {
result.push_str(&format!(
" [...{consecutive_omitted} lines omitted by triage...]\n"
));
consecutive_omitted = 0;
}
result.push_str(line);
result.push('\n');
} else {
consecutive_omitted += 1;
}
}
if consecutive_omitted > 0 {
result.push_str(&format!(
" [...{consecutive_omitted} lines omitted by triage...]\n"
));
}
result.push_str(&format!(
"[lean-ctx: {removed} lines filtered by triage (level {level}) — rerun with raw=true for the unfiltered output]"
));
(result, removed)
}
fn extract_task_keywords(task_class: &str, intent: &str) -> Vec<String> {
let mut keywords = task_class
.split(|character: char| !character.is_alphanumeric())
.chain(intent.split(|character: char| !character.is_alphanumeric()))
.filter(|keyword| !keyword.is_empty())
.map(str::to_lowercase)
.collect::<Vec<_>>();
keywords.sort_unstable();
keywords.dedup();
keywords
}
fn is_boilerplate_line(line: &str) -> bool {
line.starts_with("//")
&& !line.contains("TODO")
&& !line.contains("FIXME")
&& !line.contains("SAFETY")
}
#[cfg(test)]
#[path = "context_gate_tests.rs"]
mod tests;