Skip to main content

langchain_rust/memory/
token_buffer.rs

1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use tiktoken_rs::{bpe_for_model, cl100k_base_singleton};
6use tokio::sync::Mutex;
7
8use crate::schemas::{
9    memory::{BaseMemory, MemoryError},
10    messages::{Message, MessageType},
11};
12
13type TokenCounter = Arc<dyn Fn(&str) -> usize + Send + Sync>;
14
15/// Conversation memory that keeps the most recent messages that fit in `max_tokens`.
16///
17/// When a new message pushes the buffer over the limit, the oldest messages are dropped
18/// until it fits again. A tool result left at the front without the AI message that
19/// requested it is dropped as well, so the history stays valid for chat APIs.
20/// A single message bigger than `max_tokens` is not kept.
21///
22/// Tokens are counted with tiktoken (`cl100k_base` by default) over each message's
23/// content plus its tool calls, which is an approximation for non-OpenAI models.
24///
25/// ```rust,ignore
26/// let memory = TokenBufferMemory::new(2000).with_model("gpt-4o")?;
27/// ```
28pub struct TokenBufferMemory {
29    max_tokens: usize,
30    counter: TokenCounter,
31    messages: VecDeque<(Message, usize)>,
32    total_tokens: usize,
33}
34
35impl TokenBufferMemory {
36    pub fn new(max_tokens: usize) -> Self {
37        Self {
38            max_tokens,
39            counter: Arc::new(|text: &str| cl100k_base_singleton().encode_ordinary(text).len()),
40            messages: VecDeque::new(),
41            total_tokens: 0,
42        }
43    }
44
45    /// Count tokens with the tokenizer tiktoken uses for `model` (e.g. `gpt-4o`).
46    pub fn with_model(mut self, model: &str) -> Result<Self, MemoryError> {
47        let bpe = bpe_for_model(model).map_err(|e| MemoryError::Other(e.to_string()))?;
48        self.counter = Arc::new(move |text: &str| bpe.encode_ordinary(text).len());
49        Ok(self)
50    }
51
52    /// Use a custom token counter, e.g. one matching a non-OpenAI model's tokenizer.
53    pub fn with_token_counter<F>(mut self, counter: F) -> Self
54    where
55        F: Fn(&str) -> usize + Send + Sync + 'static,
56    {
57        self.counter = Arc::new(counter);
58        self
59    }
60
61    /// Tokens currently held in the buffer.
62    pub fn total_tokens(&self) -> usize {
63        self.total_tokens
64    }
65
66    fn count(&self, message: &Message) -> usize {
67        let mut tokens = (self.counter)(&message.content);
68        if let Some(tool_calls) = &message.tool_calls {
69            tokens += (self.counter)(&tool_calls.to_string());
70        }
71        tokens
72    }
73
74    fn pop_front(&mut self) {
75        if let Some((_, tokens)) = self.messages.pop_front() {
76            self.total_tokens -= tokens;
77        }
78    }
79
80    fn prune(&mut self) {
81        while self.total_tokens > self.max_tokens && !self.messages.is_empty() {
82            self.pop_front();
83        }
84        while matches!(
85            self.messages.front(),
86            Some((m, _)) if m.message_type == MessageType::ToolMessage
87        ) {
88            self.pop_front();
89        }
90    }
91}
92
93impl From<TokenBufferMemory> for Arc<dyn BaseMemory> {
94    fn from(memory: TokenBufferMemory) -> Self {
95        Arc::new(memory)
96    }
97}
98
99impl From<TokenBufferMemory> for Arc<Mutex<dyn BaseMemory>> {
100    fn from(memory: TokenBufferMemory) -> Self {
101        Arc::new(Mutex::new(memory))
102    }
103}
104
105#[async_trait]
106impl BaseMemory for TokenBufferMemory {
107    async fn messages(&self) -> Result<Vec<Message>, MemoryError> {
108        Ok(self.messages.iter().map(|(m, _)| m.clone()).collect())
109    }
110
111    async fn add_message(&mut self, message: Message) -> Result<(), MemoryError> {
112        let tokens = self.count(&message);
113        self.total_tokens += tokens;
114        self.messages.push_back((message, tokens));
115        self.prune();
116        Ok(())
117    }
118
119    async fn clear(&mut self) -> Result<(), MemoryError> {
120        self.messages.clear();
121        self.total_tokens = 0;
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use serde_json::json;
130
131    /// One token per whitespace-separated word, so limits are easy to reason about.
132    fn words(max_tokens: usize) -> TokenBufferMemory {
133        TokenBufferMemory::new(max_tokens).with_token_counter(|t| t.split_whitespace().count())
134    }
135
136    async fn contents(memory: &TokenBufferMemory) -> Vec<String> {
137        memory
138            .messages()
139            .await
140            .unwrap()
141            .into_iter()
142            .map(|m| m.content)
143            .collect()
144    }
145
146    #[tokio::test]
147    async fn keeps_everything_under_the_limit() {
148        let mut memory = words(10);
149        memory.add_user_message(&"one two three").await.unwrap();
150        memory.add_ai_message(&"four five").await.unwrap();
151        assert_eq!(memory.total_tokens(), 5);
152        assert_eq!(contents(&memory).await, vec!["one two three", "four five"]);
153    }
154
155    #[tokio::test]
156    async fn drops_oldest_messages_when_over_the_limit() {
157        let mut memory = words(5);
158        memory.add_user_message(&"a b").await.unwrap();
159        memory.add_ai_message(&"c d").await.unwrap();
160        memory.add_user_message(&"e f").await.unwrap(); // 6 > 5 -> drop "a b"
161        assert_eq!(contents(&memory).await, vec!["c d", "e f"]);
162        assert_eq!(memory.total_tokens(), 4);
163
164        memory.add_ai_message(&"g h i j").await.unwrap(); // 8 -> drop "c d", "e f"
165        assert_eq!(contents(&memory).await, vec!["g h i j"]);
166        assert_eq!(memory.total_tokens(), 4);
167    }
168
169    #[tokio::test]
170    async fn message_bigger_than_limit_is_not_kept() {
171        let mut memory = words(3);
172        memory.add_user_message(&"short").await.unwrap();
173        memory
174            .add_ai_message(&"this one is way too long")
175            .await
176            .unwrap();
177        assert!(contents(&memory).await.is_empty());
178        assert_eq!(memory.total_tokens(), 0);
179    }
180
181    #[tokio::test]
182    async fn tool_calls_count_and_orphan_tool_results_are_dropped() {
183        let mut memory = words(6);
184        memory
185            .add_message(
186                Message::new_ai_message("").with_tool_calls(json!([{"id": "call_1", "a": 1}])),
187            )
188            .await
189            .unwrap();
190        assert!(memory.total_tokens() > 0, "tool calls must be counted");
191        memory
192            .add_message(Message::new_tool_message("r1 r2", "call_1"))
193            .await
194            .unwrap();
195        memory.add_ai_message(&"x y z w").await.unwrap();
196
197        // The AI tool-call message is evicted; the tool result must not lead the window.
198        let messages = memory.messages().await.unwrap();
199        assert_eq!(messages.len(), 1);
200        assert_eq!(messages[0].message_type, MessageType::AIMessage);
201        assert_eq!(messages[0].content, "x y z w");
202        assert_eq!(memory.total_tokens(), 4);
203    }
204
205    #[tokio::test]
206    async fn clear_resets_tokens() {
207        let mut memory = words(10);
208        memory.add_user_message(&"a b c").await.unwrap();
209        memory.clear().await.unwrap();
210        assert_eq!(memory.total_tokens(), 0);
211        assert!(contents(&memory).await.is_empty());
212    }
213
214    #[tokio::test]
215    async fn default_and_model_tokenizers_count_real_tokens() {
216        let mut memory = TokenBufferMemory::new(1000);
217        memory.add_user_message(&"hello world").await.unwrap();
218        assert_eq!(memory.total_tokens(), 2);
219
220        let mut memory = TokenBufferMemory::new(1000).with_model("gpt-4o").unwrap();
221        memory.add_user_message(&"hello world").await.unwrap();
222        assert_eq!(memory.total_tokens(), 2);
223
224        assert!(TokenBufferMemory::new(10)
225            .with_model("not-a-model")
226            .is_err());
227    }
228
229    #[tokio::test]
230    async fn works_behind_the_shared_mutex() {
231        let memory: Arc<Mutex<dyn BaseMemory>> = words(2).into();
232        memory.lock().await.add_user_message(&"a b").await.unwrap();
233        memory.lock().await.add_ai_message(&"c").await.unwrap();
234        assert_eq!(memory.lock().await.to_string().await.unwrap(), "ai: c");
235    }
236}