use anyhow::Result;
use std::path::Path;
use crate::token_count::estimate_content_tokens;
#[derive(Debug, Clone)]
pub struct TokenBudget {
total: usize,
used: usize,
reserved: usize,
}
#[derive(Debug, Clone)]
pub struct FileEstimate {
pub min_tokens: usize,
pub tokens: usize,
pub max_tokens: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Depth {
Overview,
Signatures,
Full,
Dependencies,
}
impl Depth {
pub fn parse(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"overview" => Ok(Self::Overview),
"signatures" => Ok(Self::Signatures),
"full" => Ok(Self::Full),
"dependencies" => Ok(Self::Dependencies),
_ => anyhow::bail!("Unknown depth level: {}", s),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Overview => "overview",
Self::Signatures => "signatures",
Self::Full => "full",
Self::Dependencies => "dependencies",
}
}
}
impl std::fmt::Display for Depth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Depth {
pub fn default_tokens(&self) -> usize {
match self {
Self::Overview => 50,
Self::Signatures => 200,
Self::Full => 1000,
Self::Dependencies => 300,
}
}
pub fn downgrade(&self) -> Option<Self> {
match self {
Self::Full => Some(Self::Signatures),
Self::Signatures => Some(Self::Overview),
Self::Overview => None,
Self::Dependencies => Some(Self::Signatures),
}
}
}
impl TokenBudget {
pub fn new(total: usize) -> Self {
Self {
total,
used: 0,
reserved: 0,
}
}
pub fn reserve(&mut self, percent: usize) {
self.reserved = (self.total * percent) / 100;
}
pub fn exhausted(&self) -> bool {
self.used + self.reserved >= self.total
}
pub fn remaining(&self) -> usize {
self.total.saturating_sub(self.used + self.reserved)
}
pub fn used(&self) -> usize {
self.used
}
pub fn allocate(&mut self, requested: usize) -> usize {
let available = self.remaining();
let granted = requested.min(available);
self.used += granted;
granted
}
pub async fn estimate_file(&self, path: &Path, depth: &Depth) -> Result<FileEstimate> {
let content = match tokio::fs::read_to_string(path).await {
Ok(c) => c,
Err(_) => {
let base = depth.default_tokens();
return Ok(FileEstimate {
min_tokens: base / 2,
tokens: base,
max_tokens: base * 2,
});
}
};
let total_tokens = estimate_content_tokens(&content);
let estimate = match depth {
Depth::Overview => {
let tokens = (total_tokens / 20).clamp(20, 100);
FileEstimate {
min_tokens: 10,
tokens,
max_tokens: tokens + 50,
}
}
Depth::Signatures => {
let tokens = (total_tokens / 5).clamp(50, 500);
FileEstimate {
min_tokens: 20,
tokens,
max_tokens: tokens + 200,
}
}
Depth::Full => {
FileEstimate {
min_tokens: total_tokens / 2,
tokens: total_tokens,
max_tokens: total_tokens + 100,
}
}
Depth::Dependencies => {
let tokens = (total_tokens / 10).clamp(50, 500);
FileEstimate {
min_tokens: 30,
tokens,
max_tokens: tokens + 100,
}
}
};
Ok(estimate)
}
pub fn suggest_depth(&self, files: &[FileMeta]) -> Depth {
let available = self.remaining();
if files.is_empty() {
return Depth::Signatures;
}
let overview_coverage = available / Depth::Overview.default_tokens();
let signatures_coverage = available / Depth::Signatures.default_tokens();
let full_coverage = available / Depth::Full.default_tokens();
let total_files = files.len();
if signatures_coverage >= total_files {
Depth::Signatures
} else if overview_coverage >= total_files {
Depth::Overview
} else if full_coverage >= 1 {
Depth::Signatures
} else {
Depth::Overview
}
}
pub async fn suggest_depth_for_file(&self, path: &Path) -> Result<Depth> {
let remaining = self.remaining();
for depth in [Depth::Full, Depth::Signatures, Depth::Overview] {
let estimate = self.estimate_file(path, &depth).await?;
if estimate.tokens <= remaining {
return Ok(depth);
}
}
Ok(Depth::Overview)
}
pub fn utilization_pct(&self) -> f64 {
if self.total == 0 {
return 0.0;
}
(self.used as f64 / self.total as f64) * 100.0
}
}
#[derive(Debug, Clone)]
pub struct FileMeta {
pub path: String,
pub size_bytes: u64,
pub language: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PlanBudget {
pub max_iterations: usize,
pub max_tokens: usize,
pub current_iteration: usize,
pub token_budget: TokenBudget,
}
impl PlanBudget {
pub fn new(max_iterations: usize, max_tokens: usize) -> Self {
Self {
max_iterations,
max_tokens,
current_iteration: 0,
token_budget: TokenBudget::new(max_tokens),
}
}
pub fn iterations_remaining(&self) -> usize {
self.max_iterations.saturating_sub(self.current_iteration)
}
pub fn iteration_exhausted(&self) -> bool {
self.current_iteration >= self.max_iterations
}
pub fn next_iteration(&mut self) {
self.current_iteration += 1;
}
}
#[cfg(test)]
#[path = "../../../tests/unit/tools/introspect/budget/budget_test.rs"]
mod tests;