use alloc::string::{String, ToString};
use core::fmt;
use unicode_segmentation::UnicodeSegmentation;
pub const TOKENIZER_VERSION: &str = "shepherd-prompt-v1-uax29";
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BudgetClass {
Skill,
AlwaysLoadedSkill,
Role,
Reference,
Doctrine,
Command,
AlwaysLoadedBundle,
HarnessSkillSet,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Measurement {
pub lines: usize,
pub words: usize,
pub utf8_bytes: usize,
pub prompt_tokens: usize,
}
pub struct BudgetLimits;
impl BudgetLimits {
#[must_use]
pub const fn for_class(class: BudgetClass) -> (usize, usize, usize) {
match class {
BudgetClass::Skill => (100, 500, 6 * 1024),
BudgetClass::AlwaysLoadedSkill => (60, 200, 3 * 1024),
BudgetClass::Role => (100, 600, 7 * 1024),
BudgetClass::Reference => (220, 1_500, 16 * 1024),
BudgetClass::Doctrine => (160, 1_000, 12 * 1024),
BudgetClass::Command => (140, 750, 9 * 1024),
BudgetClass::AlwaysLoadedBundle => (300, 2_000, 22 * 1024),
BudgetClass::HarnessSkillSet => (700, 3_500, 42 * 1024),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BudgetError {
Empty {
name: String,
},
Exceeded {
name: String,
metric: &'static str,
actual: usize,
limit: usize,
},
}
impl fmt::Display for BudgetError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty { name } => write!(formatter, "{name}: prompt surface is empty"),
Self::Exceeded {
name,
metric,
actual,
limit,
} => write!(
formatter,
"{name}: {metric} budget exceeded ({actual} > {limit})"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for BudgetError {}
#[must_use]
pub fn measure_text(text: &str) -> Measurement {
let words = text.unicode_words().count();
let prompt_tokens = text
.split_word_bounds()
.map(|segment| {
if segment.chars().all(char::is_whitespace) {
0
} else if segment.unicode_words().next().is_some() {
segment.len().div_ceil(4).max(1)
} else {
segment
.chars()
.filter(|character| !character.is_whitespace())
.count()
}
})
.sum();
Measurement {
lines: text.lines().count(),
words,
utf8_bytes: text.len(),
prompt_tokens,
}
}
pub fn validate_budget(
name: &str,
class: BudgetClass,
text: &str,
) -> Result<Measurement, BudgetError> {
if text.is_empty() {
return Err(BudgetError::Empty {
name: name.to_string(),
});
}
let measured = measure_text(text);
let (line_limit, word_limit, byte_limit) = BudgetLimits::for_class(class);
for (metric, actual, limit) in [
("lines", measured.lines, line_limit),
("words", measured.words, word_limit),
("utf8_bytes", measured.utf8_bytes, byte_limit),
] {
if actual > limit {
return Err(BudgetError::Exceeded {
name: name.to_string(),
metric,
actual,
limit,
});
}
}
Ok(measured)
}