use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptEntry {
#[serde(default)]
pub uuid: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_uuid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_tool_assistant_uuid: Option<String>,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
pub role: String,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<crate::llm::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 = "Option::is_none")]
pub usage: Option<super::UsageMeta>,
pub timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audit: Option<crate::tools::AuditMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CompactBoundaryEntry {
#[serde(rename = "type")]
pub(crate) entry_type: String,
pub(crate) subtype: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) turn: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) compacted_count: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) summary_uuid: Option<String>,
pub(crate) timestamp: String,
}
#[derive(Debug, Clone)]
pub enum LoadedEntry {
Message(Box<TranscriptEntry>),
CompactBoundary { turn: Option<u32>, removed: usize },
}
pub fn entry_to_message(entry: TranscriptEntry) -> crate::message::Message {
use crate::message::Role;
let role = match entry.role.as_str() {
"system" => Role::System,
"user" => Role::User,
"assistant" => Role::Assistant,
"tool" => Role::Tool,
_ => Role::User,
};
crate::message::Message {
role,
content: entry.content,
tool_calls: entry.tool_calls,
tool_call_id: entry.tool_call_id,
reasoning_content: entry.reasoning_content,
is_compaction_summary: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Role;
fn make_entry(role: &str) -> TranscriptEntry {
TranscriptEntry {
uuid: String::new(),
parent_uuid: None,
source_tool_assistant_uuid: None,
id: "test-id".into(),
parent_id: None,
role: role.to_string(),
content: "hello".into(),
tool_calls: vec![],
tool_call_id: None,
reasoning_content: None,
usage: None,
timestamp: "2026-01-01T00:00:00Z".into(),
audit: None,
}
}
#[test]
fn entry_to_message_system_role() {
assert_eq!(entry_to_message(make_entry("system")).role, Role::System);
}
#[test]
fn entry_to_message_user_role() {
assert_eq!(entry_to_message(make_entry("user")).role, Role::User);
}
#[test]
fn entry_to_message_assistant_role() {
assert_eq!(
entry_to_message(make_entry("assistant")).role,
Role::Assistant
);
}
#[test]
fn entry_to_message_tool_role() {
assert_eq!(entry_to_message(make_entry("tool")).role, Role::Tool);
}
#[test]
fn entry_to_message_unknown_role_maps_to_user() {
assert_eq!(entry_to_message(make_entry("bogus")).role, Role::User);
}
#[test]
fn entry_to_message_is_not_compaction_summary() {
let msg = entry_to_message(make_entry("assistant"));
assert!(
!msg.is_compaction_summary,
"entry_to_message must always produce is_compaction_summary=false"
);
}
#[test]
fn entry_to_message_propagates_content() {
let mut e = make_entry("user");
e.content = "unique-payload-xyz".into();
let msg = entry_to_message(e);
assert_eq!(msg.content, "unique-payload-xyz");
}
}