diff --git a/src/agent.rs b/src/agent.rs index ced19df..5bec0c9 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -53,6 +53,7 @@ pub fn plan_infos_from_state(state: &AgentState) -> Vec { let status = match s.status { StepStatus::Pending => "pending", StepStatus::Running => "running", + StepStatus::WaitingApproval => "waiting_approval", StepStatus::Done => "done", StepStatus::Failed => "failed", }; @@ -319,7 +320,7 @@ async fn agent_loop( let result = run_agent_loop( &llm, &exec, &pool, &broadcast_tx, &project_id, &workflow_id, &requirement, &workdir, &mgr, - &instructions, None, ext_tools, + &instructions, None, ext_tools, &mut rx, ).await; let final_status = if result.is_ok() { "done" } else { "failed" }; @@ -427,7 +428,7 @@ async fn agent_loop( let result = run_agent_loop( &llm, &exec, &pool, &broadcast_tx, &project_id, &workflow_id, &wf.requirement, &workdir, &mgr, - &instructions, Some(state), None, + &instructions, Some(state), None, &mut rx, ).await; let final_status = if result.is_ok() { "done" } else { "failed" }; @@ -608,6 +609,13 @@ fn build_execution_tools() -> Vec { }, "required": ["content"] })), + make_tool("wait_for_approval", "暂停执行,等待用户确认后继续。用于关键决策点,如:确认方案、确认配置变更、确认危险操作等。用户的回复会作为反馈返回。", serde_json::json!({ + "type": "object", + "properties": { + "reason": { "type": "string", "description": "说明为什么需要用户确认" } + }, + "required": ["reason"] + })), tool_kb_search(), tool_kb_read(), ] @@ -637,6 +645,7 @@ fn build_feedback_prompt(project_id: &str, state: &AgentState, feedback: &str) - let status = match s.status { StepStatus::Done => " [done]", StepStatus::Running => " [running]", + StepStatus::WaitingApproval => " [waiting]", StepStatus::Failed => " [FAILED]", StepStatus::Pending => "", }; @@ -982,6 +991,7 @@ async fn run_agent_loop( instructions: &str, initial_state: Option, external_tools: Option<&ExternalToolManager>, + event_rx: &mut mpsc::Receiver, ) -> anyhow::Result<()> { let planning_tools = build_planning_tools(); let mut execution_tools = build_execution_tools(); @@ -1134,6 +1144,64 @@ async fn run_agent_loop( state.current_step_chat_history.push(ChatMessage::tool_result(&tc.id, "Scratchpad 已更新。")); } + "wait_for_approval" => { + let reason = args["reason"].as_str().unwrap_or("等待确认"); + + // Mark step as WaitingApproval + if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) { + step.status = StepStatus::WaitingApproval; + } + let _ = broadcast_tx.send(WsMessage::PlanUpdate { + workflow_id: workflow_id.to_string(), + steps: plan_infos_from_state(&state), + }); + let _ = broadcast_tx.send(WsMessage::WorkflowStatusUpdate { + workflow_id: workflow_id.to_string(), + status: "waiting_approval".into(), + }); + let _ = sqlx::query("UPDATE workflows SET status = 'waiting_approval' WHERE id = ?") + .bind(workflow_id) + .execute(pool) + .await; + save_state_snapshot(pool, workflow_id, cur, &state).await; + log_execution(pool, broadcast_tx, workflow_id, cur, "wait_for_approval", reason, reason, "waiting").await; + + tracing::info!("[workflow {}] Waiting for approval: {}", workflow_id, reason); + + // Block until we receive a Comment event + let approval_content = loop { + match event_rx.recv().await { + Some(AgentEvent::Comment { content, .. }) => break content, + Some(_) => continue, + None => return Err(anyhow::anyhow!("Event channel closed while waiting for approval")), + } + }; + + tracing::info!("[workflow {}] Approval received: {}", workflow_id, approval_content); + + // Resume: restore Running status + if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) { + step.status = StepStatus::Running; + step.user_feedbacks.push(approval_content.clone()); + } + let _ = sqlx::query("UPDATE workflows SET status = 'executing' WHERE id = ?") + .bind(workflow_id) + .execute(pool) + .await; + let _ = broadcast_tx.send(WsMessage::WorkflowStatusUpdate { + workflow_id: workflow_id.to_string(), + status: "executing".into(), + }); + let _ = broadcast_tx.send(WsMessage::PlanUpdate { + workflow_id: workflow_id.to_string(), + steps: plan_infos_from_state(&state), + }); + + state.current_step_chat_history.push( + ChatMessage::tool_result(&tc.id, &format!("用户已确认。反馈: {}", approval_content)) + ); + } + "update_requirement" => { let new_req = args["requirement"].as_str().unwrap_or(""); let _ = sqlx::query("UPDATE workflows SET requirement = ? WHERE id = ?") diff --git a/src/state.rs b/src/state.rs index a7aba58..b8c1f2d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -19,6 +19,7 @@ pub enum AgentPhase { pub enum StepStatus { Pending, Running, + WaitingApproval, Done, Failed, } @@ -120,7 +121,7 @@ impl AgentState { /// 全部 Done 时返回 None。 pub fn first_actionable_step(&self) -> Option { self.steps.iter() - .find(|s| matches!(s.status, StepStatus::Pending | StepStatus::Running)) + .find(|s| matches!(s.status, StepStatus::Pending | StepStatus::Running | StepStatus::WaitingApproval)) .map(|s| s.order) } @@ -141,6 +142,7 @@ impl AgentState { let marker = match s.status { StepStatus::Done => " done", StepStatus::Running => " >> current", + StepStatus::WaitingApproval => " ⏳ waiting", StepStatus::Failed => " FAILED", StepStatus::Pending => "", }; diff --git a/web/src/components/CommentSection.vue b/web/src/components/CommentSection.vue index 086c7aa..df63634 100644 --- a/web/src/components/CommentSection.vue +++ b/web/src/components/CommentSection.vue @@ -4,6 +4,7 @@ import { ref, nextTick } from 'vue' const props = defineProps<{ disabled?: boolean quotes: string[] + waitingApproval?: boolean }>() const emit = defineEmits<{ @@ -50,7 +51,10 @@ defineExpose({ focusInput })