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 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub scope: Option<Vec<String>>,
70 #[serde(flatten)]
74 pub extra: Map<String, Value>,
75}
76
77#[derive(Serialize, Deserialize, Clone, PartialEq)]
81pub struct ModelSelection {
82 pub provider: String,
83 pub model: String,
84}
85
86#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Debug)]
90pub struct Usage {
91 pub input: u64,
92 pub output: u64,
93 pub cache_read: u64,
94 pub cache_write: u64,
95}
96
97#[derive(Serialize, Deserialize, Clone, PartialEq)]
98pub struct ToolRecord {
99 pub name: String,
100 pub args: Value,
101 pub result: String,
103}
104
105impl Message {
106 pub fn new(from: String, content: String, ts: i64) -> Self {
107 Self {
108 from,
109 content,
110 ts,
111 id: Some(Uuid::new_v4()),
112 reply_to: None,
113 agent: false,
114 tool: None,
115 usage: None,
116 error: false,
117 config: None,
118 extra: Map::new(),
119 }
120 }
121
122 pub fn config_entry(from: String, ts: i64, config: ChatConfig) -> Self {
125 let mut m = Self::new(from, String::new(), ts);
126 m.config = Some(config);
127 m
128 }
129}
130
131pub struct Buffer {
132 pub messages: Vec<Message>,
133}
134
135impl Buffer {
136 pub fn new(bytes: &[u8]) -> Self {
137 let mut messages: Vec<Message> = std::str::from_utf8(bytes)
138 .unwrap_or_default()
139 .lines()
140 .filter_map(|line| serde_json::from_str(line).ok())
141 .collect();
142 messages.sort_by_key(|m| m.ts);
143 Self { messages }
144 }
145
146 pub fn merge(base: &[u8], local: &[u8], remote: &[u8]) -> Vec<u8> {
147 let base = Self::new(base);
148 let mut local = Self::new(local);
149 let remote = Self::new(remote);
150
151 for msg in &remote.messages {
152 if !base.messages.contains(msg) && !local.messages.contains(msg) {
153 local.messages.push(msg.clone());
154 }
155 }
156
157 local
158 .messages
159 .retain(|msg| !base.messages.contains(msg) || remote.messages.contains(msg));
160
161 local.messages.sort_by_key(|m| m.ts);
162 local.serialize()
163 }
164
165 pub fn serialize(&self) -> Vec<u8> {
166 let mut out = self
167 .messages
168 .iter()
169 .filter_map(|m| serde_json::to_string(m).ok())
170 .collect::<Vec<_>>()
171 .join("\n");
172 if !out.is_empty() {
173 out.push('\n');
174 }
175 out.into_bytes()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn line(from: &str, content: &str, ts: i64) -> String {
184 serde_json::to_string(&Message::new(from.into(), content.into(), ts)).unwrap() + "\n"
185 }
186
187 #[test]
190 fn merge_unions_concurrent_appends() {
191 let base = line("a", "hello", 1);
192 let local = base.clone() + &line("a", "one", 2);
193 let remote = base.clone() + &line("b", "two", 3);
194
195 let merged =
196 Buffer::new(&Buffer::merge(base.as_bytes(), local.as_bytes(), remote.as_bytes()))
197 .messages;
198
199 let contents: Vec<_> = merged.iter().map(|m| m.content.as_str()).collect();
200 assert_eq!(contents, ["hello", "one", "two"]);
201 }
202
203 #[test]
208 fn clear_vs_concurrent_send_converges() {
209 let base = line("a", "one", 1) + &line("b", "two", 2);
210 let cleared = String::new();
211 let extended = base.clone() + &line("b", "three", 3);
212
213 let sender_view = Buffer::merge(base.as_bytes(), extended.as_bytes(), cleared.as_bytes());
215 let clearer_view = Buffer::merge(base.as_bytes(), cleared.as_bytes(), extended.as_bytes());
217
218 let contents = |bytes: &[u8]| {
219 Buffer::new(bytes)
220 .messages
221 .iter()
222 .map(|m| m.content.clone())
223 .collect::<Vec<_>>()
224 };
225 assert_eq!(contents(&sender_view), ["three"]);
226 assert_eq!(contents(&clearer_view), ["three"]);
227 }
228
229 #[test]
232 fn merge_preserves_unknown_fields() {
233 let base = line("a", "hello", 1);
234 let remote = base.clone()
235 + "{\"from\":\"b\",\"content\":\"hi\",\"ts\":2,\"reactions\":\"abc\",\"agent\":true}\n";
236
237 let merged = Buffer::merge(base.as_bytes(), base.as_bytes(), remote.as_bytes());
238 let merged = String::from_utf8(merged).unwrap();
239
240 assert!(merged.contains("\"reactions\":\"abc\""), "unknown field stripped: {merged}");
241 assert!(merged.contains("\"agent\":true"));
242 }
243
244 #[test]
247 fn merge_preserves_unknown_config_fields() {
248 let base = line("a", "hello", 1);
249 let remote = base.clone()
250 + "{\"from\":\"b\",\"content\":\"\",\"ts\":2,\"config\":{\"scope\":[\"/x/\"],\"future_knob\":7}}\n";
251
252 let merged = Buffer::merge(base.as_bytes(), base.as_bytes(), remote.as_bytes());
253 let merged = String::from_utf8(merged).unwrap();
254
255 assert!(merged.contains("\"scope\":[\"/x/\"]"), "scope stripped: {merged}");
256 assert!(merged.contains("\"future_knob\":7"), "unknown config field stripped: {merged}");
257 }
258}