use crate::constants::messages::NO_CONTENT_MESSAGE;
use crate::utils::messages::{
AssistantMessage, AssistantMessageContent, ContentBlock, Message, MessageContent, NormalizedMessage,
NormalizedUserMessage, UserMessageExtra,
};
use std::collections::HashSet;
const SYNTHETIC_TOOL_RESULT_PLACEHOLDER: &str =
"[Synthetic error: tool result missing due to conversation resume / truncation]";
#[derive(Debug, Clone)]
pub struct StrictPairingError {
pub message_types: String,
}
impl std::fmt::Display for StrictPairingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ensureToolResultPairing: tool_use/tool_result pairing mismatch detected (strict mode). \
Refusing to repair — would inject synthetic placeholders into model context. \
Message structure: {}",
self.message_types
)
}
}
impl std::error::Error for StrictPairingError {}
pub fn get_strict_tool_result_pairing() -> bool {
std::env::var("AI_CODE_STRICT_TOOL_RESULT_PAIRING")
.ok()
.as_ref()
.map(|v| v == "true" || v == "1" || v == "yes")
.or_else(|| {
std::env::var("CLAUDE_CODE_STRICT_TOOL_RESULT_PAIRING")
.ok()
.as_ref()
.map(|v| v == "true" || v == "1" || v == "yes")
})
.unwrap_or(false)
}
fn json_tool_result_id(block: &serde_json::Value) -> Option<String> {
block
.get("type")
.and_then(|t| t.as_str())
.filter(|t| *t == "tool_result")
.and_then(|_| block.get("tool_use_id"))
.and_then(|id| id.as_str())
.map(String::from)
}
fn json_server_tool_use_id(block: &serde_json::Value) -> Option<String> {
let block_type = block.get("type")?.as_str()?;
if block_type == "server_tool_use" || block_type == "mcp_tool_use" {
block.get("id").and_then(|id| id.as_str()).map(String::from)
} else {
None
}
}
fn json_tool_use_id(block: &serde_json::Value) -> Option<String> {
block
.get("type")
.and_then(|t| t.as_str())
.filter(|t| *t == "tool_use")
.and_then(|_| block.get("id"))
.and_then(|id| id.as_str())
.map(String::from)
}
fn json_is_tool_result(block: &serde_json::Value) -> bool {
block.get("type").and_then(|t| t.as_str()) == Some("tool_result")
}
fn json_is_tool_use(block: &serde_json::Value) -> bool {
block.get("type").and_then(|t| t.as_str()) == Some("tool_use")
}
fn json_is_orphaned_server_tool_use(
block: &serde_json::Value,
server_result_ids: &HashSet<String>,
) -> bool {
let block_type = block.get("type").and_then(|t| t.as_str());
match block_type {
Some("server_tool_use" | "mcp_tool_use") => {
let id = block.get("id").and_then(|i| i.as_str());
id.map(|id| !server_result_ids.contains(id)).unwrap_or(false)
}
_ => false,
}
}
fn content_block_tool_result_id(block: &ContentBlock) -> Option<String> {
match block {
ContentBlock::ToolResult { tool_use_id, .. } => Some(tool_use_id.clone()),
_ => None,
}
}
fn content_block_is_tool_result(block: &ContentBlock) -> bool {
matches!(block, ContentBlock::ToolResult { .. })
}
fn user_message_to_json_blocks(content: &MessageContent) -> Vec<serde_json::Value> {
match content {
MessageContent::Blocks(blocks) => {
blocks
.iter()
.map(|b| serde_json::to_value(b).unwrap_or_else(|_| serde_json::json!({})))
.collect()
}
MessageContent::String(s) => {
vec![serde_json::json!({"type": "text", "text": s})]
}
}
}
fn content_has_tool_results(content: &MessageContent) -> bool {
match content {
MessageContent::Blocks(blocks) => blocks.iter().any(content_block_is_tool_result),
MessageContent::String(_) => false,
}
}
fn create_normalized_user(
content: MessageContent,
is_meta: bool,
) -> NormalizedUserMessage {
NormalizedUserMessage {
message: content,
extra: UserMessageExtra {
is_meta: Some(is_meta),
is_visible_in_transcript_only: None,
is_virtual: None,
is_compact_summary: None,
summarize_metadata: None,
tool_use_result: None,
mcp_meta: None,
uuid: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
image_paste_ids: None,
source_tool_assistant_uuid: None,
permission_mode: None,
origin: None,
parent_uuid: None,
},
}
}
pub fn ensure_tool_result_pairing(
messages: &[NormalizedMessage],
) -> Result<Vec<NormalizedMessage>, StrictPairingError> {
let strict_mode = get_strict_tool_result_pairing();
let mut result: Vec<NormalizedMessage> = Vec::new();
let mut repaired = false;
let mut all_seen_tool_use_ids: HashSet<String> = HashSet::new();
for i in 0..messages.len() {
let msg = &messages[i];
match msg {
NormalizedMessage::Assistant(assistant) => {
let content_vec = &assistant.message.content;
let server_result_ids: HashSet<String> = content_vec
.iter()
.filter_map(|block| json_tool_result_id(block))
.collect();
let mut seen_tool_use_ids: HashSet<String> = HashSet::new();
let mut final_content: Vec<serde_json::Value> = Vec::new();
for block in content_vec {
let block_type = block.get("type").and_then(|t| t.as_str());
let mut should_keep = true;
if block_type == Some("tool_use") {
if let Some(tool_id) = json_tool_use_id(block) {
if all_seen_tool_use_ids.contains(&tool_id) {
repaired = true;
should_keep = false;
} else {
all_seen_tool_use_ids.insert(tool_id.clone());
seen_tool_use_ids.insert(tool_id);
}
}
}
if should_keep
&& json_is_orphaned_server_tool_use(block, &server_result_ids)
{
repaired = true;
should_keep = false;
}
if should_keep {
final_content.push(block.clone());
}
}
let assistant_content_changed = final_content.len() != content_vec.len();
if final_content.is_empty() {
final_content.push(serde_json::json!({
"type": "text",
"text": "[Tool use interrupted]",
}));
}
let assistant_msg = if assistant_content_changed {
let mut new_content = final_content;
if new_content.len() == 1 {
if let Some(text) = new_content[0].get("text").and_then(|t| t.as_str())
&& new_content[0].get("type").and_then(|t| t.as_str()) == Some("text")
{
let mut new_assistant = assistant.clone();
new_assistant.message.content = vec![new_content.remove(0)];
return if repaired && strict_mode {
let message_types = build_message_types(messages);
Err(StrictPairingError { message_types })
} else {
result.push(NormalizedMessage::Assistant(new_assistant));
let tool_use_ids: Vec<String> =
seen_tool_use_ids.into_iter().collect();
process_next_message_pairing(
&messages,
i,
&tool_use_ids,
&mut result,
&mut repaired,
strict_mode,
);
return Ok(result);
};
}
}
let mut new_assistant = assistant.clone();
new_assistant.message.content = new_content;
NormalizedMessage::Assistant(new_assistant)
} else {
msg.clone()
};
result.push(assistant_msg);
let tool_use_ids: Vec<String> = seen_tool_use_ids.into_iter().collect();
process_next_message_pairing(
&messages,
i,
&tool_use_ids,
&mut result,
&mut repaired,
strict_mode,
);
}
NormalizedMessage::User(user) => {
if result
.last()
.map_or(true, |m| !matches!(m, NormalizedMessage::Assistant(_)))
{
if content_has_tool_results(&user.message) {
match &user.message {
MessageContent::Blocks(blocks) => {
let stripped: Vec<ContentBlock> = blocks
.iter()
.filter(|block| !content_block_is_tool_result(block))
.cloned()
.collect();
if stripped.len() != blocks.len() {
repaired = true;
if !stripped.is_empty() {
let mut new_user = user.clone();
new_user.message = MessageContent::Blocks(stripped);
result.push(NormalizedMessage::User(new_user));
} else if result.is_empty() {
let placeholder =
NormalizedMessage::User(create_normalized_user(
MessageContent::String(
NO_CONTENT_MESSAGE.to_string(),
),
true,
));
result.push(placeholder);
}
continue;
}
}
MessageContent::String(_) => {
}
}
}
}
result.push(msg.clone());
}
NormalizedMessage::Progress(_)
| NormalizedMessage::System(_)
| NormalizedMessage::Attachment(_) => {
result.push(msg.clone());
}
}
}
if repaired && strict_mode {
let message_types = build_message_types(messages);
return Err(StrictPairingError { message_types });
}
Ok(result)
}
fn process_next_message_pairing(
messages: &[NormalizedMessage],
assistant_idx: usize,
tool_use_ids: &[String],
result: &mut Vec<NormalizedMessage>,
repaired: &mut bool,
_strict_mode: bool,
) {
let next_msg = messages.get(assistant_idx + 1);
let mut existing_tool_result_ids: HashSet<String> = HashSet::new();
let mut has_duplicate_tool_results = false;
if let Some(NormalizedMessage::User(user)) = next_msg {
match &user.message {
MessageContent::Blocks(blocks) => {
for block in blocks {
if let Some(tr_id) = content_block_tool_result_id(block) {
if existing_tool_result_ids.contains(&tr_id) {
has_duplicate_tool_results = true;
}
existing_tool_result_ids.insert(tr_id);
}
}
}
MessageContent::String(_) => {}
}
}
let tool_use_id_set: HashSet<String> = tool_use_ids.iter().cloned().collect();
let missing_ids: Vec<String> = tool_use_ids
.iter()
.filter(|id| !existing_tool_result_ids.contains(*id))
.cloned()
.collect();
let orphaned_ids: Vec<String> = existing_tool_result_ids
.iter()
.filter(|id| !tool_use_id_set.contains(*id))
.cloned()
.collect();
if missing_ids.is_empty() && orphaned_ids.is_empty() && !has_duplicate_tool_results {
return;
}
*repaired = true;
let synthetic_blocks: Vec<serde_json::Value> = missing_ids
.iter()
.map(|id| {
serde_json::json!({
"type": "tool_result",
"tool_use_id": id,
"content": [{"type": "text", "text": SYNTHETIC_TOOL_RESULT_PLACEHOLDER}],
"is_error": true,
})
})
.collect();
if let Some(NormalizedMessage::User(user)) = next_msg {
let mut content: Vec<serde_json::Value> = user_message_to_json_blocks(&user.message);
if !orphaned_ids.is_empty() || has_duplicate_tool_results {
let orphaned_set: HashSet<String> = orphaned_ids.into_iter().collect();
let mut seen_tr_ids: HashSet<String> = HashSet::new();
content = content
.into_iter()
.filter(|block| {
if json_is_tool_result(block) {
if let Some(tr_id) = json_tool_result_id(block) {
if orphaned_set.contains(&tr_id) {
return false;
}
if seen_tr_ids.contains(&tr_id) {
return false;
}
seen_tr_ids.insert(tr_id);
}
}
true
})
.collect();
}
let patched_content: Vec<serde_json::Value> =
synthetic_blocks.into_iter().chain(content.into_iter()).collect();
if !patched_content.is_empty() {
let patched_blocks: Vec<ContentBlock> = patched_content
.iter()
.filter_map(|v| serde_json::from_value(v.clone()).ok())
.collect();
let mut patched_user = user.clone();
if !patched_blocks.is_empty() {
patched_user.message = MessageContent::Blocks(patched_blocks);
} else {
patched_user.message = MessageContent::String(NO_CONTENT_MESSAGE.to_string());
}
let patched_next = NormalizedMessage::User(patched_user);
result.push(patched_next);
} else {
let placeholder_msg = NormalizedMessage::User(create_normalized_user(
MessageContent::String(NO_CONTENT_MESSAGE.to_string()),
true,
));
result.push(placeholder_msg);
}
} else {
if !synthetic_blocks.is_empty() {
let synthetic_blocks_as_content: Vec<ContentBlock> = synthetic_blocks
.iter()
.filter_map(|v| serde_json::from_value(v.clone()).ok())
.collect();
let synthetic_user = NormalizedMessage::User(create_normalized_user(
MessageContent::Blocks(synthetic_blocks_as_content),
true,
));
result.push(synthetic_user);
}
}
}
fn build_message_types(messages: &[NormalizedMessage]) -> String {
messages
.iter()
.enumerate()
.map(|(idx, m)| {
match m {
NormalizedMessage::Assistant(assistant) => {
let tool_uses: Vec<String> = assistant
.message
.content
.iter()
.filter_map(|b| json_tool_use_id(b))
.collect();
let server_tool_uses: Vec<String> = assistant
.message
.content
.iter()
.filter_map(json_server_tool_use_id)
.collect();
let mut parts = vec![
format!("id={}", assistant.message.id),
format!("tool_uses=[{}]", tool_uses.join(",")),
];
if !server_tool_uses.is_empty() {
parts.push(format!(
"server_tool_uses=[{}]",
server_tool_uses.join(",")
));
}
format!("[{}] assistant({})", idx, parts.join(", "))
}
NormalizedMessage::User(user) => {
if let MessageContent::Blocks(blocks) = &user.message {
let tool_results: Vec<String> = blocks
.iter()
.filter_map(content_block_tool_result_id)
.collect();
if !tool_results.is_empty() {
return format!(
"[{}] user(tool_results=[{}])",
idx,
tool_results.join(",")
);
}
}
match m {
NormalizedMessage::User(_) => format!("[{}] user", idx),
NormalizedMessage::Progress(_) => format!("[{}] progress", idx),
NormalizedMessage::System(_) => format!("[{}] system", idx),
NormalizedMessage::Attachment(_) => format!("[{}] attachment", idx),
NormalizedMessage::Assistant(_) => unreachable!(),
}
}
NormalizedMessage::Progress(_) => format!("[{}] progress", idx),
NormalizedMessage::System(_) => format!("[{}] system", idx),
NormalizedMessage::Attachment(_) => format!("[{}] attachment", idx),
}
})
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::messages::{
AssistantMessageContent, AssistantMessageExtra, NormalizedAssistantMessage, Usage,
};
fn make_assistant_msg(id: &str, content: Vec<serde_json::Value>) -> NormalizedMessage {
NormalizedMessage::Assistant(NormalizedAssistantMessage {
message: AssistantMessageContent {
id: id.to_string(),
container: None,
model: "claude-sonnet-4-20250514".to_string(),
role: "assistant".to_string(),
stop_reason: Some("tool_use".to_string()),
stop_sequence: None,
message_type: "message".to_string(),
usage: Some(Usage::default()),
content,
context_management: None,
},
extra: AssistantMessageExtra {
request_id: None,
api_error: None,
error: None,
error_details: None,
is_api_error_message: Some(false),
is_virtual: None,
is_meta: None,
advisor_model: None,
uuid: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
parent_uuid: None,
},
})
}
fn make_normalized_user(content: Vec<ContentBlock>) -> NormalizedMessage {
NormalizedMessage::User(NormalizedUserMessage {
message: MessageContent::Blocks(content),
extra: UserMessageExtra {
is_meta: None,
is_visible_in_transcript_only: None,
is_virtual: None,
is_compact_summary: None,
summarize_metadata: None,
tool_use_result: None,
mcp_meta: None,
uuid: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
image_paste_ids: None,
source_tool_assistant_uuid: None,
permission_mode: None,
origin: None,
parent_uuid: None,
},
})
}
fn make_tool_result_block(tool_use_id: &str, _content: &str) -> ContentBlock {
ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: None,
is_error: Some(false),
}
}
}
pub fn smoosh_system_reminder_siblings(
messages: &[NormalizedMessage],
) -> Vec<NormalizedMessage> {
messages
.iter()
.map(|msg| match msg {
NormalizedMessage::User(user) => {
let content = &user.message;
let MessageContent::Blocks(blocks) = content else {
return msg.clone();
};
if !blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. })) {
return msg.clone();
}
let mut sr_texts: Vec<String> = Vec::new();
let mut kept: Vec<ContentBlock> = Vec::new();
for b in blocks {
if let ContentBlock::Text { text } = b {
if text.starts_with("<system-reminder>") {
sr_texts.push(text.clone());
} else {
kept.push(b.clone());
}
} else {
kept.push(b.clone());
}
}
if sr_texts.is_empty() {
return msg.clone();
}
let last_tr_idx = kept.iter().rposition(|b| matches!(b, ContentBlock::ToolResult { .. }));
let last_tr_idx = match last_tr_idx {
Some(idx) => idx,
None => return msg.clone(),
};
let smooshed = match &kept[last_tr_idx] {
ContentBlock::ToolResult {
tool_use_id,
content: existing_content,
is_error,
} => smoosh_into_tool_result(tool_use_id, existing_content, is_error, &sr_texts),
_ => return msg.clone(),
};
match smooshed {
None => msg.clone(),
Some(new_block) => {
let mut new_content = kept.clone();
new_content[last_tr_idx] = new_block;
NormalizedMessage::User(NormalizedUserMessage {
message: MessageContent::Blocks(new_content),
extra: user.extra.clone(),
})
}
}
}
_ => msg.clone(),
})
.collect()
}
fn smoosh_into_tool_result(
tool_use_id: &str,
existing_content: &Option<Vec<ContentBlock>>,
is_error: &Option<bool>,
blocks: &[String],
) -> Option<ContentBlock> {
if blocks.is_empty() {
return Some(ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: existing_content.clone(),
is_error: *is_error,
});
}
if let Some(ref existing) = existing_content {
if existing.iter().any(|b| matches!(b, ContentBlock::ToolReference { .. })) {
return None;
}
}
let is_error = is_error == &Some(true);
if is_error {
let text_blocks: Vec<String> = blocks
.iter()
.filter(|t| !t.is_empty())
.cloned()
.collect();
if text_blocks.is_empty() {
return Some(ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: existing_content.clone(),
is_error: *is_error,
});
}
let new_content: Option<Vec<ContentBlock>> = Some(vec![ContentBlock::Text {
text: text_blocks.join("\n\n"),
}]);
return Some(ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: new_content,
is_error: Some(true),
});
}
let all_text = blocks.iter().all(|b| !b.is_empty());
if all_text && (existing_content.is_none() || matches!(existing_content, Some(v) if v.iter().all(|c| matches!(c, ContentBlock::Text { .. })))) {
let existing_texts: Vec<String> = match existing_content {
Some(ref v) => v.iter()
.filter_map(|b| {
if let ContentBlock::Text { text } = b {
if text.trim().is_empty() { None } else { Some(text.trim().to_string()) }
} else { None }
})
.collect(),
None => Vec::new(),
};
let joined: Vec<String> = [existing_texts, blocks.iter().filter(|b| !b.is_empty()).cloned().collect()].concat();
let text = joined.iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<_>>()
.join("\n\n");
if !text.is_empty() {
return Some(ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: Some(vec![ContentBlock::Text { text }]),
is_error: *is_error,
});
}
}
let base: Vec<ContentBlock> = match existing_content {
None => vec![],
Some(ref v) if v.is_empty() => vec![],
Some(ref v) => v.clone(),
};
let merged: Vec<ContentBlock> = {
let mut all: Vec<ContentBlock> = base;
for b in blocks {
let t = b.trim().to_string();
if t.is_empty() { continue; }
if let Some(last) = all.last_mut() {
if let ContentBlock::Text { text: ref mut txt } = last {
*txt = format!("{}\n\n{}", txt, t);
continue;
}
}
all.push(ContentBlock::Text { text: t });
}
all
};
Some(ContentBlock::ToolResult {
tool_use_id: tool_use_id.to_string(),
content: Some(merged),
is_error: *is_error,
})
}
#[test]
fn test_empty_messages() {
let messages: Vec<NormalizedMessage> = vec![];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_no_tool_uses_no_changes() {
let messages = vec![
make_normalized_user(vec![ContentBlock::Text {
text: "hello".to_string(),
}]),
make_assistant_msg("msg-1", vec![serde_json::json!({"type": "text", "text": "hi"})]),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn test_tool_use_without_result_injects_synthetic() {
let messages = vec![
make_normalized_user(vec![ContentBlock::Text {
text: "hello".to_string(),
}]),
make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Bash",
"input": {"command": "ls"}
})],
),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert_eq!(result.len(), 3);
if let NormalizedMessage::User(synthetic) = &result[2] {
match &synthetic.message {
MessageContent::Blocks(blocks) => {
assert_eq!(blocks.len(), 1);
assert!(matches!(
&blocks[0],
ContentBlock::ToolResult { tool_use_id, is_error, .. }
if tool_use_id == "tool-1" && *is_error == Some(true)
));
}
MessageContent::String(_) => panic!("Expected blocks"),
}
} else {
panic!("Expected user message");
}
}
#[test]
fn test_tool_use_with_matching_result() {
let tool_result = make_tool_result_block("tool-1", "file1 file2");
let messages = vec![
make_normalized_user(vec![ContentBlock::Text {
text: "hello".to_string(),
}]),
make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Bash",
"input": {"command": "ls"}
})],
),
make_normalized_user(vec![tool_result]),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert_eq!(result.len(), 3);
}
#[test]
fn test_duplicate_tool_use_across_assistant_messages() {
let tool_result = make_tool_result_block("tool-1", "result");
let messages = vec![
make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Bash",
"input": {"command": "ls"}
})],
),
make_normalized_user(vec![tool_result]),
make_assistant_msg(
"msg-2",
vec![serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Bash",
"input": {"command": "ls"}
})],
),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
if let NormalizedMessage::Assistant(asst) = &result[2] {
let tool_uses: Vec<_> = asst
.message
.content
.iter()
.filter(|b| json_is_tool_use(b))
.collect();
assert!(tool_uses.is_empty(), "Duplicate tool_use should be stripped");
}
}
#[test]
fn test_orphaned_server_tool_use_stripped() {
let messages = vec![make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "server_tool_use",
"id": "server-1",
"name": "web_search",
"input": {"query": "test"}
})],
)];
let result = ensure_tool_result_pairing(&messages).unwrap();
if let NormalizedMessage::Assistant(asst) = &result[0] {
let server_uses: Vec<_> = asst
.message
.content
.iter()
.filter(|b| {
b.get("type")
.and_then(|t| t.as_str())
.is_some_and(|t| t == "server_tool_use" || t == "mcp_tool_use")
})
.collect();
assert!(
server_uses.is_empty(),
"Orphaned server_tool_use should be stripped"
);
}
}
#[test]
fn test_multiple_missing_tool_results() {
let messages = vec![
make_normalized_user(vec![ContentBlock::Text {
text: "hello".to_string(),
}]),
make_assistant_msg(
"msg-1",
vec![
serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Read",
"input": {"path": "/etc/hosts"}
}),
serde_json::json!({
"type": "tool_use",
"id": "tool-2",
"name": "Bash",
"input": {"command": "ls"}
}),
],
),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert_eq!(result.len(), 3);
if let NormalizedMessage::User(synthetic) = &result[2] {
match &synthetic.message {
MessageContent::Blocks(blocks) => {
assert_eq!(blocks.len(), 2);
for block in blocks {
if let ContentBlock::ToolResult { tool_use_id, is_error, .. } = block {
assert!(tool_use_id == "tool-1" || tool_use_id == "tool-2");
assert_eq!(*is_error, Some(true));
}
}
}
_ => panic!("Expected blocks"),
}
}
}
#[test]
fn test_all_server_tool_uses_stripped_leaves_placeholder() {
let messages = vec![make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "server_tool_use",
"id": "server-1",
"name": "web_search",
"input": {"query": "test"}
})],
)];
let result = ensure_tool_result_pairing(&messages).unwrap();
if let NormalizedMessage::Assistant(asst) = &result[0] {
assert!(!asst.message.content.is_empty());
let has_placeholder = asst
.message
.content
.iter()
.any(|b| {
b.get("type").and_then(|t| t.as_str()) == Some("text")
&& b.get("text")
.and_then(|t| t.as_str())
== Some("[Tool use interrupted]")
});
assert!(has_placeholder, "Should have [Tool use interrupted] placeholder");
}
}
#[test]
fn test_orphaned_user_at_start() {
let messages = vec![make_normalized_user(vec![make_tool_result_block(
"orphan-1",
"result",
)])];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_tool_result_with_matching_tool_use_preserved() {
let tool_result = make_tool_result_block("tool-1", "hello world");
let messages = vec![
make_normalized_user(vec![ContentBlock::Text {
text: "hello".to_string(),
}]),
make_assistant_msg(
"msg-1",
vec![serde_json::json!({
"type": "tool_use",
"id": "tool-1",
"name": "Bash",
"input": {"command": "echo hello"}
})],
),
make_normalized_user(vec![tool_result]),
];
let result = ensure_tool_result_pairing(&messages).unwrap();
assert_eq!(result.len(), 3);
if let NormalizedMessage::User(user) = &result[2] {
match &user.message {
MessageContent::Blocks(blocks) => {
assert_eq!(blocks.len(), 1);
assert!(matches!(&blocks[0], ContentBlock::ToolResult { tool_use_id, .. }
if tool_use_id == "tool-1"));
}
_ => panic!("Expected blocks"),
}
}
}
#[test]
fn test_server_tool_use_with_result_preserved() {
let server_tool_use = serde_json::json!({
"type": "server_tool_use",
"id": "server-1",
"name": "web_search",
"input": {"query": "test"}
});
let server_tool_result = serde_json::json!({
"type": "tool_result",
"tool_use_id": "server-1",
"content": "search results",
});
let messages = vec![make_assistant_msg(
"msg-1",
vec![server_tool_use.clone(), server_tool_result],
)];
let result = ensure_tool_result_pairing(&messages).unwrap();
if let NormalizedMessage::Assistant(asst) = &result[0] {
let server_uses: Vec<_> = asst
.message
.content
.iter()
.filter(|b| {
b.get("type")
.and_then(|t| t.as_str())
.is_some_and(|t| t == "server_tool_use" || t == "mcp_tool_use")
})
.collect();
assert_eq!(
server_uses.len(),
1,
"Server tool_use with matching result should be preserved"
);
}
}
}