summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/directives.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
-rw-r--r--makima/src/server/handlers/directives.rs200
1 files changed, 198 insertions, 2 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 6060171..f03dccf 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -9,12 +9,13 @@ use axum::{
use uuid::Uuid;
use crate::db::models::{
- CleanupTasksResponse, CreateDirectiveRequest,
+ CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest,
CreateDirectiveStepRequest, Directive, DirectiveListResponse,
- DirectiveStep, DirectiveWithSteps,
+ DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse,
UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest,
};
use crate::db::repository;
+use crate::orchestration::directive::build_order_pickup_prompt;
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;
@@ -927,3 +928,198 @@ pub async fn cleanup_tasks(
}
}
}
+
+// =============================================================================
+// Order Pickup
+// =============================================================================
+
+/// Pick up available open orders for a directive by spawning a planning task.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/pick-up-orders",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Orders picked up", body = PickUpOrdersResponse),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn pick_up_orders(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive ownership and get directive with steps
+ let (directive, steps) =
+ match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some((d, s))) => (d, s),
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Fetch available orders
+ let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id).await {
+ Ok(o) => o,
+ Err(e) => {
+ tracing::error!("Failed to fetch available orders: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("FETCH_ORDERS_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // If no orders available, return early
+ if orders.is_empty() {
+ return Json(PickUpOrdersResponse {
+ message: "No orders available to pick up".to_string(),
+ order_count: 0,
+ task_id: None,
+ })
+ .into_response();
+ }
+
+ let order_count = orders.len() as i64;
+ let order_ids: Vec<Uuid> = orders.iter().map(|o| o.id).collect();
+
+ // Get generation and goal history for the planning prompt
+ let generation =
+ match repository::get_directive_max_generation(pool, id).await {
+ Ok(g) => g + 1,
+ Err(e) => {
+ tracing::error!("Failed to get max generation: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GENERATION_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ let goal_history = match repository::get_directive_goal_history(pool, id, 3).await {
+ Ok(h) => h,
+ Err(e) => {
+ tracing::warn!("Failed to get goal history: {}", e);
+ vec![]
+ }
+ };
+
+ // Build the specialized planning prompt
+ let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &goal_history);
+
+ // Link orders to the directive
+ if let Err(e) =
+ repository::bulk_link_orders_to_directive(pool, auth.owner_id, &order_ids, id).await
+ {
+ tracing::error!("Failed to link orders to directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LINK_ORDERS_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+
+ // Create the planning task
+ let req = CreateTaskRequest {
+ contract_id: None,
+ name: format!("Pick up orders: {}", directive.title),
+ description: Some("Directive order pickup planning task".to_string()),
+ plan,
+ parent_task_id: None,
+ is_supervisor: false,
+ priority: 0,
+ repository_url: directive.repository_url.clone(),
+ base_branch: directive.base_branch.clone(),
+ target_branch: None,
+ merge_mode: None,
+ target_repo_path: None,
+ completion_action: None,
+ continue_from_task_id: None,
+ copy_files: None,
+ checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
+ supervisor_worktree_task_id: None,
+ directive_id: Some(directive.id),
+ directive_step_id: None,
+ };
+
+ let task = match repository::create_task_for_owner(pool, auth.owner_id, req).await {
+ Ok(t) => t,
+ Err(e) => {
+ tracing::error!("Failed to create pickup planning task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_TASK_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Assign as orchestrator task
+ if let Err(e) = repository::assign_orchestrator_task(pool, id, task.id).await {
+ tracing::error!("Failed to assign orchestrator task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("ASSIGN_TASK_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+
+ // Cancel old planning tasks
+ let cancelled = repository::cancel_old_planning_tasks(pool, id, task.id).await;
+ if let Ok(count) = cancelled {
+ if count > 0 {
+ tracing::info!(
+ directive_id = %id,
+ cancelled_count = count,
+ "Cancelled old planning tasks superseded by order pickup"
+ );
+ }
+ }
+
+ // Set directive to active if draft/idle/paused
+ match directive.status.as_str() {
+ "draft" | "idle" | "paused" => {
+ if let Err(e) = repository::set_directive_status(pool, auth.owner_id, id, "active").await
+ {
+ tracing::warn!("Failed to set directive status to active: {}", e);
+ }
+ }
+ _ => {}
+ }
+
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+
+ Json(PickUpOrdersResponse {
+ message: format!("Picked up {} orders", order_count),
+ order_count,
+ task_id: Some(task.id),
+ })
+ .into_response()
+}