use std::collections::BTreeMap;
use axum::Json;
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use salvor_core::{Event, EventEnvelope, PendingCall, RunId, RunStatus, derive_state};
use salvor_runtime::{
RuntimeError, validate_against_schema, validate_extension_input, validate_labels,
};
use serde::Deserialize;
use serde_json::{Value, json};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use uuid::Uuid;
use crate::dispatch::{Disposition, ResumeKind, classify};
use crate::error::ApiError;
use crate::json;
use crate::state::{AppState, BuiltAgent};
#[derive(Debug, Deserialize)]
struct StartRequest {
agent: String,
#[serde(default)]
input: Value,
#[serde(default)]
run_id: Option<String>,
#[serde(default)]
labels: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Default, Deserialize)]
struct ResumeRequest {
#[serde(default)]
input: Option<Value>,
}
#[derive(Debug, Deserialize)]
struct ResolveRequest {
output: Value,
}
#[derive(Debug, Default, Deserialize)]
struct AbandonRequest {
#[serde(default)]
reason: Option<String>,
}
enum DriveVerb {
Start(Value, Option<BTreeMap<String, String>>),
Resume(Value),
Recover,
}
pub async fn start(
State(state): State<AppState>,
body: Bytes,
) -> Result<impl IntoResponse, ApiError> {
let request: StartRequest = parse_body(&body)?;
if let Some(labels) = &request.labels {
validate_labels(labels).map_err(ApiError::BadRequest)?;
}
let registered = state.agent(&request.agent).ok_or_else(|| {
ApiError::UnknownAgent(format!("no agent registered under `{}`", request.agent))
})?;
let built = state
.build_agent(registered.definition)
.await
.map_err(ApiError::BadRequest)?;
let run_id = match &request.run_id {
Some(text) => parse_run_id(text)?,
None => RunId::new(),
};
let log = state.store().read_log(run_id).await.map_err(store_error)?;
if !log.is_empty() {
close_servers(built.servers).await;
return Err(ApiError::RunExists(format!(
"run {} already has recorded history; resume or recover it instead",
run_id.as_uuid()
)));
}
spawn_drive(
state,
run_id,
built,
DriveVerb::Start(request.input, request.labels),
);
Ok((
StatusCode::CREATED,
Json(json!({ "run": run_id.as_uuid().to_string(), "status": "running" })),
))
}
pub async fn list(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
let store = state.store();
let summaries = store.list_runs().await.map_err(store_error)?;
let mut runs = Vec::with_capacity(summaries.len());
for summary in summaries {
let mut entry = json!({
"run": summary.run_id.as_uuid().to_string(),
"event_count": summary.event_count,
"first_recorded_at": rfc3339(summary.first_recorded_at),
"last_recorded_at": rfc3339(summary.last_recorded_at),
});
match store.read_log(summary.run_id).await {
Ok(log) => {
let derived = derive_state(&log);
let step_count = log
.iter()
.filter(|envelope| matches!(envelope.event, Event::ModelCallRequested { .. }))
.count();
let map = entry.as_object_mut().expect("entry is a JSON object");
map.insert("status".to_owned(), json::status(&derived.status));
map.insert(
"usage".to_owned(),
json!({
"input_tokens": derived.usage.input_tokens,
"output_tokens": derived.usage.output_tokens,
}),
);
map.insert("step_count".to_owned(), json!(step_count));
if let Some(hash) = recorded_agent_hash(&log) {
map.insert("agent_def_hash".to_owned(), json!(hash));
}
if let Some(labels) = recorded_labels(&log) {
map.insert("labels".to_owned(), json!(labels));
}
if let Some(driver) = driver_evidence(&state, summary.run_id, &derived.status) {
map.insert("driver".to_owned(), json!(driver));
}
}
Err(error) => {
tracing::warn!(
run = %summary.run_id.as_uuid(),
%error,
"list: run's log could not be read; status and folded fields omitted for it"
);
}
}
runs.push(entry);
}
Ok(Json(json!({ "runs": runs })))
}
pub async fn get(
State(state): State<AppState>,
Path(run_id_text): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
let run_id = parse_run_id(&run_id_text)?;
let log = state.store().read_log(run_id).await.map_err(store_error)?;
if log.is_empty() {
if state.is_run_active(run_id) {
return Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"status": { "state": "running" },
"event_count": 0,
"usage": { "input_tokens": 0, "output_tokens": 0 },
"pending": Value::Null,
"driver": "attached",
})));
}
return Err(unknown_run(run_id));
}
let derived = derive_state(&log);
let mut body = json!({
"run": run_id.as_uuid().to_string(),
"status": json::status(&derived.status),
"event_count": log.len(),
"usage": {
"input_tokens": derived.usage.input_tokens,
"output_tokens": derived.usage.output_tokens,
},
"pending": json::pending(derived.pending_call.as_ref()),
"first_recorded_at": rfc3339(log[0].recorded_at),
"last_recorded_at": rfc3339(log[log.len() - 1].recorded_at),
});
if let Some(driver) = driver_evidence(&state, run_id, &derived.status) {
body["driver"] = json!(driver);
}
Ok(Json(body))
}
pub async fn replay(
State(state): State<AppState>,
Path(run_id_text): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
let run_id = parse_run_id(&run_id_text)?;
let log = state.store().read_log(run_id).await.map_err(store_error)?;
if log.is_empty() {
return Err(unknown_run(run_id));
}
Ok(Json(json::run_state(&derive_state(&log))))
}
pub async fn resume(
State(state): State<AppState>,
Path(run_id_text): Path<String>,
body: Bytes,
) -> Result<impl IntoResponse, ApiError> {
let run_id = parse_run_id(&run_id_text)?;
let request: ResumeRequest = parse_body_or_default(&body)?;
let log = state.store().read_log(run_id).await.map_err(store_error)?;
if log.is_empty() {
return Err(unknown_run(run_id));
}
let derived = derive_state(&log);
match classify(&derived) {
Disposition::Completed(output) => Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"outcome": "completed",
"status": { "state": "completed", "output": output },
}))
.into_response()),
Disposition::Failed(error) => Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"outcome": "failed",
"status": { "state": "failed", "error": error },
}))
.into_response()),
Disposition::Abandoned { .. } => Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"outcome": "abandoned",
"status": json::status(&derived.status),
}))
.into_response()),
Disposition::NotStarted => Err(unknown_run(run_id)),
Disposition::Reconcile(pending) => Err(ApiError::NeedsReconciliation {
message: format!(
"run {} needs reconciliation: a write was recorded but never completed, so it \
may or may not have taken effect. Verify externally, then resolve it",
run_id.as_uuid()
),
intent: reconcile_intent(&log, &pending),
}),
Disposition::Resume(kind) => {
let input = request.input.ok_or_else(|| {
ApiError::BadRequest(
"this run is parked awaiting input; send a body of {\"input\": <json>}"
.to_owned(),
)
})?;
match kind {
ResumeKind::Suspension => {
if let RunStatus::Suspended { input_schema, .. } = &derived.status {
validate_against_schema(&input, input_schema)
.map_err(ApiError::BadRequest)?;
}
}
ResumeKind::Budget => {
validate_extension_input(&input).map_err(ApiError::BadRequest)?;
}
}
if crate::graph::is_graph_run(&log) {
return crate::graph::drive_resume(state, run_id, &log, Some(input)).await;
}
let built = rebuild_agent(&state, &log).await?;
spawn_drive(state, run_id, built, DriveVerb::Resume(input));
Ok(driving(run_id).into_response())
}
Disposition::Recover => {
if request.input.is_some() {
tracing::warn!(
run_id = %run_id.as_uuid(),
"this run crashed mid-step; the resume input is ignored when recovering"
);
}
if crate::graph::is_graph_run(&log) {
return crate::graph::drive_resume(state, run_id, &log, None).await;
}
let built = rebuild_agent(&state, &log).await?;
spawn_drive(state, run_id, built, DriveVerb::Recover);
Ok(driving(run_id).into_response())
}
}
}
pub async fn resolve(
State(state): State<AppState>,
Path(run_id_text): Path<String>,
body: Bytes,
) -> Result<impl IntoResponse, ApiError> {
let run_id = parse_run_id(&run_id_text)?;
let request: ResolveRequest = parse_body(&body)?;
let log = state.store().read_log(run_id).await.map_err(store_error)?;
if log.is_empty() {
return Err(unknown_run(run_id));
}
match state.runtime().resolve(run_id, request.output).await {
Ok(_) => {
let log = state.store().read_log(run_id).await.map_err(store_error)?;
let derived = derive_state(&log);
Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"resolved": true,
"status": json::status(&derived.status),
})))
}
Err(RuntimeError::NotReconcilable { status, .. }) => Err(ApiError::WrongState(format!(
"run {} does not need reconciliation (status: {status}); there is no dangling write \
to resolve",
run_id.as_uuid()
))),
Err(error) => Err(ApiError::Internal(error.to_string())),
}
}
pub async fn abandon(
State(state): State<AppState>,
Path(run_id_text): Path<String>,
body: Bytes,
) -> Result<impl IntoResponse, ApiError> {
let run_id = parse_run_id(&run_id_text)?;
let request: AbandonRequest = parse_body_or_default(&body)?;
match state.runtime().abandon(run_id, request.reason).await {
Ok(_) => {
let log = state.store().read_log(run_id).await.map_err(store_error)?;
let derived = derive_state(&log);
let appended_seq = log.last().map(|env| env.seq.get());
Ok(Json(json!({
"run": run_id.as_uuid().to_string(),
"abandoned": true,
"appended_seq": appended_seq,
"status": json::status(&derived.status),
})))
}
Err(RuntimeError::UnknownRun { .. }) => Err(unknown_run(run_id)),
Err(RuntimeError::AlreadyTerminal { status, .. }) => Err(ApiError::WrongState(format!(
"run {} is already terminal (status: {status}); there is nothing left to abandon",
run_id.as_uuid()
))),
Err(error) => Err(ApiError::Internal(error.to_string())),
}
}
fn spawn_drive(state: AppState, run_id: RunId, built: BuiltAgent, verb: DriveVerb) {
state.begin_run(run_id);
let task_state = state.clone();
let handle = tokio::spawn(async move {
let BuiltAgent { agent, servers } = built;
let mut runtime = task_state
.runtime()
.with_record_prompts(agent.record_prompts());
let result = match verb {
DriveVerb::Start(input, labels) => {
if let Some(labels) = labels {
runtime = runtime.with_labels(labels);
} else if let Some(labels) = agent.labels() {
runtime = runtime.with_labels(labels.clone());
}
runtime.start_with_id(&agent, run_id, input).await
}
DriveVerb::Resume(input) => runtime.resume(&agent, run_id, input).await,
DriveVerb::Recover => runtime.recover(&agent, run_id).await,
};
close_servers(servers).await;
if let Err(error) = result {
tracing::error!(run_id = %run_id.as_uuid(), %error, "run drive ended with an error");
}
task_state.end_run(run_id);
});
state.set_handle(run_id, handle);
}
async fn rebuild_agent(state: &AppState, log: &[EventEnvelope]) -> Result<BuiltAgent, ApiError> {
let hash = recorded_agent_hash(log)
.ok_or_else(|| ApiError::Internal("run log has no RunStarted event".to_owned()))?;
let registered = state.agent(&hash).ok_or_else(|| {
ApiError::UnknownAgent(format!(
"the agent `{hash}` this run started under is not registered on this server; register \
its definition, then resume"
))
})?;
state
.build_agent(registered.definition)
.await
.map_err(ApiError::BadRequest)
}
fn driver_evidence(state: &AppState, run_id: RunId, status: &RunStatus) -> Option<&'static str> {
if matches!(
status,
RunStatus::Completed { .. } | RunStatus::Failed { .. } | RunStatus::Abandoned { .. }
) {
return None;
}
if state.is_run_active(run_id) || state.client_run_driver_live(run_id) {
Some("attached")
} else {
Some("none")
}
}
fn recorded_agent_hash(log: &[EventEnvelope]) -> Option<String> {
log.iter().find_map(|envelope| match &envelope.event {
Event::RunStarted { agent_def_hash, .. } => Some(agent_def_hash.clone()),
_ => None,
})
}
fn recorded_labels(log: &[EventEnvelope]) -> Option<BTreeMap<String, String>> {
log.iter()
.find_map(|envelope| match &envelope.event {
Event::RunStarted { labels, .. } => Some(labels.clone()),
_ => None,
})
.flatten()
.filter(|labels: &BTreeMap<String, String>| !labels.is_empty())
}
fn reconcile_intent(log: &[EventEnvelope], pending: &PendingCall) -> Value {
let mut intent = json::pending(Some(pending));
if let PendingCall::Tool { seq, .. } = pending
&& let Some(envelope) = log.iter().find(|envelope| envelope.seq == *seq)
{
intent["recorded_at"] = json!(rfc3339(envelope.recorded_at));
}
intent
}
fn driving(run_id: RunId) -> impl IntoResponse {
(
StatusCode::ACCEPTED,
Json(json!({
"run": run_id.as_uuid().to_string(),
"status": "running",
"outcome": "driving",
})),
)
}
async fn close_servers(servers: Vec<salvor_tools::mcp::McpServer>) {
for server in servers {
if let Err(error) = server.close().await {
tracing::warn!(%error, "MCP session did not close cleanly");
}
}
}
fn parse_body<T: for<'de> Deserialize<'de>>(body: &Bytes) -> Result<T, ApiError> {
serde_json::from_slice(body)
.map_err(|error| ApiError::BadRequest(format!("request body is not valid JSON: {error}")))
}
fn parse_body_or_default<T: Default + for<'de> Deserialize<'de>>(
body: &Bytes,
) -> Result<T, ApiError> {
if body.is_empty() {
return Ok(T::default());
}
parse_body(body)
}
fn parse_run_id(text: &str) -> Result<RunId, ApiError> {
Uuid::parse_str(text).map(RunId::from_uuid).map_err(|_| {
ApiError::BadRequest(format!("`{text}` is not a valid run id (expected a UUID)"))
})
}
fn unknown_run(run_id: RunId) -> ApiError {
ApiError::UnknownRun(format!("no run {} in this store", run_id.as_uuid()))
}
fn store_error(error: salvor_store::StoreError) -> ApiError {
ApiError::Internal(format!("store: {error}"))
}
fn rfc3339(ts: OffsetDateTime) -> String {
ts.format(&Rfc3339).unwrap_or_default()
}