use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum HistoryMessage {
FunctionCall(FunctionCallHistoryMessage),
Conversation(ConversationHistoryMessage),
Unknown(serde_json::Value),
}
impl HistoryMessage {
pub fn conversation(role: ConversationRole, content: impl Into<String>) -> Self {
Self::Conversation(ConversationHistoryMessage::new(role, content))
}
pub fn function_calls(calls: impl IntoIterator<Item = HistoryFunctionCall>) -> Self {
Self::FunctionCall(FunctionCallHistoryMessage::new(calls))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HistoryMessageType {
#[default]
History,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ConversationHistoryMessage {
#[serde(rename = "type", default)]
pub message_type: HistoryMessageType,
pub role: ConversationRole,
pub content: String,
}
impl ConversationHistoryMessage {
pub fn new(role: ConversationRole, content: impl Into<String>) -> Self {
Self {
message_type: HistoryMessageType::History,
role,
content: content.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ConversationRole {
User,
Assistant,
Unknown(String),
}
impl ConversationRole {
pub fn as_str(&self) -> &str {
match self {
ConversationRole::User => "user",
ConversationRole::Assistant => "assistant",
ConversationRole::Unknown(value) => value,
}
}
}
impl Serialize for ConversationRole {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ConversationRole {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"user" => ConversationRole::User,
"assistant" => ConversationRole::Assistant,
_ => ConversationRole::Unknown(value),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FunctionCallHistoryMessage {
#[serde(rename = "type", default)]
pub message_type: HistoryMessageType,
pub function_calls: Vec<HistoryFunctionCall>,
}
impl FunctionCallHistoryMessage {
pub fn new(calls: impl IntoIterator<Item = HistoryFunctionCall>) -> Self {
Self {
message_type: HistoryMessageType::History,
function_calls: calls.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HistoryFunctionCall {
pub id: String,
pub name: String,
pub client_side: bool,
pub arguments: String,
pub response: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
impl HistoryFunctionCall {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
client_side: bool,
arguments: impl Into<String>,
response: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
client_side,
arguments: arguments.into(),
response: response.into(),
thought_signature: None,
}
}
pub fn with_thought_signature(mut self, signature: impl Into<String>) -> Self {
self.thought_signature = Some(signature.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn conversation_round_trip() {
let raw = json!({
"type": "History",
"role": "user",
"content": "Hello, agent."
});
let msg: ConversationHistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(msg.role, ConversationRole::User);
assert_eq!(msg.content, "Hello, agent.");
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn conversation_assistant_role() {
let raw = json!({
"type": "History",
"role": "assistant",
"content": "Hi there!"
});
let msg: ConversationHistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(msg.role, ConversationRole::Assistant);
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn function_call_round_trip_minimal() {
let raw = json!({
"type": "History",
"function_calls": [{
"id": "f1",
"name": "get_weather",
"client_side": true,
"arguments": "{\"city\":\"NYC\"}",
"response": "{\"temp\":72}"
}]
});
let msg: FunctionCallHistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(msg.function_calls.len(), 1);
assert_eq!(msg.function_calls[0].id, "f1");
assert!(msg.function_calls[0].client_side);
assert!(msg.function_calls[0].thought_signature.is_none());
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn function_call_round_trip_with_thought_signature() {
let raw = json!({
"type": "History",
"function_calls": [{
"id": "f2",
"name": "lookup",
"client_side": false,
"arguments": "{}",
"response": "{}",
"thought_signature": "sig-abc"
}]
});
let msg: FunctionCallHistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(
msg.function_calls[0].thought_signature.as_deref(),
Some("sig-abc")
);
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn enum_dispatches_conversation() {
let raw = json!({
"type": "History",
"role": "user",
"content": "Hi"
});
let msg: HistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(msg, HistoryMessage::Conversation(_)));
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn enum_dispatches_function_call() {
let raw = json!({
"type": "History",
"function_calls": [{
"id": "f1",
"name": "fn",
"client_side": false,
"arguments": "{}",
"response": "{}"
}]
});
let msg: HistoryMessage = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(msg, HistoryMessage::FunctionCall(_)));
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn enum_constructor_helpers() {
let conv = HistoryMessage::conversation(ConversationRole::User, "Hello");
match &conv {
HistoryMessage::Conversation(c) => {
assert_eq!(c.role, ConversationRole::User);
assert_eq!(c.content, "Hello");
}
_ => panic!("expected conversation"),
}
let fc = HistoryMessage::function_calls([HistoryFunctionCall::new(
"f1", "fn", true, "{}", "{}",
)]);
match &fc {
HistoryMessage::FunctionCall(f) => {
assert_eq!(f.function_calls.len(), 1);
assert!(f.function_calls[0].client_side);
}
_ => panic!("expected function call"),
}
}
#[test]
fn function_call_with_thought_signature_builder() {
let call = HistoryFunctionCall::new("f1", "lookup", false, "{}", "{}")
.with_thought_signature("sig");
assert_eq!(call.thought_signature.as_deref(), Some("sig"));
}
#[test]
fn history_type_defaults_to_history() {
let msg = ConversationHistoryMessage::new(ConversationRole::Assistant, "Hi");
assert_eq!(msg.message_type, HistoryMessageType::History);
let serialized = serde_json::to_value(&msg).unwrap();
assert_eq!(serialized["type"], "History");
}
#[test]
fn history_type_default_when_absent_in_input() {
let raw = json!({ "role": "user", "content": "x" });
let msg: ConversationHistoryMessage = serde_json::from_value(raw).unwrap();
assert_eq!(msg.message_type, HistoryMessageType::History);
}
#[test]
fn unknown_role_round_trips_to_the_wire_string() {
let raw = json!({
"type": "History",
"role": "system",
"content": "You are a helpful agent."
});
let msg: ConversationHistoryMessage = serde_json::from_value(raw.clone())
.expect("an unrecognized role must deserialize, not error");
assert_eq!(msg.role, ConversationRole::Unknown("system".to_string()));
assert_eq!(msg.role.as_str(), "system");
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn unknown_role_still_dispatches_to_conversation() {
let raw = json!({
"type": "History",
"role": "tool",
"content": "{}"
});
let msg: HistoryMessage = serde_json::from_value(raw.clone())
.expect("an unrecognized role must deserialize, not error");
match &msg {
HistoryMessage::Conversation(c) => {
assert_eq!(c.role, ConversationRole::Unknown("tool".to_string()));
}
other => panic!("expected a conversation entry, got {other:?}"),
}
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn known_roles_still_serialize_lowercase() {
assert_eq!(ConversationRole::User.as_str(), "user");
assert_eq!(ConversationRole::Assistant.as_str(), "assistant");
assert_eq!(
serde_json::to_value(ConversationRole::User).unwrap(),
json!("user")
);
assert_eq!(
serde_json::to_value(ConversationRole::Assistant).unwrap(),
json!("assistant")
);
}
#[test]
fn unknown_history_shape_lands_in_unknown_with_its_json() {
let raw = json!({
"type": "History",
"tool_result": { "id": "t1", "output": "42" }
});
let msg: HistoryMessage = serde_json::from_value(raw.clone())
.expect("an unmodeled history shape must deserialize, not error");
match &msg {
HistoryMessage::Unknown(value) => assert_eq!(value, &raw),
other => panic!("expected the Unknown catch-all, got {other:?}"),
}
assert_eq!(serde_json::to_value(&msg).unwrap(), raw);
}
#[test]
fn unknown_variant_is_tried_last() {
let conversation = json!({ "type": "History", "role": "user", "content": "Hi" });
let function_call = json!({
"type": "History",
"function_calls": [{
"id": "f1", "name": "fn", "client_side": true,
"arguments": "{}", "response": "{}"
}]
});
assert!(matches!(
serde_json::from_value::<HistoryMessage>(conversation).unwrap(),
HistoryMessage::Conversation(_)
));
assert!(matches!(
serde_json::from_value::<HistoryMessage>(function_call).unwrap(),
HistoryMessage::FunctionCall(_)
));
}
#[test]
fn list_of_history_messages() {
let raw = json!([
{ "type": "History", "role": "user", "content": "Hi" },
{
"type": "History",
"function_calls": [{
"id": "f1", "name": "fn", "client_side": true,
"arguments": "{}", "response": "{}"
}]
},
{ "type": "History", "role": "assistant", "content": "Hello!" }
]);
let msgs: Vec<HistoryMessage> = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(msgs.len(), 3);
assert!(matches!(msgs[0], HistoryMessage::Conversation(_)));
assert!(matches!(msgs[1], HistoryMessage::FunctionCall(_)));
assert!(matches!(msgs[2], HistoryMessage::Conversation(_)));
assert_eq!(serde_json::to_value(&msgs).unwrap(), raw);
}
}