//! Session and thread model for turn-based agent interactions. //! //! A Session contains one or more Threads. Each Thread represents a //! conversation/interaction sequence with the agent. Threads contain //! Turns, which are request/response pairs. //! //! This model supports: //! - Undo: Roll back to a previous turn //! - Interrupt: Cancel the current turn mid-execution //! - Compaction: Summarize old turns to save context //! - Resume: Continue from a saved checkpoint use std::collections::HashMap; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::llm::ChatMessage; /// A session containing one or more threads. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { /// Unique session ID. pub id: Uuid, /// User ID that owns this session. pub user_id: String, /// Active thread ID. pub active_thread: Option, /// All threads in this session. pub threads: HashMap, /// When the session was created. pub created_at: DateTime, /// When the session was last active. pub last_active_at: DateTime, /// Session metadata. pub metadata: serde_json::Value, } impl Session { /// Create a new session. pub fn new(user_id: impl Into) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), user_id: user_id.into(), active_thread: None, threads: HashMap::new(), created_at: now, last_active_at: now, metadata: serde_json::Value::Null, } } /// Create a new thread in this session. pub fn create_thread(&mut self) -> &mut Thread { let thread = Thread::new(self.id); let thread_id = thread.id; self.threads.insert(thread_id, thread); self.active_thread = Some(thread_id); self.last_active_at = Utc::now(); self.threads.get_mut(&thread_id).expect("just inserted") } /// Get the active thread. pub fn active_thread(&self) -> Option<&Thread> { self.active_thread.and_then(|id| self.threads.get(&id)) } /// Get the active thread mutably. pub fn active_thread_mut(&mut self) -> Option<&mut Thread> { self.active_thread.and_then(|id| self.threads.get_mut(&id)) } /// Get or create the active thread. pub fn get_or_create_thread(&mut self) -> &mut Thread { if self.active_thread.is_none() { self.create_thread(); } self.active_thread_mut().expect("just created") } /// Switch to a different thread. pub fn switch_thread(&mut self, thread_id: Uuid) -> bool { if self.threads.contains_key(&thread_id) { self.active_thread = Some(thread_id); self.last_active_at = Utc::now(); true } else { false } } } /// State of a thread. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ThreadState { /// Thread is idle, waiting for input. Idle, /// Thread is processing a turn. Processing, /// Thread is waiting for user approval. AwaitingApproval, /// Thread has completed (no more turns expected). Completed, /// Thread was interrupted. Interrupted, } /// A conversation thread within a session. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Thread { /// Unique thread ID. pub id: Uuid, /// Parent session ID. pub session_id: Uuid, /// Current state. pub state: ThreadState, /// Turns in this thread. pub turns: Vec, /// When the thread was created. pub created_at: DateTime, /// When the thread was last updated. pub updated_at: DateTime, /// Thread metadata (e.g., title, tags). pub metadata: serde_json::Value, } impl Thread { /// Create a new thread. pub fn new(session_id: Uuid) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), session_id, state: ThreadState::Idle, turns: Vec::new(), created_at: now, updated_at: now, metadata: serde_json::Value::Null, } } /// Get the current turn number (1-indexed for display). pub fn turn_number(&self) -> usize { self.turns.len() + 1 } /// Get the last turn. pub fn last_turn(&self) -> Option<&Turn> { self.turns.last() } /// Get the last turn mutably. pub fn last_turn_mut(&mut self) -> Option<&mut Turn> { self.turns.last_mut() } /// Start a new turn with user input. pub fn start_turn(&mut self, user_input: impl Into) -> &mut Turn { let turn_number = self.turns.len(); let turn = Turn::new(turn_number, user_input); self.turns.push(turn); self.state = ThreadState::Processing; self.updated_at = Utc::now(); self.turns.last_mut().expect("just pushed") } /// Complete the current turn with a response. pub fn complete_turn(&mut self, response: impl Into) { if let Some(turn) = self.turns.last_mut() { turn.complete(response); } self.state = ThreadState::Idle; self.updated_at = Utc::now(); } /// Fail the current turn with an error. pub fn fail_turn(&mut self, error: impl Into) { if let Some(turn) = self.turns.last_mut() { turn.fail(error); } self.state = ThreadState::Idle; self.updated_at = Utc::now(); } /// Mark the thread as awaiting approval. pub fn await_approval(&mut self) { self.state = ThreadState::AwaitingApproval; self.updated_at = Utc::now(); } /// Interrupt the current turn. pub fn interrupt(&mut self) { if let Some(turn) = self.turns.last_mut() { turn.interrupt(); } self.state = ThreadState::Interrupted; self.updated_at = Utc::now(); } /// Resume after interruption. pub fn resume(&mut self) { if self.state == ThreadState::Interrupted { self.state = ThreadState::Idle; self.updated_at = Utc::now(); } } /// Get all messages for context building. pub fn messages(&self) -> Vec { let mut messages = Vec::new(); for turn in &self.turns { messages.push(ChatMessage::user(&turn.user_input)); if let Some(ref response) = turn.response { messages.push(ChatMessage::assistant(response)); } } messages } /// Truncate turns to a specific count (keeping most recent). pub fn truncate_turns(&mut self, keep: usize) { if self.turns.len() > keep { let drain_count = self.turns.len() - keep; self.turns.drain(0..drain_count); // Re-number remaining turns for (i, turn) in self.turns.iter_mut().enumerate() { turn.turn_number = i; } } } /// Restore thread state from a checkpoint's messages. /// /// Clears existing turns and rebuilds from message pairs. /// Messages should alternate: user, assistant, user, assistant... pub fn restore_from_messages(&mut self, messages: Vec) { self.turns.clear(); self.state = ThreadState::Idle; // Messages alternate: user, assistant, user, assistant... let mut iter = messages.into_iter().peekable(); let mut turn_number = 0; while let Some(msg) = iter.next() { if msg.role == crate::llm::Role::User { let mut turn = Turn::new(turn_number, &msg.content); // Check if next is assistant response if let Some(next) = iter.peek() { if next.role == crate::llm::Role::Assistant { let response = iter.next().expect("peeked"); turn.complete(&response.content); } } self.turns.push(turn); turn_number += 1; } } self.updated_at = Utc::now(); } } /// State of a turn. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum TurnState { /// Turn is being processed. Processing, /// Turn completed successfully. Completed, /// Turn failed with an error. Failed, /// Turn was interrupted. Interrupted, } /// A single turn (request/response pair) in a thread. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Turn { /// Turn number (0-indexed). pub turn_number: usize, /// User input that started this turn. pub user_input: String, /// Agent response (if completed). pub response: Option, /// Tool calls made during this turn. pub tool_calls: Vec, /// Turn state. pub state: TurnState, /// When the turn started. pub started_at: DateTime, /// When the turn completed. pub completed_at: Option>, /// Error message (if failed). pub error: Option, } impl Turn { /// Create a new turn. pub fn new(turn_number: usize, user_input: impl Into) -> Self { Self { turn_number, user_input: user_input.into(), response: None, tool_calls: Vec::new(), state: TurnState::Processing, started_at: Utc::now(), completed_at: None, error: None, } } /// Complete this turn. pub fn complete(&mut self, response: impl Into) { self.response = Some(response.into()); self.state = TurnState::Completed; self.completed_at = Some(Utc::now()); } /// Fail this turn. pub fn fail(&mut self, error: impl Into) { self.error = Some(error.into()); self.state = TurnState::Failed; self.completed_at = Some(Utc::now()); } /// Interrupt this turn. pub fn interrupt(&mut self) { self.state = TurnState::Interrupted; self.completed_at = Some(Utc::now()); } /// Record a tool call. pub fn record_tool_call(&mut self, name: impl Into, params: serde_json::Value) { self.tool_calls.push(TurnToolCall { name: name.into(), parameters: params, result: None, error: None, }); } /// Record tool call result. pub fn record_tool_result(&mut self, result: serde_json::Value) { if let Some(call) = self.tool_calls.last_mut() { call.result = Some(result); } } /// Record tool call error. pub fn record_tool_error(&mut self, error: impl Into) { if let Some(call) = self.tool_calls.last_mut() { call.error = Some(error.into()); } } } /// Record of a tool call made during a turn. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TurnToolCall { /// Tool name. pub name: String, /// Parameters passed to the tool. pub parameters: serde_json::Value, /// Result from the tool (if successful). pub result: Option, /// Error from the tool (if failed). pub error: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_session_creation() { let mut session = Session::new("user-123"); assert!(session.active_thread.is_none()); session.create_thread(); assert!(session.active_thread.is_some()); } #[test] fn test_thread_turns() { let mut thread = Thread::new(Uuid::new_v4()); thread.start_turn("Hello"); assert_eq!(thread.state, ThreadState::Processing); assert_eq!(thread.turns.len(), 1); thread.complete_turn("Hi there!"); assert_eq!(thread.state, ThreadState::Idle); assert_eq!(thread.turns[0].response, Some("Hi there!".to_string())); } #[test] fn test_thread_messages() { let mut thread = Thread::new(Uuid::new_v4()); thread.start_turn("First message"); thread.complete_turn("First response"); thread.start_turn("Second message"); thread.complete_turn("Second response"); let messages = thread.messages(); assert_eq!(messages.len(), 4); } #[test] fn test_turn_tool_calls() { let mut turn = Turn::new(0, "Test input"); turn.record_tool_call("echo", serde_json::json!({"message": "test"})); turn.record_tool_result(serde_json::json!("test")); assert_eq!(turn.tool_calls.len(), 1); assert!(turn.tool_calls[0].result.is_some()); } #[test] fn test_restore_from_messages() { let mut thread = Thread::new(Uuid::new_v4()); // First add some turns thread.start_turn("Original message"); thread.complete_turn("Original response"); // Now restore from different messages let messages = vec![ ChatMessage::user("Hello"), ChatMessage::assistant("Hi there!"), ChatMessage::user("How are you?"), ChatMessage::assistant("I'm good!"), ]; thread.restore_from_messages(messages); assert_eq!(thread.turns.len(), 2); assert_eq!(thread.turns[0].user_input, "Hello"); assert_eq!(thread.turns[0].response, Some("Hi there!".to_string())); assert_eq!(thread.turns[1].user_input, "How are you?"); assert_eq!(thread.turns[1].response, Some("I'm good!".to_string())); assert_eq!(thread.state, ThreadState::Idle); } #[test] fn test_restore_from_messages_incomplete_turn() { let mut thread = Thread::new(Uuid::new_v4()); // Messages with incomplete last turn (no assistant response) let messages = vec![ ChatMessage::user("Hello"), ChatMessage::assistant("Hi there!"), ChatMessage::user("How are you?"), ]; thread.restore_from_messages(messages); assert_eq!(thread.turns.len(), 2); assert_eq!(thread.turns[1].user_input, "How are you?"); assert!(thread.turns[1].response.is_none()); } }