use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde_json::Value;
use uuid::Uuid;
use crate::error::{Error, Result};
use crate::memory::ContextProvider;
#[derive(Clone, Default)]
pub struct SessionState {
inner: Arc<Mutex<HashMap<String, Value>>>,
}
impl SessionState {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, key: impl Into<String>, value: Value) -> Option<Value> {
self.inner.lock().unwrap().insert(key.into(), value)
}
pub fn get(&self, key: &str) -> Option<Value> {
self.inner.lock().unwrap().get(key).cloned()
}
pub fn remove(&self, key: &str) -> Option<Value> {
self.inner.lock().unwrap().remove(key)
}
pub fn contains_key(&self, key: &str) -> bool {
self.inner.lock().unwrap().contains_key(key)
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.inner.lock().unwrap().is_empty()
}
pub fn snapshot(&self) -> HashMap<String, Value> {
self.inner.lock().unwrap().clone()
}
pub fn shares_storage_with(&self, other: &SessionState) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
impl From<HashMap<String, Value>> for SessionState {
fn from(map: HashMap<String, Value>) -> Self {
Self {
inner: Arc::new(Mutex::new(map)),
}
}
}
impl std::fmt::Debug for SessionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map().entries(self.snapshot()).finish()
}
}
#[derive(Clone)]
pub struct AgentSession {
session_id: String,
service_session_id: Option<String>,
pub state: SessionState,
pub context_providers: Vec<Arc<dyn ContextProvider>>,
}
impl std::fmt::Debug for AgentSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentSession")
.field("session_id", &self.session_id)
.field("service_session_id", &self.service_session_id)
.field("state", &self.state)
.field("context_providers", &self.context_providers.len())
.finish()
}
}
impl Default for AgentSession {
fn default() -> Self {
Self::new()
}
}
impl AgentSession {
pub fn new() -> Self {
Self {
session_id: Uuid::new_v4().to_string(),
service_session_id: None,
state: SessionState::new(),
context_providers: Vec::new(),
}
}
pub fn child(&self) -> AgentSession {
AgentSession {
session_id: self.session_id.clone(),
service_session_id: None,
state: self.state.clone(),
context_providers: Vec::new(),
}
}
pub fn service(id: impl Into<String>) -> Self {
Self {
service_session_id: Some(id.into()),
..Self::new()
}
}
pub fn with_context_providers(mut self, providers: Vec<Arc<dyn ContextProvider>>) -> Self {
self.context_providers = providers;
self
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn service_session_id(&self) -> Option<&str> {
self.service_session_id.as_deref()
}
pub fn set_service_session_id(&mut self, id: impl Into<String>) {
self.service_session_id = Some(id.into());
}
pub fn try_adopt_service_session_id(&mut self, id: &str) -> bool {
if self.service_session_id.as_deref() == Some(id) {
return false;
}
self.service_session_id = Some(id.to_string());
true
}
pub fn to_dict(&self) -> Value {
serde_json::json!({
"session_id": self.session_id,
"service_session_id": self.service_session_id,
"state": self.state.snapshot(),
})
}
pub fn from_dict(state: &Value) -> Result<Self> {
let session_id = state
.get("session_id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| Uuid::new_v4().to_string());
let service_session_id = state
.get("service_session_id")
.and_then(Value::as_str)
.map(str::to_string);
let session_state: HashMap<String, Value> = match state.get("state") {
Some(v) if !v.is_null() => serde_json::from_value(v.clone()).map_err(|e| {
Error::Serialization(format!("failed to restore session state: {e}"))
})?,
_ => HashMap::new(),
};
Ok(Self {
session_id,
service_session_id,
state: SessionState::from(session_state),
context_providers: Vec::new(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_session_has_a_generated_id_and_no_service_id() {
let session = AgentSession::new();
assert!(!session.session_id().is_empty());
assert!(session.service_session_id().is_none());
assert!(session.state.is_empty());
}
#[test]
fn service_session_sets_service_session_id_and_still_has_a_local_id() {
let session = AgentSession::service("svc-1");
assert_eq!(session.service_session_id(), Some("svc-1"));
assert!(!session.session_id().is_empty());
}
#[test]
fn try_adopt_service_session_id_reports_whether_it_was_new() {
let mut session = AgentSession::new();
assert!(session.try_adopt_service_session_id("conv-1"));
assert_eq!(session.service_session_id(), Some("conv-1"));
assert!(!session.try_adopt_service_session_id("conv-1"));
assert!(session.try_adopt_service_session_id("conv-2"));
assert_eq!(session.service_session_id(), Some("conv-2"));
}
#[test]
fn to_dict_from_dict_round_trips_session_id_service_id_and_state() {
let session = AgentSession::service("svc-9");
session.state.insert("key", serde_json::json!("value"));
let original_id = session.session_id().to_string();
let state = session.to_dict();
assert_eq!(state["session_id"], original_id);
assert_eq!(state["service_session_id"], "svc-9");
assert_eq!(state["state"]["key"], "value");
assert!(state.get("messages").is_none());
assert!(state.get("chat_message_store_state").is_none());
let restored = AgentSession::from_dict(&state).unwrap();
assert_eq!(restored.session_id(), original_id);
assert_eq!(restored.service_session_id(), Some("svc-9"));
assert_eq!(restored.state.get("key"), Some(serde_json::json!("value")));
assert!(restored.context_providers.is_empty());
}
#[test]
fn clones_share_the_state_bag_by_reference() {
let session = AgentSession::new();
let clone = session.clone();
clone
.state
.insert("written-via-clone", serde_json::json!(1));
assert_eq!(
session.state.get("written-via-clone"),
Some(serde_json::json!(1)),
"a clone must be a view onto the same state bag"
);
assert!(session.state.shares_storage_with(&clone.state));
}
#[test]
fn child_shares_id_and_state_but_isolates_the_service_pointer() {
let parent = AgentSession::service("svc-parent");
parent.state.insert("k", serde_json::json!("v"));
let child = parent.child();
assert_eq!(child.session_id(), parent.session_id());
assert_eq!(
child.service_session_id(),
None,
"the parent's server-side conversation pointer must not leak to the child"
);
assert!(child.context_providers.is_empty());
assert_eq!(child.state.get("k"), Some(serde_json::json!("v")));
child.state.insert("from-child", serde_json::json!(2));
assert_eq!(parent.state.get("from-child"), Some(serde_json::json!(2)));
let mut child = child;
child.set_service_session_id("svc-child");
assert_eq!(parent.service_session_id(), Some("svc-parent"));
}
#[test]
fn from_dict_generates_a_session_id_when_absent() {
let restored = AgentSession::from_dict(&serde_json::json!({})).unwrap();
assert!(!restored.session_id().is_empty());
assert!(restored.service_session_id().is_none());
assert!(restored.state.is_empty());
}
}