use std::collections::{HashMap, HashSet};
use locode_protocol::{ContentBlock, Message, ResultChunk, Role};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RepairStats {
pub synthesized: usize,
pub deduped: usize,
}
impl RepairStats {
#[must_use]
pub fn is_noop(self) -> bool {
self.synthesized == 0 && self.deduped == 0
}
}
const DANGLING_RESULT_TEXT: &str =
"tool result missing: this call was not completed (synthesized to keep the transcript valid)";
pub fn repair_pairing(messages: &mut Vec<Message>) -> RepairStats {
let deduped = dedup_duplicate_results(messages);
let synthesized = repair_dangling(messages);
RepairStats {
synthesized,
deduped,
}
}
fn dedup_duplicate_results(messages: &mut Vec<Message>) -> usize {
let mut last: HashMap<&str, (usize, usize)> = HashMap::new();
for (mi, message) in messages.iter().enumerate() {
for (bi, block) in message.content.iter().enumerate() {
if let ContentBlock::ToolResult { tool_use_id, .. } = block {
last.insert(tool_use_id.as_str(), (mi, bi));
}
}
}
if last.len()
== messages
.iter()
.flat_map(|m| &m.content)
.filter(|b| matches!(b, ContentBlock::ToolResult { .. }))
.count()
{
return 0;
}
let winners: HashSet<(usize, usize)> = last.into_values().collect();
let mut removed = 0;
for (mi, message) in messages.iter_mut().enumerate() {
let mut bi = 0;
message.content.retain(|block| {
let here = bi;
bi += 1;
let is_result = matches!(block, ContentBlock::ToolResult { .. });
let keep = !is_result || winners.contains(&(mi, here));
if !keep {
removed += 1;
}
keep
});
}
messages.retain(|m| !m.content.is_empty());
removed
}
fn repair_dangling(messages: &mut Vec<Message>) -> usize {
let answered: HashSet<String> = messages
.iter()
.flat_map(|m| &m.content)
.filter_map(|b| match b {
ContentBlock::ToolResult { tool_use_id, .. } => Some(tool_use_id.clone()),
_ => None,
})
.collect();
let mut synthesized = 0;
let mut i = 0;
while i < messages.len() {
if messages[i].role == Role::Assistant {
let dangling: Vec<String> = messages[i]
.content
.iter()
.filter_map(|b| match b {
ContentBlock::ToolUse { id, .. } if !answered.contains(id) => Some(id.clone()),
_ => None,
})
.collect();
if !dangling.is_empty() {
synthesized += dangling.len();
let synth: Vec<ContentBlock> = dangling
.into_iter()
.map(|id| ContentBlock::ToolResult {
tool_use_id: id,
content: vec![ResultChunk::Text {
text: DANGLING_RESULT_TEXT.to_owned(),
}],
is_error: true,
})
.collect();
if messages.get(i + 1).is_some_and(|m| m.role == Role::User) {
let existing = std::mem::take(&mut messages[i + 1].content);
let mut merged = synth;
merged.extend(existing);
messages[i + 1].content = merged;
} else {
messages.insert(
i + 1,
Message {
role: Role::User,
content: synth,
},
);
}
}
}
i += 1;
}
synthesized
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn assistant_tool_use(id: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: id.into(),
name: "echo".into(),
input: json!({}),
}],
}
}
fn user_result(id: &str, text: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: id.into(),
content: vec![ResultChunk::Text { text: text.into() }],
is_error: false,
}],
}
}
#[test]
fn dangling_tool_use_gets_synthetic_result() {
let mut messages = vec![
Message {
role: Role::User,
content: vec![ContentBlock::Text { text: "go".into() }],
},
assistant_tool_use("c1"), ];
let stats = repair_pairing(&mut messages);
assert_eq!(stats.synthesized, 1);
let last = messages.last().expect("a message");
assert_eq!(last.role, Role::User);
assert!(matches!(
last.content.first(),
Some(ContentBlock::ToolResult { tool_use_id, is_error: true, .. }) if tool_use_id == "c1"
));
}
#[test]
fn duplicate_results_keep_the_last() {
let mut messages = vec![
assistant_tool_use("c1"),
Message {
role: Role::User,
content: vec![
ContentBlock::ToolResult {
tool_use_id: "c1".into(),
content: vec![ResultChunk::Text {
text: "first".into(),
}],
is_error: false,
},
ContentBlock::ToolResult {
tool_use_id: "c1".into(),
content: vec![ResultChunk::Text {
text: "second".into(),
}],
is_error: false,
},
],
},
];
let stats = repair_pairing(&mut messages);
assert_eq!(stats.deduped, 1);
assert_eq!(stats.synthesized, 0);
let results: Vec<_> = messages
.iter()
.flat_map(|m| &m.content)
.filter(|b| matches!(b, ContentBlock::ToolResult { .. }))
.collect();
assert_eq!(results.len(), 1, "only the last result should survive");
assert!(matches!(
results[0],
ContentBlock::ToolResult { content, .. }
if matches!(content.first(), Some(ResultChunk::Text { text }) if text == "second")
));
}
#[test]
fn valid_transcript_is_unchanged() {
let mut messages = vec![assistant_tool_use("c1"), user_result("c1", "ok")];
let before = messages.clone();
let stats = repair_pairing(&mut messages);
assert!(stats.is_noop());
assert_eq!(messages, before, "a paired transcript must pass through");
}
#[test]
fn repair_is_idempotent() {
let mut messages = vec![assistant_tool_use("c1")];
let first = repair_pairing(&mut messages);
assert_eq!(first.synthesized, 1);
let second = repair_pairing(&mut messages);
assert!(second.is_noop(), "second pass must find nothing to fix");
}
}