use crate::message::{ToolCall, ToolResultData};
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum IrPart {
Text(String),
Thinking(String),
ToolCall(ToolCall),
}
impl IrPart {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(text.into())
}
#[must_use]
pub fn thinking(text: impl Into<String>) -> Self {
Self::Thinking(text.into())
}
#[must_use]
pub fn tool_call(call: ToolCall) -> Self {
Self::ToolCall(call)
}
}
#[derive(Debug, Clone, Default)]
pub struct IrMessage {
pub parts: Vec<IrPart>,
}
impl IrMessage {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, part: IrPart) {
self.parts.push(part);
}
#[must_use]
pub fn text(&self) -> String {
self.parts
.iter()
.filter_map(|part| match part {
IrPart::Text(text) => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn thinking(&self) -> Vec<String> {
self.parts
.iter()
.filter_map(|part| match part {
IrPart::Thinking(text) => Some(text.clone()),
_ => None,
})
.collect()
}
#[must_use]
pub fn tool_calls(&self) -> Vec<ToolCall> {
self.parts
.iter()
.filter_map(|part| match part {
IrPart::ToolCall(call) => Some(call.clone()),
_ => None,
})
.collect()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
}
#[must_use]
pub fn part_text(part: &Value) -> Option<&str> {
part["text"]
.as_str()
.or_else(|| part["input_text"].as_str())
.or_else(|| part["output_text"].as_str())
}
#[must_use]
pub fn extract_text(content: Option<&Value>) -> String {
match content {
Some(Value::Array(parts)) => parts
.iter()
.filter_map(part_text)
.map(str::to_string)
.collect::<Vec<_>>()
.join("\n"),
Some(Value::String(text)) => text.clone(),
_ => String::new(),
}
}
#[must_use]
pub fn parse_tool_arguments(raw: &Value) -> Value {
if let Some(text) = raw.as_str()
&& let Ok(parsed) = serde_json::from_str::<Value>(text)
{
return parsed;
}
raw.clone()
}
#[must_use]
pub fn build_tool_call(name: impl Into<String>, arguments: Option<&Value>) -> ToolCall {
let arguments = arguments.map_or(Value::Null, parse_tool_arguments);
ToolCall {
name: name.into(),
arguments,
}
}
#[must_use]
pub fn build_tool_result(
fallback_name: impl Into<String>,
content_text: impl Into<String>,
is_error: bool,
) -> ToolResultData {
ToolResultData {
tool_name: fallback_name.into(),
content: content_text.into(),
is_error,
}
}
#[must_use]
pub fn extract_tool_result_from_part(part: &Value) -> Option<ToolResultData> {
let is_result = matches!(
part["type"].as_str(),
Some("tool_result" | "toolResult" | "toolResponse")
);
if !is_result && part.get("toolResult").is_none() {
return None;
}
let tool_name = part["name"]
.as_str()
.or_else(|| part["toolName"].as_str())
.or_else(|| part["call_id"].as_str())
.unwrap_or("tool")
.to_string();
let content = if let Some(content) = part.get("content") {
extract_text(Some(content))
} else if let Some(content) = part.get("output") {
extract_text(Some(content))
} else if let Some(tool_result_value) = part.get("toolResult") {
extract_text(
tool_result_value
.get("value")
.and_then(|v| v.get("content")),
)
} else {
String::new()
};
let is_error = part["isError"].as_bool().unwrap_or(false)
|| part["is_error"].as_bool().unwrap_or(false)
|| matches!(
part["status"].as_str(),
Some("error" | "failed" | "failure")
)
|| part["toolResult"]["status"]
.as_str()
.is_some_and(|status| status == "error");
Some(build_tool_result(tool_name, content, is_error))
}