use std::path::{Path, PathBuf};
use agent_client_protocol::{
Client, ConnectionTo,
schema::v1::{
AvailableCommand, CloseSessionRequest, CloseSessionResponse, CurrentModeUpdate, Error,
ListSessionsRequest, ListSessionsResponse, NewSessionRequest, NewSessionResponse,
SessionId, SessionInfo, SessionModeState, SessionUpdate, SetSessionModeRequest,
SetSessionModeResponse,
},
};
use super::{ServeConfig, announce_commands, notify};
use crate::{
history,
mode::ModeError,
session::{AcpSession, SessionRegistry},
};
use basis_core::{PersistedSession, RunError, provider::ProviderError};
pub(super) async fn list_sessions(
config: &ServeConfig,
request: ListSessionsRequest,
) -> Result<ListSessionsResponse, Error> {
if !config.source.lists_sessions() {
return Err(Error::method_not_found());
}
let cwd = request.cwd.ok_or_else(|| {
Error::invalid_params().data("cwd is required: basis lists conversations per workspace")
})?;
let sessions = config
.source
.list_sessions(cwd.clone())
.await
.map_err(setup_failed)?;
Ok(ListSessionsResponse::new(
sessions
.into_iter()
.map(|session| session_info(session, &cwd))
.collect(),
))
}
pub(super) fn session_info(session: PersistedSession, cwd: &Path) -> SessionInfo {
SessionInfo::new(session.agent_id, cwd.to_path_buf()).title(session.name)
}
pub(super) struct Opened {
pub(super) response: NewSessionResponse,
pub(super) commands: Vec<AvailableCommand>,
}
pub(super) async fn new_session(
config: &ServeConfig,
sessions: &SessionRegistry,
request: NewSessionRequest,
) -> Result<Opened, Error> {
let mcp = crate::from_acp(&request.mcp_servers).map_err(|error| setup_failed(error.into()))?;
let run = config
.source
.create(request.cwd, mcp)
.await
.map_err(setup_failed)?;
let commands = crate::available_commands(&run.context().templates);
let session = AcpSession::new(run, config.initial_mode);
let modes = session.modes().state();
let id = sessions.insert(session);
Ok(Opened {
response: NewSessionResponse::new(id).modes(modes),
commands,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Replay {
Yes,
No,
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn open_persisted(
config: &ServeConfig,
sessions: &SessionRegistry,
connection: &ConnectionTo<Client>,
session_id: SessionId,
cwd: PathBuf,
mcp_servers: Vec<agent_client_protocol::schema::v1::McpServer>,
replay: Replay,
) -> Result<SessionModeState, Error> {
let session = match sessions.get(&session_id) {
Some(session) => session,
None => {
let mcp = crate::from_acp(&mcp_servers).map_err(|error| setup_failed(error.into()))?;
let run = config
.source
.resume(&session_id.0, cwd, mcp)
.await
.map_err(setup_failed)?;
let session = AcpSession::new(run, config.initial_mode);
sessions.insert(session.clone());
session
}
};
{
let run = session.lock_turn().await;
let commands = crate::available_commands(&run.context().templates);
let updates = match replay {
Replay::Yes => history::replay(run.history()),
Replay::No => Vec::new(),
};
drop(run);
for update in updates {
if notify(connection, &session_id, update).is_err() {
break;
}
}
let _ = announce_commands(connection, &session_id, commands);
}
Ok(session.modes().state())
}
pub(super) fn set_mode(
sessions: &SessionRegistry,
connection: &ConnectionTo<Client>,
request: &SetSessionModeRequest,
) -> Result<SetSessionModeResponse, Error> {
let session = sessions
.get(&request.session_id)
.ok_or_else(|| Error::invalid_params().data("unknown session"))?;
session
.modes()
.set(&request.mode_id)
.map_err(|error: ModeError| Error::invalid_params().data(error.to_string()))?;
let _ = notify(
connection,
&request.session_id,
SessionUpdate::CurrentModeUpdate(CurrentModeUpdate::new(request.mode_id.clone())),
);
Ok(SetSessionModeResponse::new())
}
pub(super) fn close_session(
sessions: &SessionRegistry,
request: &CloseSessionRequest,
) -> Result<CloseSessionResponse, Error> {
let session = sessions
.remove(&request.session_id)
.ok_or_else(|| Error::invalid_params().data("unknown session"))?;
session.cancel();
Ok(CloseSessionResponse::new())
}
pub(super) fn setup_failed(error: basis_core::RunError) -> Error {
if is_missing_credential(&error) {
return Error::auth_required().data(error.to_string());
}
Error::internal_error().data(error.to_string())
}
fn is_missing_credential(error: &basis_core::RunError) -> bool {
matches!(
error,
RunError::Provider(
ProviderError::NoCredential
| ProviderError::MissingCredential { .. }
| ProviderError::NoCompatibleCredential
)
)
}