An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1use crate::daemon::DaemonConfig;
2use crate::daemon::api::AppState;
3use crate::daemon::api::{agents, graph, projects, search};
4use crate::daemon::ws;
5use axum::routing::{delete, get, post};
6use axum::{Json, Router};
7use tokio_util::sync::CancellationToken;
8use tower_http::cors::{Any, CorsLayer};
9
10/// Create the axum Router with all routes and middleware
11pub fn create_router(state: AppState) -> Router {
12 let cors = CorsLayer::new()
13 .allow_origin(Any)
14 .allow_methods(Any)
15 .allow_headers(Any);
16
17 Router::new()
18 // Health
19 .route("/api/health", get(health_check))
20 // Projects
21 .route(
22 "/api/projects",
23 get(projects::list_projects).post(projects::create_project),
24 )
25 .route(
26 "/api/projects/{id}",
27 get(projects::get_project).delete(projects::delete_project),
28 )
29 // Goals
30 .route(
31 "/api/projects/{id}/goals",
32 get(graph::list_goals).post(graph::create_goal),
33 )
34 // Nodes
35 .route(
36 "/api/nodes/{id}",
37 get(graph::get_node).patch(graph::update_node),
38 )
39 .route("/api/nodes/{id}/children", post(graph::create_child))
40 // Edges
41 .route("/api/edges", post(graph::create_edge))
42 .route("/api/edges/{id}", delete(graph::delete_edge))
43 // Goal tree
44 .route("/api/goals/{id}/tree", get(graph::get_goal_tree))
45 // Tasks
46 .route("/api/goals/{id}/tasks", get(graph::list_tasks))
47 .route("/api/goals/{id}/tasks/ready", get(graph::list_ready_tasks))
48 .route("/api/goals/{id}/tasks/next", get(graph::next_task))
49 // Decisions
50 .route("/api/projects/{id}/decisions", get(graph::list_decisions))
51 .route(
52 "/api/projects/{id}/decisions/history",
53 get(graph::decisions_history),
54 )
55 .route(
56 "/api/projects/{id}/decisions/export",
57 post(graph::export_decisions),
58 )
59 // Search
60 .route("/api/projects/{id}/search", post(search::search_nodes))
61 // Sessions
62 .route("/api/goals/{id}/sessions", get(graph::list_sessions))
63 .route("/api/sessions/{id}", get(graph::get_session))
64 // Agents
65 .route("/api/goals/{id}/agents", get(agents::list_agents))
66 // Graph import/export
67 .route(
68 "/api/projects/{id}/graph/export",
69 get(graph::export_all_goals),
70 )
71 .route("/api/goals/{id}/export", get(graph::export_goal_toml))
72 .route("/api/projects/{id}/graph/import", post(graph::import_graph))
73 .route("/api/projects/{id}/graph/diff", post(graph::diff_graph))
74 // WebSocket
75 .route("/ws", get(ws::ws_handler))
76 // Static file serving (fallback for non-API routes)
77 .fallback(crate::daemon::static_files::static_handler)
78 .layer(cors)
79 .with_state(state)
80}
81
82async fn health_check() -> Json<serde_json::Value> {
83 Json(serde_json::json!({ "status": "ok" }))
84}
85
86/// Start the axum server, blocking until the shutdown token is cancelled
87pub async fn start_server(
88 config: &DaemonConfig,
89 state: AppState,
90 shutdown: CancellationToken,
91) -> anyhow::Result<()> {
92 let router = create_router(state);
93 let addr = config.socket_addr()?;
94
95 let listener = tokio::net::TcpListener::bind(addr).await?;
96 tracing::info!("Daemon listening on {}", addr);
97
98 axum::serve(listener, router)
99 .with_graceful_shutdown(async move {
100 shutdown.cancelled().await;
101 })
102 .await?;
103
104 Ok(())
105}