use crate::error::{SageError, SageResult};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId(u64);
impl SessionId {
#[must_use]
pub fn new(id: u64) -> Self {
Self(id)
}
#[must_use]
pub fn value(&self) -> u64 {
self.0
}
}
impl std::fmt::Display for SessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "session-{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct SenderHandle {
reply_tx: mpsc::Sender<crate::agent::Message>,
pub protocol: Option<String>,
pub session_id: Option<SessionId>,
}
impl SenderHandle {
#[must_use]
pub fn new(
reply_tx: mpsc::Sender<crate::agent::Message>,
protocol: Option<String>,
session_id: Option<SessionId>,
) -> Self {
Self {
reply_tx,
protocol,
session_id,
}
}
pub async fn send<M: serde::Serialize>(&self, msg: M) -> SageResult<()> {
let message = crate::agent::Message::new(msg)?;
self.reply_tx
.send(message)
.await
.map_err(|e| SageError::Agent(format!("Failed to send reply: {e}")))
}
}
#[derive(Debug)]
pub struct SessionState {
pub protocol: String,
pub state: Box<dyn ProtocolStateMachine>,
pub role: String,
pub partner: SenderHandle,
}
#[derive(Debug, Default)]
pub struct SessionRegistry {
sessions: HashMap<SessionId, SessionState>,
next_session_id: AtomicU64,
}
impl SessionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn next_id(&self) -> SessionId {
SessionId(self.next_session_id.fetch_add(1, Ordering::SeqCst))
}
pub fn start_session(
&mut self,
session_id: SessionId,
protocol: String,
role: String,
state: Box<dyn ProtocolStateMachine>,
partner: SenderHandle,
) {
self.sessions.insert(
session_id,
SessionState {
protocol,
state,
role,
partner,
},
);
}
#[must_use]
pub fn get(&self, session_id: &SessionId) -> Option<&SessionState> {
self.sessions.get(session_id)
}
pub fn get_mut(&mut self, session_id: &SessionId) -> Option<&mut SessionState> {
self.sessions.get_mut(session_id)
}
pub fn remove(&mut self, session_id: &SessionId) -> Option<SessionState> {
self.sessions.remove(session_id)
}
#[must_use]
pub fn has(&self, session_id: &SessionId) -> bool {
self.sessions.contains_key(session_id)
}
#[must_use]
pub fn len(&self) -> usize {
self.sessions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
}
#[derive(Debug, Clone)]
pub enum ProtocolViolation {
UnexpectedMessage {
protocol: String,
expected: String,
received: String,
state: String,
},
EarlyTermination {
protocol: String,
state: String,
},
WrongSender {
protocol: String,
expected_role: String,
actual_role: String,
},
NoSession {
session_id: SessionId,
},
ReplyOutsideHandler,
}
impl std::fmt::Display for ProtocolViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProtocolViolation::UnexpectedMessage {
protocol,
expected,
received,
state,
} => write!(
f,
"unexpected message in protocol '{}': expected '{}', got '{}' (state: {})",
protocol, expected, received, state
),
ProtocolViolation::EarlyTermination { protocol, state } => {
write!(
f,
"protocol '{}' terminated early in state '{}'",
protocol, state
)
}
ProtocolViolation::WrongSender {
protocol,
expected_role,
actual_role,
} => write!(
f,
"wrong sender in protocol '{}': expected role '{}', got '{}'",
protocol, expected_role, actual_role
),
ProtocolViolation::NoSession { session_id } => {
write!(f, "no session found with id {}", session_id)
}
ProtocolViolation::ReplyOutsideHandler => {
write!(f, "reply() called outside of message handler")
}
}
}
}
impl From<ProtocolViolation> for SageError {
fn from(v: ProtocolViolation) -> Self {
SageError::Protocol(v.to_string())
}
}
pub trait ProtocolStateMachine: Send + Sync + std::fmt::Debug {
fn state_name(&self) -> &str;
fn can_send(&self, msg_type: &str, from_role: &str) -> bool;
fn can_receive(&self, msg_type: &str, to_role: &str) -> bool;
fn transition(&mut self, msg_type: &str) -> Result<(), ProtocolViolation>;
fn is_terminal(&self) -> bool;
fn protocol_name(&self) -> &str;
fn clone_box(&self) -> Box<dyn ProtocolStateMachine>;
}
pub type SharedSessionRegistry = Arc<RwLock<SessionRegistry>>;
#[must_use]
pub fn shared_registry() -> SharedSessionRegistry {
Arc::new(RwLock::new(SessionRegistry::new()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_id_display() {
let id = SessionId::new(42);
assert_eq!(format!("{}", id), "session-42");
assert_eq!(id.value(), 42);
}
#[test]
fn session_registry_basic() {
let registry = SessionRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
let id1 = registry.next_id();
let id2 = registry.next_id();
assert_ne!(id1, id2);
}
#[test]
fn protocol_violation_display() {
let violation = ProtocolViolation::UnexpectedMessage {
protocol: "PingPong".to_string(),
expected: "Pong".to_string(),
received: "Ping".to_string(),
state: "AwaitingPong".to_string(),
};
let msg = format!("{}", violation);
assert!(msg.contains("PingPong"));
assert!(msg.contains("Pong"));
assert!(msg.contains("Ping"));
}
#[test]
fn protocol_violation_to_error() {
let violation = ProtocolViolation::ReplyOutsideHandler;
let error: SageError = violation.into();
assert!(matches!(error, SageError::Protocol(_)));
}
}