//! Tool call correlation — matches assistant ToolUse blocks with their //! corresponding user ToolResult blocks by ID across a message sequence. use std::collections::HashMap; use crate::types::{ Content, ContentBlock, RawAssistantMessage, RawMessage, RawUserMessage, ToolCall, ToolExchange, ToolName, ToolResultData, }; /// Extract tool calls from an assistant message's content blocks. fn extract_tool_calls(msg: &RawAssistantMessage) -> Vec { let source_uuid = msg.uuid.clone().unwrap_or_default(); msg.message .content .iter() .filter_map(|block| { if let ContentBlock::ToolUse { id, name, input, .. } = block { Some(ToolCall { id: id.clone(), name: ToolName::from(name.clone()), input: input.clone(), source_message_uuid: source_uuid.clone(), }) } else { None } }) .collect() } /// Extract tool results from a user message's content blocks. fn extract_tool_results(msg: &RawUserMessage) -> Vec { let source_uuid = msg.uuid.clone().unwrap_or_default(); match &msg.message.content { Content::Blocks(blocks) => blocks .iter() .filter_map(|block| { if let ContentBlock::ToolResult { tool_use_id, content, is_error } = block { // Extract text content from the tool result let text_content = content.as_ref().and_then(|c| match c { Content::Text(s) => Some(s.clone()), Content::Blocks(bs) => { // Concatenate text blocks let texts: Vec<&str> = bs .iter() .filter_map(|b| { if let ContentBlock::Text { text } = b { Some(text.as_str()) } else { None } }) .collect(); if texts.is_empty() { None } else { Some(texts.join("\n")) } } }); Some(ToolResultData { tool_use_id: tool_use_id.clone(), content: text_content, is_error: *is_error, source_message_uuid: source_uuid.clone(), }) } else { None } }) .collect(), Content::Text(_) => Vec::new(), } } /// Correlate tool calls with their results across a message sequence. /// /// Iterates messages in order, collecting ToolUse blocks from assistant /// messages and matching them by ID to ToolResult blocks in subsequent user /// messages. Any tool calls that never received a result are emitted with /// `result: None`. pub fn correlate_tools(messages: &[RawMessage]) -> Vec { let mut pending: HashMap = HashMap::new(); let mut exchanges: Vec = Vec::new(); for msg in messages { match msg { RawMessage::Assistant(asst) => { for call in extract_tool_calls(asst) { pending.insert(call.id.clone(), call); } } RawMessage::User(user) => { for result in extract_tool_results(user) { if let Some(call) = pending.remove(&result.tool_use_id) { exchanges.push(ToolExchange { call, result: Some(result) }); } } } _ => {} } } // Emit unmatched calls (no result found) for (_id, call) in pending { exchanges.push(ToolExchange { call, result: None }); } exchanges }