1use anyhow::Result;
18use axum::{
19 Json, Router,
20 extract::{Path, State},
21 http::StatusCode,
22 response::IntoResponse,
23 routing::{delete, get, post},
24};
25use serde::Deserialize;
26use std::net::SocketAddr;
27use tower_http::cors::CorsLayer;
28use tower_http::trace::TraceLayer;
29
30use wactorz_core::ActorSystem;
31use wactorz_core::message::{ActorCommand, Message};
32
33#[derive(Clone)]
35pub struct AppState {
36 pub system: ActorSystem,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct SendMessageRequest {
42 pub content: String,
43 #[serde(rename = "type", default)]
44 pub message_type: String,
45}
46
47#[derive(Debug, Deserialize)]
49pub struct ChatRequest {
50 pub message: String,
51 pub agent_name: Option<String>,
52}
53
54pub struct RestServer {
56 state: AppState,
57 addr: SocketAddr,
58}
59
60impl RestServer {
61 pub fn new(system: ActorSystem, addr: SocketAddr) -> Self {
62 Self {
63 state: AppState { system },
64 addr,
65 }
66 }
67
68 pub fn router(&self) -> Router {
70 Router::new()
71 .route("/health", get(health_handler))
72 .route("/actors", get(list_actors_handler))
73 .route("/actors/:id", get(get_actor_handler))
74 .route("/actors/:id/message", post(send_message_handler))
75 .route("/actors/:id", delete(stop_actor_handler))
76 .route("/actors/:id/pause", post(pause_actor_handler))
77 .route("/actors/:id/resume", post(resume_actor_handler))
78 .route("/actors/:id/metrics", get(get_metrics_handler))
79 .route("/chat", post(chat_handler))
80 .layer(CorsLayer::permissive())
81 .layer(TraceLayer::new_for_http())
82 .with_state(self.state.clone())
83 }
84
85 pub async fn serve(self) -> Result<()> {
87 let router = self.router();
88 let listener = tokio::net::TcpListener::bind(self.addr).await?;
89 tracing::info!("REST API listening on {}", self.addr);
90 axum::serve(listener, router).await?;
91 Ok(())
92 }
93}
94
95async fn health_handler() -> impl IntoResponse {
98 Json(serde_json::json!({ "status": "ok" }))
99}
100
101async fn list_actors_handler(State(state): State<AppState>) -> impl IntoResponse {
102 let actors = state.system.registry.list().await;
103 let body: Vec<_> = actors
104 .iter()
105 .map(|e| {
106 serde_json::json!({
107 "id": e.id,
108 "name": e.name,
109 "state": format!("{}", e.state),
110 "protected": e.protected,
111 })
112 })
113 .collect();
114 Json(body)
115}
116
117async fn get_actor_handler(
118 State(state): State<AppState>,
119 Path(id): Path<String>,
120) -> impl IntoResponse {
121 match state.system.registry.get(&id).await {
122 Some(entry) => Json(serde_json::json!({
123 "id": entry.id,
124 "name": entry.name,
125 "state": format!("{}", entry.state),
126 "protected": entry.protected,
127 }))
128 .into_response(),
129 None => (StatusCode::NOT_FOUND, "actor not found").into_response(),
130 }
131}
132
133async fn send_message_handler(
134 State(state): State<AppState>,
135 Path(id): Path<String>,
136 Json(body): Json<SendMessageRequest>,
137) -> axum::response::Response {
138 let msg = Message::text(None, Some(id.clone()), body.content);
139 match state.system.registry.send(&id, msg).await {
140 Ok(_) => (StatusCode::OK, Json(serde_json::json!({"status": "sent"}))).into_response(),
141 Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(),
142 }
143}
144
145async fn stop_actor_handler(
146 State(state): State<AppState>,
147 Path(id): Path<String>,
148) -> axum::response::Response {
149 let entry = match state.system.registry.get(&id).await {
150 Some(e) => e,
151 None => return (StatusCode::NOT_FOUND, "actor not found").into_response(),
152 };
153 if entry.protected {
154 return (StatusCode::FORBIDDEN, "actor is protected").into_response();
155 }
156 let msg = Message::command(id.clone(), ActorCommand::Stop);
157 match state.system.registry.send(&id, msg).await {
158 Ok(_) => (StatusCode::OK, "stopping").into_response(),
159 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
160 }
161}
162
163async fn get_metrics_handler(
164 State(state): State<AppState>,
165 Path(id): Path<String>,
166) -> axum::response::Response {
167 match state.system.registry.get(&id).await {
168 Some(e) => Json(e.metrics.snapshot()).into_response(),
169 None => (StatusCode::NOT_FOUND, "actor not found").into_response(),
170 }
171}
172
173async fn pause_actor_handler(
174 State(state): State<AppState>,
175 Path(id): Path<String>,
176) -> axum::response::Response {
177 let entry = match state.system.registry.get(&id).await {
178 Some(e) => e,
179 None => return (StatusCode::NOT_FOUND, "actor not found").into_response(),
180 };
181 if entry.protected {
182 return (StatusCode::FORBIDDEN, "actor is protected").into_response();
183 }
184 let msg = Message::command(id.clone(), ActorCommand::Pause);
185 match state.system.registry.send(&id, msg).await {
186 Ok(_) => (
187 StatusCode::OK,
188 Json(serde_json::json!({"status": "pausing"})),
189 )
190 .into_response(),
191 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
192 }
193}
194
195async fn resume_actor_handler(
196 State(state): State<AppState>,
197 Path(id): Path<String>,
198) -> axum::response::Response {
199 let entry = match state.system.registry.get(&id).await {
200 Some(e) => e,
201 None => return (StatusCode::NOT_FOUND, "actor not found").into_response(),
202 };
203 if entry.protected {
204 return (StatusCode::FORBIDDEN, "actor is protected").into_response();
205 }
206 let msg = Message::command(id.clone(), ActorCommand::Resume);
207 match state.system.registry.send(&id, msg).await {
208 Ok(_) => (
209 StatusCode::OK,
210 Json(serde_json::json!({"status": "resuming"})),
211 )
212 .into_response(),
213 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
214 }
215}
216
217async fn chat_handler(
218 State(state): State<AppState>,
219 Json(body): Json<ChatRequest>,
220) -> axum::response::Response {
221 let target_name = body.agent_name.as_deref().unwrap_or("main-actor");
222 match state.system.registry.get_by_name(target_name).await {
223 None => (
224 StatusCode::NOT_FOUND,
225 format!("agent '{target_name}' not found"),
226 )
227 .into_response(),
228 Some(entry) => {
229 let msg = Message::text(None, Some(entry.id.clone()), body.message);
230 match state.system.registry.send(&entry.id, msg).await {
231 Ok(_) => Json(serde_json::json!({"status": "sent", "agent": target_name}))
232 .into_response(),
233 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
234 }
235 }
236 }
237}