1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3use uuid::Uuid;
4
5#[derive(Serialize, Deserialize, Clone, PartialEq)]
6pub struct Message {
7 pub from: String,
8 pub content: String,
9 pub ts: i64,
10 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pub id: Option<Uuid>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub reply_to: Option<Uuid>,
21 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
24 pub agent: bool,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub tool: Option<ToolRecord>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub usage: Option<Usage>,
36 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
40 pub error: bool,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub config: Option<ChatConfig>,
46 #[serde(flatten)]
49 pub extra: Map<String, Value>,
50}
51
52#[derive(Serialize, Deserialize, Clone, PartialEq, Default)]
56pub struct ChatConfig {
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub model: Option<ModelSelection>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub effort: Option<String>,
64}
65
66#[derive(Serialize, Deserialize, Clone, PartialEq)]
70pub struct ModelSelection {
71 pub provider: String,
72 pub model: String,
73}
74
75#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Debug)]
79pub struct Usage {
80 pub input: u64,
81 pub output: u64,
82 pub cache_read: u64,
83 pub cache_write: u64,
84}
85
86#[derive(Serialize, Deserialize, Clone, PartialEq)]
87pub struct ToolRecord {
88 pub name: String,
89 pub args: Value,
90 pub result: String,
92}
93
94impl Message {
95 pub fn new(from: String, content: String, ts: i64) -> Self {
96 Self {
97 from,
98 content,
99 ts,
100 id: Some(Uuid::new_v4()),
101 reply_to: None,
102 agent: false,
103 tool: None,
104 usage: None,
105 error: false,
106 config: None,
107 extra: Map::new(),
108 }
109 }
110
111 pub fn config_entry(from: String, ts: i64, config: ChatConfig) -> Self {
114 let mut m = Self::new(from, String::new(), ts);
115 m.config = Some(config);
116 m
117 }
118}
119
120pub struct Buffer {
121 pub messages: Vec<Message>,
122}
123
124impl Buffer {
125 pub fn new(bytes: &[u8]) -> Self {
126 let mut messages: Vec<Message> = std::str::from_utf8(bytes)
127 .unwrap_or_default()
128 .lines()
129 .filter_map(|line| serde_json::from_str(line).ok())
130 .collect();
131 messages.sort_by_key(|m| m.ts);
132 Self { messages }
133 }
134
135 pub fn merge(base: &[u8], local: &[u8], remote: &[u8]) -> Vec<u8> {
136 let base = Self::new(base);
137 let mut local = Self::new(local);
138 let remote = Self::new(remote);
139
140 for msg in &remote.messages {
141 if !base.messages.contains(msg) && !local.messages.contains(msg) {
142 local.messages.push(msg.clone());
143 }
144 }
145
146 local
147 .messages
148 .retain(|msg| !base.messages.contains(msg) || remote.messages.contains(msg));
149
150 local.messages.sort_by_key(|m| m.ts);
151 local.serialize()
152 }
153
154 pub fn serialize(&self) -> Vec<u8> {
155 let mut out = self
156 .messages
157 .iter()
158 .filter_map(|m| serde_json::to_string(m).ok())
159 .collect::<Vec<_>>()
160 .join("\n");
161 if !out.is_empty() {
162 out.push('\n');
163 }
164 out.into_bytes()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn line(from: &str, content: &str, ts: i64) -> String {
173 serde_json::to_string(&Message::new(from.into(), content.into(), ts)).unwrap() + "\n"
174 }
175
176 #[test]
179 fn merge_unions_concurrent_appends() {
180 let base = line("a", "hello", 1);
181 let local = base.clone() + &line("a", "one", 2);
182 let remote = base.clone() + &line("b", "two", 3);
183
184 let merged =
185 Buffer::new(&Buffer::merge(base.as_bytes(), local.as_bytes(), remote.as_bytes()))
186 .messages;
187
188 let contents: Vec<_> = merged.iter().map(|m| m.content.as_str()).collect();
189 assert_eq!(contents, ["hello", "one", "two"]);
190 }
191
192 #[test]
197 fn clear_vs_concurrent_send_converges() {
198 let base = line("a", "one", 1) + &line("b", "two", 2);
199 let cleared = String::new();
200 let extended = base.clone() + &line("b", "three", 3);
201
202 let sender_view = Buffer::merge(base.as_bytes(), extended.as_bytes(), cleared.as_bytes());
204 let clearer_view = Buffer::merge(base.as_bytes(), cleared.as_bytes(), extended.as_bytes());
206
207 let contents = |bytes: &[u8]| {
208 Buffer::new(bytes)
209 .messages
210 .iter()
211 .map(|m| m.content.clone())
212 .collect::<Vec<_>>()
213 };
214 assert_eq!(contents(&sender_view), ["three"]);
215 assert_eq!(contents(&clearer_view), ["three"]);
216 }
217
218 #[test]
221 fn merge_preserves_unknown_fields() {
222 let base = line("a", "hello", 1);
223 let remote = base.clone()
224 + "{\"from\":\"b\",\"content\":\"hi\",\"ts\":2,\"reactions\":\"abc\",\"agent\":true}\n";
225
226 let merged = Buffer::merge(base.as_bytes(), base.as_bytes(), remote.as_bytes());
227 let merged = String::from_utf8(merged).unwrap();
228
229 assert!(merged.contains("\"reactions\":\"abc\""), "unknown field stripped: {merged}");
230 assert!(merged.contains("\"agent\":true"));
231 }
232}