mod auth;
mod handlers;
mod rate_limit;
pub use auth::{AuthConfig, JwtConfig};
pub use handlers::map_agent_event;
pub use rate_limit::RateLimiter;
use auth::{auth_config_from_env, auth_middleware};
use handlers::{
agui_run, create_session, delete_session, fork_session, get_session, health, list_sessions,
list_slash_commands, list_tools, metrics_handler, openapi_spec, patch_session, run_agent,
send_session_message, session_clear_goal, session_events, session_interrupt,
session_plan_confirm, session_plan_reject, session_set_goal,
};
use rate_limit::{metrics_middleware, rate_limit_middleware, rate_limiter_from_env};
use axum::{
routing::{get, post},
Router,
};
use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use crate::config::Config;
use crate::llm::LlmProvider;
use crate::runtime::AgentRuntime;
use crate::tools::plan_mode::PlanApprovalGate;
use crate::tools::ToolRegistry;
#[derive(Default)]
pub struct Metrics {
pub requests_total: AtomicU64,
pub requests_active: AtomicU64,
pub agent_runs_total: AtomicU64,
pub agent_runs_success: AtomicU64,
pub agent_runs_failed: AtomicU64,
pub tokens_prompt_total: AtomicU64,
pub tokens_completion_total: AtomicU64,
pub agent_steps_total: AtomicU64,
}
pub struct SessionState {
pub id: String,
pub created_at: String,
pub title: Option<String>,
pub runtime: Arc<tokio::sync::Mutex<AgentRuntime>>,
pub plan_approval_gate: Arc<PlanApprovalGate>,
pub interrupt_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
pub struct SessionInfo {
pub id: String,
pub created_at: String,
pub message_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
pub struct CreateSessionRequest {
pub system_prompt: Option<String>,
}
#[derive(serde::Serialize, Debug)]
pub struct CreateSessionResponse {
pub id: String,
pub created_at: String,
}
#[derive(serde::Deserialize, Debug)]
pub struct SessionMessageRequest {
pub content: String,
}
#[derive(serde::Serialize, Debug)]
pub struct SessionMessageResponse {
pub role: String,
pub content: String,
}
#[derive(serde::Serialize, Debug)]
pub struct SessionDetailResponse {
pub id: String,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub messages: Vec<serde_json::Value>,
pub todos: Vec<crate::tools::todo::TodoItem>,
pub status: String,
pub pending_plan: Option<String>,
pub goal: Option<crate::runtime::GoalState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_prompt: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
pub struct SetGoalRequest {
pub condition: String,
pub max_turns: Option<u32>,
}
#[derive(serde::Serialize, Debug)]
pub struct GoalResponse {
pub status: String,
}
#[derive(Clone, serde::Serialize, Debug)]
pub struct SlashCommandInfo {
pub name: String,
pub description: String,
pub source: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub argument_hint: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SseContentBlock {
Text { text: String },
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SseEvent {
Message {
role: String,
content: Vec<SseContentBlock>,
},
PartialMessage { text: String, step: usize },
ToolCall { name: String, step: usize },
ToolResult { name: String, success: bool },
Done {
finish_reason: String,
total_steps: usize,
},
Error { message: String },
PlanProposed { plan: String },
GoalContinuing { reason: String, turns: u32 },
GoalAchieved { condition: String, turns: u32 },
ToolProgress {
tool_use_id: String,
tool_name: String,
elapsed_ms: u64,
},
}
#[derive(Clone)]
pub struct AppState {
pub tools: Vec<ToolInfo>,
pub tool_registry: ToolRegistry,
pub config: Config,
pub provider: Arc<dyn LlmProvider>,
pub sessions: Arc<RwLock<HashMap<String, SessionState>>>,
pub event_channels: Arc<RwLock<HashMap<String, broadcast::Sender<SseEvent>>>>,
pub metrics: Arc<Metrics>,
pub slash_commands: Arc<Vec<SlashCommandInfo>>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(serde::Deserialize, Debug)]
pub struct RunRequest {
pub goal: String,
pub max_steps: Option<u32>,
pub system_prompt: Option<String>,
}
#[derive(serde::Serialize, Debug)]
pub struct RunResponse {
pub status: String,
pub finish_reason: String,
pub messages: Vec<serde_json::Value>,
pub usage: UsageInfo,
}
#[derive(serde::Serialize, Debug)]
pub struct UsageInfo {
pub total_steps: u32,
pub total_tokens: u64,
}
#[derive(serde::Serialize, Debug)]
pub struct ErrorResponse {
pub status: String,
pub error: String,
}
#[derive(serde::Deserialize, Debug, Default)]
pub struct ListSessionsQuery {
pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub fn build_router(state: AppState) -> Router {
build_router_with_auth(state, auth_config_from_env())
}
pub fn build_router_with_auth(state: AppState, auth: AuthConfig) -> Router {
build_router_with_auth_and_rate_limit(state, auth, rate_limiter_from_env())
}
pub fn build_router_with_auth_and_rate_limit(
state: AppState,
auth: AuthConfig,
limiter: RateLimiter,
) -> Router {
Router::new()
.route("/health", get(health))
.route("/tools", get(list_tools))
.route("/run", post(run_agent))
.route("/sessions", post(create_session))
.route("/sessions", get(list_sessions))
.route("/sessions/{id}", get(get_session))
.route("/sessions/{id}", axum::routing::delete(delete_session))
.route("/sessions/{id}", axum::routing::patch(patch_session))
.route("/sessions/{id}/messages", post(send_session_message))
.route("/sessions/{id}/events", get(session_events))
.route("/sessions/{id}/plan/confirm", post(session_plan_confirm))
.route("/sessions/{id}/plan/reject", post(session_plan_reject))
.route("/sessions/{id}/goal", post(session_set_goal))
.route(
"/sessions/{id}/goal",
axum::routing::delete(session_clear_goal),
)
.route("/sessions/{id}/interrupt", post(session_interrupt))
.route("/sessions/{id}/fork", post(fork_session))
.route("/slash-commands", get(list_slash_commands))
.route("/agui", post(agui_run))
.route("/openapi.json", get(openapi_spec))
.route("/metrics", get(metrics_handler))
.layer(axum::middleware::from_fn_with_state(
state.metrics.clone(),
metrics_middleware,
))
.layer(axum::middleware::from_fn_with_state(
limiter,
rate_limit_middleware,
))
.layer(axum::middleware::from_fn_with_state(auth, auth_middleware))
.with_state(Arc::new(state))
}
pub fn build_openapi_spec() -> serde_json::Value {
serde_json::json!({
"openapi": "3.0.3",
"info": {
"title": "Recursive Agent API",
"version": "0.4.0",
"description": "HTTP API for the Recursive coding agent"
},
"paths": {
"/health": {
"get": {
"summary": "Health check",
"description": "Returns 'ok' if the server is running.",
"responses": {
"200": {
"description": "Server is healthy",
"content": {
"text/plain": {
"schema": { "type": "string", "example": "ok" }
}
}
}
}
}
},
"/tools": {
"get": {
"summary": "List registered tools",
"description": "Returns the JSON array of tools available to the agent.",
"responses": {
"200": {
"description": "Array of tool descriptors",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/ToolInfo" }
}
}
}
}
}
}
},
"/run": {
"post": {
"summary": "Run the agent",
"description": "Execute the agent with a goal and return the outcome.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/RunRequest" }
}
}
},
"responses": {
"200": {
"description": "Agent completed successfully",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/RunResponse" }
}
}
},
"400": {
"description": "Invalid request (e.g. empty goal)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"422": { "description": "Request body failed deserialization" },
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
}
}
},
"/sessions": {
"get": {
"summary": "List sessions",
"description": "Returns all active sessions.",
"responses": {
"200": {
"description": "Array of session info objects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/SessionInfo" }
}
}
}
}
}
},
"post": {
"summary": "Create a session",
"description": "Create a new multi-turn conversation session.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateSessionRequest" }
}
}
},
"responses": {
"201": {
"description": "Session created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateSessionResponse" }
}
}
}
}
}
},
"/sessions/{id}": {
"get": {
"summary": "Get session detail",
"description": "Returns session metadata and full message transcript.",
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "string" }
}],
"responses": {
"200": {
"description": "Session detail with messages",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SessionDetailResponse" }
}
}
},
"404": { "description": "Session not found" }
}
},
"delete": {
"summary": "Delete a session",
"description": "Remove a session and its transcript.",
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "string" }
}],
"responses": {
"204": { "description": "Session deleted" },
"404": { "description": "Session not found" }
}
}
},
"/sessions/{id}/messages": {
"post": {
"summary": "Send a message",
"description": "Send a user message in a session and get the assistant response.",
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "string" }
}],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SessionMessageRequest" }
}
}
},
"responses": {
"200": {
"description": "Assistant response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SessionMessageResponse" }
}
}
},
"404": {
"description": "Session not found",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
}
}
},
"/sessions/{id}/events": {
"get": {
"summary": "Subscribe to session events",
"description": "SSE stream of real-time agent events for a session.",
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "string" }
}],
"responses": {
"200": {
"description": "SSE event stream",
"content": {
"text/event-stream": {
"schema": { "type": "string" }
}
}
},
"404": { "description": "Session not found" }
}
}
},
"/agui": {
"post": {
"summary": "Run an AG-UI agent",
"description": "Drive a recursive agent run via the AG-UI protocol \
(https://docs.ag-ui.com). Body is an AG-UI RunAgentInput; the \
response is an SSE stream of AG-UI events (RunStarted, \
TextMessageStart/Content/End, ToolCall*, RunFinished, ...).",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "type": "object" },
"description": "AG-UI RunAgentInput payload"
}
}
},
"responses": {
"200": {
"description": "AG-UI SSE event stream",
"content": {
"text/event-stream": {
"schema": { "type": "string" }
}
}
},
"400": { "description": "Invalid AG-UI RunAgentInput" }
}
}
},
"/openapi.json": {
"get": {
"summary": "OpenAPI specification",
"description": "Returns this OpenAPI 3.0.3 spec as JSON.",
"responses": {
"200": {
"description": "OpenAPI spec document",
"content": {
"application/json": {
"schema": { "type": "object" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"ToolInfo": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"parameters": { "type": "object" }
},
"required": ["name", "description", "parameters"]
},
"RunRequest": {
"type": "object",
"properties": {
"goal": { "type": "string" },
"max_steps": { "type": "integer", "nullable": true },
"system_prompt": { "type": "string", "nullable": true }
},
"required": ["goal"]
},
"RunResponse": {
"type": "object",
"properties": {
"status": { "type": "string" },
"finish_reason": { "type": "string" },
"messages": { "type": "array", "items": { "type": "object" } },
"usage": { "$ref": "#/components/schemas/UsageInfo" }
},
"required": ["status", "finish_reason", "messages", "usage"]
},
"UsageInfo": {
"type": "object",
"properties": {
"total_steps": { "type": "integer" },
"total_tokens": { "type": "integer" }
},
"required": ["total_steps", "total_tokens"]
},
"ErrorResponse": {
"type": "object",
"properties": {
"status": { "type": "string" },
"error": { "type": "string" }
},
"required": ["status", "error"]
},
"CreateSessionRequest": {
"type": "object",
"properties": {
"system_prompt": { "type": "string", "nullable": true }
}
},
"CreateSessionResponse": {
"type": "object",
"properties": {
"id": { "type": "string" },
"created_at": { "type": "string" }
},
"required": ["id", "created_at"]
},
"SessionInfo": {
"type": "object",
"properties": {
"id": { "type": "string" },
"created_at": { "type": "string" },
"message_count": { "type": "integer" }
},
"required": ["id", "created_at", "message_count"]
},
"SessionDetailResponse": {
"type": "object",
"properties": {
"id": { "type": "string" },
"created_at": { "type": "string" },
"messages": { "type": "array", "items": { "type": "object" } }
},
"required": ["id", "created_at", "messages"]
},
"SessionMessageRequest": {
"type": "object",
"properties": {
"content": { "type": "string" }
},
"required": ["content"]
},
"SessionMessageResponse": {
"type": "object",
"properties": {
"role": { "type": "string" },
"content": { "type": "string" }
},
"required": ["role", "content"]
}
}
}
})
}