Skip to main content

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}