use super::{Dialect, RenderedToolResult};
use crate::messages::{ContentBlock, Message, MessageContent, TextContent, UserMessage};
use crate::tools::Tool;
pub fn encode_inband_tool_history(
messages: &[Message],
dialect: Dialect,
tools: &[Tool],
) -> Vec<Message> {
let mut out: Vec<Message> = Vec::with_capacity(messages.len());
let mut i = 0usize;
while i < messages.len() {
match &messages[i] {
Message::Assistant(m) => {
out.push(encode_assistant(m, dialect, tools));
i += 1;
}
Message::ToolResult(_) => {
let start = i;
while i < messages.len() && matches!(messages[i], Message::ToolResult(_)) {
i += 1;
}
out.push(encode_tool_results(&messages[start..i], dialect));
}
other => {
out.push(other.clone());
i += 1;
}
}
}
out
}
fn encode_assistant(
message: &crate::messages::AssistantMessage,
dialect: Dialect,
tools: &[Tool],
) -> Message {
let tool_calls: Vec<_> = message
.content
.iter()
.filter_map(|b| b.as_tool_call())
.cloned()
.collect();
if tool_calls.is_empty() {
return Message::Assistant(message.clone());
}
let prose: Vec<&str> = message.content.iter().filter_map(|b| b.as_text()).collect();
let prose = prose.join("\n");
let rendered = dialect.render_tool_calls(&tool_calls, tools);
let text = if prose.trim().is_empty() {
rendered
} else {
format!("{}\n{}", prose.trim_end(), rendered)
};
let mut out = message.clone();
out.content = vec![ContentBlock::Text(TextContent::new(text))];
Message::Assistant(out)
}
fn encode_tool_results(results: &[Message], dialect: Dialect) -> Message {
let mut dialect_results = Vec::with_capacity(results.len());
let mut timestamp = chrono::Utc::now().timestamp_millis();
for (index, result) in results.iter().enumerate() {
if let Message::ToolResult(tr) = result {
if index == 0 {
timestamp = tr.timestamp;
}
dialect_results.push(RenderedToolResult {
id: tr.tool_call_id.clone(),
name: tr.tool_name.clone(),
index,
text: tr.text_content().unwrap_or_default(),
is_error: tr.is_error,
});
}
}
let text = dialect.render_tool_results(&dialect_results);
let mut user = UserMessage::new(MessageContent::Text(text));
user.timestamp = timestamp;
Message::User(user)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Api;
use crate::messages::{AssistantMessage, ToolCall, ToolResultMessage};
use serde_json::json;
fn echo_tool() -> Tool {
Tool {
name: "echo".to_string(),
description: "Echo".to_string(),
parameters: json!({
"type": "object",
"properties": {"msg": {"type": "string"}},
}),
}
}
#[test]
fn assistant_tool_calls_become_text() {
let tools = vec![echo_tool()];
let mut assistant = AssistantMessage::new(Api::OpenAiCompletions, "test", "model");
assistant.content = vec![
ContentBlock::Text(TextContent::new("Running echo.")),
ContentBlock::ToolCall(ToolCall::new("c1", "echo", json!({"msg": "hi"}))),
];
let messages = vec![Message::Assistant(assistant)];
let encoded = encode_inband_tool_history(&messages, Dialect::Xml, &tools);
assert_eq!(encoded.len(), 1);
let Message::Assistant(a) = &encoded[0] else {
panic!("expected assistant");
};
let text = a.text_content();
assert!(text.contains("Running echo."));
assert!(text.contains("invoke"));
assert!(text.contains("echo"));
assert!(a.content.iter().all(|b| b.as_tool_call().is_none()));
}
#[test]
fn tool_result_run_collapses_to_single_user_message() {
let results = vec![
Message::ToolResult(ToolResultMessage::new(
"c1",
"echo",
vec![ContentBlock::Text(TextContent::new("out1"))],
)),
Message::ToolResult(ToolResultMessage::new(
"c2",
"echo",
vec![ContentBlock::Text(TextContent::new("out2"))],
)),
];
let encoded = encode_inband_tool_history(&results, Dialect::Xml, &[]);
assert_eq!(encoded.len(), 1);
let Message::User(u) = &encoded[0] else {
panic!("expected user");
};
let text = match &u.content {
MessageContent::Text(s) => s.clone(),
_ => String::new(),
};
assert!(text.contains("out1"));
assert!(text.contains("out2"));
}
#[test]
fn plain_messages_pass_through() {
let user = Message::User(UserMessage::new("hello"));
let encoded = encode_inband_tool_history(std::slice::from_ref(&user), Dialect::Xml, &[]);
assert_eq!(encoded.len(), 1);
assert_eq!(encoded[0].text_content().unwrap(), "hello");
}
#[test]
fn mixed_history_preserves_order() {
let tools = vec![echo_tool()];
let mut assistant = AssistantMessage::new(Api::OpenAiCompletions, "test", "model");
assistant.content = vec![ContentBlock::ToolCall(ToolCall::new(
"c1",
"echo",
json!({"msg": "x"}),
))];
let messages = vec![
Message::User(UserMessage::new("go")),
Message::Assistant(assistant),
Message::ToolResult(ToolResultMessage::new(
"c1",
"echo",
vec![ContentBlock::Text(TextContent::new("done"))],
)),
];
let encoded = encode_inband_tool_history(&messages, Dialect::Xml, &tools);
assert_eq!(encoded.len(), 3);
assert!(matches!(encoded[0], Message::User(_)));
assert!(matches!(encoded[1], Message::Assistant(_)));
assert!(matches!(encoded[2], Message::User(_))); }
}