use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use super::{
session_host::{PromptGate, SessionHost},
AcpClientPort, AcpStartup,
};
use agent_client_protocol::schema::v1::{
DeleteSessionRequest, DeleteSessionResponse, SessionDeleteCapabilities, SessionListCapabilities,
};
use agent_client_protocol::{
schema::v1::{
AgentCapabilities, AuthenticateRequest, CancelNotification, CloseSessionRequest,
CloseSessionResponse, InitializeRequest, InitializeResponse, LoadSessionRequest,
LoadSessionResponse, McpCapabilities, NewSessionRequest, NewSessionResponse,
PromptCapabilities, PromptRequest, PromptResponse,
SessionAdditionalDirectoriesCapabilities, SessionCapabilities, SessionCloseCapabilities,
SessionId, SessionNotification, SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, StopReason,
},
schema::ProtocolVersion,
Error as AcpError,
};
#[path = "agent_publication.rs"]
mod publication;
use publication::PublicationGuard;
#[derive(Debug)]
pub(super) struct SessionStart<Response> {
pub(super) response: Response,
pub(super) commands: SessionNotification,
pub(super) publication: PublicationGuard,
}
struct LiveSession {
host: Arc<tokio::sync::Mutex<Option<SessionHost>>>,
cancel: Arc<PromptGate>,
replaced: Arc<AtomicBool>,
}
#[derive(Clone, Copy, Debug)]
enum HostWait {
Prompt,
Settings,
}
enum SessionHostLock {
Ready(HostGuard),
Closed,
}
pub(super) struct RhoAcpAgent {
startup: AcpStartup,
sessions: tokio::sync::Mutex<HashMap<SessionId, Arc<LiveSession>>>,
install_gate: tokio::sync::RwLock<()>,
install_locks: tokio::sync::Mutex<HashMap<SessionId, Arc<tokio::sync::Mutex<()>>>>,
closed: AtomicBool,
}
impl RhoAcpAgent {
pub(super) fn new(startup: AcpStartup) -> Self {
Self {
startup,
sessions: tokio::sync::Mutex::new(HashMap::new()),
install_gate: tokio::sync::RwLock::new(()),
install_locks: tokio::sync::Mutex::new(HashMap::new()),
closed: AtomicBool::new(false),
}
}
pub(super) fn initialize(request: &InitializeRequest) -> InitializeResponse {
InitializeResponse::new(ProtocolVersion::V1)
.agent_capabilities(
AgentCapabilities::new()
.load_session(true)
.prompt_capabilities(
PromptCapabilities::new()
.image(true)
.audio(false)
.embedded_context(true),
)
.mcp_capabilities(McpCapabilities::new().http(true))
.session_capabilities(
SessionCapabilities::new()
.additional_directories(SessionAdditionalDirectoriesCapabilities::new())
.close(SessionCloseCapabilities::new())
.list(SessionListCapabilities::new())
.delete(SessionDeleteCapabilities::new()),
),
)
.auth_methods(super::auth::methods(&request.client_capabilities))
.agent_info(
agent_client_protocol::schema::v1::Implementation::new(
"rho",
env!("CARGO_PKG_VERSION"),
)
.title("Rho"),
)
}
pub(super) fn authenticate(_request: &AuthenticateRequest) -> AcpError {
not_yet_supported("authenticate")
}
pub(super) async fn delete_session(
&self,
request: DeleteSessionRequest,
) -> Result<DeleteSessionResponse, AcpError> {
let _gate = self.begin_install().await?;
let target = super::session_list::delete_target(request.session_id.clone()).await?;
let _install = self.retire(&request.session_id).await;
super::session_list::delete(target).await
}
async fn retire(&self, session_id: &SessionId) -> tokio::sync::OwnedMutexGuard<()> {
let install = self.install_lock_for(session_id).await.lock_owned().await;
let live = self.sessions.lock().await.remove(session_id);
if let Some(live) = live {
shutdown_live(live).await;
}
install
}
pub(super) async fn set_session_mode(
self: &Arc<Self>,
request: SetSessionModeRequest,
port: &dyn AcpClientPort,
) -> Result<SetSessionModeResponse, AcpError> {
let Some(mode) = super::permission::parse_mode_id(request.mode_id.0.as_ref()) else {
return Err(AcpError::invalid_params().data(format!(
"unknown session mode '{}'",
request.mode_id.0.as_ref()
)));
};
let mut host = self.lock_settings_host(&request.session_id).await?;
let result = host.set_permission_mode(mode, &self.startup.config).await;
host.refresh_advertisements(&self.startup.config, port)
.await;
result?;
Ok(SetSessionModeResponse::new())
}
pub(super) async fn new_session(
self: &Arc<Self>,
request: NewSessionRequest,
) -> Result<SessionStart<NewSessionResponse>, AcpError> {
let _gate = self.begin_install().await?;
let (mut host, response) = SessionHost::create(&self.startup, request)
.await
.map_err(session_start_error)?;
let session_id = response.session_id.clone();
let commands = host.available_commands(&self.startup.config);
let live = LiveSession::new(host);
let publication = PublicationGuard::new(&live, &session_id).await;
self.publish(session_id, live).await;
Ok(SessionStart {
response,
commands,
publication,
})
}
pub(super) async fn load_session(
self: &Arc<Self>,
request: LoadSessionRequest,
port: &dyn AcpClientPort,
) -> Result<SessionStart<LoadSessionResponse>, AcpError> {
let _gate = self.begin_install().await?;
let session_id = request.session_id.clone();
let install = self.install_lock_for(&session_id).await;
let _install = install.lock().await;
let previous = { self.sessions.lock().await.get(&session_id).cloned() };
let mut retired = None;
if let Some(previous) = &previous {
previous.cancel.cancel();
let host = previous.host.lock().await;
previous.replaced.store(true, Ordering::Release);
retired = Some(host);
}
let (mut host, response) = match SessionHost::load(&self.startup, request, port).await {
Ok(loaded) => loaded,
Err(error) => {
if let Some(previous) = &previous {
previous.replaced.store(false, Ordering::Release);
}
return Err(session_start_error(error));
}
};
let commands = host.available_commands(&self.startup.config);
let old_host = retired.and_then(|mut slot| slot.take());
let live = LiveSession::new(host);
let publication = PublicationGuard::new(&live, &session_id).await;
self.publish_locked(session_id, live, old_host).await;
Ok(SessionStart {
response,
commands,
publication,
})
}
pub(super) async fn prompt(
self: &Arc<Self>,
request: PromptRequest,
port: &dyn AcpClientPort,
) -> Result<PromptResponse, AcpError> {
let session_id = request.session_id.clone();
match self
.lock_session_host(&session_id, HostWait::Prompt)
.await?
{
SessionHostLock::Ready(mut host) => host
.prompt(request, port, &self.startup)
.await
.map_err(prompt_error),
SessionHostLock::Closed => Ok(PromptResponse::new(StopReason::Cancelled)),
}
}
async fn lock_session_host(
&self,
session_id: &SessionId,
wait: HostWait,
) -> Result<SessionHostLock, AcpError> {
let mut live = self.live_session(session_id).await?;
loop {
let locked = live.lock_host(session_id, wait).await;
if live.cancel.is_closed() {
return Ok(SessionHostLock::Closed);
}
if !live.replaced.load(Ordering::Acquire) {
return locked.map(SessionHostLock::Ready);
}
match (wait, locked) {
(HostWait::Settings, _) => return Err(reloaded_session(session_id)),
(HostWait::Prompt, Ok(host)) => return Ok(SessionHostLock::Ready(host)),
(HostWait::Prompt, Err(busy)) => {
live = self.successor(session_id, &live).await?.ok_or(busy)?;
}
}
}
}
async fn lock_settings_host(&self, session_id: &SessionId) -> Result<HostGuard, AcpError> {
match self
.lock_session_host(session_id, HostWait::Settings)
.await?
{
SessionHostLock::Ready(host) => Ok(host),
SessionHostLock::Closed => Err(missing_session(session_id)),
}
}
async fn successor(
&self,
session_id: &SessionId,
replaced: &Arc<LiveSession>,
) -> Result<Option<Arc<LiveSession>>, AcpError> {
let install = self.install_lock_for(session_id).await;
drop(install.lock().await);
let next = self.live_session(session_id).await?;
Ok((!Arc::ptr_eq(&next, replaced)).then_some(next))
}
async fn live_session(&self, session_id: &SessionId) -> Result<Arc<LiveSession>, AcpError> {
let sessions = self.sessions.lock().await;
sessions
.get(session_id)
.cloned()
.ok_or_else(|| missing_session(session_id))
}
pub(super) async fn set_config_option(
self: &Arc<Self>,
request: SetSessionConfigOptionRequest,
port: &dyn AcpClientPort,
) -> Result<SetSessionConfigOptionResponse, AcpError> {
let session_id = request.session_id.clone();
let mut host = self.lock_settings_host(&session_id).await?;
let result = host.set_config_option(request, &self.startup.config).await;
host.refresh_advertisements(&self.startup.config, port)
.await;
result
}
pub(super) async fn cancel(&self, notification: CancelNotification) {
let live = {
let sessions = self.sessions.lock().await;
sessions.get(¬ification.session_id).cloned()
};
if let Some(live) = live {
live.cancel.cancel();
}
}
pub(super) async fn close_session(
&self,
request: CloseSessionRequest,
) -> Result<CloseSessionResponse, AcpError> {
let _gate = self.begin_install().await?;
let session_id = request.session_id;
let install = self.install_lock_for(&session_id).await;
let _install = install.lock().await;
let live = self
.sessions
.lock()
.await
.remove(&session_id)
.ok_or_else(|| missing_session(&session_id))?;
shutdown_live(live).await;
Ok(CloseSessionResponse::new())
}
pub(super) async fn shutdown_all(&self) {
let _gate = self.install_gate.write().await;
self.closed.store(true, Ordering::Release);
let lives: Vec<Arc<LiveSession>> = {
let mut sessions = self.sessions.lock().await;
sessions.drain().map(|(_, live)| live).collect()
};
self.install_locks.lock().await.clear();
for live in lives {
shutdown_live(live).await;
}
}
async fn begin_install(&self) -> Result<tokio::sync::RwLockReadGuard<'_, ()>, AcpError> {
let gate = self.install_gate.read().await;
if self.closed.load(Ordering::Acquire) {
return Err(agent_stopped());
}
Ok(gate)
}
#[cfg(test)]
async fn install(&self, session_id: SessionId, host: SessionHost) {
self.publish(session_id, LiveSession::new(host)).await;
}
async fn publish(&self, session_id: SessionId, live: Arc<LiveSession>) {
let install = self.install_lock_for(&session_id).await;
let _install = install.lock().await;
if self.closed.load(Ordering::Acquire) {
shutdown_live(live).await;
return;
}
let previous = { self.sessions.lock().await.get(&session_id).cloned() };
let old_host = if let Some(previous) = previous {
previous.replaced.store(true, Ordering::Release);
previous.cancel.cancel();
previous.host.lock().await.take()
} else {
None
};
self.publish_locked(session_id, live, old_host).await;
}
async fn publish_locked(
&self,
session_id: SessionId,
live: Arc<LiveSession>,
old_host: Option<SessionHost>,
) {
let outcome = {
let mut sessions = self.sessions.lock().await;
if self.closed.load(Ordering::Acquire) {
Err(live)
} else {
sessions.insert(session_id, live);
Ok(())
}
};
if let Some(host) = old_host {
host.shutdown().await;
}
if let Err(rejected) = outcome {
shutdown_live(rejected).await;
}
}
async fn install_lock_for(&self, session_id: &SessionId) -> Arc<tokio::sync::Mutex<()>> {
let mut locks = self.install_locks.lock().await;
locks
.entry(session_id.clone())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
}
#[cfg(test)]
impl RhoAcpAgent {
pub(super) async fn install_for_tests(&self, session_id: SessionId, host: SessionHost) {
self.install(session_id, host).await;
}
pub(super) async fn hold_host_for_tests(
&self,
session_id: &SessionId,
) -> (
tokio::sync::OwnedMutexGuard<Option<SessionHost>>,
Arc<AtomicBool>,
) {
let live = self.live_session(session_id).await.expect("live session");
let held = Arc::clone(&live.host).lock_owned().await;
(held, Arc::clone(&live.replaced))
}
}
fn not_yet_supported(method: &str) -> AcpError {
AcpError::invalid_request().data(format!("{method} is not yet supported"))
}
fn missing_session(session_id: &SessionId) -> AcpError {
AcpError::resource_not_found(Some(session_id.to_string()))
}
pub(super) fn busy_session(session_id: &SessionId) -> AcpError {
AcpError::invalid_request().data(format!(
"session '{session_id}' already has an active prompt"
))
}
fn reloaded_session(session_id: &SessionId) -> AcpError {
AcpError::invalid_request().data(format!(
"session '{session_id}' was reloaded before this change applied; send it again"
))
}
pub(super) fn session_apply_error(session: &rho_sdk::Session, error: rho_sdk::Error) -> AcpError {
match error {
rho_sdk::Error::SessionBusy => busy_session(&SessionId::new(session.id().as_str())),
error => AcpError::internal_error().data(error.to_string()),
}
}
fn prompt_error(error: anyhow::Error) -> AcpError {
match error.downcast::<super::commands::CommandRejected>() {
Ok(rejected) => AcpError::invalid_params().data(rejected.to_string()),
Err(error) => host_error(error),
}
}
fn host_error(error: anyhow::Error) -> AcpError {
AcpError::internal_error().data(error.to_string())
}
fn session_start_error(error: anyhow::Error) -> AcpError {
match error.downcast::<super::session_host::RejectedDirectory>() {
Ok(rejected) => AcpError::invalid_params().data(rejected.to_string()),
Err(error) => host_error(error),
}
}
fn agent_stopped() -> AcpError {
AcpError::internal_error().data("agent is shut down")
}
impl LiveSession {
fn new(host: SessionHost) -> Arc<Self> {
let cancel = host.cancel_handle();
let replaced = host.replaced_flag();
Arc::new(Self {
host: Arc::new(tokio::sync::Mutex::new(Some(host))),
cancel,
replaced,
})
}
fn try_lock_host(&self, session_id: &SessionId) -> Result<HostGuard, AcpError> {
let guard = Arc::clone(&self.host)
.try_lock_owned()
.map_err(|_| busy_session(session_id))?;
HostGuard::occupied(guard, session_id)
}
async fn lock_host(
&self,
session_id: &SessionId,
wait: HostWait,
) -> Result<HostGuard, AcpError> {
let queues = match wait {
HostWait::Prompt => self.cancel.admits_queued_prompt(),
HostWait::Settings => true,
};
match self.try_lock_host(session_id) {
Err(busy) if !queues => Err(busy),
Err(_) => HostGuard::occupied(Arc::clone(&self.host).lock_owned().await, session_id),
held => held,
}
}
}
struct HostGuard(tokio::sync::OwnedMutexGuard<Option<SessionHost>>);
const HOST_SLOT_INVARIANT: &str = "HostGuard::occupied rejects an empty slot";
impl HostGuard {
fn occupied(
guard: tokio::sync::OwnedMutexGuard<Option<SessionHost>>,
session_id: &SessionId,
) -> Result<Self, AcpError> {
if guard.is_none() {
return Err(busy_session(session_id));
}
Ok(Self(guard))
}
}
impl Deref for HostGuard {
type Target = SessionHost;
fn deref(&self) -> &Self::Target {
self.0.as_ref().expect(HOST_SLOT_INVARIANT)
}
}
impl DerefMut for HostGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().expect(HOST_SLOT_INVARIANT)
}
}
async fn shutdown_live(live: Arc<LiveSession>) {
live.cancel.close();
let host = live.host.lock().await.take();
if let Some(host) = host {
host.shutdown().await;
}
}
#[cfg(test)]
#[path = "agent_tests.rs"]
mod tests;