codetether_agent/server/
mod.rs1use 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#[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(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
69async fn health() -> &'static str {
71 "ok"
72}
73
74#[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#[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#[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
132async 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#[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 if req.message.trim().is_empty() {
155 return Err((
156 StatusCode::BAD_REQUEST,
157 "Message cannot be empty".to_string(),
158 ));
159 }
160
161 tracing::info!(
163 session_id = %id,
164 message_len = req.message.len(),
165 "Received prompt request"
166 );
167
168 Err((
170 StatusCode::NOT_IMPLEMENTED,
171 "Prompt execution not yet implemented".to_string(),
172 ))
173}
174
175async fn get_config(State(state): State<AppState>) -> Json<Config> {
177 Json((*state.config).clone())
178}
179
180async fn list_providers() -> Json<Vec<String>> {
182 Json(vec![
183 "openai".to_string(),
184 "anthropic".to_string(),
185 "google".to_string(),
186 ])
187}
188
189async 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}