pub mod app_generator; pub mod app_logs; pub mod ask_later; pub mod autotask_api; pub mod designer_ai; pub mod intent_classifier; pub mod intent_compiler; pub mod safety_layer; pub mod task_manifest; pub mod task_types; pub use app_generator::{ AppGenerator, AppStructure, FileType, GeneratedApp, GeneratedFile, GeneratedPage, PageType, SyncResult, }; pub use task_manifest::{ create_manifest_from_llm_response, FieldDefinition, FileDefinition, ItemStatus, ItemType, ManifestBuilder, ManifestItem, ManifestSection, ManifestStatus, MonitorDefinition, PageDefinition, ProcessingStats, SchedulerDefinition, SectionStatus, SectionType, TableDefinition, TaskManifest, TerminalLine, TerminalLineType, ToolDefinition, }; pub use app_logs::{ generate_client_logger_js, get_designer_error_context, log_generator_error, log_generator_info, log_runtime_error, log_validation_error, start_log_cleanup_scheduler, AppLogEntry, AppLogStore, ClientLogRequest, LogLevel, LogQueryParams, LogSource, LogStats, APP_LOGS, }; pub use ask_later::{ask_later_keyword, PendingInfoItem}; pub use autotask_api::{ apply_recommendation_handler, cancel_task_handler, classify_intent_handler, compile_intent_handler, create_and_execute_handler, execute_plan_handler, execute_task_handler, get_approvals_handler, get_decisions_handler, get_manifest_handler, get_pending_items_handler, get_stats_handler, get_task_handler, get_task_logs_handler, list_tasks_handler, pause_task_handler, resume_task_handler, simulate_plan_handler, simulate_task_handler, submit_approval_handler, submit_decision_handler, submit_pending_item_handler, }; pub use designer_ai::DesignerAI; pub use task_types::{AutoTask, AutoTaskStatus, ExecutionMode, TaskPriority}; pub use intent_classifier::{ClassifiedIntent, IntentClassifier, IntentType}; pub use intent_compiler::{CompiledIntent, IntentCompiler}; pub use safety_layer::{AuditEntry, ConstraintCheckResult, SafetyLayer, SimulationResult}; use crate::core::urls::ApiUrls; use crate::shared::state::AppState; use axum::{ extract::{ ws::{Message, WebSocket, WebSocketUpgrade}, Path, Query, State, }, response::IntoResponse, }; use futures::{SinkExt, StreamExt}; use log::{debug, error, info, warn}; use std::collections::HashMap; use std::sync::Arc; pub fn configure_autotask_routes() -> axum::Router> { use axum::routing::{get, post}; axum::Router::new() .route(ApiUrls::AUTOTASK_CREATE, post(create_and_execute_handler)) .route(ApiUrls::AUTOTASK_CLASSIFY, post(classify_intent_handler)) .route(ApiUrls::AUTOTASK_COMPILE, post(compile_intent_handler)) .route(ApiUrls::AUTOTASK_EXECUTE, post(execute_plan_handler)) .route(ApiUrls::AUTOTASK_SIMULATE, post(simulate_plan_handler)) .route(ApiUrls::AUTOTASK_LIST, get(list_tasks_handler)) .route(ApiUrls::AUTOTASK_GET, get(get_task_handler)) .route(ApiUrls::AUTOTASK_STATS, get(get_stats_handler)) .route(ApiUrls::AUTOTASK_PAUSE, post(pause_task_handler)) .route(ApiUrls::AUTOTASK_RESUME, post(resume_task_handler)) .route(ApiUrls::AUTOTASK_CANCEL, post(cancel_task_handler)) .route(ApiUrls::AUTOTASK_TASK_SIMULATE, post(simulate_task_handler)) .route(ApiUrls::AUTOTASK_DECISIONS, get(get_decisions_handler)) .route(ApiUrls::AUTOTASK_DECIDE, post(submit_decision_handler)) .route(ApiUrls::AUTOTASK_APPROVALS, get(get_approvals_handler)) .route(ApiUrls::AUTOTASK_APPROVE, post(submit_approval_handler)) .route(ApiUrls::AUTOTASK_TASK_EXECUTE, post(execute_task_handler)) .route(ApiUrls::AUTOTASK_LOGS, get(get_task_logs_handler)) .route("/api/autotask/:task_id/manifest", get(get_manifest_handler)) .route(ApiUrls::AUTOTASK_RECOMMENDATIONS_APPLY, post(apply_recommendation_handler)) .route(ApiUrls::AUTOTASK_PENDING, get(get_pending_items_handler)) .route(ApiUrls::AUTOTASK_PENDING_ITEM, post(submit_pending_item_handler)) .route("/api/app-logs/client", post(handle_client_logs)) .route("/api/app-logs/list", get(handle_list_logs)) .route("/api/app-logs/stats", get(handle_log_stats)) .route("/api/app-logs/clear/:app_name", post(handle_clear_logs)) .route("/api/app-logs/logger.js", get(handle_logger_js)) .route("/ws/task-progress", get(task_progress_websocket_handler)) .route("/ws/task-progress/:task_id", get(task_progress_by_id_websocket_handler)) } pub async fn task_progress_websocket_handler( ws: WebSocketUpgrade, State(state): State>, Query(params): Query>, ) -> impl IntoResponse { let task_filter = params.get("task_id").cloned(); info!( "Task progress WebSocket connection request, filter: {:?}", task_filter ); ws.on_upgrade(move |socket| handle_task_progress_websocket(socket, state, task_filter)) } pub async fn task_progress_by_id_websocket_handler( ws: WebSocketUpgrade, State(state): State>, Path(task_id): Path, ) -> impl IntoResponse { info!( "Task progress WebSocket connection for task: {}", task_id ); ws.on_upgrade(move |socket| handle_task_progress_websocket(socket, state, Some(task_id))) } async fn handle_task_progress_websocket( socket: WebSocket, state: Arc, task_filter: Option, ) { let (mut sender, mut receiver) = socket.split(); info!("Task progress WebSocket connected, filter: {:?}", task_filter); let welcome = serde_json::json!({ "type": "connected", "message": "Connected to task progress stream", "filter": task_filter, "timestamp": chrono::Utc::now().to_rfc3339() }); if let Ok(welcome_str) = serde_json::to_string(&welcome) { if sender.send(Message::Text(welcome_str)).await.is_err() { error!("Failed to send welcome message to task progress WebSocket"); return; } } let mut broadcast_rx = if let Some(broadcast_tx) = state.task_progress_broadcast.as_ref() { broadcast_tx.subscribe() } else { warn!("No task progress broadcast channel available"); return; }; let task_filter_clone = task_filter.clone(); let send_task = tokio::spawn(async move { loop { match broadcast_rx.recv().await { Ok(event) => { let is_manifest = event.step == "manifest_update" || event.event_type == "manifest_update"; let should_send = task_filter_clone.is_none() || task_filter_clone.as_ref() == Some(&event.task_id); if is_manifest { info!( "[WS_HANDLER] Received manifest_update event: task={}, should_send={}, filter={:?}", event.task_id, should_send, task_filter_clone ); } if should_send { match serde_json::to_string(&event) { Ok(json_str) => { if is_manifest { info!( "[WS_HANDLER] Sending manifest_update to WebSocket: {} bytes, task={}", json_str.len(), event.task_id ); } else { debug!( "Sending task progress to WebSocket: {} - {}", event.task_id, event.step ); } match sender.send(Message::Text(json_str)).await { Ok(()) => { if is_manifest { info!("[WS_HANDLER] manifest_update SENT successfully to WebSocket"); } } Err(e) => { error!("[WS_HANDLER] Failed to send to WebSocket: {:?}", e); break; } } } Err(e) => { error!("[WS_HANDLER] Failed to serialize event: {:?}", e); } } } } Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { warn!("Task progress WebSocket lagged by {} messages", n); } Err(tokio::sync::broadcast::error::RecvError::Closed) => { info!("Task progress broadcast channel closed"); break; } } } }); let recv_task = tokio::spawn(async move { while let Some(msg) = receiver.next().await { match msg { Ok(Message::Text(text)) => { debug!("Received text from task progress WebSocket: {}", text); if text == "ping" { debug!("Received ping from task progress client"); } } Ok(Message::Ping(data)) => { debug!("Received ping from task progress WebSocket"); drop(data); } Ok(Message::Pong(_)) => { debug!("Received pong from task progress WebSocket"); } Ok(Message::Close(_)) => { info!("Task progress WebSocket client disconnected"); break; } Ok(Message::Binary(_)) => { debug!("Received binary from task progress WebSocket (ignored)"); } Err(e) => { // TLS close_notify errors are normal when browser tab closes debug!("Task progress WebSocket closed: {}", e); break; } } } }); tokio::select! { _ = send_task => { info!("Task progress send task completed"); } _ = recv_task => { info!("Task progress receive task completed"); } } info!("Task progress WebSocket connection closed, filter: {:?}", task_filter); } async fn handle_client_logs( axum::Json(payload): axum::Json, ) -> impl axum::response::IntoResponse { for log in payload.logs { APP_LOGS.log_client(log, None, None); } axum::Json(serde_json::json!({"success": true})) } #[derive(serde::Deserialize)] struct ClientLogsPayload { logs: Vec, } async fn handle_list_logs( axum::extract::Query(params): axum::extract::Query, ) -> impl axum::response::IntoResponse { let logs = APP_LOGS.get_logs(¶ms); axum::Json(logs) } async fn handle_log_stats() -> impl axum::response::IntoResponse { let stats = APP_LOGS.get_stats(); axum::Json(stats) } async fn handle_clear_logs( axum::extract::Path(app_name): axum::extract::Path, ) -> impl axum::response::IntoResponse { APP_LOGS.clear_app_logs(&app_name); axum::Json( serde_json::json!({"success": true, "message": format!("Logs cleared for {}", app_name)}), ) } async fn handle_logger_js() -> impl axum::response::IntoResponse { ( [(axum::http::header::CONTENT_TYPE, "application/javascript")], generate_client_logger_js(), ) }