use aion_core::{AssistantSessionFrame, AssistantSessionId};
use aion_proto::WireError;
use axum::{
extract::{
Path, Query, State,
ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade, close_code},
},
response::{IntoResponse, Response},
};
use futures::{SinkExt, StreamExt};
use serde::Deserialize;
use tokio::sync::broadcast::error::RecvError;
use super::assistant_sessions::{invalid_id, session_refusal};
use super::auth::WsCaller;
use crate::namespace::grants::{ASSISTANT_SESSIONS, require_grant};
use crate::stream::socket::send_wire_error;
use crate::{CallerIdentity, ServerError, ServerState};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub(crate) struct AssistantWatchQuery {
after: Option<String>,
}
pub(crate) async fn assistant_session_socket(
websocket: WebSocketUpgrade,
State(state): State<ServerState>,
WsCaller(caller): WsCaller,
Path(id): Path<String>,
Query(query): Query<AssistantWatchQuery>,
) -> Response {
websocket
.on_upgrade(move |socket| async move {
if let Err(error) = serve_assistant_socket(socket, state, caller, id, query).await {
tracing::warn!(
error = %error,
"an assistant session subscription ended with an error"
);
}
})
.into_response()
}
async fn serve_assistant_socket(
socket: WebSocket,
state: ServerState,
caller: CallerIdentity,
id: String,
query: AssistantWatchQuery,
) -> Result<(), ServerError> {
let (mut socket_tx, mut socket_rx) = socket.split();
if let Err(error) = require_grant(&caller, &ASSISTANT_SESSIONS) {
return terminal(&mut socket_tx, error.to_wire_error()).await;
}
let session = match AssistantSessionId::parse(&id) {
Ok(session) => session,
Err(error) => return terminal(&mut socket_tx, invalid_id(&error.to_string())).await,
};
let after = match parse_after(query.after.as_deref()) {
Ok(after) => after,
Err(wire) => return terminal(&mut socket_tx, wire).await,
};
let watch = state
.assistant_sessions()
.watch(caller.subject(), session, after)
.await;
let (replayed, live) = match watch {
Ok(watch) => watch,
Err(error) => {
let (_status, wire) = session_refusal(&error);
return terminal(&mut socket_tx, wire).await;
}
};
let mut last_index = after;
for frame in replayed {
last_index = Some(frame.index);
if !write_frame(&mut socket_tx, &frame).await? {
return Ok(());
}
}
forward_live(&mut socket_tx, &mut socket_rx, live, last_index).await
}
async fn forward_live<Tx, Rx>(
socket_tx: &mut Tx,
socket_rx: &mut Rx,
mut live: tokio::sync::broadcast::Receiver<AssistantSessionFrame>,
mut last_index: Option<u64>,
) -> Result<(), ServerError>
where
Tx: futures::Sink<Message> + Unpin,
<Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
Rx: futures::Stream<Item = Result<Message, axum::Error>> + Unpin,
{
loop {
tokio::select! {
client_message = socket_rx.next() => {
match client_message {
Some(Ok(Message::Close(_))) | None => return Ok(()),
Some(Ok(message)) => drop(message),
Some(Err(error)) => {
drop(error);
return Ok(());
}
}
}
received = live.recv() => {
match received {
Ok(frame) => {
if last_index.is_some_and(|seen| frame.index <= seen) {
continue;
}
last_index = Some(frame.index);
if !write_frame(socket_tx, &frame).await? {
return Ok(());
}
}
Err(RecvError::Closed) => return finish(socket_tx).await,
Err(RecvError::Lagged(missed)) => {
return terminal(socket_tx, lagged(missed, last_index)).await;
}
}
}
}
}
}
async fn write_frame<Tx>(
socket_tx: &mut Tx,
frame: &AssistantSessionFrame,
) -> Result<bool, ServerError>
where
Tx: futures::Sink<Message> + Unpin,
<Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
{
let payload = match serde_json::to_string(frame) {
Ok(payload) => payload,
Err(source) => {
let wire = WireError::backend(format!(
"the assistant session frame at index {} could not be encoded for the stream \
({source}); the stream is ended rather than continued with a frame missing from \
it",
frame.index
));
return terminal(socket_tx, wire).await.map(|()| false);
}
};
Ok(socket_tx.send(Message::Text(payload.into())).await.is_ok())
}
fn parse_after(after: Option<&str>) -> Result<Option<u64>, WireError> {
match after {
None => Ok(None),
Some(text) => text.parse::<u64>().map(Some).map_err(|source| {
WireError::invalid_input(format!(
"`after` is the transcript index a client last saw, and `{text}` is not one \
({source}); omit it to replay the whole conversation"
))
.with_error_type(INVALID_AFTER_TYPE)
}),
}
}
fn lagged(missed: u64, last_index: Option<u64>) -> WireError {
let resume = match last_index {
Some(index) => format!("reconnect with `?after={index}` to read what was missed"),
None => "reconnect with no `after` to read the conversation from its start".to_owned(),
};
WireError::lagged(format!(
"this assistant session produced frames faster than the socket drained them and {missed} \
frame(s) were dropped; the stream is ended rather than continued with a gap in it — \
{resume}"
))
.with_error_type(LAGGED_TYPE)
}
async fn terminal<Tx>(socket_tx: &mut Tx, wire: WireError) -> Result<(), ServerError>
where
Tx: futures::Sink<Message> + Unpin,
<Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
{
send_wire_error(socket_tx, &wire).await?;
Err(ServerError::Wire { wire })
}
async fn finish<Tx>(socket_tx: &mut Tx) -> Result<(), ServerError>
where
Tx: futures::Sink<Message> + Unpin,
<Tx as futures::Sink<Message>>::Error: std::fmt::Debug,
{
let close = CloseFrame {
code: close_code::NORMAL,
reason: SESSION_COMPLETE_REASON.into(),
};
let close_result = socket_tx.send(Message::Close(Some(close))).await;
drop(close_result);
Ok(())
}
const SESSION_COMPLETE_REASON: &str = "assistant session stream complete";
const INVALID_AFTER_TYPE: &str = "AssistantWatchCursorInvalid";
const LAGGED_TYPE: &str = "AssistantSessionLagged";