use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{ResponseItem, Usage, responses::ResponseHistory};
use super::{
compaction,
context::{ContextManager, assign_missing_response_item_ids, has_well_formed_tool_calls},
};
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(try_from = "uuid::Uuid", into = "uuid::Uuid")]
pub struct SessionId(uuid::Uuid);
impl SessionId {
#[must_use]
pub fn new() -> Self {
Self(uuid::Uuid::now_v7())
}
#[must_use]
pub const fn as_uuid(self) -> uuid::Uuid {
self.0
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for SessionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl fmt::Debug for SessionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("SessionId").field(&self.0).finish()
}
}
impl std::str::FromStr for SessionId {
type Err = SessionIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(uuid::Uuid::parse_str(value)?)
}
}
impl TryFrom<uuid::Uuid> for SessionId {
type Error = SessionIdError;
fn try_from(value: uuid::Uuid) -> Result<Self, Self::Error> {
if value.get_version_num() != 7 {
return Err(SessionIdError::WrongVersion {
version: value.get_version_num(),
});
}
Ok(Self(value))
}
}
impl From<SessionId> for uuid::Uuid {
fn from(value: SessionId) -> Self {
value.0
}
}
#[derive(Debug, thiserror::Error)]
pub enum SessionIdError {
#[error("invalid session UUID")]
InvalidUuid(#[from] uuid::Error),
#[error("session IDs must be UUIDv7, got UUIDv{version}")]
WrongVersion {
version: usize,
},
}
#[doc(hidden)]
#[derive(Clone)]
pub struct ManagedSessionState {
context: ContextManager,
delta_start: usize,
previous_response_id: Option<String>,
history_revision: u64,
server_reasoning_included: bool,
}
impl ManagedSessionState {
#[must_use]
pub fn new(mut items: Vec<ResponseItem>) -> Self {
assign_missing_response_item_ids(&mut items);
Self {
context: ContextManager::new(items),
delta_start: 0,
previous_response_id: None,
history_revision: 0,
server_reasoning_included: false,
}
}
pub fn resume(mut items: Vec<ResponseItem>) -> Result<Self, ManagedSessionStateError> {
if items.is_empty() {
return Err(ManagedSessionStateError::EmptyHistory);
}
assign_missing_response_item_ids(&mut items);
if !has_well_formed_tool_calls(&items) {
return Err(ManagedSessionStateError::MalformedToolCalls);
}
let history_len = items.len();
let mut state = Self::new(items);
if state.context.len() != history_len {
return Err(ManagedSessionStateError::UnsupportedHistoryItem);
}
state.context.commit_tail();
state.delta_start = state.context.len();
Ok(state)
}
#[must_use]
pub fn history_len(&self) -> usize {
self.context.len()
}
#[must_use]
pub fn history_is_empty(&self) -> bool {
self.context.is_empty()
}
#[must_use]
pub fn history(&self) -> impl ExactSizeIterator<Item = &ResponseItem> {
self.context.iter()
}
#[must_use]
pub fn flattened_history(&self) -> Vec<ResponseItem> {
self.context.flattened_items()
}
#[must_use]
pub fn shared_history(&self) -> ResponseHistory {
self.context.shared_items()
}
#[must_use]
pub fn prompt_history(&self) -> ResponseHistory {
self.context.prompt_items()
}
#[doc(hidden)]
#[must_use]
pub fn prompt_history_with_repair(&self) -> (ResponseHistory, bool) {
self.context.prompt_items_with_repair()
}
#[doc(hidden)]
pub fn adopt_prompt_history(&mut self, history: ResponseHistory) {
self.context.adopt_prompt_items(history);
}
pub fn append(&mut self, items: impl IntoIterator<Item = ResponseItem>) {
self.context.record_items(items);
}
pub fn update_token_info(&mut self, usage: Option<&Usage>) {
self.context.update_token_info(usage);
}
pub const fn observe_server_reasoning(&mut self, included: bool) {
self.server_reasoning_included |= included;
}
#[must_use]
pub fn active_context_tokens(&self) -> u64 {
self.context
.active_context_tokens(self.server_reasoning_included)
}
#[must_use]
pub const fn delta_start(&self) -> usize {
self.delta_start
}
#[must_use]
pub fn previous_response_id(&self) -> Option<&str> {
self.previous_response_id.as_deref()
}
pub fn set_previous_response_id(&mut self, response_id: impl Into<String>) {
let response_id = response_id.into();
self.previous_response_id = (!response_id.is_empty()).then_some(response_id);
}
pub fn clear_delta(&mut self) {
self.delta_start = self.context.len();
}
pub fn reset_for_full_request(&mut self) {
self.delta_start = 0;
self.previous_response_id = None;
}
pub fn commit(&mut self) -> Result<(), ManagedSessionStateError> {
if self.previous_response_id.is_none() {
return Err(ManagedSessionStateError::MissingResponseId);
}
self.context.commit_tail();
self.delta_start = self.context.len();
Ok(())
}
pub fn commit_interrupted(&mut self) {
self.reset_for_full_request();
self.context.commit_tail();
}
pub fn commit_tail(&mut self) {
self.context.commit_tail();
}
pub fn install_compaction(
&mut self,
item: ResponseItem,
initial_context: impl IntoIterator<Item = ResponseItem>,
request_prefix: &[ResponseItem],
) {
let initial_context = initial_context.into_iter().collect::<Vec<_>>();
let history =
compaction::install_history(&self.context.flattened_items(), &initial_context, item);
self.context.replace_and_recompute(history, request_prefix);
self.reset_for_full_request();
self.history_revision = self.history_revision.saturating_add(1);
}
#[must_use]
pub const fn history_revision(&self) -> u64 {
self.history_revision
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum ManagedSessionStateError {
#[error("conversation history must not be empty")]
EmptyHistory,
#[error("conversation history contains an unsupported item")]
UnsupportedHistoryItem,
#[error("conversation history contains an unmatched or misordered tool call")]
MalformedToolCalls,
#[error("completed response did not have a response ID")]
MissingResponseId,
}
#[cfg(test)]
mod tests {
use super::{ManagedSessionState, ManagedSessionStateError};
#[test]
fn empty_response_id_cannot_commit_an_incremental_checkpoint() {
let mut state = ManagedSessionState::new(Vec::new());
state.set_previous_response_id("");
assert!(matches!(
state.commit(),
Err(ManagedSessionStateError::MissingResponseId)
));
assert_eq!(state.previous_response_id(), None);
}
}