diff options
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 203 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 4 |
3 files changed, 30 insertions, 178 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 7b13f1c..6d99179 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -13,7 +13,7 @@ use crate::db::models::{ CreateDirectiveStepRequest, Directive, DirectiveListResponse, DirectiveRevision, DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse, Task, - UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, + UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, DirectiveOrderGroupListResponse, UpdateDirectiveOrderGroupRequest, OrderListResponse, @@ -22,9 +22,8 @@ use serde::Serialize; use utoipa::ToSchema; use crate::db::repository; use crate::orchestration::directive::{ - build_cleanup_prompt, build_order_pickup_prompt, classify_goal_change, - try_cancel_running_planner, try_interrupt_planner_with_goal_edit, - GoalChangeKind, GoalEditInterruptResult, + build_cleanup_prompt, build_order_pickup_prompt, + try_cancel_running_planner, }; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; @@ -200,15 +199,19 @@ pub async fn update_directive( match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await { Ok(Some(directive)) => { // Detect "PR was just raised" — pr_url went from None to Some. - // Snapshot the current goal as a revision tied to this PR. - // Best-effort: a snapshot failure should not fail the update, - // because the directive's pr_url has already been written. + // Snapshot the active contract's body as a revision tied to + // this PR. Best-effort: a snapshot failure should not fail + // the update, because the directive's pr_url has already + // been written. if before_pr_url.is_none() { if let Some(ref new_pr_url) = directive.pr_url { + let snapshot_body = repository::get_active_contract_body(pool, directive.id) + .await + .unwrap_or_default(); if let Err(e) = repository::create_directive_revision( pool, directive.id, - &directive.goal, + &snapshot_body, new_pr_url, directive.pr_branch.as_deref(), ) @@ -859,152 +862,10 @@ async fn step_status_change( } } -/// Update a directive's goal (triggers re-planning). -#[utoipa::path( - put, - path = "/api/v1/directives/{id}/goal", - params(("id" = Uuid, Path, description = "Directive ID")), - request_body = UpdateGoalRequest, - responses( - (status = 200, description = "Goal updated", body = Directive), - (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 update_goal( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateGoalRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Fetch the current directive so we can: - // 1. Save the old goal to history (best-effort). - // 2. Decide whether to fire a goal-edit interrupt at a running planner. - let current = match repository::get_directive_for_owner(pool, auth.owner_id, id).await { - Ok(Some(d)) => Some(d), - Ok(None) => None, - Err(e) => { - tracing::warn!( - directive_id = %id, - error = %e, - "Failed to fetch current directive for goal history — continuing with goal update" - ); - None - } - }; - - // Save old goal to history before overwriting (best-effort). - if let Some(ref current) = current { - if let Err(e) = repository::save_directive_goal_history(pool, id, ¤t.goal).await { - tracing::warn!( - directive_id = %id, - error = %e, - "Failed to save goal history before update — continuing with goal update" - ); - } - } - - // Goal-edit interrupt cycle: if a planner task is currently running for - // this directive AND the goal change classifies as 'small', interrupt the - // running planner via SendMessage instead of clearing it (which would - // trigger a fresh replan on the next orchestrator tick). - let mut interrupted = false; - if let Some(ref current) = current { - if current.orchestrator_task_id.is_some() - && classify_goal_change(¤t.goal, &req.goal) == GoalChangeKind::Small - { - match try_interrupt_planner_with_goal_edit( - pool, - &state, - id, - ¤t.goal, - &req.goal, - ) - .await - { - Ok(GoalEditInterruptResult::Sent) => { - interrupted = true; - } - Ok(GoalEditInterruptResult::Skipped) => {} - Err(e) => { - tracing::warn!( - directive_id = %id, - error = %e, - "Goal-edit interrupt attempt errored — falling back to replan" - ); - } - } - } - } - - // If we successfully interrupted a running planner, persist the new goal - // WITHOUT clearing the orchestrator task — the planner will react to the - // SendMessage and adjust in-flight. Otherwise, fall through to the normal - // path which clears orchestrator_task_id and lets phase_replanning kick - // in on the next tick. - // - // CRITICAL: when going down the "clear" path, we must also CANCEL the - // running planner. Otherwise the orphaned task keeps producing add-step - // calls based on the old goal, racing the freshly-spawned replanner. - if !interrupted { - if let Some(ref current) = current { - if let Some(orch_task_id) = current.orchestrator_task_id { - if let Err(e) = try_cancel_running_planner(pool, &state, id, orch_task_id).await { - tracing::warn!( - directive_id = %id, - task_id = %orch_task_id, - error = %e, - "Failed to cancel orphaned planner — proceeding with clear anyway" - ); - } - } - } - } - - let update_result = if interrupted { - repository::update_directive_goal_keep_orchestrator(pool, auth.owner_id, id, &req.goal) - .await - } else { - repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await - }; - - let response = match update_result { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to update goal: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("UPDATE_FAILED", &e.to_string())), - ) - .into_response(); - } - }; - - // Nudge the directive reconciler so the user does not wait up to 15s for - // the next interval tick before the new planner is spawned (clear path) or - // the small-edit interrupt is consumed (keep path). Best-effort: if the - // channel is full or closed we just rely on the normal interval. - state.kick_directive_reconciler(); - - response -} +// (Goal updates now flow through the contracts API. The directive's +// orchestrator reads the active contract's body when it spawns or +// replans — see repository::get_active_contract_body and the +// orchestration module.) // ============================================================================= // Task Cleanup @@ -1404,16 +1265,13 @@ pub async fn pick_up_orders( } }; - 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); + // Build the specialized planning prompt. The orchestrator reads the + // active contract's body itself when it picks up the task; we just + // pass the directive shape + steps + orders + generation here. + let contract_body = repository::get_active_contract_body(pool, id) + .await + .unwrap_or_default(); + let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &contract_body); // Link orders to the directive if let Err(e) = @@ -1984,16 +1842,13 @@ pub async fn pick_up_dog_orders( } }; - 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); + // Build the specialized planning prompt. The orchestrator reads the + // active contract's body itself when it picks up the task; we just + // pass the directive shape + steps + orders + generation here. + let contract_body = repository::get_active_contract_body(pool, id) + .await + .unwrap_or_default(); + let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &contract_body); // Link orders to the directive if let Err(e) = diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index a3a1886..604caea 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -191,7 +191,6 @@ pub fn make_router(state: SharedState) -> Router { .route("/directives/{id}/steps/{step_id}/complete", post(directives::complete_step)) .route("/directives/{id}/steps/{step_id}/fail", post(directives::fail_step)) .route("/directives/{id}/steps/{step_id}/skip", post(directives::skip_step)) - .route("/directives/{id}/goal", put(directives::update_goal)) .route("/directives/{id}/revisions", get(directives::list_directive_revisions)) .route("/directives/{id}/new-draft", post(directives::new_directive_draft)) .route( diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index 184d12a..437285f 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -25,7 +25,7 @@ use crate::db::models::{ Task, TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry, UpdateContractRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest, - UpdateFileRequest, UpdateGoalRequest, UpdateOrderRequest, UpdateTaskRequest, + UpdateFileRequest, UpdateOrderRequest, UpdateTaskRequest, }; use crate::server::auth::{ ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse, @@ -109,7 +109,6 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage directives::complete_step, directives::fail_step, directives::skip_step, - directives::update_goal, directives::list_directive_revisions, directives::new_directive_draft, directives::create_directive_task, @@ -227,7 +226,6 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage crate::server::handlers::directives::CreateDirectiveTaskRequest, CreateDirectiveRequest, UpdateDirectiveRequest, - UpdateGoalRequest, CreateDirectiveStepRequest, UpdateDirectiveStepRequest, CleanupResponse, |
