Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/commands/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub async fn thread_execute_approved_plan(
let action = match action.as_str() {
"apply_plan" => PlanApprovalAction::ApplyPlan,
"apply_plan_with_context_reset" => PlanApprovalAction::ApplyPlanWithContextReset,
"apply_plan_with_goal" => PlanApprovalAction::ApplyPlanWithGoal,
other => {
return Err(AppError::recoverable(
crate::model::errors::ErrorSource::Thread,
Expand Down
31 changes: 28 additions & 3 deletions src-tauri/src/core/agent_run_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,23 @@ impl AgentRunManager {

plan_metadata.approval_state = IMPLEMENTATION_PLAN_APPROVED_STATE.to_string();

let implementation_prompt =
build_implementation_handoff_prompt(thread_id, &plan_metadata, action.clone());
let implementation_prompt = match action {
PlanApprovalAction::ApplyPlanWithGoal => {
// Build the goal objective text using the plan file path.
let plan_path = crate::core::plan_checkpoint::plan_file_path(thread_id)
.map(|p| p.display().to_string())
.unwrap_or_default();
format!(

This comment was marked as outdated.

"Please follow the implementation plan {} to complete all implementation, and conduct reviews at each stage following the plan, ensuring the implementation follows the design in the plan and meets quality standards.",
plan_path
)
}
_ => build_implementation_handoff_prompt(thread_id, &plan_metadata, action.clone()),
};
let (history_override, context_seed_messages) = match action {
PlanApprovalAction::ApplyPlan => (None, None),
PlanApprovalAction::ApplyPlanWithContextReset => {
PlanApprovalAction::ApplyPlanWithContextReset
| PlanApprovalAction::ApplyPlanWithGoal => {
let message_bundle = self
.build_context_reset_message_bundle(thread_id, &plan_metadata)
.await?;
Expand All @@ -454,6 +466,19 @@ impl AgentRunManager {
}
};

// For ApplyPlanWithGoal: create a persistent goal before starting the

This comment was marked as outdated.

// implementation run so the goal continuation loop can drive execution.
if let PlanApprovalAction::ApplyPlanWithGoal = action {

This comment was marked as outdated.

let goal_manager = crate::core::goal_manager::GoalManager::new(
self.pool.clone(),
thread_id.to_string(),
Arc::clone(&self.goal_runtime_state),
);
goal_manager
.create_goal(&implementation_prompt, None)
.await?;
}

let result = self
.start_run_with_options(
thread_id,
Expand Down
17 changes: 11 additions & 6 deletions src-tauri/src/core/agent_run_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ pub(crate) fn build_implementation_handoff_prompt(
PlanApprovalAction::ApplyPlanWithContextReset => {
"The user approved this plan after clearing the planning conversation from the implementation context."
}
PlanApprovalAction::ApplyPlanWithGoal => {
"The user approved this plan for goal-driven implementation with context reset."
}
};
let plan_file_note = crate::core::plan_checkpoint::plan_file_path(thread_id)
.filter(|path| path.exists())
Expand All @@ -99,12 +102,14 @@ pub(crate) fn build_implementation_handoff_prompt(
&plan_markdown,
)
}
PlanApprovalAction::ApplyPlanWithContextReset => render_handoff_template_no_plan(
include_str!("prompt/templates/handoff/without_plan.tpl.md"),
action_note,
&metadata.artifact.plan_revision.to_string(),
&plan_file_note,
),
PlanApprovalAction::ApplyPlanWithContextReset | PlanApprovalAction::ApplyPlanWithGoal => {
render_handoff_template_no_plan(
include_str!("prompt/templates/handoff/without_plan.tpl.md"),
action_note,
&metadata.artifact.plan_revision.to_string(),
&plan_file_note,
)
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions src-tauri/src/core/agent_session_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,8 +1039,13 @@ impl AgentSession {
let approval_message_id = uuid::Uuid::now_v7().to_string();
let plan_metadata =
build_plan_message_metadata(artifact.clone(), &self.spec.run_id, &self.spec.run_mode);
let approval_metadata =
build_approval_prompt_metadata(artifact.plan_revision, &plan_message_id);
let approval_metadata = build_approval_prompt_metadata(
&self.pool,
&self.spec.thread_id,
artifact.plan_revision,
&plan_message_id,
)
.await;

let plan_message = MessageRecord {
id: plan_message_id.clone(),
Expand Down
164 changes: 146 additions & 18 deletions src-tauri/src/core/plan_checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ pub const IMPLEMENTATION_PLAN_SUPERSEDED_STATE: &str = "superseded";
pub enum PlanApprovalAction {
ApplyPlan,
ApplyPlanWithContextReset,
ApplyPlanWithGoal,
}

impl PlanApprovalAction {
pub fn label(&self) -> &'static str {
match self {
Self::ApplyPlan => "按计划实施",
Self::ApplyPlanWithContextReset => "清理上下文后按计划实施",
Self::ApplyPlanWithContextReset => "清理上下文后实施",
Self::ApplyPlanWithGoal => "按计划设置Goal后实施",
}
}
}
Expand Down Expand Up @@ -103,27 +105,44 @@ pub fn build_plan_message_metadata(
}
}

pub fn build_approval_prompt_metadata(
pub async fn build_approval_prompt_metadata(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] New async DB query on hot path without early return

Every plan approval prompt now performs an additional async database query to check for existing goals. While this is a lightweight lookup, it adds latency to the approval flow and could be avoided by caching or deferring the check.

Suggestion: Consider whether the goal-option visibility check can be performed only when the plan message is first built, or cache the existence of goals for the thread in memory. Alternatively, rely on the frontend to always send 'apply_plan_with_goal' and validate server-side only when the action is actually invoked.

Risk: Slight increase in plan approval prompt construction latency (~1-5 ms per DB roundtrip). Not significant for interactive use but adds up under concurrent load.

Confidence: 0.85

[From SubAgent: performance]

pool: &sqlx::SqlitePool,
thread_id: &str,
plan_revision: u32,
plan_message_id: &str,
) -> ApprovalPromptMetadata {
let mut options = vec![
PlanApprovalOption {
action: PlanApprovalAction::ApplyPlan,
label: PlanApprovalAction::ApplyPlan.label().to_string(),
},
PlanApprovalOption {
action: PlanApprovalAction::ApplyPlanWithContextReset,
label: PlanApprovalAction::ApplyPlanWithContextReset
.label()
.to_string(),
},
];

// Only offer the goal-driven option when the thread has no existing goal
// record (any status: active, paused, budget_limited, or complete).
if crate::persistence::repo::goal_repo::find_by_thread_id(pool, thread_id)

This comment was marked as outdated.

.await
.map(|opt| opt.is_none())
.unwrap_or(false)
{
options.push(PlanApprovalOption {
action: PlanApprovalAction::ApplyPlanWithGoal,
label: PlanApprovalAction::ApplyPlanWithGoal.label().to_string(),
});
}

ApprovalPromptMetadata {
kind: IMPLEMENTATION_PLAN_APPROVAL_KIND.to_string(),
plan_revision,
plan_message_id: plan_message_id.to_string(),
state: IMPLEMENTATION_PLAN_PENDING_STATE.to_string(),
options: vec![
PlanApprovalOption {
action: PlanApprovalAction::ApplyPlan,
label: PlanApprovalAction::ApplyPlan.label().to_string(),
},
PlanApprovalOption {
action: PlanApprovalAction::ApplyPlanWithContextReset,
label: PlanApprovalAction::ApplyPlanWithContextReset
.label()
.to_string(),
},
],
options,
expires_on_new_user_message: true,
approved_action: None,
}
Expand Down Expand Up @@ -424,6 +443,24 @@ mod tests {
parse_approval_prompt_metadata, parse_plan_message_metadata, plan_design_markdown,
plan_markdown, write_plan_file_to, PlanApprovalAction,
};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr;

async fn setup_test_pool() -> sqlx::SqlitePool {
let options = SqliteConnectOptions::from_str("sqlite::memory:")
.expect("invalid sqlite options")
.foreign_keys(true);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await
.expect("pool");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("migrations");
pool
}

#[test]
fn plan_artifact_builder_accepts_string_and_object_steps() {
Expand Down Expand Up @@ -470,8 +507,9 @@ mod tests {
);
}

#[test]
fn metadata_round_trip_is_stable() {
#[tokio::test]
async fn metadata_round_trip_is_stable() {
let pool = setup_test_pool().await;
let artifact = build_plan_artifact_from_tool_input(
&serde_json::json!({
"title": "Implement checkpoint",
Expand All @@ -484,18 +522,108 @@ mod tests {
parse_plan_message_metadata(&serde_json::to_value(&metadata).unwrap()).unwrap();
assert_eq!(parsed.artifact, artifact);

let approval = build_approval_prompt_metadata(artifact.plan_revision, "msg-plan");
let approval = build_approval_prompt_metadata(
&pool,
"thread-no-goal",
artifact.plan_revision,
"msg-plan",
)
.await;
let parsed_approval =
parse_approval_prompt_metadata(&serde_json::to_value(&approval).unwrap()).unwrap();
assert_eq!(parsed_approval.plan_revision, 1);
assert_eq!(parsed_approval.options.len(), 2);
// With no existing goal, the third option (ApplyPlanWithGoal) is included.
assert_eq!(parsed_approval.options.len(), 3);
assert_eq!(
parsed_approval.options[0].action,
PlanApprovalAction::ApplyPlan
);
assert!(approval_prompt_markdown(&artifact).contains("Implement checkpoint"));
}

#[tokio::test]
async fn build_approval_prompt_metadata_suppresses_goal_option_when_goal_exists() {
use crate::model::goal::{GoalRecord, GoalStatus};
use crate::persistence::repo::goal_repo;
use chrono::Utc;

let pool = setup_test_pool().await;

// Seed workspace + thread to satisfy the goals → threads FK.
let now = Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO workspaces (id, name, path, canonical_path, display_path,
is_default, is_git, auto_work_tree, status, created_at, updated_at)
VALUES ('ws-goal', 'Test', '/tmp/test', '/tmp/test', '/tmp/test',
0, 0, 0, 'ready', ?, ?)",
)
.bind(&now)
.bind(&now)
.execute(&pool)
.await
.expect("seed workspace");

sqlx::query(
"INSERT INTO threads (id, workspace_id, title, status, last_active_at, created_at, updated_at)
VALUES ('thread-with-goal', 'ws-goal', 'Test', 'idle', ?, ?, ?)",
)
.bind(&now)
.bind(&now)
.bind(&now)
.execute(&pool)
.await
.expect("seed thread");

// Insert an active goal for this thread.
let goal = GoalRecord {
id: "goal-1".to_string(),
thread_id: "thread-with-goal".to_string(),
objective: "Build feature X".to_string(),
status: GoalStatus::Active,
token_budget: None,
tokens_used: 0,
turns_used: 0,
max_turns: 50,
pause_reason: None,
pause_detail: None,
evidence: None,
last_evaluated_run_id: None,
judge_passed: false,
judge_completeness: None,
judge_findings: None,
judge_summary: None,
judge_evaluated_run_id: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
goal_repo::insert(&pool, &goal).await.expect("insert goal");

let artifact = build_plan_artifact_from_tool_input(
&serde_json::json!({"title": "Test", "summary": "Test plan."}),
1,
);
let approval = build_approval_prompt_metadata(
&pool,
"thread-with-goal",
artifact.plan_revision,
"msg-plan",
)
.await;

// When a goal already exists, only 2 options should be returned
// (ApplyPlan and ApplyPlanWithContextReset), excluding ApplyPlanWithGoal.
assert_eq!(approval.options.len(), 2);
assert_eq!(approval.options[0].action, PlanApprovalAction::ApplyPlan);
assert_eq!(
approval.options[1].action,
PlanApprovalAction::ApplyPlanWithContextReset
);
assert!(!approval
.options
.iter()
.any(|o| o.action == PlanApprovalAction::ApplyPlanWithGoal));
}

#[test]
fn plan_markdown_renders_new_structured_sections() {
let artifact = build_plan_artifact_from_tool_input(
Expand Down
Loading
Loading