Prune large tool outputs from messages to reclaim context space. Iterates backward from recent messages, protecting the first `TOOL_OUTPUT_PROTECT_TOKENS` worth of tool output tokens. Only prunes outputs that would save at least `MIN_PRUNE_SAVINGS_TOKENS`. Returns `Some(pruned_messages)` if any outputs were pruned, or `None` if no pruning was needed.
(messages: &[Message])
| 155 | /// Returns `Some(pruned_messages)` if any outputs were pruned, or `None` |
| 156 | /// if no pruning was needed. |
| 157 | pub(crate) fn prune_tool_outputs(messages: &[Message]) -> Option<Vec<Message>> { |
| 158 | // First pass: estimate total tool output tokens (backward) |
| 159 | let mut tool_outputs: Vec<(usize, usize, usize)> = Vec::new(); // (msg_idx, block_idx, token_count) |
| 160 | |
| 161 | for (msg_idx, msg) in messages.iter().enumerate() { |
| 162 | for (block_idx, block) in msg.content.iter().enumerate() { |
| 163 | if let ContentBlock::ToolResult { content, .. } = block { |
| 164 | let text = content.as_text(); |
| 165 | let token_count = estimate_tokens(&text); |
| 166 | if token_count > 0 { |
| 167 | tool_outputs.push((msg_idx, block_idx, token_count)); |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | if tool_outputs.is_empty() { |
| 174 | return None; |
| 175 | } |
| 176 | |
| 177 | // Calculate total tool output tokens |
| 178 | let total_tool_tokens: usize = tool_outputs.iter().map(|(_, _, t)| *t).sum(); |
| 179 | |
| 180 | // If total is small, no pruning needed |
| 181 | if total_tool_tokens <= TOOL_OUTPUT_PROTECT_TOKENS { |
| 182 | return None; |
| 183 | } |
| 184 | |
| 185 | // Iterate from oldest to newest, protecting the most recent outputs |
| 186 | // We prune old outputs first, keeping recent ones intact |
| 187 | let mut protected_tokens = 0usize; |
| 188 | let mut to_prune: Vec<(usize, usize)> = Vec::new(); |
| 189 | let mut savings = 0usize; |
| 190 | |
| 191 | // Walk backward (newest first) to protect recent outputs |
| 192 | for &(msg_idx, block_idx, token_count) in tool_outputs.iter().rev() { |
| 193 | if protected_tokens < TOOL_OUTPUT_PROTECT_TOKENS { |
| 194 | protected_tokens += token_count; |
| 195 | } else { |
| 196 | to_prune.push((msg_idx, block_idx)); |
| 197 | savings += token_count; |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | // Only prune if savings are significant |
| 202 | if savings < MIN_PRUNE_SAVINGS_TOKENS { |
| 203 | return None; |
| 204 | } |
| 205 | |
| 206 | // Apply pruning |
| 207 | let mut pruned = messages.to_vec(); |
| 208 | for (msg_idx, block_idx) in &to_prune { |
| 209 | if let Some(msg) = pruned.get_mut(*msg_idx) { |
| 210 | if let Some(ContentBlock::ToolResult { content, .. }) = msg.content.get_mut(*block_idx) |
| 211 | { |
| 212 | *content = ToolResultContentField::Text(PRUNED_MARKER.to_string()); |
| 213 | } |
| 214 | } |