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 | PruneScope::BeforeLastMessages(0) => None,
236 PruneScope::BeforeLastMessages(n) => Some(n),
237 };
238 let protected_from = keep_last.map_or(messages.len(), |n| messages.len().saturating_sub(n));
239
240 let mut kept = Kept {
241 tool_call_ids: HashSet::new(),
242 approval_ids: HashSet::new(),
243 approval_tool_names: HashMap::new(),
244 protected_from,
245 };
246 for message in &messages[protected_from..] {
247 for reference in message_refs(message) {
248 match reference {
249 ToolRef::Call(id, _) => {
250 kept.tool_call_ids.insert(id.clone());
251 }
252 ToolRef::Approval(id, _) => {
253 kept.approval_ids.insert(id.clone());
254 }
255 }
256 }
257 }
258
259 let mut call_tool_names: HashMap<ToolCallId, ToolName> = HashMap::new();
260 let mut approval_calls: Vec<(ApprovalId, ToolCallId)> = Vec::new();
261 for message in messages.iter() {
262 for reference in message_refs(message) {
263 match reference {
264 ToolRef::Call(id, name) => {
265 call_tool_names.insert(id.clone(), name.clone());
266 }
267 ToolRef::Approval(approval_id, Some(call_id)) => {
268 approval_calls.push((approval_id.clone(), call_id.clone()));
269 }
270 ToolRef::Approval(_, None) => {}
271 }
272 }
273 }
274 for (approval_id, call_id) in approval_calls {
275 if let Some(name) = call_tool_names.get(&call_id) {
276 kept.approval_tool_names.insert(approval_id, name.clone());
277 }
278 }
279
280 for message in messages.iter_mut().take(kept.protected_from) {
281 match message {
282 Message::Assistant(assistant) => {
283 if let AssistantContent::Parts(parts) = &mut assistant.content {
284 parts.retain(|part| match part {
285 AssistantPart::ToolCall(call) => {
286 kept.keeps_call(&call.tool_call_id, &call.tool_name, rule)
287 }
288 AssistantPart::ToolResult(result) => {
289 kept.keeps_call(&result.tool_call_id, &result.tool_name, rule)
290 }
291 AssistantPart::ToolApprovalRequest(request) => {
292 kept.keeps_approval(&request.approval_id, rule)
293 }
294 _ => true,
295 });
296 }
297 }
298 Message::Tool(tool) => {
299 tool.content.retain(|part| match part {
300 ToolPart::ToolResult(result) => {
301 kept.keeps_call(&result.tool_call_id, &result.tool_name, rule)
302 }
303 ToolPart::ToolApprovalResponse(response) => {
304 kept.keeps_approval(&response.approval_id, rule)
305 }
306 });
307 }
308 Message::System(_) | Message::User(_) => {}
309 }
310 }
311}