recursive/session/
serialize.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TranscriptEntry {
10 #[serde(default)]
12 pub uuid: String,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub parent_uuid: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub source_tool_assistant_uuid: Option<String>,
21 pub id: String,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub parent_id: Option<String>,
24 pub role: String,
25 pub content: String,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub tool_calls: Vec<crate::llm::ToolCall>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub tool_call_id: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub reasoning_content: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub usage: Option<super::UsageMeta>,
35 pub timestamp: String,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub audit: Option<crate::tools::AuditMeta>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
47pub(crate) struct CompactBoundaryEntry {
48 #[serde(rename = "type")]
49 pub(crate) entry_type: String,
50 pub(crate) subtype: String,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub(crate) turn: Option<u32>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub(crate) compacted_count: Option<usize>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub(crate) summary_uuid: Option<String>,
57 pub(crate) timestamp: String,
58}
59
60#[derive(Debug, Clone)]
78pub enum LoadedEntry {
79 Message(Box<TranscriptEntry>),
81 CompactBoundary { turn: Option<u32>, removed: usize },
84}
85
86pub fn entry_to_message(entry: TranscriptEntry) -> crate::message::Message {
101 use crate::message::Role;
102 let role = match entry.role.as_str() {
103 "system" => Role::System,
104 "user" => Role::User,
105 "assistant" => Role::Assistant,
106 "tool" => Role::Tool,
107 _ => Role::User,
108 };
109 crate::message::Message {
110 role,
111 content: entry.content,
112 tool_calls: entry.tool_calls,
113 tool_call_id: entry.tool_call_id,
114 reasoning_content: entry.reasoning_content,
115 is_compaction_summary: false,
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::message::Role;
123
124 fn make_entry(role: &str) -> TranscriptEntry {
125 TranscriptEntry {
126 uuid: String::new(),
127 parent_uuid: None,
128 source_tool_assistant_uuid: None,
129 id: "test-id".into(),
130 parent_id: None,
131 role: role.to_string(),
132 content: "hello".into(),
133 tool_calls: vec![],
134 tool_call_id: None,
135 reasoning_content: None,
136 usage: None,
137 timestamp: "2026-01-01T00:00:00Z".into(),
138 audit: None,
139 }
140 }
141
142 #[test]
143 fn entry_to_message_system_role() {
144 assert_eq!(entry_to_message(make_entry("system")).role, Role::System);
145 }
146
147 #[test]
148 fn entry_to_message_user_role() {
149 assert_eq!(entry_to_message(make_entry("user")).role, Role::User);
150 }
151
152 #[test]
153 fn entry_to_message_assistant_role() {
154 assert_eq!(
155 entry_to_message(make_entry("assistant")).role,
156 Role::Assistant
157 );
158 }
159
160 #[test]
161 fn entry_to_message_tool_role() {
162 assert_eq!(entry_to_message(make_entry("tool")).role, Role::Tool);
163 }
164
165 #[test]
166 fn entry_to_message_unknown_role_maps_to_user() {
167 assert_eq!(entry_to_message(make_entry("bogus")).role, Role::User);
168 }
169
170 #[test]
171 fn entry_to_message_is_not_compaction_summary() {
172 let msg = entry_to_message(make_entry("assistant"));
173 assert!(
174 !msg.is_compaction_summary,
175 "entry_to_message must always produce is_compaction_summary=false"
176 );
177 }
178
179 #[test]
180 fn entry_to_message_propagates_content() {
181 let mut e = make_entry("user");
182 e.content = "unique-payload-xyz".into();
183 let msg = entry_to_message(e);
184 assert_eq!(msg.content, "unique-payload-xyz");
185 }
186}