use serde::{Deserialize, Serialize};
use crate::llm::ToolCall;
#[inline]
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub is_compaction_summary: bool,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: content.into(),
tool_calls: Vec::new(),
tool_call_id: None,
reasoning_content: None,
is_compaction_summary: false,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: content.into(),
tool_calls: Vec::new(),
tool_call_id: None,
reasoning_content: None,
is_compaction_summary: false,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
tool_calls: Vec::new(),
tool_call_id: None,
reasoning_content: None,
is_compaction_summary: false,
}
}
pub fn assistant_with_tool_calls(
content: impl Into<String>,
tool_calls: Vec<ToolCall>,
) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
tool_calls,
tool_call_id: None,
reasoning_content: None,
is_compaction_summary: false,
}
}
pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: content.into(),
tool_calls: Vec::new(),
tool_call_id: Some(tool_call_id.into()),
reasoning_content: None,
is_compaction_summary: false,
}
}
pub fn with_compaction_summary(mut self) -> Self {
self.is_compaction_summary = true;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compaction_summary_bit_default_false() {
let m = Message::user("hi");
assert!(!m.is_compaction_summary);
}
#[test]
fn with_compaction_summary_sets_bit() {
let m = Message::system("...").with_compaction_summary();
assert!(m.is_compaction_summary);
}
#[test]
fn compaction_summary_bit_omitted_from_json_when_false() {
let m = Message::user("hi");
let json = serde_json::to_string(&m).unwrap();
assert!(!json.contains("is_compaction_summary"));
}
#[test]
fn compaction_summary_bit_included_in_json_when_true() {
let m = Message::system("summary").with_compaction_summary();
let json = serde_json::to_string(&m).unwrap();
assert!(
json.contains("\"is_compaction_summary\":true"),
"is_compaction_summary must be serialised when true; got: {json}"
);
let restored: Message = serde_json::from_str(&json).unwrap();
assert!(
restored.is_compaction_summary,
"is_compaction_summary must survive a serde round-trip"
);
}
#[test]
fn is_false_returns_true_for_false_value() {
assert!(is_false(&false));
}
#[test]
fn is_false_returns_false_for_true_value() {
assert!(!is_false(&true));
}
#[test]
fn constructor_roles_are_correct() {
assert_eq!(Message::system("").role, Role::System);
assert_eq!(Message::user("").role, Role::User);
assert_eq!(Message::assistant("").role, Role::Assistant);
assert_eq!(
Message::assistant_with_tool_calls("", Vec::new()).role,
Role::Assistant
);
assert_eq!(Message::tool_result("id", "").role, Role::Tool);
}
#[test]
fn tool_result_sets_tool_call_id() {
let m = Message::tool_result("call-123", "output");
assert_eq!(m.tool_call_id.as_deref(), Some("call-123"));
}
#[test]
fn constructors_set_empty_tool_calls_by_default() {
assert!(Message::system("").tool_calls.is_empty());
assert!(Message::user("").tool_calls.is_empty());
assert!(Message::assistant("").tool_calls.is_empty());
assert!(Message::tool_result("", "").tool_calls.is_empty());
}
#[test]
fn assistant_with_tool_calls_stores_tool_calls() {
use crate::llm::ToolCall;
let tc = ToolCall {
id: "tc-1".to_string(),
name: "Read".to_string(),
arguments: serde_json::json!({}),
};
let m = Message::assistant_with_tool_calls("calling", vec![tc]);
assert_eq!(m.tool_calls.len(), 1, "tool_calls must be stored");
assert_eq!(m.tool_calls[0].id, "tc-1");
}
#[test]
fn constructors_store_content() {
assert_eq!(Message::system("sys-content").content, "sys-content");
assert_eq!(Message::user("user-content").content, "user-content");
assert_eq!(Message::assistant("asst-content").content, "asst-content");
assert_eq!(Message::tool_result("id", "tool-out").content, "tool-out");
}
}