1use std::fmt;
2use std::str::FromStr;
3
4use tea_protocol::{ModelId, ModelRef, ProfileId, ProtocolTimestamp, ProviderId, SessionId};
5use thiserror::Error;
6
7use crate::{SessionStoreError, SessionStoreFuture};
8
9pub const MAX_SESSION_NAME_BYTES: usize = 256;
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct SessionName(String);
15
16impl SessionName {
17 pub fn new(value: impl Into<String>) -> Result<Self, SessionNameError> {
23 let value = value.into();
24 let value = value.trim();
25 if value.is_empty() {
26 return Err(SessionNameError::Empty);
27 }
28 if value.len() > MAX_SESSION_NAME_BYTES {
29 return Err(SessionNameError::TooLong);
30 }
31 if value.chars().any(char::is_control) {
32 return Err(SessionNameError::ControlCharacter);
33 }
34 Ok(Self(value.to_owned()))
35 }
36
37 #[must_use]
39 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42}
43
44impl fmt::Display for SessionName {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 formatter.write_str(self.as_str())
47 }
48}
49
50impl FromStr for SessionName {
51 type Err = SessionNameError;
52
53 fn from_str(value: &str) -> Result<Self, Self::Err> {
54 Self::new(value)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
60pub enum SessionNameError {
61 #[error("session name cannot be empty")]
63 Empty,
64 #[error("session name exceeds {MAX_SESSION_NAME_BYTES} bytes")]
66 TooLong,
67 #[error("session name cannot contain control characters")]
69 ControlCharacter,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SessionCatalogEntry {
75 session_id: SessionId,
76 name: Option<SessionName>,
77 updated_at: ProtocolTimestamp,
78 profile_id: ProfileId,
79 model: Option<ModelRef>,
80 message_count: usize,
81 pending_approval_count: usize,
82}
83
84impl SessionCatalogEntry {
85 #[allow(clippy::too_many_arguments)]
86 const fn new(
87 session_id: SessionId,
88 name: Option<SessionName>,
89 updated_at: ProtocolTimestamp,
90 profile_id: ProfileId,
91 model: Option<ModelRef>,
92 message_count: usize,
93 pending_approval_count: usize,
94 ) -> Self {
95 Self {
96 session_id,
97 name,
98 updated_at,
99 profile_id,
100 model,
101 message_count,
102 pending_approval_count,
103 }
104 }
105
106 pub fn from_snapshot(
112 snapshot: &crate::SessionSnapshot,
113 name: Option<SessionName>,
114 ) -> Result<Self, SessionStoreError> {
115 catalog_entry(snapshot, name)
116 }
117
118 #[must_use]
120 pub const fn session_id(&self) -> SessionId {
121 self.session_id
122 }
123
124 #[must_use]
126 pub const fn name(&self) -> Option<&SessionName> {
127 self.name.as_ref()
128 }
129
130 #[must_use]
132 pub const fn updated_at(&self) -> ProtocolTimestamp {
133 self.updated_at
134 }
135
136 #[must_use]
138 pub const fn profile_id(&self) -> &ProfileId {
139 &self.profile_id
140 }
141
142 #[must_use]
144 pub const fn model_id(&self) -> Option<&ModelId> {
145 match &self.model {
146 Some(model) => Some(model.model_id()),
147 None => None,
148 }
149 }
150
151 #[must_use]
153 pub const fn provider_id(&self) -> Option<&ProviderId> {
154 match &self.model {
155 Some(model) => Some(model.provider_id()),
156 None => None,
157 }
158 }
159
160 #[must_use]
162 pub const fn model_ref(&self) -> Option<&ModelRef> {
163 self.model.as_ref()
164 }
165
166 #[must_use]
168 pub const fn message_count(&self) -> usize {
169 self.message_count
170 }
171
172 #[must_use]
174 pub const fn pending_approval_count(&self) -> usize {
175 self.pending_approval_count
176 }
177}
178
179pub trait SessionCatalog: fmt::Debug + Send + Sync {
181 fn list_sessions(&self) -> SessionStoreFuture<'_, Vec<SessionCatalogEntry>>;
183
184 fn set_session_name(
186 &self,
187 session_id: SessionId,
188 name: Option<SessionName>,
189 ) -> SessionStoreFuture<'_, ()>;
190
191 fn session_name(&self, session_id: SessionId) -> SessionStoreFuture<'_, Option<SessionName>>;
193}
194
195pub(crate) fn catalog_entry(
196 snapshot: &crate::SessionSnapshot,
197 name: Option<SessionName>,
198) -> Result<SessionCatalogEntry, SessionStoreError> {
199 let state = snapshot.state();
200 let updated_at = snapshot
201 .records()
202 .last()
203 .map(tea_protocol::RecordEnvelope::timestamp)
204 .ok_or_else(|| {
205 SessionStoreError::new(
206 crate::SessionStoreErrorCode::InvalidRecord,
207 "stored session has no durable records",
208 )
209 })?;
210 Ok(SessionCatalogEntry::new(
211 state.session_id(),
212 name,
213 updated_at,
214 state.configuration().profile_id().clone(),
215 state.configuration().model_ref().cloned(),
216 state.messages().len(),
217 state.pending_approvals().len(),
218 ))
219}
220
221pub(crate) fn sort_catalog(entries: &mut [SessionCatalogEntry]) {
222 entries.sort_by(|left, right| {
223 right
224 .updated_at()
225 .cmp(&left.updated_at())
226 .then_with(|| left.session_id().cmp(&right.session_id()))
227 });
228}