use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::Json;
use serde::Serialize;
use crate::error::AppError;
use crate::routes::{paginate, PageParams};
use crate::server::AppState;
pub(crate) async fn handle_health(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
Json(serde_json::json!({
"status": "ok",
"version": env!("CARGO_PKG_VERSION"),
}))
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct ComponentStatus {
pub healthy: bool,
pub detail: Option<String>,
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct MemoryHealth {
pub enabled: bool,
pub index_size: usize,
pub total_entries: usize,
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct AgentHealth {
pub active_count: usize,
pub total_forked: u64,
pub total_completed: u64,
pub total_failed: u64,
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct ComponentHealth {
pub state_store: ComponentStatus,
pub event_bus: ComponentStatus,
pub memory: MemoryHealth,
pub agents: AgentHealth,
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct StatusResponse {
service: String,
status: String,
version: String,
channels: Vec<String>,
uptime: String,
components: Option<ComponentHealth>,
}
pub(crate) async fn handle_status(state: State<Arc<AppState>>) -> Json<StatusResponse> {
let uptime = state.start_time.elapsed();
let uptime_str = format!(
"{}h {}m {}s",
uptime.as_secs() / 3600,
(uptime.as_secs() % 3600) / 60,
uptime.as_secs() % 60
);
let state_store_healthy = state.kernel.state.workspace_path().exists();
let event_bus_healthy = true;
let (mem_index_size, mem_total) = state.kernel.agents.memory_stats().await;
let memory_health = MemoryHealth {
enabled: true,
index_size: mem_index_size,
total_entries: mem_total,
};
let active_count = state
.kernel
.agents
.list()
.await
.map(|agents| {
agents
.iter()
.filter(|a| {
matches!(
a.status,
oxios_kernel::AgentStatus::Running
| oxios_kernel::AgentStatus::Starting
| oxios_kernel::AgentStatus::Idle
)
})
.count()
})
.unwrap_or(0);
let (total_forked, total_completed, total_failed) = parse_agent_metrics();
let agent_health = AgentHealth {
active_count,
total_forked,
total_completed,
total_failed,
};
let components = Some(ComponentHealth {
state_store: ComponentStatus {
healthy: state_store_healthy,
detail: if state_store_healthy {
None
} else {
Some("base path not found".to_string())
},
},
event_bus: ComponentStatus {
healthy: event_bus_healthy,
detail: None,
},
memory: memory_health,
agents: agent_health,
});
Json(StatusResponse {
service: "oxios".into(),
status: "running".into(),
version: env!("CARGO_PKG_VERSION").into(),
channels: vec!["web".into()],
uptime: uptime_str,
components,
})
}
fn parse_agent_metrics() -> (u64, u64, u64) {
let export = oxios_kernel::metrics::registry().export();
let mut forked = 0u64;
let mut completed = 0u64;
let mut failed = 0u64;
for line in export.lines() {
if line.starts_with("oxios_agents_forked_total ") {
forked = line
.rsplit(' ')
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
} else if line.starts_with("oxios_agents_completed_total ") {
completed = line
.rsplit(' ')
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
} else if line.starts_with("oxios_agents_failed_total ") {
failed = line
.rsplit(' ')
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
}
(forked, completed, failed)
}
#[derive(Debug, Serialize, Clone)]
pub(crate) struct AgentSummary {
id: String,
name: String,
status: String,
created_at: String,
seed_id: Option<String>,
}
pub(crate) async fn handle_agents_list(
state: State<Arc<AppState>>,
Query(params): Query<PageParams>,
) -> Json<serde_json::Value> {
match state.kernel.agents.list().await {
Ok(agents) => {
let items: Vec<AgentSummary> = agents
.into_iter()
.map(|a| AgentSummary {
id: a.id.to_string(),
name: a.name,
status: format!("{:?}", a.status),
created_at: a.created_at.to_rfc3339(),
seed_id: a.seed_id.map(|s| s.to_string()),
})
.collect();
Json(paginate(&items, ¶ms))
}
Err(e) => {
tracing::error!(error = %e, "Failed to list agents");
Json(paginate(&Vec::<AgentSummary>::new(), ¶ms))
}
}
}
pub(crate) async fn handle_agent_kill(
state: State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<(), AppError> {
tracing::info!(agent_id = %id, "Kill agent requested");
state.kernel.agents.kill(&id).await.map_err(|e| {
tracing::warn!(error = %e, "Agent not found");
AppError::NotFound("agent not found".into())
})
}
pub(crate) async fn handle_config_get(
state: State<Arc<AppState>>,
) -> Result<Json<serde_json::Value>, AppError> {
let config = state.config.read();
match serde_json::to_value(&*config) {
Ok(json) => Ok(Json(json)),
Err(e) => {
tracing::error!(error = %e, "Failed to serialize config");
Err(AppError::Internal("failed to serialize config".into()))
}
}
}
pub(crate) async fn handle_config_put(
state: State<Arc<AppState>>,
Json(body): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, AppError> {
tracing::info!("Config update requested");
let updated: oxios_kernel::OxiosConfig = match serde_json::from_value(body.clone()) {
Ok(cfg) => cfg,
Err(e) => {
tracing::warn!(error = %e, "Invalid config shape");
return Err(AppError::BadRequest(format!("Invalid config: {e}")));
}
};
let content = toml::to_string_pretty(&updated)
.map_err(|e: toml::ser::Error| AppError::Internal(e.to_string()))?;
if let Err(e) = tokio::fs::write(&state.config_path, content).await {
tracing::error!(error = %e, "Failed to persist config");
return Err(AppError::Internal(e.to_string()));
}
tracing::info!(path = %state.config_path.display(), "Config persisted");
*state.config.write() = updated;
tracing::info!("Config hot-reloaded from {}", state.config_path.display());
Ok(Json(body))
}