use anyhow::{bail, Result};
use crate::context::ContextLimits;
use crate::retrieval_router::{
RetrievalPlan, RETRIEVAL_PLAN_SCHEMA_VERSION, RETRIEVAL_ROUTER_POLICY_VERSION,
};
use super::domain::{ContextRequest, SectionBudgets, CONTEXT_BUNDLE_SCHEMA_VERSION};
const CHARS_PER_TOKEN: u32 = 4;
pub(super) const REASON_SELECTED_CHANNEL: &str = "channel_default_selected";
pub(super) const REASON_SELECTED_RELEVANCE: &str = "relevance_selected";
pub(super) const REASON_QUARANTINED_TRUST: &str = "quarantined_trust";
pub(super) const REASON_PROJECT_SCOPE_MISMATCH: &str = "project_scope_mismatch";
pub(super) const REASON_BRANCH_SCOPE_MISMATCH: &str = "branch_scope_mismatch";
pub(super) const REASON_SUPERSEDED_EXCLUDED: &str = "superseded_excluded";
pub(super) const REASON_CANONICAL_ONLY_DEGRADED: &str = "canonical_only_degraded";
pub(super) const REASON_CHANNEL_ITEM_LIMIT: &str = "channel_item_limit";
pub(super) const REASON_CHANNEL_TOKEN_BUDGET: &str = "channel_token_budget";
pub(super) const REASON_TOTAL_TOKEN_BUDGET: &str = "total_token_budget";
pub(super) const REASON_PLAN_BLOCKED: &str = "plan_blocked";
pub(super) const REASON_CANONICAL_LOAD_FAILED: &str = "canonical_load_failed";
pub(super) fn estimate_tokens(text: &str) -> u32 {
let chars = text.chars().count() as u32;
chars.div_ceil(CHARS_PER_TOKEN)
}
pub(crate) fn validate_request(request: &ContextRequest) -> Result<()> {
if request.schema_version != CONTEXT_BUNDLE_SCHEMA_VERSION {
bail!(
"unsupported ContextRequest schema_version {} (expected {})",
request.schema_version,
CONTEXT_BUNDLE_SCHEMA_VERSION
);
}
if request.project.key.trim().is_empty() {
bail!("ContextRequest.project.key must not be empty");
}
if request.token_budget == 0 {
bail!("ContextRequest.token_budget must be greater than zero");
}
Ok(())
}
pub(super) fn validate_plan(plan: &RetrievalPlan) -> Result<()> {
if plan.schema_version != RETRIEVAL_PLAN_SCHEMA_VERSION {
bail!(
"unsupported RetrievalPlan schema_version {} (expected {})",
plan.schema_version,
RETRIEVAL_PLAN_SCHEMA_VERSION
);
}
if plan.policy_version != RETRIEVAL_ROUTER_POLICY_VERSION {
bail!(
"unsupported RetrievalPlan policy_version {:?} (expected {:?})",
plan.policy_version,
RETRIEVAL_ROUTER_POLICY_VERSION
);
}
if plan.filters.project.trim().is_empty() {
bail!("RetrievalPlan.filters.project must not be empty");
}
if plan.section_budgets.total_tokens == 0 {
bail!("RetrievalPlan.section_budgets.total_tokens must be greater than zero");
}
if plan.plan_hash.is_empty() {
bail!("RetrievalPlan.plan_hash must not be empty");
}
Ok(())
}
pub(crate) fn section_budgets(total_token_budget: u32) -> SectionBudgets {
section_budgets_from_limits(total_token_budget, &ContextLimits::default())
}
pub(crate) fn section_budgets_from_limits(
total_token_budget: u32,
limits: &ContextLimits,
) -> SectionBudgets {
let to_tokens = |chars: usize| (chars as u32).div_ceil(CHARS_PER_TOKEN);
SectionBudgets {
total_tokens: total_token_budget,
preferences: to_tokens(limits.preference_char_limit),
lessons: to_tokens(limits.lesson_char_limit),
core: to_tokens(limits.core_char_limit),
workstreams: to_tokens(1_200),
memory_index: to_tokens(limits.memory_index_char_limit),
sessions: to_tokens(2_200),
}
}