use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use aion_core::{
AssistantSessionEvent, AssistantSessionFrame, AssistantSessionId, ContentType, Payload,
};
use aion_integration_acp::AcpSession;
use aion_store::AssistantSessionStore;
use chrono::Utc;
use tokio::process::{ChildStdin, ChildStdout};
use tokio::sync::{Mutex, broadcast};
use super::error::AssistantSessionError;
pub(crate) type HarnessSession = AcpSession<ChildStdout, ChildStdin>;
#[derive(Clone)]
pub(crate) struct Recorder {
session_id: AssistantSessionId,
store: Arc<dyn AssistantSessionStore>,
events: broadcast::Sender<AssistantSessionFrame>,
}
impl Recorder {
pub(crate) fn new(
session_id: AssistantSessionId,
store: Arc<dyn AssistantSessionStore>,
events: broadcast::Sender<AssistantSessionFrame>,
) -> Self {
Self {
session_id,
store,
events,
}
}
pub(crate) const fn session_id(&self) -> AssistantSessionId {
self.session_id
}
pub(crate) async fn record(
&self,
event: AssistantSessionEvent,
) -> Result<u64, AssistantSessionError> {
let bytes = serde_json::to_vec(&event).map_err(|error| {
AssistantSessionError::Internal(format!(
"an assistant session frame is not encodable: {error}"
))
})?;
let index = self
.store
.append_assistant_transcript_event(
&self.session_id,
Utc::now(),
Payload::new(ContentType::Json, bytes),
)
.await?;
self.cache_commands(&event).await?;
self.cache_config_options(&event).await?;
let _ = self.events.send(AssistantSessionFrame { index, event });
Ok(index)
}
async fn cache_commands(
&self,
event: &AssistantSessionEvent,
) -> Result<(), AssistantSessionError> {
let AssistantSessionEvent::AvailableCommands { commands } = event else {
return Ok(());
};
let Some(mut record) = self.store.get_assistant_session(&self.session_id).await? else {
tracing::warn!(
session = %self.session_id,
"an assistant session advertised commands after its record disappeared; the \
transcript holds them and the listing cache does not"
);
return Ok(());
};
record.commands.clone_from(commands);
record.updated_at = Utc::now();
self.store.put_assistant_session(record).await?;
Ok(())
}
async fn cache_config_options(
&self,
event: &AssistantSessionEvent,
) -> Result<(), AssistantSessionError> {
let AssistantSessionEvent::ConfigOptions { options } = event else {
return Ok(());
};
let Some(mut record) = self.store.get_assistant_session(&self.session_id).await? else {
tracing::warn!(
session = %self.session_id,
"an assistant session advertised configuration options after its record \
disappeared; the transcript holds them and the listing cache does not"
);
return Ok(());
};
record.config_options.clone_from(options);
record.updated_at = Utc::now();
self.store.put_assistant_session(record).await?;
Ok(())
}
pub(crate) fn subscribe(&self) -> broadcast::Receiver<AssistantSessionFrame> {
self.events.subscribe()
}
}
pub(crate) struct LiveSession {
session_id: AssistantSessionId,
session: Mutex<Option<HarnessSession>>,
recorder: Recorder,
busy: AtomicBool,
acp_session_ref: String,
load_session: bool,
initial_config_options: std::sync::Mutex<Option<serde_json::Value>>,
}
impl LiveSession {
pub(crate) fn new(
session_id: AssistantSessionId,
session: HarnessSession,
recorder: Recorder,
) -> Self {
let acp_session_ref = session.session_id().to_string();
let load_session = session.supports_load_session();
let initial_config_options = session.initial_config_options().cloned();
Self {
session_id,
session: Mutex::new(Some(session)),
recorder,
busy: AtomicBool::new(false),
acp_session_ref,
load_session,
initial_config_options: std::sync::Mutex::new(initial_config_options),
}
}
pub(crate) fn take_initial_config_options(&self) -> Option<serde_json::Value> {
match self.initial_config_options.lock() {
Ok(mut held) => held.take(),
Err(poisoned) => poisoned.into_inner().take(),
}
}
pub(crate) const fn session_id(&self) -> AssistantSessionId {
self.session_id
}
pub(crate) fn acp_session_ref(&self) -> &str {
&self.acp_session_ref
}
pub(crate) const fn supports_load_session(&self) -> bool {
self.load_session
}
pub(crate) const fn recorder(&self) -> &Recorder {
&self.recorder
}
pub(crate) fn claim_turn(&self) -> Result<(), AssistantSessionError> {
if self
.busy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return Err(AssistantSessionError::Busy {
session_id: self.session_id,
});
}
Ok(())
}
pub(crate) fn release_turn(&self) {
self.busy.store(false, Ordering::SeqCst);
}
pub(crate) async fn with_session<T>(
&self,
operation: impl AsyncFnOnce(&HarnessSession) -> T,
) -> Option<T> {
let guard = self.session.lock().await;
let session = guard.as_ref()?;
Some(operation(session).await)
}
pub(crate) async fn is_alive(&self) -> bool {
self.session
.lock()
.await
.as_ref()
.is_some_and(AcpSession::is_live)
}
pub(crate) async fn close(&self) {
let taken = self.session.lock().await.take();
if let Some(session) = taken {
session.close().await;
}
}
}