1use std::time::Duration;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct MessageId(u64);
11
12impl MessageId {
13 pub fn get(self) -> u64 {
15 self.0
16 }
17}
18
19impl std::fmt::Display for MessageId {
20 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(formatter, "{}", self.0)
22 }
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub enum ChatRole {
28 User,
30 Assistant,
32 System,
34}
35
36impl ChatRole {
37 pub fn label(self) -> &'static str {
39 match self {
40 Self::User => "user",
41 Self::Assistant => "assistant",
42 Self::System => "system",
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Default)]
49pub enum MessageState {
50 #[default]
52 Complete,
53 Streaming,
55 Stopped,
57 Error {
59 message: String,
61 },
62}
63
64impl MessageState {
65 pub fn is_streaming(&self) -> bool {
67 matches!(self, Self::Streaming)
68 }
69}
70
71#[derive(Clone, Debug, PartialEq)]
73pub struct SourceRef {
74 pub id: String,
76 pub title: String,
78 pub location: Option<String>,
80 pub score: Option<f32>,
82}
83
84impl SourceRef {
85 pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
87 Self {
88 id: id.into(),
89 title: title.into(),
90 location: None,
91 score: None,
92 }
93 }
94
95 pub fn location(mut self, location: impl Into<String>) -> Self {
97 self.location = Some(location.into());
98 self
99 }
100
101 pub fn score(mut self, score: f32) -> Self {
103 self.score = Some(score);
104 self
105 }
106}
107
108#[derive(Clone, Debug, Default, PartialEq)]
110pub struct MessageMeta {
111 pub model: Option<String>,
113 pub chunks: Option<usize>,
115 pub sources: Vec<SourceRef>,
117 pub tokens: Option<usize>,
119 pub elapsed: Option<Duration>,
121}
122
123impl MessageMeta {
124 pub fn summary(&self) -> String {
129 let mut parts = Vec::new();
130 if let Some(model) = &self.model {
131 parts.push(model.clone());
132 }
133 if let Some(chunks) = self.chunks {
134 parts.push(format!("{chunks} chunks"));
135 }
136 if !self.sources.is_empty() {
137 let count = self.sources.len();
138 parts.push(if count == 1 {
139 "1 source".to_owned()
140 } else {
141 format!("{count} sources")
142 });
143 }
144 if let Some(tokens) = self.tokens {
145 parts.push(format!("{tokens} tokens"));
146 }
147 if let Some(elapsed) = self.elapsed {
148 parts.push(format!("{:.1} s", elapsed.as_secs_f32()));
149 }
150 parts.join(" · ")
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.model.is_none()
156 && self.chunks.is_none()
157 && self.sources.is_empty()
158 && self.tokens.is_none()
159 && self.elapsed.is_none()
160 }
161
162 pub fn add_source(&mut self, source: SourceRef) -> bool {
166 if self.sources.iter().any(|existing| existing.id == source.id) {
167 return false;
168 }
169 self.sources.push(source);
170 true
171 }
172}
173
174#[derive(Clone, Debug, PartialEq)]
176pub struct ChatMessage {
177 id: MessageId,
178 role: ChatRole,
179 text: String,
180 state: MessageState,
181 meta: Option<MessageMeta>,
182}
183
184impl ChatMessage {
185 pub fn id(&self) -> MessageId {
187 self.id
188 }
189
190 pub fn role(&self) -> ChatRole {
192 self.role
193 }
194
195 pub fn text(&self) -> &str {
197 &self.text
198 }
199
200 pub fn state(&self) -> &MessageState {
202 &self.state
203 }
204
205 pub fn meta(&self) -> Option<&MessageMeta> {
207 self.meta.as_ref()
208 }
209}
210
211#[derive(Clone, Debug, PartialEq)]
213pub struct NewMessage {
214 pub role: ChatRole,
216 pub text: String,
218 pub state: MessageState,
220 pub meta: Option<MessageMeta>,
222}
223
224impl NewMessage {
225 pub fn user(text: impl Into<String>) -> Self {
227 Self::new(ChatRole::User, text)
228 }
229
230 pub fn assistant(text: impl Into<String>) -> Self {
232 Self::new(ChatRole::Assistant, text)
233 }
234
235 pub fn system(text: impl Into<String>) -> Self {
237 Self::new(ChatRole::System, text)
238 }
239
240 pub fn new(role: ChatRole, text: impl Into<String>) -> Self {
242 Self {
243 role,
244 text: text.into(),
245 state: MessageState::Complete,
246 meta: None,
247 }
248 }
249
250 pub fn streaming(mut self) -> Self {
252 self.state = MessageState::Streaming;
253 self
254 }
255
256 pub fn state(mut self, state: MessageState) -> Self {
258 self.state = state;
259 self
260 }
261
262 pub fn meta(mut self, meta: MessageMeta) -> Self {
264 self.meta = Some(meta);
265 self
266 }
267}
268
269#[derive(Debug, Default)]
279pub struct ChatState {
280 next_id: u64,
281 messages: Vec<ChatMessage>,
282}
283
284impl ChatState {
285 pub fn messages(&self) -> &[ChatMessage] {
287 &self.messages
288 }
289
290 pub fn len(&self) -> usize {
292 self.messages.len()
293 }
294
295 pub fn is_empty(&self) -> bool {
297 self.messages.is_empty()
298 }
299
300 pub fn get(&self, id: MessageId) -> Option<&ChatMessage> {
302 self.messages.iter().find(|message| message.id == id)
303 }
304
305 pub fn last(&self) -> Option<&ChatMessage> {
307 self.messages.last()
308 }
309
310 pub fn last_assistant(&self) -> Option<&ChatMessage> {
312 self.messages
313 .iter()
314 .rev()
315 .find(|message| message.role == ChatRole::Assistant)
316 }
317
318 pub fn streaming_id(&self) -> Option<MessageId> {
320 self.messages
321 .iter()
322 .rev()
323 .find(|message| message.state.is_streaming())
324 .map(|message| message.id)
325 }
326
327 pub fn is_streaming(&self) -> bool {
329 self.streaming_id().is_some()
330 }
331
332 pub fn push(&mut self, message: NewMessage) -> MessageId {
334 let id = MessageId(self.next_id);
335 self.next_id += 1;
336 self.messages.push(ChatMessage {
337 id,
338 role: message.role,
339 text: message.text,
340 state: message.state,
341 meta: message.meta,
342 });
343 id
344 }
345
346 pub fn insert_before(&mut self, before: MessageId, message: NewMessage) -> Option<MessageId> {
351 let index = self
352 .messages
353 .iter()
354 .position(|candidate| candidate.id == before)?;
355 let id = MessageId(self.next_id);
356 self.next_id += 1;
357 self.messages.insert(
358 index,
359 ChatMessage {
360 id,
361 role: message.role,
362 text: message.text,
363 state: message.state,
364 meta: message.meta,
365 },
366 );
367 Some(id)
368 }
369
370 pub fn push_user(&mut self, text: impl Into<String>) -> MessageId {
372 self.push(NewMessage::user(text))
373 }
374
375 pub fn push_system(&mut self, text: impl Into<String>) -> MessageId {
377 self.push(NewMessage::system(text))
378 }
379
380 pub fn begin_assistant(&mut self) -> MessageId {
382 self.push(NewMessage::assistant("").streaming())
383 }
384
385 pub fn append(&mut self, id: MessageId, delta: &str) -> bool {
390 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
391 return false;
392 };
393 if !message.state.is_streaming() {
394 return false;
395 }
396 message.text.push_str(delta);
397 true
398 }
399
400 pub fn finish(&mut self, id: MessageId) -> bool {
402 self.transition(id, &MessageState::Complete)
403 }
404
405 pub fn stop(&mut self, id: MessageId) -> bool {
407 self.transition(id, &MessageState::Stopped)
408 }
409
410 pub fn fail(&mut self, id: MessageId, error: impl Into<String>) -> bool {
412 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
413 return false;
414 };
415 message.state = MessageState::Error {
416 message: error.into(),
417 };
418 true
419 }
420
421 pub fn set_meta(&mut self, id: MessageId, meta: MessageMeta) -> bool {
423 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
424 return false;
425 };
426 message.meta = Some(meta);
427 true
428 }
429
430 pub fn set_text(&mut self, id: MessageId, text: impl Into<String>) -> bool {
433 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
434 return false;
435 };
436 message.text = text.into();
437 true
438 }
439
440 pub fn add_source(&mut self, id: MessageId, source: SourceRef) -> bool {
444 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
445 return false;
446 };
447 message
448 .meta
449 .get_or_insert_with(MessageMeta::default)
450 .add_source(source)
451 }
452
453 pub fn clear(&mut self) {
455 self.messages.clear();
456 }
457
458 pub fn remove(&mut self, id: MessageId) -> bool {
462 let before = self.messages.len();
463 self.messages.retain(|message| message.id != id);
464 self.messages.len() != before
465 }
466
467 pub fn replace(&mut self, messages: impl IntoIterator<Item = NewMessage>) -> Vec<MessageId> {
469 self.messages.clear();
470 messages
471 .into_iter()
472 .map(|message| self.push(message))
473 .collect()
474 }
475
476 fn transition(&mut self, id: MessageId, next: &MessageState) -> bool {
477 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
478 return false;
479 };
480 if !message.state.is_streaming() {
481 return false;
482 }
483 message.state = next.clone();
484 true
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn a_streamed_message_is_reduced_in_order() {
494 let mut state = ChatState::default();
495 let user = state.push_user("Explain the method");
496 let assistant = state.begin_assistant();
497
498 assert_eq!(state.len(), 2);
499 assert_eq!(state.messages()[0].id(), user);
500 assert_eq!(state.messages()[0].role(), ChatRole::User);
501 assert!(state.append(assistant, "The "));
502 assert!(state.append(assistant, "method."));
503 assert!(state.finish(assistant));
504
505 let message = state.get(assistant).expect("assistant message");
506 assert_eq!(message.text(), "The method.");
507 assert_eq!(message.state(), &MessageState::Complete);
508 assert!(!state.is_streaming());
509 }
510
511 #[test]
512 fn stopping_keeps_the_partial_body_and_rejects_late_tokens() {
513 let mut state = ChatState::default();
514 let assistant = state.begin_assistant();
515 assert!(state.append(assistant, "Partial"));
516
517 assert!(state.stop(assistant));
518 assert!(!state.append(assistant, " late"));
519 assert!(!state.finish(assistant));
520
521 let message = state.get(assistant).expect("assistant message");
522 assert_eq!(message.text(), "Partial");
523 assert_eq!(message.state(), &MessageState::Stopped);
524 }
525
526 #[test]
527 fn failing_keeps_the_partial_body_and_reason() {
528 let mut state = ChatState::default();
529 let assistant = state.begin_assistant();
530 assert!(state.append(assistant, "Partial"));
531
532 assert!(state.fail(assistant, "connection reset"));
533
534 let message = state.get(assistant).expect("assistant message");
535 assert_eq!(message.text(), "Partial");
536 assert_eq!(
537 message.state(),
538 &MessageState::Error {
539 message: "connection reset".to_owned()
540 }
541 );
542 }
543
544 #[test]
545 fn metadata_and_sources_are_attached_and_deduplicated() {
546 let mut state = ChatState::default();
547 let assistant = state.begin_assistant();
548 assert!(state.set_meta(
549 assistant,
550 MessageMeta {
551 model: Some("deepseek-chat".to_owned()),
552 chunks: Some(12),
553 ..Default::default()
554 }
555 ));
556 assert!(state.add_source(assistant, SourceRef::new("paper", "Paper")));
557 assert!(!state.add_source(assistant, SourceRef::new("paper", "Paper again")));
558
559 let message = state.get(assistant).expect("assistant message");
560 let meta = message.meta().expect("meta");
561 assert_eq!(meta.sources.len(), 1);
562 assert_eq!(meta.summary(), "deepseek-chat · 12 chunks · 1 source");
563 }
564
565 #[test]
566 fn removing_a_message_keeps_the_rest_in_order() {
567 let mut state = ChatState::default();
568 let first = state.push_user("first");
569 let second = state.push_user("second");
570 let third = state.push_user("third");
571
572 assert!(state.remove(second));
573 assert!(!state.remove(second));
574 assert_eq!(
575 state
576 .messages()
577 .iter()
578 .map(ChatMessage::id)
579 .collect::<Vec<_>>(),
580 vec![first, third]
581 );
582 }
583
584 #[test]
585 fn inserting_before_keeps_the_transcript_order() {
586 let mut state = ChatState::default();
587 let user = state.push_user("question");
588 let assistant = state.begin_assistant();
589
590 let note = state
591 .insert_before(assistant, NewMessage::system("Attached: @Kano2025"))
592 .expect("insert before the assistant");
593 assert!(note > assistant);
594 assert_eq!(
595 state
596 .messages()
597 .iter()
598 .map(ChatMessage::role)
599 .collect::<Vec<_>>(),
600 vec![ChatRole::User, ChatRole::System, ChatRole::Assistant]
601 );
602 assert_eq!(state.messages()[0].id(), user);
603 assert_eq!(state.messages()[2].id(), assistant);
604
605 assert!(
606 state
607 .insert_before(MessageId(999), NewMessage::system("late"))
608 .is_none()
609 );
610 assert_eq!(state.len(), 3);
611 }
612
613 #[test]
614 fn replacing_a_transcript_assigns_fresh_identities() {
615 let mut state = ChatState::default();
616 let old = state.push_user("old");
617 let ids = state.replace([NewMessage::user("new"), NewMessage::assistant("answer")]);
618
619 assert_eq!(state.len(), 2);
620 assert_eq!(state.messages()[0].text(), "new");
621 assert!(ids.iter().all(|id| *id > old));
622 }
623
624 #[test]
625 fn append_and_transitions_ignore_unknown_ids() {
626 let mut state = ChatState::default();
627 let id = MessageId(999);
628 assert!(!state.append(id, "x"));
629 assert!(!state.finish(id));
630 assert!(!state.stop(id));
631 assert!(!state.fail(id, "x"));
632 assert!(!state.set_meta(id, MessageMeta::default()));
633 assert!(!state.add_source(id, SourceRef::new("s", "S")));
634 }
635
636 #[test]
637 fn a_finished_message_does_not_accept_more_tokens() {
638 let mut state = ChatState::default();
639 let assistant = state.begin_assistant();
640 assert!(state.finish(assistant));
641 assert!(!state.append(assistant, "late"));
642
643 assert_eq!(state.get(assistant).expect("assistant").text(), "");
644 }
645}