Skip to main content

codetether_agent/server/
mod.rs

1//! HTTP Server
2//!
3//! Main API server for the CodeTether Agent
4
5use crate::a2a;
6use crate::cli::ServeArgs;
7use crate::config::Config;
8use anyhow::Result;
9use axum::{
10    extract::{Query, State},
11    http::StatusCode,
12    response::Json,
13    routing::{get, post},
14    Router,
15};
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use tower_http::cors::{Any, CorsLayer};
19use tower_http::trace::TraceLayer;
20
21/// Server state shared across handlers
22#[derive(Clone)]
23pub struct AppState {
24    pub config: Arc<Config>,
25}
26
27/// Start the HTTP server
28pub async fn serve(args: ServeArgs) -> Result<()> {
29    let config = Config::load().await?;
30    let state = AppState {
31        config: Arc::new(config),
32    };
33
34    let addr = format!("{}:{}", args.hostname, args.port);
35
36    // Build the agent card
37    let agent_card = a2a::server::A2AServer::default_card(&format!("http://{}", addr));
38    let a2a_server = a2a::server::A2AServer::new(agent_card);
39
40    // Build A2A router separately
41    let a2a_router = a2a_server.router();
42
43    let app = Router::new()
44        // Health check
45        .route("/health", get(health))
46        // API routes
47        .route("/api/version", get(get_version))
48        .route("/api/session", get(list_sessions).post(create_session))
49        .route("/api/session/:id", get(get_session))
50        .route("/api/session/:id/prompt", post(prompt_session))
51        .route("/api/config", get(get_config))
52        .route("/api/provider", get(list_providers))
53        .route("/api/agent", get(list_agents))
54        .with_state(state)
55        // A2A routes (nested to work with different state type)
56        .nest("/a2a", a2a_router)
57        // Middleware
58        .layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any))
59        .layer(TraceLayer::new_for_http());
60
61    let listener = tokio::net::TcpListener::bind(&addr).await?;
62    tracing::info!("Server listening on http://{}", addr);
63
64    axum::serve(listener, app).await?;
65
66    Ok(())
67}
68
69/// Health check response
70async fn health() -> &'static str {
71    "ok"
72}
73
74/// Version info
75#[derive(Serialize)]
76struct VersionInfo {
77    version: &'static str,
78    name: &'static str,
79}
80
81async fn get_version() -> Json<VersionInfo> {
82    Json(VersionInfo {
83        version: env!("CARGO_PKG_VERSION"),
84        name: env!("CARGO_PKG_NAME"),
85    })
86}
87
88/// List sessions
89#[derive(Deserialize)]
90struct ListSessionsQuery {
91    limit: Option<usize>,
92}
93
94async fn list_sessions(
95    Query(query): Query<ListSessionsQuery>,
96) -> Result<Json<Vec<crate::session::SessionSummary>>, (StatusCode, String)> {
97    let sessions = crate::session::list_sessions()
98        .await
99        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
100
101    let limit = query.limit.unwrap_or(50);
102    Ok(Json(sessions.into_iter().take(limit).collect()))
103}
104
105/// Create a new session
106#[derive(Deserialize)]
107struct CreateSessionRequest {
108    title: Option<String>,
109    agent: Option<String>,
110}
111
112async fn create_session(
113    Json(req): Json<CreateSessionRequest>,
114) -> Result<Json<crate::session::Session>, (StatusCode, String)> {
115    let mut session = crate::session::Session::new()
116        .await
117        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
118
119    session.title = req.title;
120    if let Some(agent) = req.agent {
121        session.agent = agent;
122    }
123
124    session
125        .save()
126        .await
127        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
128
129    Ok(Json(session))
130}
131
132/// Get a session by ID
133async fn get_session(
134    axum::extract::Path(id): axum::extract::Path<String>,
135) -> Result<Json<crate::session::Session>, (StatusCode, String)> {
136    let session = crate::session::Session::load(&id)
137        .await
138        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
139
140    Ok(Json(session))
141}
142
143/// Prompt a session
144#[derive(Deserialize)]
145struct PromptRequest {
146    message: String,
147}
148
149async fn prompt_session(
150    axum::extract::Path(id): axum::extract::Path<String>,
151    Json(req): Json<PromptRequest>,
152) -> Result<Json<crate::session::SessionResult>, (StatusCode, String)> {
153    // Validate the message is not empty
154    if req.message.trim().is_empty() {
155        return Err((
156            StatusCode::BAD_REQUEST,
157            "Message cannot be empty".to_string(),
158        ));
159    }
160    
161    // Log the prompt request (uses the message field)
162    tracing::info!(
163        session_id = %id,
164        message_len = req.message.len(),
165        "Received prompt request"
166    );
167    
168    // TODO: Implement actual prompting
169    Err((
170        StatusCode::NOT_IMPLEMENTED,
171        "Prompt execution not yet implemented".to_string(),
172    ))
173}
174
175/// Get configuration
176async fn get_config(State(state): State<AppState>) -> Json<Config> {
177    Json((*state.config).clone())
178}
179
180/// List providers
181async fn list_providers() -> Json<Vec<String>> {
182    Json(vec![
183        "openai".to_string(),
184        "anthropic".to_string(),
185        "google".to_string(),
186    ])
187}
188
189/// List agents
190async fn list_agents() -> Json<Vec<crate::agent::AgentInfo>> {
191    let registry = crate::agent::AgentRegistry::with_builtins();
192    Json(registry.list().into_iter().cloned().collect())
193}