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:
@@ -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" }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user