use whycodes_core::types::{ContentBlock, Message, MessageContent, Role};
pub const MISSING_RESULT: &str = "Tool result missing: this call was interrupted and never ran.";
pub fn paired_messages(messages: &[Message]) -> Option<Vec<Message>> {
let mut out: Vec<Message> = Vec::with_capacity(messages.len());
let mut open: Vec<String> = Vec::new();
let mut lenient = false;
let mut changed = false;
for msg in messages {
match msg.role {
Role::Tool => {
let id = msg.tool_call_id.as_deref().unwrap_or("");
let owned = take_open(&mut open, id) || lenient;
changed |= !owned;
push_if(&mut out, msg, owned);
}
Role::Assistant => {
changed |= close_open(&mut out, &mut open);
lenient = record_calls(msg, &mut open);
out.push(msg.clone());
}
Role::User | Role::System => {
changed |= close_open(&mut out, &mut open);
lenient = false;
out.push(msg.clone());
}
}
}
changed |= close_open(&mut out, &mut open);
changed.then_some(out)
}
fn take_open(open: &mut Vec<String>, id: &str) -> bool {
let before = open.len();
open.retain(|o| o != id);
open.len() != before
}
fn push_if(out: &mut Vec<Message>, msg: &Message, keep: bool) {
if keep {
out.push(msg.clone());
}
}
fn record_calls(msg: &Message, open: &mut Vec<String>) -> bool {
let MessageContent::Blocks(blocks) = &msg.content else {
return false;
};
let mut unnamed = false;
for block in blocks {
if let ContentBlock::ToolUse { id, .. } = block {
unnamed |= id.is_empty();
open.push(id.clone());
}
}
open.retain(|id| !id.is_empty());
unnamed
}
fn close_open(out: &mut Vec<Message>, open: &mut Vec<String>) -> bool {
let any = !open.is_empty();
for id in open.drain(..) {
out.push(Message {
role: Role::Tool,
content: MessageContent::Text(MISSING_RESULT.to_string()),
tool_call_id: Some(id),
name: None,
created_at: None,
});
}
any
}
#[cfg(test)]
#[path = "pairing_tests.rs"]
mod tests;