rig_core/conversation/turn.rs
1//! Turn records: where each turn of a conversation began and how it ended,
2//! kept as asides, so a saved session tells a stopped or failed turn from
3//! an answered one without the app that ran it.
4
5use serde::{Deserialize, Serialize};
6
7use super::{Aside, AsideId, Author, Conversation, MessageId};
8use crate::message::CallId;
9
10/// The kind of an aside holding a [`TurnRecord`].
11pub const TURN: &str = "rig.turn";
12
13/// One turn of a conversation, from the message that asked for it to how
14/// it ended. Kept as an aside of kind [`TURN`] by [`Author::rig`], put
15/// after the last entry when the turn begins and replaced when it ends.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct TurnRecord {
18 /// How many turns began before this one.
19 pub index: u64,
20 /// Where it begins ([`Conversation::turn_start`]):
21 /// [`asked`](Conversation::asked) is what it answers,
22 /// [`request`](Conversation::request) the message that asked for it.
23 pub start: MessageId,
24 /// How it ended; `None` while it runs, and when a crash or an exit cut
25 /// it short.
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub end: Option<TurnEnd>,
28}
29
30/// How a turn ended.
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(tag = "how", rename_all = "snake_case")]
33pub enum TurnEnd {
34 /// The model answered.
35 Answered {
36 /// The reply entry the turn ended on; `None` when the last entry was
37 /// no reply.
38 #[serde(default, rename = "by", skip_serializing_if = "Option::is_none")]
39 answer: Option<MessageId>,
40 },
41 /// The turn failed, for `why`.
42 Failed {
43 /// Why.
44 why: String,
45 },
46 /// The turn was stopped before an answer: by the user when `why` is
47 /// `None`.
48 Stopped {
49 /// Why, when it was not the user.
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 why: Option<String>,
52 /// The turn's last entry when a stop cut it off, which a front draws
53 /// the stop under; `None` when the turn had no entry of its own yet,
54 /// or ended as stopped in its own course, such as a compaction
55 /// ending it.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 at: Option<MessageId>,
58 /// The tool calls the stop answered: those that had no result of
59 /// their own.
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 calls: Vec<CallId>,
62 },
63}
64
65impl TurnRecord {
66 /// The record `aside` holds, when it is one: of kind [`TURN`], its
67 /// value a record.
68 pub fn of(aside: &Aside) -> Option<Self> {
69 (aside.kind == TURN)
70 .then(|| Self::deserialize(&aside.value).ok())
71 .flatten()
72 }
73}
74
75impl Conversation {
76 /// The turn records, oldest first, with their asides' ids; the newest
77 /// is `turns().next_back()`, found from the end.
78 pub fn turns(&self) -> impl DoubleEndedIterator<Item = (AsideId, TurnRecord)> + '_ {
79 self.asides()
80 .filter_map(|(id, aside)| Some((id, TurnRecord::of(aside)?)))
81 }
82
83 /// Records a turn that begins now, numbered after the last record and
84 /// starting at [`turn_start`](Self::turn_start). Returns the new record.
85 pub fn open_turn(&mut self) -> TurnRecord {
86 let last = self.turns().next_back().map(|(_, record)| record.index);
87 let record = TurnRecord {
88 index: last.map_or(0, |index| index.saturating_add(1)),
89 start: self.turn_start(),
90 end: None,
91 };
92 let after = self.entries().next_back().map(|entry| entry.id);
93 let value = serde_json::to_value(&record).unwrap_or_default();
94 self.add_aside(Aside::new(TURN, Author::rig(), value).after(after));
95 record
96 }
97
98 /// Ends the last turn record, when it is open, with `end`. Returns
99 /// whether it was open.
100 pub fn end_turn(&mut self, end: TurnEnd) -> bool {
101 let Some((id, record)) = self.turns().next_back() else {
102 return false;
103 };
104 let Some(aside) = self.aside(id).filter(|_| record.end.is_none()).cloned() else {
105 return false;
106 };
107 let ended = TurnRecord {
108 end: Some(end),
109 ..record
110 };
111 let value = serde_json::to_value(ended).unwrap_or_default();
112 self.replace_aside(id, Aside { value, ..aside })
113 }
114}