codetether_agent/server/
mod.rs1use 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#[derive(Clone)]
23pub struct AppState {
24 pub config: Arc<Config>,
25}
26
27pub 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 let agent_card = a2a::server::A2AServer::default_card(&format!("http://{}", addr));
38 let a2a_server = a2a::server::A2AServer::new(agent_card);
39
40 let a2a_router = a2a_server.router();
42
43 let app = Router::new()
44 .route("/health", get(health))
46 .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 .nest("/a2a", a2a_router)
57 .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
74async fn health() -> &'static str {
76 "ok"
77}
78
79#[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#[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#[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
137async 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#[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 if req.message.trim().is_empty() {
160 return Err((
161 StatusCode::BAD_REQUEST,
162 "Message cannot be empty".to_string(),
163 ));
164 }
165
166 tracing::info!(
168 session_id = %id,
169 message_len = req.message.len(),
170 "Received prompt request"
171 );
172
173 Err((
175 StatusCode::NOT_IMPLEMENTED,
176 "Prompt execution not yet implemented".to_string(),
177 ))
178}
179
180async fn get_config(State(state): State<AppState>) -> Json<Config> {
182 Json((*state.config).clone())
183}
184
185async fn list_providers() -> Json<Vec<String>> {
187 Json(vec![
188 "openai".to_string(),
189 "anthropic".to_string(),
190 "google".to_string(),
191 ])
192}
193
194async 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}