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 add_source(&mut self, id: MessageId, source: SourceRef) -> bool {
434 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
435 return false;
436 };
437 message
438 .meta
439 .get_or_insert_with(MessageMeta::default)
440 .add_source(source)
441 }
442
443 pub fn clear(&mut self) {
445 self.messages.clear();
446 }
447
448 pub fn remove(&mut self, id: MessageId) -> bool {
452 let before = self.messages.len();
453 self.messages.retain(|message| message.id != id);
454 self.messages.len() != before
455 }
456
457 pub fn replace(&mut self, messages: impl IntoIterator<Item = NewMessage>) -> Vec<MessageId> {
459 self.messages.clear();
460 messages
461 .into_iter()
462 .map(|message| self.push(message))
463 .collect()
464 }
465
466 fn transition(&mut self, id: MessageId, next: &MessageState) -> bool {
467 let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
468 return false;
469 };
470 if !message.state.is_streaming() {
471 return false;
472 }
473 message.state = next.clone();
474 true
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn a_streamed_message_is_reduced_in_order() {
484 let mut state = ChatState::default();
485 let user = state.push_user("Explain the method");
486 let assistant = state.begin_assistant();
487
488 assert_eq!(state.len(), 2);
489 assert_eq!(state.messages()[0].id(), user);
490 assert_eq!(state.messages()[0].role(), ChatRole::User);
491 assert!(state.append(assistant, "The "));
492 assert!(state.append(assistant, "method."));
493 assert!(state.finish(assistant));
494
495 let message = state.get(assistant).expect("assistant message");
496 assert_eq!(message.text(), "The method.");
497 assert_eq!(message.state(), &MessageState::Complete);
498 assert!(!state.is_streaming());
499 }
500
501 #[test]
502 fn stopping_keeps_the_partial_body_and_rejects_late_tokens() {
503 let mut state = ChatState::default();
504 let assistant = state.begin_assistant();
505 assert!(state.append(assistant, "Partial"));
506
507 assert!(state.stop(assistant));
508 assert!(!state.append(assistant, " late"));
509 assert!(!state.finish(assistant));
510
511 let message = state.get(assistant).expect("assistant message");
512 assert_eq!(message.text(), "Partial");
513 assert_eq!(message.state(), &MessageState::Stopped);
514 }
515
516 #[test]
517 fn failing_keeps_the_partial_body_and_reason() {
518 let mut state = ChatState::default();
519 let assistant = state.begin_assistant();
520 assert!(state.append(assistant, "Partial"));
521
522 assert!(state.fail(assistant, "connection reset"));
523
524 let message = state.get(assistant).expect("assistant message");
525 assert_eq!(message.text(), "Partial");
526 assert_eq!(
527 message.state(),
528 &MessageState::Error {
529 message: "connection reset".to_owned()
530 }
531 );
532 }
533
534 #[test]
535 fn metadata_and_sources_are_attached_and_deduplicated() {
536 let mut state = ChatState::default();
537 let assistant = state.begin_assistant();
538 assert!(state.set_meta(
539 assistant,
540 MessageMeta {
541 model: Some("deepseek-chat".to_owned()),
542 chunks: Some(12),
543 ..Default::default()
544 }
545 ));
546 assert!(state.add_source(assistant, SourceRef::new("paper", "Paper")));
547 assert!(!state.add_source(assistant, SourceRef::new("paper", "Paper again")));
548
549 let message = state.get(assistant).expect("assistant message");
550 let meta = message.meta().expect("meta");
551 assert_eq!(meta.sources.len(), 1);
552 assert_eq!(meta.summary(), "deepseek-chat · 12 chunks · 1 source");
553 }
554
555 #[test]
556 fn removing_a_message_keeps_the_rest_in_order() {
557 let mut state = ChatState::default();
558 let first = state.push_user("first");
559 let second = state.push_user("second");
560 let third = state.push_user("third");
561
562 assert!(state.remove(second));
563 assert!(!state.remove(second));
564 assert_eq!(
565 state
566 .messages()
567 .iter()
568 .map(ChatMessage::id)
569 .collect::<Vec<_>>(),
570 vec![first, third]
571 );
572 }
573
574 #[test]
575 fn inserting_before_keeps_the_transcript_order() {
576 let mut state = ChatState::default();
577 let user = state.push_user("question");
578 let assistant = state.begin_assistant();
579
580 let note = state
581 .insert_before(assistant, NewMessage::system("Attached: @Kano2025"))
582 .expect("insert before the assistant");
583 assert!(note > assistant);
584 assert_eq!(
585 state
586 .messages()
587 .iter()
588 .map(ChatMessage::role)
589 .collect::<Vec<_>>(),
590 vec![ChatRole::User, ChatRole::System, ChatRole::Assistant]
591 );
592 assert_eq!(state.messages()[0].id(), user);
593 assert_eq!(state.messages()[2].id(), assistant);
594
595 assert!(
596 state
597 .insert_before(MessageId(999), NewMessage::system("late"))
598 .is_none()
599 );
600 assert_eq!(state.len(), 3);
601 }
602
603 #[test]
604 fn replacing_a_transcript_assigns_fresh_identities() {
605 let mut state = ChatState::default();
606 let old = state.push_user("old");
607 let ids = state.replace([NewMessage::user("new"), NewMessage::assistant("answer")]);
608
609 assert_eq!(state.len(), 2);
610 assert_eq!(state.messages()[0].text(), "new");
611 assert!(ids.iter().all(|id| *id > old));
612 }
613
614 #[test]
615 fn append_and_transitions_ignore_unknown_ids() {
616 let mut state = ChatState::default();
617 let id = MessageId(999);
618 assert!(!state.append(id, "x"));
619 assert!(!state.finish(id));
620 assert!(!state.stop(id));
621 assert!(!state.fail(id, "x"));
622 assert!(!state.set_meta(id, MessageMeta::default()));
623 assert!(!state.add_source(id, SourceRef::new("s", "S")));
624 }
625
626 #[test]
627 fn a_finished_message_does_not_accept_more_tokens() {
628 let mut state = ChatState::default();
629 let assistant = state.begin_assistant();
630 assert!(state.finish(assistant));
631 assert!(!state.append(assistant, "late"));
632
633 assert_eq!(state.get(assistant).expect("assistant").text(), "");
634 }
635}