use std::sync::Arc;
use axum::Router;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Json;
use axum::routing::{get, post};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::agent::context::ConversationContext;
use crate::agent::r#loop::AgentEvent;
use crate::agent::prompt;
use crate::project_cache;
use super::state::AppState;
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/api/health", get(health))
.route("/api/config", get(get_config))
.route("/api/message", post(send_message))
}
#[derive(Serialize)]
struct HealthResponse {
status: &'static str,
version: &'static str,
agent_active: bool,
}
async fn health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
Json(HealthResponse {
status: "ok",
version: env!("CARGO_PKG_VERSION"),
agent_active: state.is_agent_active(),
})
}
#[derive(Serialize)]
struct ConfigResponse {
model: String,
base_url: String,
context_max_tokens: usize,
}
async fn get_config(State(state): State<Arc<AppState>>) -> Json<ConfigResponse> {
Json(ConfigResponse {
model: state.config.model.clone(),
base_url: state.config.base_url.clone(),
context_max_tokens: state.config.context_max_tokens,
})
}
#[derive(Deserialize)]
struct MessageRequest {
message: String,
#[serde(default)]
mode: Option<String>,
}
#[derive(Serialize)]
struct MessageResponse {
accepted: bool,
reason: Option<String>,
}
async fn send_message(
State(state): State<Arc<AppState>>,
Json(req): Json<MessageRequest>,
) -> (StatusCode, Json<MessageResponse>) {
if req.message.trim().is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(MessageResponse {
accepted: false,
reason: Some("Empty message".into()),
}),
);
}
if !state.try_claim_agent() {
return (
StatusCode::CONFLICT,
Json(MessageResponse {
accepted: false,
reason: Some("Agent is already processing a request".into()),
}),
);
}
let context = {
let mut ctx_guard = state.context.lock().await;
if let Some(ctx) = ctx_guard.take() {
ctx
} else {
let working_dir = state.working_dir().await;
let snap = {
let wd = working_dir.clone();
let query = req.message.clone();
tokio::task::spawn_blocking(move || {
project_cache::global()
.get_or_build(&wd)
.snapshot_with_query(&query, 10)
})
.await
.unwrap()
};
let system_prompt = prompt::build_default_prompt(
&snap.map_string,
snap.file_count,
snap.symbol_count,
None,
);
ConversationContext::with_budget(
system_prompt,
state.config.context_max_tokens,
state.config.compaction_threshold,
)
}
};
let (event_tx, event_rx) = mpsc::unbounded_channel::<AgentEvent>();
let cancel = CancellationToken::new();
let bus = state.event_bus.clone();
let config = state.config.clone();
let client = state.client.clone();
let working_dir = state.working_dir().await;
let user_msg = req.message;
let state_clone = Arc::clone(&state);
let lsp_manager = crate::lsp::manager::LspManager::new(working_dir.clone());
tokio::spawn(async move {
let bridge = super::spawn_event_bridge(event_rx, bus, state_clone.clone());
let agent_mode = req.mode.as_deref().unwrap_or("default");
let params = crate::agent::r#loop::AgentParams {
client,
config,
context,
user_msg,
working_dir,
event_tx,
cancel,
lsp_manager,
trust_level: crate::trust::TrustLevel::Full,
approval_gate: crate::agent::approval::ApprovalGate::headless(
state_clone.approve_mode.clone(),
),
images: Vec::new(),
};
if agent_mode == "architect" {
crate::agent::r#loop::run_architect_code_loop(params).await;
} else {
crate::agent::r#loop::run_with_mode(params).await;
}
let _ = bridge.await;
state_clone.set_agent_active(false);
});
(
StatusCode::ACCEPTED,
Json(MessageResponse {
accepted: true,
reason: None,
}),
)
}