diff options
Diffstat (limited to 'makima/src/orchestration/engine.rs')
| -rw-r--r-- | makima/src/orchestration/engine.rs | 186 |
1 files changed, 158 insertions, 28 deletions
diff --git a/makima/src/orchestration/engine.rs b/makima/src/orchestration/engine.rs index 5bbb99f..954b857 100644 --- a/makima/src/orchestration/engine.rs +++ b/makima/src/orchestration/engine.rs @@ -13,7 +13,10 @@ use thiserror::Error; use tokio::sync::broadcast; use uuid::Uuid; -use crate::db::models::{AddStepRequest, ChainStep, Directive, DirectiveEvent, UpdateStepRequest}; +use crate::db::models::{ + AddStepRequest, ChainStep, CreateContractRequest, CreateTaskRequest, Directive, + DirectiveEvent, UpdateStepRequest, +}; use crate::db::repository::{self, RepositoryError}; use super::planner::{ChainPlanner, GeneratedChain, PlannerError}; @@ -262,24 +265,82 @@ impl DirectiveEngine { // Chain Management // ======================================================================== - /// Generate initial chain from directive. + /// Generate initial chain from directive using LLM. async fn generate_initial_chain( &self, directive: &Directive, ) -> Result<GeneratedChain, EngineError> { // Build planning prompt - let _prompt = self.planner.build_planning_prompt(directive); + let prompt = self.planner.build_planning_prompt(directive); + + // Try LLM chain generation, fall back to default if unavailable + let chain = match self.generate_chain_via_llm(&prompt).await { + Ok(chain) => { + tracing::info!( + "LLM generated chain with {} steps for directive {}", + chain.steps.len(), + directive.id + ); + chain + } + Err(e) => { + tracing::warn!( + "LLM chain generation failed ({}), using default chain for directive {}", + e, + directive.id + ); + self.build_default_chain(directive) + } + }; + + // Validate the chain + self.planner.validate_chain(&chain)?; + + Ok(chain) + } + + /// Call LLM to generate a chain from the planning prompt. + async fn generate_chain_via_llm(&self, prompt: &str) -> Result<GeneratedChain, EngineError> { + use crate::llm::claude::{ClaudeClient, ClaudeModel, Message, MessageContent}; + + let client = ClaudeClient::from_env(ClaudeModel::Sonnet) + .map_err(|e| EngineError::LlmError(format!("Failed to create LLM client: {}", e)))?; + + let messages = vec![Message { + role: "user".to_string(), + content: MessageContent::Text(prompt.to_string()), + }]; + + let result = client + .chat_with_tools(messages, &[]) + .await + .map_err(|e| EngineError::LlmError(format!("LLM call failed: {}", e)))?; + + let response_text = result + .content + .ok_or_else(|| EngineError::LlmError("Empty LLM response".to_string()))?; + + self.planner + .parse_plan_response(&response_text) + .map_err(|e| EngineError::Planner(e)) + } - // TODO: Call LLM to generate chain - // For now, return a simple placeholder chain - let chain = GeneratedChain { - name: format!("{}-chain", directive.title.to_lowercase().replace(' ', "-")), + /// Build a default chain when LLM is unavailable. + fn build_default_chain(&self, directive: &Directive) -> GeneratedChain { + GeneratedChain { + name: format!( + "{}-chain", + directive.title.to_lowercase().replace(' ', "-") + ), description: format!("Execution plan for: {}", directive.goal), steps: vec![ super::planner::GeneratedStep { name: "research".to_string(), step_type: "research".to_string(), - description: "Research and understand the requirements".to_string(), + description: format!( + "Research and understand the requirements for: {}", + directive.goal + ), depends_on: vec![], requirement_ids: vec![], contract_template: None, @@ -287,7 +348,7 @@ impl DirectiveEngine { super::planner::GeneratedStep { name: "implement".to_string(), step_type: "implement".to_string(), - description: "Implement the solution".to_string(), + description: format!("Implement the solution for: {}", directive.goal), depends_on: vec!["research".to_string()], requirement_ids: vec![], contract_template: None, @@ -301,12 +362,7 @@ impl DirectiveEngine { contract_template: None, }, ], - }; - - // Validate the chain - self.planner.validate_chain(&chain)?; - - Ok(chain) + } } /// Create database steps from a generated chain. @@ -408,16 +464,32 @@ impl DirectiveEngine { let failed_step = steps.iter().find(|s| s.status == "failed"); // Build replan prompt - let _prompt = self.planner.build_replan_prompt( + let prompt = self.planner.build_replan_prompt( &directive, &completed_steps.iter().map(|s| (*s).clone()).collect::<Vec<_>>(), failed_step.map(|s| &*s), reason, ); - // TODO: Call LLM to regenerate chain - // For now, just create a new chain with similar structure - let new_chain = self.generate_initial_chain(&directive).await?; + // Try LLM regeneration, fall back to default + let new_chain = match self.generate_chain_via_llm(&prompt).await { + Ok(chain) => { + tracing::info!( + "LLM regenerated chain with {} steps for directive {}", + chain.steps.len(), + directive.id + ); + chain + } + Err(e) => { + tracing::warn!( + "LLM chain regeneration failed ({}), using default chain for directive {}", + e, + directive.id + ); + self.build_default_chain(&directive) + } + }; // Supersede old chain repository::supersede_chain(&self.pool, current_chain.id).await?; @@ -514,18 +586,74 @@ impl DirectiveEngine { }); // Get contract details from step template - let (_name, _description, _contract_type, _initial_phase) = + let (name, description, contract_type, initial_phase) = self.get_contract_details(directive, step); - // TODO: Actually create the contract via the contracts handler - // For now, just update the step status to running - // In a full implementation, this would: - // 1. Create contract via POST /api/v1/contracts - // 2. Create supervisor task via POST /api/v1/tasks - // 3. Link contract and task to step - // 4. Update step status to running + // Create contract for this step + let contract = repository::create_contract_for_owner( + &self.pool, + directive.owner_id, + CreateContractRequest { + name: name.clone(), + description: description.clone(), + contract_type: Some(contract_type), + template_id: None, + initial_phase: Some(initial_phase), + autonomous_loop: Some(directive.autonomy_level == "full_auto"), + phase_guard: Some(true), + local_only: Some(false), + auto_merge_local: None, + }, + ) + .await + .map_err(|e| EngineError::ContractCreation(format!("Failed to create contract: {}", e)))?; + + // Build task plan from step description and task_plan + let task_plan = step + .task_plan + .clone() + .unwrap_or_else(|| { + format!( + "## Step: {}\n\n{}\n\n## Directive Goal\n{}", + step.name, + description.as_deref().unwrap_or("Complete this step."), + directive.goal, + ) + }); + + // Create supervisor task linked to the contract + let task = repository::create_task_for_owner( + &self.pool, + directive.owner_id, + CreateTaskRequest { + contract_id: Some(contract.id), + name: name.clone(), + description: description.clone(), + plan: task_plan, + parent_task_id: None, + is_supervisor: true, + priority: 5, + repository_url: directive.repository_url.clone(), + base_branch: directive.base_branch.clone(), + target_branch: None, + merge_mode: Some("pr".to_string()), + target_repo_path: None, + completion_action: Some("pr".to_string()), + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + }, + ) + .await + .map_err(|e| EngineError::ContractCreation(format!("Failed to create task: {}", e)))?; + + // Link contract and task to step + repository::update_step_contract(&self.pool, step.id, contract.id, Some(task.id)).await?; - // Placeholder: mark step as running + // Update step status to running repository::update_step_status(&self.pool, step.id, "running").await?; self.emit_event(EngineEvent::StepStatusChanged { directive_id: directive.id, @@ -541,6 +669,8 @@ impl DirectiveEngine { serde_json::json!({ "step_id": step.id, "step_name": step.name, + "contract_id": contract.id, + "task_id": task.id, }), "system", ) |
