use async_openai::types::{
ChatCompletionRequestMessage, ChatCompletionRequestToolMessage,
ChatCompletionRequestToolMessageContent,
};
use std::collections::HashSet;
pub const ORPHAN_TOOL_STUB_CONTENT: &str = "Tool response unavailable (upstream interruption or parse failure). \
Continue from prior state; do not rely on this tool's output.";
pub fn pair_orphan_tool_calls(messages: &mut Vec<ChatCompletionRequestMessage>) {
let mut i = 0;
while i < messages.len() {
let ChatCompletionRequestMessage::Assistant(asst) = &messages[i] else {
i += 1;
continue;
};
let Some(tcs) = &asst.tool_calls else {
i += 1;
continue;
};
if tcs.is_empty() {
i += 1;
continue;
}
let expected_ids: Vec<String> = tcs.iter().map(|tc| tc.id.clone()).collect();
let mut j = i + 1;
let mut seen: HashSet<String> = HashSet::new();
while j < messages.len() {
let ChatCompletionRequestMessage::Tool(tm) = &messages[j] else {
break;
};
seen.insert(tm.tool_call_id.clone());
j += 1;
}
for id in &expected_ids {
if !seen.contains(id) {
messages.insert(j, stub_tool_response(id));
j += 1;
}
}
i = j;
}
}
pub fn stub_tool_response(tool_call_id: &str) -> ChatCompletionRequestMessage {
ChatCompletionRequestMessage::Tool(ChatCompletionRequestToolMessage {
content: ChatCompletionRequestToolMessageContent::Text(
ORPHAN_TOOL_STUB_CONTENT.to_string(),
),
tool_call_id: tool_call_id.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use async_openai::types::{
ChatCompletionMessageToolCall, ChatCompletionRequestAssistantMessage,
ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestUserMessage,
ChatCompletionRequestUserMessageContent, ChatCompletionToolType, FunctionCall,
};
fn user(text: &str) -> ChatCompletionRequestMessage {
ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
content: ChatCompletionRequestUserMessageContent::Text(text.to_string()),
name: None,
})
}
fn assistant_text(text: &str) -> ChatCompletionRequestMessage {
ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage {
content: Some(ChatCompletionRequestAssistantMessageContent::Text(
text.to_string(),
)),
..Default::default()
})
}
fn assistant_with_tool_calls(ids: &[&str]) -> ChatCompletionRequestMessage {
let tool_calls: Vec<ChatCompletionMessageToolCall> = ids
.iter()
.map(|id| ChatCompletionMessageToolCall {
id: id.to_string(),
r#type: ChatCompletionToolType::Function,
function: FunctionCall {
name: "search_deliberation".to_string(),
arguments: "{}".to_string(),
},
})
.collect();
ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage {
tool_calls: Some(tool_calls),
..Default::default()
})
}
fn tool(id: &str, content: &str) -> ChatCompletionRequestMessage {
ChatCompletionRequestMessage::Tool(ChatCompletionRequestToolMessage {
content: ChatCompletionRequestToolMessageContent::Text(content.to_string()),
tool_call_id: id.to_string(),
})
}
fn tool_call_id_of(msg: &ChatCompletionRequestMessage) -> Option<&str> {
if let ChatCompletionRequestMessage::Tool(t) = msg {
Some(t.tool_call_id.as_str())
} else {
None
}
}
#[test]
fn empty_messages_unchanged() {
let mut messages: Vec<ChatCompletionRequestMessage> = vec![];
pair_orphan_tool_calls(&mut messages);
assert!(messages.is_empty());
}
#[test]
fn assistant_without_tool_calls_unchanged() {
let mut messages = vec![user("hi"), assistant_text("hello")];
let before = messages.len();
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), before);
}
#[test]
fn fully_paired_tool_calls_unchanged() {
let mut messages = vec![
user("query"),
assistant_with_tool_calls(&["id_a", "id_b"]),
tool("id_a", "result_a"),
tool("id_b", "result_b"),
assistant_text("done"),
];
let len_before = messages.len();
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), len_before, "no stubs should be inserted");
}
#[test]
fn single_orphan_gets_stub() {
let mut messages = vec![
user("propose something"),
assistant_with_tool_calls(&["d0a738497"]),
];
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), 3);
assert_eq!(tool_call_id_of(&messages[2]), Some("d0a738497"));
}
#[test]
fn partial_pairing_fills_missing_ids_only() {
let mut messages = vec![
user("q"),
assistant_with_tool_calls(&["id_a", "id_b"]),
tool("id_a", "result_a"),
];
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), 4);
assert_eq!(tool_call_id_of(&messages[2]), Some("id_a"));
assert_eq!(tool_call_id_of(&messages[3]), Some("id_b"));
}
#[test]
fn stub_inserted_before_next_non_tool_message() {
let mut messages = vec![
assistant_with_tool_calls(&["id_x"]),
user("follow-up from the operator"),
];
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), 3);
assert_eq!(tool_call_id_of(&messages[1]), Some("id_x"));
match &messages[2] {
ChatCompletionRequestMessage::User(_) => (),
_ => panic!("user message should be at index 2"),
}
}
#[test]
fn multiple_assistant_groups_independently_validated() {
let mut messages = vec![
assistant_with_tool_calls(&["id_a"]),
tool("id_a", "res_a"),
assistant_text("intermediate thought"),
user("another turn"),
assistant_with_tool_calls(&["id_b", "id_c"]),
];
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), 7);
assert_eq!(tool_call_id_of(&messages[5]), Some("id_b"));
assert_eq!(tool_call_id_of(&messages[6]), Some("id_c"));
}
#[test]
fn idempotent_second_run_is_noop() {
let mut messages = vec![
user("q"),
assistant_with_tool_calls(&["id_a"]),
];
pair_orphan_tool_calls(&mut messages);
let after_first = messages.clone();
pair_orphan_tool_calls(&mut messages);
assert_eq!(
messages.len(),
after_first.len(),
"second run must not add more stubs"
);
}
#[test]
fn stub_tool_response_has_expected_shape() {
let stub = stub_tool_response("call_abc");
match stub {
ChatCompletionRequestMessage::Tool(t) => {
assert_eq!(t.tool_call_id, "call_abc");
match t.content {
ChatCompletionRequestToolMessageContent::Text(s) => {
assert!(s.contains("Tool response unavailable"));
}
_ => panic!("stub content should be text"),
}
}
_ => panic!("stub must be a Tool message"),
}
}
#[test]
fn empty_tool_calls_vec_ignored() {
let mut messages = vec![
user("q"),
ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage {
tool_calls: Some(vec![]),
..Default::default()
}),
];
let before = messages.len();
pair_orphan_tool_calls(&mut messages);
assert_eq!(messages.len(), before);
}
}