use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use everruns::SessionId;
use everruns::ag_ui::{
AgUiError, AgUiOptions, Interrupt, InterruptSource, ResumeEntry, ResumeOutcome, RunAgentInput,
approval_decision, approval_interrupt, question_interrupt, question_outcome, sse_response,
};
use everruns::approval::ApprovalDecision;
use everruns::ask_user::Outcome;
use serde_json::json;
use tokio::sync::broadcast;
use crate::host::{ApiError, Host, NewSession};
pub(crate) fn route(agent: &str) -> String {
format!("/v1/channels/{agent}/ag-ui")
}
fn thread_channel(agent: &str) -> String {
format!("ag-ui:{agent}")
}
pub(crate) async fn run(
State(host): State<Arc<Host>>,
Path(agent): Path<String>,
body: Bytes,
) -> Response {
respond(&host, &agent, &body).await
}
pub(crate) async fn respond(host: &Arc<Host>, agent: &str, body: &[u8]) -> Response {
match start(host, agent, body).await {
Ok(response) => response,
Err(err) => crate::server::Failure::from(err).into_response(),
}
}
async fn start(host: &Arc<Host>, agent: &str, body: &[u8]) -> crate::Result<Response> {
let input: RunAgentInput = serde_json::from_slice(body)
.map_err(|err| ApiError::BadRequest(format!("invalid RunAgentInput: {err}")))?;
if input.thread_id.trim().is_empty() {
return Err(ApiError::BadRequest("threadId is required".into()).into());
}
let (session, created) = thread_session(host, agent, &input.thread_id).await?;
let options = AgUiOptions::new()
.interrupts(HostInterrupts { host: host.clone() })
.seed_history(created);
let run = host.ag_ui(&session, input, options).await?;
Ok(sse_response(run))
}
async fn thread_session(host: &Host, agent: &str, thread: &str) -> crate::Result<(String, bool)> {
if host.app.agent(agent).is_none_or(|entry| entry.sub) {
return Err(ApiError::NotFound(format!("agent {agent}")).into());
}
let channel = thread_channel(agent);
if let Some(session) = host.thread_session(&channel, thread)? {
return Ok((session, false));
}
let session = host
.create_session(NewSession {
agent: Some(agent.to_string()),
metadata: Some(json!({ "channel": "ag-ui", "agent": agent, "thread": thread })),
..NewSession::default()
})
.await?;
host.bind_thread(&channel, thread, &session)?;
Ok((session, true))
}
struct HostInterrupts {
host: Arc<Host>,
}
enum Open {
Approval(Interrupt),
Question(Interrupt, Vec<everruns::ask_user::Question>),
}
impl Open {
fn interrupt(&self) -> &Interrupt {
match self {
Open::Approval(interrupt) | Open::Question(interrupt, _) => interrupt,
}
}
}
enum Answer {
Approval(ApprovalDecision),
Question(Outcome),
}
impl HostInterrupts {
fn open(&self, session_id: SessionId) -> Vec<Open> {
let id = session_id.to_string();
let approvals = self.host.pending_approvals(&id).into_iter().map(|view| {
Open::Approval(approval_interrupt(&everruns::ToolCall {
id: view.tool_call_id,
name: view.tool_name,
arguments: view.arguments,
}))
});
let questions = self.host.pending_questions(&id).into_iter().map(|view| {
Open::Question(
question_interrupt(&view.tool_call_id, &view.questions),
view.questions,
)
});
approvals.chain(questions).collect()
}
}
impl InterruptSource for HostInterrupts {
fn interrupts(&self, session_id: SessionId) -> Vec<Interrupt> {
self.open(session_id)
.iter()
.map(|open| open.interrupt().clone())
.collect()
}
fn subscribe(&self) -> broadcast::Receiver<SessionId> {
self.host.parked_on.subscribe()
}
fn resume(
&self,
session_id: SessionId,
entries: &[ResumeEntry],
) -> Result<ResumeOutcome, AgUiError> {
let open = self.open(session_id);
if open.is_empty() {
return Ok(ResumeOutcome::NothingOpen);
}
let entry_for = |id: &str| entries.iter().find(|entry| entry.interrupt_id == id);
if open
.iter()
.any(|open| entry_for(&open.interrupt().id).is_none())
{
return Ok(ResumeOutcome::StillOpen(
open.iter().map(|open| open.interrupt().clone()).collect(),
));
}
let mut answers = Vec::with_capacity(open.len());
for open in &open {
let id = &open.interrupt().id;
let Some(entry) = entry_for(id) else {
continue;
};
let answer = match open {
Open::Approval(_) => Answer::Approval(approval_decision(entry)?),
Open::Question(_, questions) => {
Answer::Question(question_outcome(entry, id, questions)?)
}
};
answers.push((id.clone(), answer));
}
let id = session_id.to_string();
for (tool_call_id, answer) in answers {
let _ = match answer {
Answer::Approval(decision) => {
self.host.decide_approval(&id, &tool_call_id, decision)
}
Answer::Question(outcome) => self.host.answer_questions(
&id,
Some(&tool_call_id),
outcome.status,
outcome.answers,
),
};
}
Ok(ResumeOutcome::Resumed)
}
}