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    Router,
11    extract::{Query, State},
12    http::StatusCode,
13    response::Json,
14    routing::{get, post},
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(
59            CorsLayer::new()
60                .allow_origin(Any)
61                .allow_methods(Any)
62                .allow_headers(Any),
63        )
64        .layer(TraceLayer::new_for_http());
65
66    let listener = tokio::net::TcpListener::bind(&addr).await?;
67    tracing::info!("Server listening on http://{}", addr);
68
69    axum::serve(listener, app).await?;
70
71    Ok(())
72}
73
74/// Health check response
75async fn health() -> &'static str {
76    "ok"
77}
78
79/// Version info
80#[derive(Serialize)]
81struct VersionInfo {
82    version: &'static str,
83    name: &'static str,
84}
85
86async fn get_version() -> Json<VersionInfo> {
87    Json(VersionInfo {
88        version: env!("CARGO_PKG_VERSION"),
89        name: env!("CARGO_PKG_NAME"),
90    })
91}
92
93/// List sessions
94#[derive(Deserialize)]
95struct ListSessionsQuery {
96    limit: Option<usize>,
97}
98
99async fn list_sessions(
100    Query(query): Query<ListSessionsQuery>,
101) -> Result<Json<Vec<crate::session::SessionSummary>>, (StatusCode, String)> {
102    let sessions = crate::session::list_sessions()
103        .await
104        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
105
106    let limit = query.limit.unwrap_or(50);
107    Ok(Json(sessions.into_iter().take(limit).collect()))
108}
109
110/// Create a new session
111#[derive(Deserialize)]
112struct CreateSessionRequest {
113    title: Option<String>,
114    agent: Option<String>,
115}
116
117async fn create_session(
118    Json(req): Json<CreateSessionRequest>,
119) -> Result<Json<crate::session::Session>, (StatusCode, String)> {
120    let mut session = crate::session::Session::new()
121        .await
122        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
123
124    session.title = req.title;
125    if let Some(agent) = req.agent {
126        session.agent = agent;
127    }
128
129    session
130        .save()
131        .await
132        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
133
134    Ok(Json(session))
135}
136
137/// Get a session by ID
138async fn get_session(
139    axum::extract::Path(id): axum::extract::Path<String>,
140) -> Result<Json<crate::session::Session>, (StatusCode, String)> {
141    let session = crate::session::Session::load(&id)
142        .await
143        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
144
145    Ok(Json(session))
146}
147
148/// Prompt a session
149#[derive(Deserialize)]
150struct PromptRequest {
151    message: String,
152}
153
154async fn prompt_session(
155    axum::extract::Path(id): axum::extract::Path<String>,
156    Json(req): Json<PromptRequest>,
157) -> Result<Json<crate::session::SessionResult>, (StatusCode, String)> {
158    // Validate the message is not empty
159    if req.message.trim().is_empty() {
160        return Err((
161            StatusCode::BAD_REQUEST,
162            "Message cannot be empty".to_string(),
163        ));
164    }
165
166    // Log the prompt request (uses the message field)
167    tracing::info!(
168        session_id = %id,
169        message_len = req.message.len(),
170        "Received prompt request"
171    );
172
173    // TODO: Implement actual prompting
174    Err((
175        StatusCode::NOT_IMPLEMENTED,
176        "Prompt execution not yet implemented".to_string(),
177    ))
178}
179
180/// Get configuration
181async fn get_config(State(state): State<AppState>) -> Json<Config> {
182    Json((*state.config).clone())
183}
184
185/// List providers
186async fn list_providers() -> Json<Vec<String>> {
187    Json(vec![
188        "openai".to_string(),
189        "anthropic".to_string(),
190        "google".to_string(),
191    ])
192}
193
194/// List agents
195async fn list_agents() -> Json<Vec<crate::agent::AgentInfo>> {
196    let registry = crate::agent::AgentRegistry::with_builtins();
197    Json(registry.list().into_iter().cloned().collect())
198}