feat: support require_plan_approval in template config
Templates can now set "require_plan_approval": true in template.json to require user approval after plan generation before execution begins. On rejection, the LLM re-enters the planning loop with user feedback.
This commit is contained in:
parent
30e25f589b
commit
dae99d307a
106
src/agent.rs
106
src/agent.rs
@ -350,6 +350,7 @@ async fn agent_loop(
|
||||
};
|
||||
|
||||
let ext_tools = loaded_template.as_ref().map(|t| &t.external_tools);
|
||||
let plan_approval = loaded_template.as_ref().map_or(false, |t| t.require_plan_approval);
|
||||
|
||||
tracing::info!("Starting agent loop for workflow {}", workflow_id);
|
||||
// Run tool-calling agent loop
|
||||
@ -357,6 +358,7 @@ async fn agent_loop(
|
||||
&llm, &exec, &pool, &broadcast_tx,
|
||||
&project_id, &workflow_id, &requirement, &workdir, &mgr,
|
||||
&instructions, None, ext_tools, &mut rx,
|
||||
plan_approval,
|
||||
).await;
|
||||
|
||||
let final_status = if result.is_ok() { "done" } else { "failed" };
|
||||
@ -489,14 +491,13 @@ async fn agent_loop(
|
||||
|
||||
let instructions = read_instructions(&workdir).await;
|
||||
|
||||
// Reload external tools from template if available
|
||||
let ext_tools = if !wf.template_id.is_empty() {
|
||||
// Reload template config if available
|
||||
let loaded_template = if !wf.template_id.is_empty() {
|
||||
let tid = &wf.template_id;
|
||||
if template::is_repo_template(tid) {
|
||||
match template::extract_repo_template(tid, mgr.template_repo.as_ref()).await {
|
||||
Ok(template_dir) => {
|
||||
LoadedTemplate::load_from_dir(tid, &template_dir).await
|
||||
.ok().map(|t| t.external_tools)
|
||||
LoadedTemplate::load_from_dir(tid, &template_dir).await.ok()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to reload template {}: {}", tid, e);
|
||||
@ -504,16 +505,19 @@ async fn agent_loop(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadedTemplate::load(tid).await.ok().map(|t| t.external_tools)
|
||||
LoadedTemplate::load(tid).await.ok()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ext_tools = loaded_template.as_ref().map(|t| &t.external_tools);
|
||||
let plan_approval = loaded_template.as_ref().map_or(false, |t| t.require_plan_approval);
|
||||
|
||||
let result = run_agent_loop(
|
||||
&llm, &exec, &pool, &broadcast_tx,
|
||||
&project_id, &workflow_id, &wf.requirement, &workdir, &mgr,
|
||||
&instructions, Some(state), ext_tools.as_ref(), &mut rx,
|
||||
&instructions, Some(state), ext_tools, &mut rx,
|
||||
plan_approval,
|
||||
).await;
|
||||
|
||||
let final_status = if result.is_ok() { "done" } else { "failed" };
|
||||
@ -1638,6 +1642,7 @@ async fn run_agent_loop(
|
||||
initial_state: Option<AgentState>,
|
||||
external_tools: Option<&ExternalToolManager>,
|
||||
event_rx: &mut mpsc::Receiver<AgentEvent>,
|
||||
require_plan_approval: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let planning_tools = build_planning_tools();
|
||||
let coordinator_tools = build_coordinator_tools();
|
||||
@ -1709,21 +1714,100 @@ async fn run_agent_loop(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(first) = state.steps.first_mut() {
|
||||
first.status = StepStatus::Running;
|
||||
}
|
||||
|
||||
let _ = broadcast_tx.send(WsMessage::PlanUpdate {
|
||||
workflow_id: workflow_id.to_string(),
|
||||
steps: plan_infos_from_state(&state),
|
||||
});
|
||||
|
||||
save_state_snapshot(pool, workflow_id, 0, &state).await;
|
||||
tracing::info!("[workflow {}] Plan set ({} steps)", workflow_id, state.steps.len());
|
||||
|
||||
// If require_plan_approval, wait for user to confirm the plan
|
||||
if require_plan_approval {
|
||||
tracing::info!("[workflow {}] Waiting for plan approval", workflow_id);
|
||||
let _ = broadcast_tx.send(WsMessage::ActivityUpdate {
|
||||
workflow_id: workflow_id.to_string(),
|
||||
activity: "计划已生成 — 等待用户确认...".to_string(),
|
||||
});
|
||||
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;
|
||||
log_execution(pool, broadcast_tx, workflow_id, 0, "plan_approval", "等待确认计划", "等待用户确认执行计划", "waiting").await;
|
||||
|
||||
// Block until Comment event
|
||||
let approval_content = loop {
|
||||
match event_rx.recv().await {
|
||||
Some(AgentEvent::Comment { content, .. }) => break content,
|
||||
Some(_) => continue,
|
||||
None => {
|
||||
anyhow::bail!("Event channel closed while waiting for plan approval");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("[workflow {}] Plan approval response: {}", workflow_id, approval_content);
|
||||
|
||||
if approval_content.starts_with("rejected:") {
|
||||
let reason = approval_content.strip_prefix("rejected:").unwrap_or("").trim();
|
||||
tracing::info!("[workflow {}] Plan rejected: {}", workflow_id, reason);
|
||||
log_execution(pool, broadcast_tx, workflow_id, 0, "plan_approval", "rejected", reason, "failed").await;
|
||||
|
||||
// Feed rejection back into planning conversation so LLM can re-plan
|
||||
state.current_step_chat_history.push(ChatMessage::tool_result(
|
||||
&tc.id,
|
||||
&format!("用户拒绝了此计划: {}。请根据反馈修改计划后重新调用 update_plan。", reason),
|
||||
));
|
||||
state.steps.clear();
|
||||
|
||||
let _ = broadcast_tx.send(WsMessage::WorkflowStatusUpdate {
|
||||
workflow_id: workflow_id.to_string(),
|
||||
status: "executing".into(),
|
||||
});
|
||||
let _ = sqlx::query("UPDATE workflows SET status = 'executing' WHERE id = ?")
|
||||
.bind(workflow_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
// Stay in Planning phase, continue the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
// Approved
|
||||
let feedback = if approval_content.starts_with("approved:") {
|
||||
approval_content.strip_prefix("approved:").unwrap_or("").trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
log_execution(pool, broadcast_tx, workflow_id, 0, "plan_approval", "approved", &feedback, "done").await;
|
||||
let _ = broadcast_tx.send(WsMessage::WorkflowStatusUpdate {
|
||||
workflow_id: workflow_id.to_string(),
|
||||
status: "executing".into(),
|
||||
});
|
||||
let _ = sqlx::query("UPDATE workflows SET status = 'executing' WHERE id = ?")
|
||||
.bind(workflow_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Enter execution phase
|
||||
if let Some(first) = state.steps.first_mut() {
|
||||
first.status = StepStatus::Running;
|
||||
}
|
||||
let _ = broadcast_tx.send(WsMessage::PlanUpdate {
|
||||
workflow_id: workflow_id.to_string(),
|
||||
steps: plan_infos_from_state(&state),
|
||||
});
|
||||
state.current_step_chat_history.clear();
|
||||
state.phase = AgentPhase::Executing { step: 1 };
|
||||
phase_transition = true;
|
||||
|
||||
save_state_snapshot(pool, workflow_id, 0, &state).await;
|
||||
tracing::info!("[workflow {}] Plan set ({} steps), entering Executing", workflow_id, state.steps.len());
|
||||
tracing::info!("[workflow {}] Entering Executing", workflow_id);
|
||||
}
|
||||
// Planning phase IO tools
|
||||
_ => {
|
||||
|
||||
@ -12,6 +12,10 @@ pub struct TemplateInfo {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub match_hint: String,
|
||||
/// If true, the agent will wait for user approval after update_plan
|
||||
/// before entering the execution phase.
|
||||
#[serde(default)]
|
||||
pub require_plan_approval: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -21,6 +25,7 @@ pub struct LoadedTemplate {
|
||||
pub instructions: String,
|
||||
pub external_tools: ExternalToolManager,
|
||||
pub kb_files: Vec<(String, String)>,
|
||||
pub require_plan_approval: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@ -590,12 +595,14 @@ impl LoadedTemplate {
|
||||
name: template_id.to_string(),
|
||||
description: String::new(),
|
||||
match_hint: String::new(),
|
||||
require_plan_approval: false,
|
||||
})
|
||||
} else {
|
||||
TemplateInfo {
|
||||
name: template_id.to_string(),
|
||||
description: String::new(),
|
||||
match_hint: String::new(),
|
||||
require_plan_approval: false,
|
||||
}
|
||||
};
|
||||
|
||||
@ -610,12 +617,15 @@ impl LoadedTemplate {
|
||||
let kb_files = scan_kb_files(&kb_dir).await;
|
||||
tracing::info!("Template '{}': {} KB files", template_id, kb_files.len());
|
||||
|
||||
let require_plan_approval = info.require_plan_approval;
|
||||
|
||||
Ok(Self {
|
||||
id: template_id.to_string(),
|
||||
info,
|
||||
instructions,
|
||||
external_tools,
|
||||
kb_files,
|
||||
require_plan_approval,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user