vtcode_core/utils/session_archive/
message.rs1use serde::{Deserialize, Deserializer, Serialize};
2
3use crate::llm::provider::{AssistantPhase, Message, MessageClearAt, MessageContent, MessageRole, ToolCall};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct SessionMessage {
7 pub role: MessageRole,
8 pub content: MessageContent,
9 #[serde(
11 default,
12 skip_serializing_if = "Option::is_none",
13 deserialize_with = "deserialize_boxed_non_empty_string_opt"
14 )]
15 pub reasoning: Option<Box<String>>,
16 #[serde(
17 default,
18 skip_serializing_if = "Option::is_none",
19 deserialize_with = "deserialize_boxed_non_empty_vec_opt"
20 )]
21 pub reasoning_details: Option<Box<Vec<serde_json::Value>>>,
22 #[serde(
23 default,
24 skip_serializing_if = "Option::is_none",
25 deserialize_with = "deserialize_boxed_non_empty_vec_opt"
26 )]
27 pub tool_calls: Option<Box<Vec<ToolCall>>>,
28 #[serde(default, deserialize_with = "deserialize_boxed_non_empty_string_opt")]
29 pub tool_call_id: Option<Box<String>>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub phase: Option<AssistantPhase>,
32 #[serde(
33 default,
34 skip_serializing_if = "Option::is_none",
35 deserialize_with = "deserialize_boxed_non_empty_string_opt"
36 )]
37 pub origin_tool: Option<Box<String>>,
38 #[serde(
39 default,
40 skip_serializing_if = "Option::is_none",
41 deserialize_with = "deserialize_boxed_metadata_opt"
42 )]
43 pub metadata: Option<Box<crate::core::message_metadata::MessageMetadata>>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub clear_at: Option<MessageClearAt>,
46}
47
48impl Eq for SessionMessage {}
49
50#[expect(
51 clippy::box_collection,
52 reason = "Intentional compatibility, platform, test, or API-shape suppression."
53)]
54#[inline]
55fn boxed_non_empty_string(value: Option<String>) -> Option<Box<String>> {
56 value.and_then(|value| (!value.is_empty()).then_some(Box::new(value)))
57}
58
59#[expect(
60 clippy::box_collection,
61 reason = "Intentional compatibility, platform, test, or API-shape suppression."
62)]
63#[inline]
64fn boxed_non_empty_vec<T>(value: Option<Vec<T>>) -> Option<Box<Vec<T>>> {
65 value.and_then(|value| (!value.is_empty()).then_some(Box::new(value)))
66}
67
68#[expect(
69 clippy::box_collection,
70 reason = "Intentional compatibility, platform, test, or API-shape suppression."
71)]
72fn clone_non_empty_boxed_string(value: &Option<Box<String>>) -> Option<String> {
73 value
74 .as_deref()
75 .and_then(|value| (!value.is_empty()).then_some(value.to_owned()))
76}
77
78fn deserialize_boxed_metadata_opt<'de, D>(
79 deserializer: D,
80) -> Result<Option<Box<crate::core::message_metadata::MessageMetadata>>, D::Error>
81where
82 D: Deserializer<'de>,
83{
84 crate::core::message_metadata::MessageMetadata::deserialize(deserializer).map(|m| Some(Box::new(m)))
85}
86
87#[expect(
88 clippy::box_collection,
89 reason = "Intentional compatibility, platform, test, or API-shape suppression."
90)]
91fn clone_non_empty_boxed_vec<T: Clone>(value: &Option<Box<Vec<T>>>) -> Option<Vec<T>> {
92 value.as_deref().and_then(|value| (!value.is_empty()).then_some(value.clone()))
93}
94
95#[expect(
96 clippy::box_collection,
97 reason = "Intentional compatibility, platform, test, or API-shape suppression."
98)]
99fn deserialize_boxed_non_empty_string_opt<'de, D>(deserializer: D) -> Result<Option<Box<String>>, D::Error>
100where
101 D: Deserializer<'de>,
102{
103 Option::<String>::deserialize(deserializer).map(boxed_non_empty_string)
104}
105
106#[expect(
107 clippy::box_collection,
108 reason = "Intentional compatibility, platform, test, or API-shape suppression."
109)]
110fn deserialize_boxed_non_empty_vec_opt<'de, D, T>(deserializer: D) -> Result<Option<Box<Vec<T>>>, D::Error>
111where
112 D: Deserializer<'de>,
113 T: Deserialize<'de>,
114{
115 Option::<Vec<T>>::deserialize(deserializer).map(boxed_non_empty_vec)
116}
117
118impl SessionMessage {
119 fn base(role: MessageRole, content: MessageContent) -> Self {
120 Self {
121 role,
122 content,
123 reasoning: None,
124 reasoning_details: None,
125 tool_calls: None,
126 tool_call_id: None,
127 phase: None,
128 origin_tool: None,
129 metadata: None,
130 clear_at: None,
131 }
132 }
133
134 pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
135 Self::base(role, MessageContent::Text(content.into()))
136 }
137
138 pub fn with_content(role: MessageRole, content: MessageContent) -> Self {
139 Self::base(role, content)
140 }
141
142 pub fn with_tool_call_id(role: MessageRole, content: impl Into<String>, tool_call_id: Option<String>) -> Self {
143 Self::with_tool_call_id_content(role, MessageContent::Text(content.into()), tool_call_id)
144 }
145
146 pub fn with_tool_call_id_content(role: MessageRole, content: MessageContent, tool_call_id: Option<String>) -> Self {
147 let mut message = Self::base(role, content);
148 message.tool_call_id = boxed_non_empty_string(tool_call_id);
149 message
150 }
151}
152
153impl From<&Message> for SessionMessage {
154 fn from(message: &Message) -> Self {
155 Self {
156 role: message.role,
157 content: message.content.clone(),
158 reasoning: boxed_non_empty_string(message.reasoning.clone()),
159 reasoning_details: boxed_non_empty_vec(message.reasoning_details.clone()),
160 tool_calls: boxed_non_empty_vec(message.tool_calls.clone()),
161 tool_call_id: boxed_non_empty_string(message.tool_call_id.clone()),
162 phase: message.phase,
163 origin_tool: boxed_non_empty_string(message.origin_tool.clone()),
164 metadata: message.metadata.clone().map(Box::new),
165 clear_at: message.clear_at,
166 }
167 }
168}
169
170impl From<&SessionMessage> for Message {
171 fn from(message: &SessionMessage) -> Self {
172 Self {
173 role: message.role,
174 content: message.content.clone(),
175 reasoning: clone_non_empty_boxed_string(&message.reasoning),
176 reasoning_details: clone_non_empty_boxed_vec(&message.reasoning_details),
177 tool_calls: clone_non_empty_boxed_vec(&message.tool_calls),
178 tool_call_id: clone_non_empty_boxed_string(&message.tool_call_id),
179 phase: message.phase,
180 origin_tool: clone_non_empty_boxed_string(&message.origin_tool),
181 metadata: message.metadata.as_deref().cloned(),
182 clear_at: message.clear_at,
183 }
184 }
185}