use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
fn default_tool_status() -> String {
"success".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub name: String,
pub args: JsonValue,
#[serde(default)]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Message {
Human {
content: MessageContent,
#[serde(default)]
id: Option<String>,
},
Ai {
content: MessageContent,
#[serde(default)]
tool_calls: Vec<ToolCall>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
usage: Option<crate::traits::LlmUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thinking: Option<String>,
},
System {
content: MessageContent,
#[serde(default)]
id: Option<String>,
},
Tool {
content: MessageContent,
tool_call_id: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
id: Option<String>,
#[serde(default = "default_tool_status")]
status: String,
},
Remove {
id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
ImageUrl {
image_url: ImageUrl,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(default)]
pub detail: Option<String>,
}
impl Message {
pub fn text(&self) -> Option<&str> {
match self {
Message::Human { content, .. }
| Message::Ai { content, .. }
| Message::System { content, .. }
| Message::Tool { content, .. } => match content {
MessageContent::Text(s) => Some(s.as_str()),
MessageContent::Blocks(blocks) => {
blocks.iter().find_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
}
},
Message::Remove { .. } => None,
}
}
pub fn id(&self) -> Option<&str> {
match self {
Message::Human { id, .. }
| Message::Ai { id, .. }
| Message::System { id, .. }
| Message::Tool { id, .. } => id.as_deref(),
Message::Remove { id } => Some(id.as_str()),
}
}
pub fn has_tool_calls(&self) -> bool {
match self {
Message::Ai { tool_calls, .. } => !tool_calls.is_empty(),
_ => false,
}
}
pub fn tool_calls(&self) -> &[ToolCall] {
match self {
Message::Ai { tool_calls, .. } => tool_calls,
_ => &[],
}
}
pub fn human(content: impl Into<String>) -> Self {
Message::Human {
content: MessageContent::Text(content.into()),
id: None,
}
}
pub fn ai(content: impl Into<String>) -> Self {
Message::Ai {
content: MessageContent::Text(content.into()),
tool_calls: vec![],
id: None,
usage: None,
thinking: None,
}
}
pub fn ai_with_tool_calls(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
Message::Ai {
content: MessageContent::Text(content.into()),
tool_calls,
id: None,
usage: None,
thinking: None,
}
}
pub fn ai_with_usage(content: impl Into<String>, usage: crate::traits::LlmUsage) -> Self {
Message::Ai {
content: MessageContent::Text(content.into()),
tool_calls: vec![],
id: None,
usage: Some(usage),
thinking: None,
}
}
pub fn ai_with_tool_calls_and_usage(
content: impl Into<String>,
tool_calls: Vec<ToolCall>,
usage: crate::traits::LlmUsage,
) -> Self {
Message::Ai {
content: MessageContent::Text(content.into()),
tool_calls,
id: None,
usage: Some(usage),
thinking: None,
}
}
pub fn usage(&self) -> Option<&crate::traits::LlmUsage> {
match self {
Message::Ai { usage, .. } => usage.as_ref(),
_ => None,
}
}
pub fn thinking(&self) -> Option<&str> {
match self {
Message::Ai { thinking, .. } => thinking.as_deref(),
_ => None,
}
}
pub fn ai_with_thinking(
content: impl Into<String>,
thinking: impl Into<String>,
) -> Self {
Message::Ai {
content: MessageContent::Text(content.into()),
tool_calls: vec![],
id: None,
usage: None,
thinking: Some(thinking.into()),
}
}
pub fn system(content: impl Into<String>) -> Self {
Message::System {
content: MessageContent::Text(content.into()),
id: None,
}
}
pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Message::Tool {
content: MessageContent::Text(content.into()),
tool_call_id: tool_call_id.into(),
name: None,
id: None,
status: "success".to_string(),
}
}
pub fn tool_error(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Message::Tool {
content: MessageContent::Text(content.into()),
tool_call_id: tool_call_id.into(),
name: None,
id: None,
status: "error".to_string(),
}
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Message::Human { content, .. } => write!(f, "[Human] {}", content_text(content)),
Message::Ai { content, tool_calls, thinking, .. } => {
let text = content_text(content);
if let Some(t) = thinking {
write!(f, "[Thinking] {}\n[AI] {}", t, text)?;
if !tool_calls.is_empty() {
let calls: Vec<String> = tool_calls
.iter()
.map(|tc| format!("{}({})", tc.name, tc.args))
.collect();
write!(f, " → {}", calls.join(", "))?;
}
Ok(())
} else if tool_calls.is_empty() {
write!(f, "[AI] {}", text)
} else {
let calls: Vec<String> = tool_calls
.iter()
.map(|tc| format!("{}({})", tc.name, tc.args))
.collect();
if text.is_empty() {
write!(f, "[AI] → {}", calls.join(", "))
} else {
write!(f, "[AI] {} → {}", text, calls.join(", "))
}
}
}
Message::System { content, .. } => write!(f, "[System] {}", content_text(content)),
Message::Tool { content, name, status, .. } => {
let tool_name = name.as_deref().unwrap_or("tool");
let text = content_text(content);
if status == "error" {
write!(f, "[Tool:{}] ERROR: {}", tool_name, text)
} else {
write!(f, "[Tool:{}] {}", tool_name, text)
}
}
Message::Remove { id } => write!(f, "[Remove:{}]", id),
}
}
}
fn content_text(content: &MessageContent) -> &str {
match content {
MessageContent::Text(s) => s.as_str(),
MessageContent::Blocks(blocks) => blocks
.iter()
.find_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.unwrap_or(""),
}
}
impl From<String> for MessageContent {
fn from(s: String) -> Self {
MessageContent::Text(s)
}
}
impl From<&str> for MessageContent {
fn from(s: &str) -> Self {
MessageContent::Text(s.to_string())
}
}
pub fn add_messages(current: JsonValue, update: JsonValue) -> JsonValue {
if let Some(obj) = update.as_object() {
if obj.get("reset").and_then(|v| v.as_bool()) == Some(true) {
if let Some(msgs) = obj.get("messages").and_then(|v| v.as_array()) {
return JsonValue::Array(msgs.clone());
}
}
}
let messages: Vec<JsonValue> = match current {
JsonValue::Array(arr) => arr,
_ => vec![],
};
let new_messages: Vec<JsonValue> = match update {
JsonValue::Array(arr) => arr,
other => vec![other],
};
let mut result: Vec<JsonValue> = Vec::new();
let mut remove_ids: Vec<String> = Vec::new();
for msg in &new_messages {
if let Some(obj) = msg.as_object() {
if obj.get("type").and_then(|v| v.as_str()) == Some("remove") {
if let Some(id) = obj.get("id").and_then(|v| v.as_str()) {
remove_ids.push(id.to_string());
}
}
}
}
for msg in messages {
if let Some(id) = msg.get("id").and_then(|v| v.as_str()) {
if remove_ids.contains(&id.to_string()) {
continue;
}
}
result.push(msg);
}
for msg in new_messages {
if let Some(obj) = msg.as_object() {
if obj.get("type").and_then(|v| v.as_str()) == Some("remove") {
continue;
}
}
result.push(msg);
}
JsonValue::Array(result)
}
pub fn add_messages_ref(current: &JsonValue, update: &JsonValue) -> JsonValue {
add_messages(current.clone(), update.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_human() {
let msg = Message::human("Hello");
assert_eq!(msg.text(), Some("Hello"));
assert!(msg.id().is_none());
}
#[test]
fn test_message_ai_with_tool_calls() {
let tc = ToolCall {
name: "search".into(),
args: serde_json::json!({"query": "test"}),
id: Some("call_1".into()),
};
let msg = Message::ai_with_tool_calls("", vec![tc]);
assert!(msg.has_tool_calls());
assert_eq!(msg.tool_calls().len(), 1);
assert_eq!(msg.tool_calls()[0].name, "search");
}
#[test]
fn test_add_messages() {
let existing = serde_json::json!([
{"type": "human", "content": "Hi"},
]);
let update = serde_json::json!([
{"type": "ai", "content": "Hello"},
]);
let result = add_messages(existing, update);
assert_eq!(result.as_array().unwrap().len(), 2);
}
#[test]
fn test_remove_message() {
let existing = serde_json::json!([
{"type": "human", "content": "Hi", "id": "msg1"},
{"type": "ai", "content": "Hello", "id": "msg2"},
]);
let update = serde_json::json!([
{"type": "remove", "id": "msg1"},
]);
let result = add_messages(existing, update);
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"], "msg2");
}
#[test]
fn test_message_serialization() {
let msg = Message::human("Hello world");
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("human"));
assert!(json.contains("Hello world"));
let deserialized: Message = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.text(), Some("Hello world"));
}
}