use axum::extract::{Path, Query, State};
use axum::Json;
use serde::{Deserialize, Serialize};
use super::error::UhpFailure;
use super::state::UhpState;
use super::tasks::Response;
const DEFAULT_LIMIT: usize = 20;
#[derive(Debug, Clone, Deserialize)]
pub struct Paging {
#[serde(default)]
pub limit: Option<usize>,
#[serde(default)]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Session {
pub id: String,
pub object: &'static str,
pub turn_count: usize,
pub created_at: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct SessionList {
pub sessions: Vec<Session>,
pub has_more: bool,
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Turn {
pub id: String,
pub status: String,
pub created_at: u64,
pub model: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TurnList {
pub session_id: String,
pub turns: Vec<Turn>,
}
impl From<&Response> for Turn {
fn from(r: &Response) -> Self {
Self {
id: r.id.clone(),
status: r.status.to_owned(),
created_at: r.created_at,
model: r.model.clone(),
}
}
}
fn summarise(id: String, turns: &[Response]) -> Session {
Session {
object: "session",
turn_count: turns.len(),
created_at: turns.first().map_or(0, |t| t.created_at),
id,
}
}
pub async fn list(
State(state): State<UhpState>,
Query(paging): Query<Paging>,
) -> Json<SessionList> {
let limit = paging.limit.unwrap_or(DEFAULT_LIMIT).max(1);
let mut ids = state.store.session_ids();
ids.sort();
let start = paging
.cursor
.as_ref()
.map_or(0, |c| ids.iter().position(|i| i == c).map_or(0, |p| p + 1));
let page: Vec<Session> = ids
.iter()
.skip(start)
.take(limit)
.map(|id| {
let turns = state.store.session_turns(id).unwrap_or_default();
summarise(id.clone(), &turns)
})
.collect();
let has_more = start + page.len() < ids.len();
Json(SessionList {
next_cursor: has_more
.then(|| page.last().map(|s| s.id.clone()))
.flatten(),
has_more,
sessions: page,
})
}
pub async fn get(
State(state): State<UhpState>,
Path(id): Path<String>,
) -> Result<Json<Session>, UhpFailure> {
let turns = state.store.session_turns(&id).ok_or_else(|| {
UhpFailure::not_found("session_not_found", "no session with that id exists")
})?;
Ok(Json(summarise(id, &turns)))
}
pub async fn turns(
State(state): State<UhpState>,
Path(id): Path<String>,
) -> Result<Json<TurnList>, UhpFailure> {
let responses = state.store.session_turns(&id).ok_or_else(|| {
UhpFailure::not_found("session_not_found", "no session with that id exists")
})?;
Ok(Json(TurnList {
session_id: id,
turns: responses.iter().map(Turn::from).collect(),
}))
}
pub async fn delete(
State(state): State<UhpState>,
Path(id): Path<String>,
) -> Result<Json<Deleted>, UhpFailure> {
for running in state.store.running_in(&id) {
state.store.cancel(&running);
}
if !state.store.delete_session(&id) {
return Err(UhpFailure::not_found(
"session_not_found",
"no session with that id exists",
));
}
state.shares.revoke(&id);
super::files::remove_workdir(&id);
Ok(Json(Deleted {
id,
object: "session.deleted",
deleted: true,
}))
}
#[derive(Debug, Clone, Serialize)]
pub struct Deleted {
pub id: String,
pub object: &'static str,
pub deleted: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Map;
fn response(id: &str, created_at: u64) -> Response {
Response {
id: id.to_owned(),
object: "response",
created_at,
status: "completed",
error: None,
previous_response_id: None,
model: "m".to_owned(),
output: Vec::new(),
store: true,
usage: None,
metadata: Map::new(),
}
}
#[test]
fn a_session_summarises_its_turns_oldest_first() {
let turns = [response("resp_a", 10), response("resp_b", 20)];
let s = summarise("sess_1".to_owned(), &turns);
assert_eq!(s.turn_count, 2);
assert_eq!(
s.created_at, 10,
"a session is as old as its first turn, not its last"
);
}
#[test]
fn an_empty_session_still_summarises() {
let s = summarise("sess_empty".to_owned(), &[]);
assert_eq!(s.turn_count, 0);
assert_eq!(s.created_at, 0);
}
#[test]
fn a_turn_carries_the_shape_the_contract_names() {
let t = Turn::from(&response("resp_a", 7));
assert_eq!(t.id, "resp_a");
assert_eq!(t.status, "completed", "at least id and status are required");
assert_eq!(t.created_at, 7);
}
}