use serde::Serialize;
use crate::brain::provider::{ContentBlock, LLMRequest, Message};
use crate::brain::tokenizer::count_tokens;
use crate::brain::tools::catalog::is_core;
const BRAIN_FILE_NAMES: &[&str] = &[
"SOUL.md",
"USER.md",
"AGENTS.md",
"TOOLS.md",
"CODE.md",
"SECURITY.md",
"MEMORY.md",
"BOOT.md",
"HEARTBEAT.md",
];
const IMAGE_TOKEN_ESTIMATE: usize = 1000;
const MESSAGE_OVERHEAD_TOKENS: usize = 4;
#[derive(Debug, Clone, Serialize)]
pub struct ToolEntry {
pub name: String,
pub schema_tokens: usize,
pub is_core: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ContextManifest {
pub system_brain_bytes: usize,
pub system_brain_tokens: usize,
pub brain_files_present: Vec<String>,
pub message_count: usize,
pub message_tokens: usize,
pub tools: Vec<ToolEntry>,
pub tool_schema_tokens: usize,
pub core_tool_count: usize,
pub extended_tool_count: usize,
pub total_input_tokens: usize,
}
impl ContextManifest {
pub fn from_request(request: &LLMRequest) -> Self {
let system = request.system.as_deref().unwrap_or("");
let system_brain_bytes = system.len();
let system_brain_tokens = count_tokens(system);
let brain_files_present = BRAIN_FILE_NAMES
.iter()
.filter(|n| system.contains(**n))
.map(|n| n.to_string())
.collect();
let message_count = request.messages.len();
let message_tokens = request.messages.iter().map(message_tokens).sum();
let mut tools = Vec::new();
let mut tool_schema_tokens = 0;
let mut core_tool_count = 0;
let mut extended_tool_count = 0;
if let Some(defs) = &request.tools {
for t in defs {
let schema = serde_json::to_string(&t.input_schema).unwrap_or_default();
let schema_tokens =
count_tokens(&t.name) + count_tokens(&t.description) + count_tokens(&schema);
let core = is_core(&t.name);
if core {
core_tool_count += 1;
} else {
extended_tool_count += 1;
}
tool_schema_tokens += schema_tokens;
tools.push(ToolEntry {
name: t.name.clone(),
schema_tokens,
is_core: core,
});
}
}
let total_input_tokens = system_brain_tokens + message_tokens + tool_schema_tokens;
Self {
system_brain_bytes,
system_brain_tokens,
brain_files_present,
message_count,
message_tokens,
tools,
tool_schema_tokens,
core_tool_count,
extended_tool_count,
total_input_tokens,
}
}
pub fn has_tool(&self, name: &str) -> bool {
self.tools.iter().any(|t| t.name == name)
}
pub fn has_brain_file(&self, name: &str) -> bool {
self.brain_files_present.iter().any(|f| f == name)
}
}
fn message_tokens(m: &Message) -> usize {
let mut total = MESSAGE_OVERHEAD_TOKENS;
for block in &m.content {
total += match block {
ContentBlock::Text { text } => count_tokens(text),
ContentBlock::ToolUse { name, input, .. } => {
count_tokens(name) + count_tokens(&input.to_string())
}
ContentBlock::ToolResult { content, .. } => count_tokens(content),
ContentBlock::Thinking { thinking, .. } => count_tokens(thinking),
ContentBlock::Image { .. } => IMAGE_TOKEN_ESTIMATE,
};
}
total
}