use super::ExperimentalStats;
use crate::provider::{ContentPart, Message, Role};
use std::collections::HashSet;
pub fn repair_orphans(messages: &mut Vec<Message>) -> ExperimentalStats {
let mut stats = ExperimentalStats::default();
let fulfilled_ids = collect_fulfilled_ids(messages);
let mut to_inject: Vec<(usize, String)> = Vec::new();
for (idx, msg) in messages.iter().enumerate() {
if msg.role != Role::Assistant {
continue;
}
for part in &msg.content {
if let ContentPart::ToolCall { id, .. } = part
&& !fulfilled_ids.contains(id)
{
to_inject.push((idx, id.clone()));
}
}
}
for (idx, call_id) in to_inject.into_iter().rev() {
let placeholder = Message {
role: Role::Tool,
content: vec![ContentPart::ToolResult {
tool_call_id: call_id,
content: "[tool result unavailable: elided by context management]".into(),
}],
};
messages.insert(idx + 1, placeholder);
stats.snippet_hits += 1;
}
let mut known: HashSet<String> = HashSet::new();
let mut drops: Vec<usize> = Vec::new();
for (idx, msg) in messages.iter().enumerate() {
for part in &msg.content {
if let ContentPart::ToolCall { id, .. } = part {
known.insert(id.clone());
}
}
if msg.role == Role::Tool
&& !msg.content.is_empty()
&& msg.content.iter().all(|p| match p {
ContentPart::ToolResult { tool_call_id, .. } => !known.contains(tool_call_id),
_ => false,
})
{
drops.push(idx);
}
}
for idx in drops.into_iter().rev() {
messages.remove(idx);
stats.snippet_hits += 1;
}
stats
}
fn collect_fulfilled_ids(messages: &[Message]) -> HashSet<String> {
let mut out = HashSet::new();
for msg in messages {
if msg.role != Role::Tool {
continue;
}
for part in &msg.content {
if let ContentPart::ToolResult { tool_call_id, .. } = part {
out.insert(tool_call_id.clone());
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn call(id: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentPart::ToolCall {
id: id.into(),
name: "t".into(),
arguments: "{}".into(),
thought_signature: None,
}],
}
}
fn result(id: &str) -> Message {
Message {
role: Role::Tool,
content: vec![ContentPart::ToolResult {
tool_call_id: id.into(),
content: "ok".into(),
}],
}
}
fn user(t: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentPart::Text { text: t.into() }],
}
}
#[test]
fn well_formed_history_is_noop() {
let mut msgs = vec![user("q"), call("a"), result("a"), call("b"), result("b")];
let before = msgs.len();
let stats = repair_orphans(&mut msgs);
assert_eq!(stats.snippet_hits, 0);
assert_eq!(msgs.len(), before);
}
#[test]
fn orphan_call_gets_synthetic_result() {
let mut msgs = vec![user("q"), call("a"), user("follow-up")];
repair_orphans(&mut msgs);
assert_eq!(msgs.len(), 4);
assert_eq!(msgs[2].role, Role::Tool);
let ContentPart::ToolResult {
tool_call_id,
content,
} = &msgs[2].content[0]
else {
panic!("expected synthetic tool result");
};
assert_eq!(tool_call_id, "a");
assert!(content.contains("unavailable"));
}
#[test]
fn orphan_result_is_dropped() {
let mut msgs = vec![user("q"), result("nonexistent"), user("next")];
repair_orphans(&mut msgs);
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[0].role, Role::User);
assert_eq!(msgs[1].role, Role::User);
}
#[test]
fn mixed_orphan_and_valid_pairs() {
let mut msgs = vec![
user("q"),
call("a"), call("b"),
result("b"),
user("mid"),
result("c"), ];
let stats = repair_orphans(&mut msgs);
assert_eq!(stats.snippet_hits, 2);
assert_eq!(msgs[1].role, Role::Assistant);
assert_eq!(msgs[2].role, Role::Tool);
let ContentPart::ToolResult { tool_call_id, .. } = &msgs[2].content[0] else {
panic!();
};
assert_eq!(tool_call_id, "a");
}
}