use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{Mutex, OwnedMutexGuard};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::acp::AgentMode;
use crate::config::AgentConfig;
use crate::core::permission::PermissionDecision;
use crate::error::{Error, Result};
#[derive(Debug)]
pub struct SessionState {
pub chat_id: Uuid,
pub config: AgentConfig,
pub cwd: PathBuf,
pub skills: Vec<String>,
pub cancel: CancellationToken,
pub approved_tools: HashMap<String, PermissionDecision>,
pub mode: AgentMode,
pub prompt_lock: Arc<Mutex<()>>,
}
impl SessionState {
pub fn try_acquire_prompt_lock(&self, session_id: &str) -> Result<OwnedMutexGuard<()>> {
self.prompt_lock.clone().try_lock_owned().map_err(|_| {
Error::Other(format!(
"a prompt is already in flight for session {session_id}"
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_state() -> SessionState {
SessionState {
chat_id: Uuid::new_v4(),
config: AgentConfig::new(
"mock".into(),
"https://example.com".into(),
"key".into(),
"mock-model".into(),
5,
),
cwd: PathBuf::from("/tmp"),
skills: vec![],
cancel: CancellationToken::new(),
approved_tools: HashMap::new(),
mode: AgentMode::Code,
prompt_lock: Arc::new(Mutex::new(())),
}
}
#[test]
fn second_prompt_is_rejected_while_first_is_in_flight() {
let state = sample_state();
let _first = state.try_acquire_prompt_lock("s1").unwrap();
let second = state.try_acquire_prompt_lock("s1");
assert!(second.is_err());
}
#[test]
fn prompt_lock_is_available_again_once_the_first_guard_drops() {
let state = sample_state();
{
let _first = state.try_acquire_prompt_lock("s1").unwrap();
}
assert!(state.try_acquire_prompt_lock("s1").is_ok());
}
}