feat(plan): Add plan execution system with external tool support

Plan Execution System:
- Add PlanStep, AccumulatedPlan types for multi-turn tool call accumulation
- Implement AccumulatedPlanStatus for tracking plan lifecycle
- Support selective approval of proposed tool calls before execution

External Tools Integration:
- Add ExternalToolDefinition and ExternalToolTransport to plugins crate
- Extend ToolContext with external_tools registry
- Add external_tool_to_llm_tool conversion for LLM compatibility

JSON-RPC Communication:
- Add jsonrpc crate for JSON-RPC 2.0 protocol support
- Enable stdio-based communication with external tool servers

UI & Engine Updates:
- Add plan_panel.rs component for displaying accumulated plans
- Wire plan mode into engine loop
- Add plan mode integration tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-26 22:47:54 +01:00
parent f97bd44f05
commit 84fa08ab45
17 changed files with 2438 additions and 13 deletions

View File

@@ -19,6 +19,7 @@ llm-openai = { path = "../../llm/openai" }
tools-fs = { path = "../../tools/fs" }
tools-bash = { path = "../../tools/bash" }
tools-slash = { path = "../../tools/slash" }
tools-plan = { path = "../../tools/plan" }
auth-manager = { path = "../../platform/auth" }
config-agent = { package = "config-agent", path = "../../platform/config" }
permissions = { path = "../../platform/permissions" }

View File

@@ -214,6 +214,24 @@ impl AgentManager {
Ok(())
}
/// Execute a single tool call (used by engine for plan step execution)
pub async fn execute_single_tool(&self, tool_name: &str, args: &serde_json::Value) -> Result<String> {
let dummy_perms = permissions::PermissionManager::new(permissions::Mode::Code);
let ctx = agent_core::ToolContext::new();
let result = agent_core::execute_tool(tool_name, args, &dummy_perms, &ctx).await?;
// Notify UI of tool execution
if let Some(tx) = &self.tx_ui {
let _ = tx.send(Message::AgentResponse(AgentResponse::ToolCall {
name: tool_name.to_string(),
args: args.to_string(),
})).await;
}
Ok(result)
}
/// Execute the full reasoning loop until a final response is reached
pub async fn run(&self, input: &str) -> Result<()> {
let tools = agent_core::get_tool_definitions();

View File

@@ -1,9 +1,11 @@
use agent_core::messages::{Message, UserAction, AgentResponse};
use agent_core::messages::{Message, UserAction, AgentResponse, SystemNotification};
use agent_core::state::AppState;
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
use std::path::PathBuf;
use llm_core::LlmProvider;
use ui::ProviderManager;
use tools_plan::PlanManager;
use crate::agent_manager::AgentManager;
/// The main background task that handles logic, API calls, and state updates.
@@ -19,6 +21,9 @@ pub async fn run_engine_loop_dynamic(
.with_ui_sender(tx_ui.clone())
);
// Plan manager for persistence
let plan_manager = PlanManager::new(PathBuf::from("."));
while let Some(msg) = rx.recv().await {
match msg {
Message::UserAction(UserAction::Input(text)) => {
@@ -35,6 +40,121 @@ pub async fn run_engine_loop_dynamic(
let mut guard = state.lock().await;
guard.set_permission_result(res);
}
Message::UserAction(UserAction::FinalizePlan) => {
let mut guard = state.lock().await;
if let Some(plan) = guard.current_plan_mut() {
plan.finalize();
let total_steps = plan.steps.len();
let status = plan.status;
drop(guard);
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::PlanComplete {
total_steps,
status,
})).await;
}
}
Message::UserAction(UserAction::PlanApproval(approval)) => {
let mut guard = state.lock().await;
if let Some(plan) = guard.current_plan_mut() {
// Apply approval decisions
approval.apply_to(plan);
plan.start_execution();
// Get approved steps for execution
let approved_steps: Vec<_> = plan.steps.iter()
.filter(|s| s.is_approved())
.cloned()
.collect();
let total = approved_steps.len();
let skipped = plan.steps.iter().filter(|s| s.is_rejected()).count();
drop(guard);
// Execute approved steps
for (idx, step) in approved_steps.iter().enumerate() {
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::PlanExecuting {
step_id: step.id.clone(),
step_index: idx,
total_steps: total,
})).await;
// Execute the tool
let agent_manager_clone = agent_manager.clone();
let tx_clone = tx_ui.clone();
let step_clone = step.clone();
// Execute tool and send result
if let Err(e) = agent_manager_clone.execute_single_tool(&step_clone.tool, &step_clone.args).await {
let _ = tx_clone.send(Message::AgentResponse(AgentResponse::Error(
format!("Step {} failed: {}", idx + 1, e)
))).await;
}
}
// Mark plan as completed
let mut guard = state.lock().await;
if let Some(plan) = guard.current_plan_mut() {
plan.complete();
}
drop(guard);
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::PlanExecutionComplete {
executed: total,
skipped,
})).await;
}
}
Message::UserAction(UserAction::SavePlan(name)) => {
let guard = state.lock().await;
if let Some(plan) = guard.current_plan() {
let mut plan_to_save = plan.clone();
plan_to_save.name = Some(name.clone());
drop(guard);
match plan_manager.save_accumulated_plan(&plan_to_save).await {
Ok(path) => {
let _ = tx_ui.send(Message::System(SystemNotification::PlanSaved {
id: plan_to_save.id.clone(),
path: path.display().to_string(),
})).await;
}
Err(e) => {
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::Error(
format!("Failed to save plan: {}", e)
))).await;
}
}
}
}
Message::UserAction(UserAction::LoadPlan(id)) => {
match plan_manager.load_accumulated_plan(&id).await {
Ok(plan) => {
let name = plan.name.clone();
let steps = plan.steps.len();
let mut guard = state.lock().await;
guard.accumulated_plan = Some(plan);
drop(guard);
let _ = tx_ui.send(Message::System(SystemNotification::PlanLoaded {
id,
name,
steps,
})).await;
}
Err(e) => {
let _ = tx_ui.send(Message::AgentResponse(AgentResponse::Error(
format!("Failed to load plan: {}", e)
))).await;
}
}
}
Message::UserAction(UserAction::CancelPlan) => {
let mut guard = state.lock().await;
if let Some(plan) = guard.current_plan_mut() {
plan.cancel();
}
guard.clear_plan();
}
Message::UserAction(UserAction::Exit) => {
let mut guard = state.lock().await;
guard.running = false;