1use std::collections::HashMap;
9use std::collections::HashSet;
10
11use ferrin_spec::ApprovalId;
12use ferrin_spec::ToolCallId;
13use ferrin_spec::ToolName;
14
15use crate::message::AssistantContent;
16use crate::message::Message;
17use crate::part::AssistantPart;
18use crate::part::ToolPart;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22#[non_exhaustive]
23pub enum ReasoningPrune {
24 #[default]
26 None,
27 All,
29 BeforeLastMessage,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum PruneScope {
37 All,
39 BeforeLastMessages(usize),
43}
44
45impl PruneScope {
46 #[must_use]
48 pub const fn before_last_message() -> Self {
49 Self::BeforeLastMessages(1)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ToolCallPrune {
56 pub scope: PruneScope,
58 pub tools: Option<Vec<ToolName>>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
64#[non_exhaustive]
65pub enum EmptyMessages {
66 Keep,
68 #[default]
70 Remove,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Default)]
75pub struct PruneOptions {
76 pub reasoning: ReasoningPrune,
78 pub tool_calls: Vec<ToolCallPrune>,
80 pub empty_messages: EmptyMessages,
82}
83
84impl PruneOptions {
85 #[must_use]
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 #[must_use]
93 pub fn reasoning(mut self, reasoning: ReasoningPrune) -> Self {
94 self.reasoning = reasoning;
95 self
96 }
97
98 #[must_use]
100 pub fn tool_calls(mut self, scope: PruneScope) -> Self {
101 self.tool_calls.push(ToolCallPrune { scope, tools: None });
102 self
103 }
104
105 #[must_use]
107 pub fn tool_calls_for(
108 mut self,
109 scope: PruneScope,
110 tools: impl IntoIterator<Item = impl Into<ToolName>>,
111 ) -> Self {
112 self.tool_calls.push(ToolCallPrune {
113 scope,
114 tools: Some(tools.into_iter().map(Into::into).collect()),
115 });
116 self
117 }
118
119 #[must_use]
121 pub fn keep_empty_messages(mut self) -> Self {
122 self.empty_messages = EmptyMessages::Keep;
123 self
124 }
125}
126
127#[must_use]
129pub fn prune(mut messages: Vec<Message>, options: &PruneOptions) -> Vec<Message> {
130 prune_reasoning(&mut messages, options.reasoning);
131 for rule in &options.tool_calls {
132 prune_tool_calls(&mut messages, rule);
133 }
134 if options.empty_messages == EmptyMessages::Remove {
135 messages.retain(|message| !message.is_empty());
136 }
137 messages
138}
139
140fn prune_reasoning(messages: &mut [Message], reasoning: ReasoningPrune) {
141 let last_index = messages.len().saturating_sub(1);
142 for (index, message) in messages.iter_mut().enumerate() {
143 let keep = match reasoning {
144 ReasoningPrune::None => true,
145 ReasoningPrune::All => false,
146 ReasoningPrune::BeforeLastMessage => index == last_index,
147 };
148 if keep {
149 continue;
150 }
151 if let Message::Assistant(assistant) = message
152 && let AssistantContent::Parts(parts) = &mut assistant.content
153 {
154 parts.retain(|part| !matches!(part, AssistantPart::Reasoning(_)));
155 }
156 }
157}
158
159enum ToolRef<'a> {
160 Call(&'a ToolCallId, &'a ToolName),
161 Approval(&'a ApprovalId, Option<&'a ToolCallId>),
162}
163
164fn assistant_refs(part: &AssistantPart) -> Option<ToolRef<'_>> {
165 match part {
166 AssistantPart::ToolCall(call) => Some(ToolRef::Call(&call.tool_call_id, &call.tool_name)),
167 AssistantPart::ToolResult(result) => {
168 Some(ToolRef::Call(&result.tool_call_id, &result.tool_name))
169 }
170 AssistantPart::ToolApprovalRequest(request) => Some(ToolRef::Approval(
171 &request.approval_id,
172 Some(&request.tool_call_id),
173 )),
174 _ => None,
175 }
176}
177
178fn tool_refs(part: &ToolPart) -> Option<ToolRef<'_>> {
179 match part {
180 ToolPart::ToolResult(result) => {
181 Some(ToolRef::Call(&result.tool_call_id, &result.tool_name))
182 }
183 ToolPart::ToolApprovalResponse(response) => {
184 Some(ToolRef::Approval(&response.approval_id, None))
185 }
186 }
187}
188
189fn message_refs(message: &Message) -> Vec<ToolRef<'_>> {
190 match message {
191 Message::Assistant(assistant) => assistant
192 .content
193 .as_parts()
194 .map(|parts| parts.iter().filter_map(assistant_refs).collect())
195 .unwrap_or_default(),
196 Message::Tool(tool) => tool.content.iter().filter_map(tool_refs).collect(),
197 Message::System(_) | Message::User(_) => Vec::new(),
198 }
199}
200
201struct Kept {
202 tool_call_ids: HashSet<ToolCallId>,
203 approval_ids: HashSet<ApprovalId>,
204 approval_tool_names: HashMap<ApprovalId, ToolName>,
205 protected_from: usize,
206}
207
208impl Kept {
209 fn keeps_call(
210 &self,
211 tool_call_id: &ToolCallId,
212 tool_name: &ToolName,
213 rule: &ToolCallPrune,
214 ) -> bool {
215 self.tool_call_ids.contains(tool_call_id) || rule_keeps(rule, Some(tool_name))
216 }
217
218 fn keeps_approval(&self, approval_id: &ApprovalId, rule: &ToolCallPrune) -> bool {
219 self.approval_ids.contains(approval_id)
220 || rule_keeps(rule, self.approval_tool_names.get(approval_id))
221 }
222}
223
224fn rule_keeps(rule: &ToolCallPrune, tool_name: Option<&ToolName>) -> bool {
227 match (&rule.tools, tool_name) {
228 (Some(tools), Some(name)) => !tools.contains(name),
229 _ => false,
230 }
231}
232
233fn prune_tool_calls(messages: &mut [Message], rule: &ToolCallPrune) {
234 let keep_last = match rule.scope {
235 PruneScope::All => None,
236 PruneScope::BeforeLastMessages(n) => Some(n),
237 };
238 let protected_from = keep_last.map_or(messages.len(), |n| {
239 if n == 0 {
240 0
241 } else {
242 messages.len().saturating_sub(n)
243 }
244 });
245
246 let mut kept = Kept {
247 tool_call_ids: HashSet::new(),
248 approval_ids: HashSet::new(),
249 approval_tool_names: HashMap::new(),
250 protected_from,
251 };
252 for message in &messages[protected_from..] {
253 for reference in message_refs(message) {
254 match reference {
255 ToolRef::Call(id, _) => {
256 kept.tool_call_ids.insert(id.clone());
257 }
258 ToolRef::Approval(id, _) => {
259 kept.approval_ids.insert(id.clone());
260 }
261 }
262 }
263 }
264
265 let mut call_tool_names: HashMap<ToolCallId, ToolName> = HashMap::new();
266 let mut approval_calls: Vec<(ApprovalId, ToolCallId)> = Vec::new();
267 for message in messages.iter() {
268 for reference in message_refs(message) {
269 match reference {
270 ToolRef::Call(id, name) => {
271 call_tool_names.insert(id.clone(), name.clone());
272 }
273 ToolRef::Approval(approval_id, Some(call_id)) => {
274 approval_calls.push((approval_id.clone(), call_id.clone()));
275 }
276 ToolRef::Approval(_, None) => {}
277 }
278 }
279 }
280 for (approval_id, call_id) in approval_calls {
281 if let Some(name) = call_tool_names.get(&call_id) {
282 kept.approval_tool_names.insert(approval_id, name.clone());
283 }
284 }
285
286 for message in messages.iter_mut().take(kept.protected_from) {
287 match message {
288 Message::Assistant(assistant) => {
289 if let AssistantContent::Parts(parts) = &mut assistant.content {
290 parts.retain(|part| match part {
291 AssistantPart::ToolCall(call) => {
292 kept.keeps_call(&call.tool_call_id, &call.tool_name, rule)
293 }
294 AssistantPart::ToolResult(result) => {
295 kept.keeps_call(&result.tool_call_id, &result.tool_name, rule)
296 }
297 AssistantPart::ToolApprovalRequest(request) => {
298 kept.keeps_approval(&request.approval_id, rule)
299 }
300 _ => true,
301 });
302 }
303 }
304 Message::Tool(tool) => {
305 tool.content.retain(|part| match part {
306 ToolPart::ToolResult(result) => {
307 kept.keeps_call(&result.tool_call_id, &result.tool_name, rule)
308 }
309 ToolPart::ToolApprovalResponse(response) => {
310 kept.keeps_approval(&response.approval_id, rule)
311 }
312 });
313 }
314 Message::System(_) | Message::User(_) => {}
315 }
316 }
317}