agent_sdk/context/
estimator.rs1use crate::llm::{Content, ContentBlock, Message};
4
5pub struct TokenEstimator;
13
14impl TokenEstimator {
15 const CHARS_PER_TOKEN: usize = 4;
18
19 const MESSAGE_OVERHEAD: usize = 4;
21
22 const TOOL_USE_OVERHEAD: usize = 20;
24
25 const TOOL_RESULT_OVERHEAD: usize = 10;
27
28 #[must_use]
30 pub const fn estimate_text(text: &str) -> usize {
31 text.len().div_ceil(Self::CHARS_PER_TOKEN)
33 }
34
35 #[must_use]
37 pub fn estimate_message(message: &Message) -> usize {
38 let content_tokens = match &message.content {
39 Content::Text(text) => Self::estimate_text(text),
40 Content::Blocks(blocks) => blocks.iter().map(Self::estimate_block).sum(),
41 };
42
43 content_tokens + Self::MESSAGE_OVERHEAD
44 }
45
46 #[must_use]
48 pub fn estimate_block(block: &ContentBlock) -> usize {
49 match block {
50 ContentBlock::Text { text } => Self::estimate_text(text),
51 ContentBlock::Thinking { thinking } => Self::estimate_text(thinking),
52 ContentBlock::ToolUse { name, input, .. } => {
53 let input_str = serde_json::to_string(input).unwrap_or_default();
54 Self::estimate_text(name)
55 + Self::estimate_text(&input_str)
56 + Self::TOOL_USE_OVERHEAD
57 }
58 ContentBlock::ToolResult { content, .. } => {
59 Self::estimate_text(content) + Self::TOOL_RESULT_OVERHEAD
60 }
61 }
62 }
63
64 #[must_use]
66 pub fn estimate_history(messages: &[Message]) -> usize {
67 messages.iter().map(Self::estimate_message).sum()
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::llm::Role;
75 use serde_json::json;
76
77 #[test]
78 fn test_estimate_text() {
79 assert_eq!(TokenEstimator::estimate_text(""), 0);
81
82 assert_eq!(TokenEstimator::estimate_text("hi"), 1);
84
85 assert_eq!(TokenEstimator::estimate_text("test"), 1);
87
88 assert_eq!(TokenEstimator::estimate_text("hello"), 2);
90
91 assert_eq!(TokenEstimator::estimate_text("hello world!"), 3); }
94
95 #[test]
96 fn test_estimate_text_message() {
97 let message = Message {
98 role: Role::User,
99 content: Content::Text("Hello, how are you?".to_string()), };
101
102 let estimate = TokenEstimator::estimate_message(&message);
103 assert_eq!(estimate, 9);
105 }
106
107 #[test]
108 fn test_estimate_blocks_message() {
109 let message = Message {
110 role: Role::Assistant,
111 content: Content::Blocks(vec![
112 ContentBlock::Text {
113 text: "Let me help.".to_string(), },
115 ContentBlock::ToolUse {
116 id: "tool_123".to_string(),
117 name: "read".to_string(), input: json!({"path": "/test.txt"}), thought_signature: None,
120 },
121 ]),
122 };
123
124 let estimate = TokenEstimator::estimate_message(&message);
125 assert!(estimate > 25); }
131
132 #[test]
133 fn test_estimate_tool_result() {
134 let message = Message {
135 role: Role::User,
136 content: Content::Blocks(vec![ContentBlock::ToolResult {
137 tool_use_id: "tool_123".to_string(),
138 content: "File contents here...".to_string(), is_error: None,
140 }]),
141 };
142
143 let estimate = TokenEstimator::estimate_message(&message);
144 assert_eq!(estimate, 20);
146 }
147
148 #[test]
149 fn test_estimate_history() {
150 let messages = vec![
151 Message::user("Hello"), Message::assistant("Hi there!"), Message::user("How are you?"), ];
155
156 let estimate = TokenEstimator::estimate_history(&messages);
157 assert_eq!(estimate, 20);
158 }
159
160 #[test]
161 fn test_empty_history() {
162 let messages: Vec<Message> = vec![];
163 assert_eq!(TokenEstimator::estimate_history(&messages), 0);
164 }
165}