mirror of
https://github.com/taggart-comet/quill-code.git
synced 2026-08-31 01:45:18 +00:00
plan mode almost works
This commit is contained in:
parent
e5cf17f65d
commit
3951dc4df2
20 changed files with 159 additions and 40 deletions
Binary file not shown.
|
|
@ -2,8 +2,9 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AgentModeType {
|
||||
Build, // Full toolset for implementation
|
||||
Plan, // Read-only toolset for planning
|
||||
Build, // Full toolset for implementation
|
||||
Plan, // Read-only toolset for planning
|
||||
BuildFromPlan, // Auto-triggered: works through TODO list items sequentially
|
||||
}
|
||||
|
||||
impl AgentModeType {
|
||||
|
|
@ -11,6 +12,7 @@ impl AgentModeType {
|
|||
match self {
|
||||
AgentModeType::Build => "build",
|
||||
AgentModeType::Plan => "plan",
|
||||
AgentModeType::BuildFromPlan => "build_from_plan",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +20,7 @@ impl AgentModeType {
|
|||
match value {
|
||||
"plan" | "PLAN" | "Plan" => AgentModeType::Plan,
|
||||
"build" | "BUILD" | "Build" => AgentModeType::Build,
|
||||
"build_from_plan" | "BUILD_FROM_PLAN" => AgentModeType::BuildFromPlan,
|
||||
_ => AgentModeType::Build,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ pub fn get_system_prompt(model_type: ModelType, agent_mode: AgentModeType) -> St
|
|||
if agent_mode == AgentModeType::Plan {
|
||||
return _system_prompt_for_plan(model_type);
|
||||
}
|
||||
if agent_mode == AgentModeType::BuildFromPlan {
|
||||
return _system_prompt_for_build_from_plan(model_type);
|
||||
}
|
||||
if model_type == ModelType::OpenAI {
|
||||
format!(
|
||||
"You are Drastis, a coding agent. \n\
|
||||
|
|
@ -57,9 +60,11 @@ fn _system_prompt_for_plan(model_type: ModelType) -> String {
|
|||
format!(
|
||||
"You are Drastis, a coding agent. \n\
|
||||
You're in the Plan Mode! Use tools to gather all required information to make a detailed plan of what user wants to achieve. \n\
|
||||
Ask the user, if there any ambiguities, and clarify what needs to be done if the instructions are not clear. \n\
|
||||
When you gathered the information, use the `update_todo_list` tool to create the TODO list and ask the user if he would like to make any changes to the TODO list or is he ready to implement the plan. \n\
|
||||
You're writing plan for yourself, so make it prompt-like, you will be executing this plan, once the user confirms it. \n\
|
||||
Ask the user, if there're any ambiguities, and clarify what needs to be done if the instructions are not clear. \n\
|
||||
When you gathered the information, use the `update_todo_list` tool to create the TODO list and ask the user if he would like to make any changes to the TODO list. \n\
|
||||
The user can see the TODO list in the interface once `update_todo_list` tool is used, so don't repeat it it response - respond with a general, summarized approach. \n\
|
||||
Do not put into the TODO list the discovery and clarification steps, only the actions that need to be taken once the user confirms the plan. \n\
|
||||
You're writing plan for yourself, so make it prompt-like, the end-result while you're in the Plan Mode is a comprehensive TODO list. \n\
|
||||
When using tools, pass JSON arguments that match their parameters. \n\
|
||||
Runtime: os={}, shell={}.",
|
||||
os_name, shell_name
|
||||
|
|
@ -76,9 +81,35 @@ Runtime: os={}, shell={}.",
|
|||
}
|
||||
}
|
||||
|
||||
fn _system_prompt_for_build_from_plan(model_type: ModelType) -> String {
|
||||
let (os_name, shell_name) = get_runtime_environment();
|
||||
if model_type == ModelType::OpenAI {
|
||||
format!(
|
||||
"You are Drastis, a coding agent executing a TODO list. \n\
|
||||
You will receive one TODO item at a time via the user prompt. \n\
|
||||
Before starting, mark the item as `in_progress` if it's still `pending`. Accomplish what is described in the current item using available tools, then mark it as `completed` via `update_todo_list` (preserve other items' statuses). \n\
|
||||
After marking an item complete, continue working on the next pending item without stopping. \n\
|
||||
When all items are done, stop without calling any more tools - just tell the user that you're done, and if you encountered any detours from the original plan. \n\
|
||||
When using tools, pass JSON arguments that match their parameters. \n\
|
||||
Runtime: os={}, shell={}.",
|
||||
os_name, shell_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"You are Drastis, a coding agent executing a TODO list. \n\
|
||||
You will receive one TODO item at a time via the user prompt. \n\
|
||||
Complete the current item using available tools, then mark it as `completed` via `update_todo_list` (preserve other items' statuses). \n\
|
||||
After marking an item complete, continue working on the next pending item without stopping. \n\
|
||||
When all items are done, stop without calling any more tools. \n\
|
||||
Runtime: os={}, shell={}.",
|
||||
os_name, shell_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_todo_list_message(todo_content: &str) -> String {
|
||||
format!(
|
||||
"## Current Session TODO List\n\nBelow is the current plan/TODO list for this session:\n\n```json\n{}\n```\n\nMake sure to update the TODO list as you make progress.",
|
||||
"## Current Session TODO List\n\nBelow is the current plan/TODO list for this session:\n\n```json\n{}\n```\n",
|
||||
todo_content
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
use crate::domain::AgentModeType;
|
||||
use crate::domain::ModelType;
|
||||
use crate::domain::session::Request;
|
||||
use crate::domain::todo::TodoListStatus;
|
||||
|
||||
/// Format request history and current request as a complete prompt
|
||||
pub fn get_user_prompt(_model_type: ModelType, request: &dyn Request) -> String {
|
||||
if request.mode() == AgentModeType::BuildFromPlan {
|
||||
return _get_build_from_plan_prompt(request);
|
||||
}
|
||||
|
||||
let mut formatted = String::new();
|
||||
|
||||
// Add history section
|
||||
|
|
@ -25,4 +31,18 @@ pub fn get_user_prompt(_model_type: ModelType, request: &dyn Request) -> String
|
|||
formatted.push_str(&format!("CURRENT REQUEST: {}", request.current_request()));
|
||||
|
||||
formatted
|
||||
}
|
||||
|
||||
fn _get_build_from_plan_prompt(request: &dyn Request) -> String {
|
||||
if let Some(todo_list) = request.get_session_plan() {
|
||||
for item in &todo_list.items {
|
||||
if item.status != TodoListStatus::Completed {
|
||||
return format!(
|
||||
"Title: {}\n\nDescription:\n{}",
|
||||
item.title, item.description
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"All TODO items are completed.".to_string()
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use crate::infrastructure::AgentToUiEvent;
|
|||
use crate::infrastructure::InferenceEngine;
|
||||
use crate::repository::{
|
||||
ModelsRepository, SessionRequestStepsRepository, SessionRequestsRepository,
|
||||
UserSettingsRepository,
|
||||
TodoListRepository, UserSettingsRepository,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -86,10 +86,37 @@ impl SessionService {
|
|||
let request_settings =
|
||||
UserSettings::from(settings_row.clone()).with_current_model_name(model_name);
|
||||
|
||||
// Safeguard: if BuildFromPlan is requested but no valid TODO list exists,
|
||||
// fall back to Build mode to avoid a broken state.
|
||||
let effective_mode = if mode == crate::domain::AgentModeType::BuildFromPlan {
|
||||
let has_pending = {
|
||||
let conn = self.conn.get().map_err(|e| {
|
||||
ServiceError::Repository(format!("Failed to get database connection: {}", e))
|
||||
})?;
|
||||
let repo = TodoListRepository::new(&*conn);
|
||||
match repo.get_by_session(session.id()) {
|
||||
Ok(Some(row)) => {
|
||||
match serde_json::from_str::<crate::domain::todo::TodoList>(&row.content) {
|
||||
Ok(todo_list) => !todo_list.is_completed() && !todo_list.items.is_empty(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
if has_pending {
|
||||
crate::domain::AgentModeType::BuildFromPlan
|
||||
} else {
|
||||
crate::domain::AgentModeType::Build
|
||||
}
|
||||
} else {
|
||||
mode
|
||||
};
|
||||
|
||||
// Create a new session request
|
||||
let requests_repo = SessionRequestsRepository::new(self.conn.clone());
|
||||
let request_row = requests_repo
|
||||
.create(session.id(), settings_row.id, prompt, mode)
|
||||
.create(session.id(), settings_row.id, prompt, effective_mode)
|
||||
.map_err(|e| ServiceError::Repository(e))?;
|
||||
let request_id = request_row.id;
|
||||
|
||||
|
|
@ -101,7 +128,7 @@ impl SessionService {
|
|||
session_with_request.set_current_request(prompt.to_string());
|
||||
session_with_request.set_current_images(images.to_vec());
|
||||
session_with_request.set_current_user_settings(Some(request_settings));
|
||||
session_with_request.set_current_mode(mode);
|
||||
session_with_request.set_current_mode(effective_mode);
|
||||
session_with_request.set_conn(self.conn.clone());
|
||||
|
||||
// Run the workflow
|
||||
|
|
@ -110,7 +137,7 @@ impl SessionService {
|
|||
.run_using_bt(&mut session_with_request, cancel)
|
||||
} else {
|
||||
self.workflow
|
||||
.run(&mut session_with_request, cancel, 128, mode)
|
||||
.run(&mut session_with_request, cancel, 128, effective_mode)
|
||||
.map_err(ServiceError::Workflow)?;
|
||||
Ok(self.workflow.get_chain().clone())
|
||||
};
|
||||
|
|
|
|||
|
|
@ -130,6 +130,12 @@ impl Request for Session {
|
|||
fn get_history_steps(&self) -> Vec<crate::domain::workflow::step::ChainStep> {
|
||||
use crate::repository::SessionRequestStepsRepository;
|
||||
|
||||
// BuildFromPlan keeps context minimal — TODO list (with statuses) is
|
||||
// already included as a system message by the chain's todo_list field.
|
||||
if self.current_mode == AgentModeType::BuildFromPlan {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Return empty if no connection available
|
||||
let conn = match &self.conn {
|
||||
Some(c) => c.clone(),
|
||||
|
|
|
|||
|
|
@ -142,8 +142,9 @@ impl Tool for UpdateTodoList {
|
|||
}
|
||||
|
||||
fn desc(&self) -> String {
|
||||
"Updates the TODO list for the current session. \
|
||||
Use this in Plan mode to create or update TODO items after exploring the codebase. For every use, provide full TODO list, as it will replace the existing list."
|
||||
"Use this in Plan mode to create or update TODO items after exploring the codebase. For every use, provide full TODO list, as it will replace the existing list.\n\
|
||||
If you're not in Plan mode, only update statuses of TODO items as you finish them.
|
||||
"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
use crate::domain::tools::FileChange;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileChangesEvent {
|
||||
pub request_id: i64,
|
||||
pub changes: Vec<FileChange>,
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ use openai_agents_tracing::{SpanKind, TracingFacade};
|
|||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use crossbeam_channel::Sender;
|
||||
use crate::domain::AgentModeType;
|
||||
|
||||
/// Main workflow orchestrator that runs LLM-driven coding tasks
|
||||
/// Implements an eternal agent loop that:
|
||||
|
|
@ -64,12 +65,13 @@ impl Workflow {
|
|||
request: &mut dyn Request,
|
||||
cancel: &CancellationToken,
|
||||
max_tool_calls: usize,
|
||||
mode: crate::domain::AgentModeType,
|
||||
mode: AgentModeType,
|
||||
) -> Result<(), Error> {
|
||||
// Select toolset based on mode
|
||||
let toolset_type = match mode {
|
||||
crate::domain::AgentModeType::Build => ToolsetType::All,
|
||||
crate::domain::AgentModeType::Plan => ToolsetType::Discover,
|
||||
AgentModeType::Build => ToolsetType::All,
|
||||
AgentModeType::Plan => ToolsetType::Discover,
|
||||
AgentModeType::BuildFromPlan => ToolsetType::All,
|
||||
};
|
||||
|
||||
// Get session_id for tools that need it
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ impl InputDto {
|
|||
content: vec![InputContent::text(todo_message)],
|
||||
role: ROLE_SYSTEM.to_string(),
|
||||
kind: "message".to_string(),
|
||||
status: "in_progress".to_string(),
|
||||
status: "completed".to_string(),
|
||||
};
|
||||
result.push(todo_input);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub fn render(frame: &mut Frame, area: Rect, state: &UiState, theme: Theme) {
|
|||
let (mode_name, mode_color) = match state.agent_mode {
|
||||
AgentModeType::Build => ("BUILD", theme.info_text), // Blue
|
||||
AgentModeType::Plan => ("PLAN", Color::LightGreen),
|
||||
AgentModeType::BuildFromPlan => ("BUILD FROM PLAN", Color::LightYellow),
|
||||
};
|
||||
|
||||
// Determine hint based on current mode
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ pub mod bottom_info_panel;
|
|||
pub mod commands_menu;
|
||||
pub mod header_panel;
|
||||
pub mod input;
|
||||
pub mod left_half_body; // Conversation panel (rendered on the left)
|
||||
pub mod left_container; // Conversation panel (rendered on the left)
|
||||
pub mod loading_bar;
|
||||
pub mod permissions;
|
||||
pub mod popup_container;
|
||||
pub mod request_indicator;
|
||||
pub mod right_half_body; // TODO/file-changes panel (rendered on the right)
|
||||
pub mod right_container; // TODO/file-changes panel (rendered on the right)
|
||||
pub mod settings_panel;
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ fn handle_normal_key(
|
|||
}
|
||||
}
|
||||
KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
// Enter right_half_body review mode for whatever is displayed
|
||||
// Enter right_container review mode for whatever is displayed
|
||||
let has_file_changes = state.file_changes.is_some()
|
||||
&& !state.file_changes.as_ref().unwrap().changes.is_empty();
|
||||
let has_todo_list = state.todo_list.is_some()
|
||||
|
|
@ -234,7 +234,7 @@ fn handle_normal_key(
|
|||
}
|
||||
KeyCode::BackTab => {
|
||||
// BackTab = Shift+Tab on most terminals
|
||||
// Enter right_half_body review mode for whatever is displayed
|
||||
// Enter right_container review mode for whatever is displayed
|
||||
let has_file_changes = state.file_changes.is_some()
|
||||
&& !state.file_changes.as_ref().unwrap().changes.is_empty();
|
||||
let has_todo_list = state.todo_list.is_some()
|
||||
|
|
@ -257,10 +257,26 @@ fn handle_normal_key(
|
|||
return Ok(());
|
||||
}
|
||||
KeyCode::Tab if !key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
// Toggle agent mode between Build and Plan
|
||||
// Toggle agent mode. Plan with a pending TODO list → BuildFromPlan;
|
||||
// BuildFromPlan escapes back to Build.
|
||||
state.agent_mode = match state.agent_mode {
|
||||
AgentModeType::Build => AgentModeType::Plan,
|
||||
AgentModeType::Plan => AgentModeType::Build,
|
||||
AgentModeType::Plan => {
|
||||
let has_pending = state
|
||||
.todo_list
|
||||
.as_ref()
|
||||
.map(|list| {
|
||||
!list.items.is_empty()
|
||||
&& list.items.iter().any(|item| item.status != "completed")
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if has_pending {
|
||||
AgentModeType::BuildFromPlan
|
||||
} else {
|
||||
AgentModeType::Build
|
||||
}
|
||||
}
|
||||
AgentModeType::BuildFromPlan => AgentModeType::Build,
|
||||
};
|
||||
}
|
||||
KeyCode::Esc if !state.attached_images.is_empty() => {
|
||||
|
|
@ -624,6 +640,10 @@ fn handle_file_changes_key(
|
|||
// Return to Normal mode
|
||||
state.mode = UiMode::Normal;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Return to Normal mode
|
||||
state.mode = UiMode::Normal;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
if *selected_file > 0 {
|
||||
*selected_file -= 1;
|
||||
|
|
@ -652,7 +672,7 @@ fn handle_file_changes_key(
|
|||
.unwrap_or(0);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Backspace => {
|
||||
KeyCode::Esc | KeyCode::Left => {
|
||||
// Return to FileList view
|
||||
*view_mode = FileChangesViewMode::FileList;
|
||||
*scroll_offset = 0;
|
||||
|
|
@ -1252,6 +1272,7 @@ fn open_mode_popup(state: &mut UiState) {
|
|||
let selected = match state.agent_mode {
|
||||
AgentModeType::Build => 0,
|
||||
AgentModeType::Plan => 1,
|
||||
AgentModeType::BuildFromPlan => 0,
|
||||
};
|
||||
state.mode = UiMode::Popup(PopupState::ModeSelect { selected });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::domain::ModelType;
|
||||
use crate::domain::{AgentModeType, ModelType};
|
||||
use crate::infrastructure::cli::state::{
|
||||
FileChangesDisplay, LoadStatus, PopupState, ProgressEntry, ProgressKind, RequestIndicator,
|
||||
RequestStatusDisplay, UiMode, UiState,
|
||||
|
|
@ -242,6 +242,21 @@ fn handle_agent_event(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Revert BuildFromPlan → Build when all TODO items are completed
|
||||
if state.agent_mode == AgentModeType::BuildFromPlan {
|
||||
let all_done = state
|
||||
.todo_list
|
||||
.as_ref()
|
||||
.map(|list| {
|
||||
!list.items.is_empty()
|
||||
&& list.items.iter().all(|item| item.status == "completed")
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if all_done {
|
||||
state.agent_mode = AgentModeType::Build;
|
||||
}
|
||||
}
|
||||
}
|
||||
AgentToUiEvent::FileChangesEvent {
|
||||
request_id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::infrastructure::cli::components::{
|
||||
attachment_indicator, bottom_info_panel, commands_menu, header_panel, input, left_half_body,
|
||||
loading_bar, permissions, popup_container, request_indicator, right_half_body, settings_panel,
|
||||
attachment_indicator, bottom_info_panel, commands_menu, header_panel, input, left_container,
|
||||
loading_bar, permissions, popup_container, request_indicator, right_container, settings_panel,
|
||||
};
|
||||
use crate::infrastructure::cli::helpers::{
|
||||
centered_rect, cursor_position, list_state, panel_block,
|
||||
|
|
@ -79,10 +79,10 @@ pub fn render(frame: &mut Frame, state: &UiState) {
|
|||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[1]);
|
||||
left_half_body::render(frame, body_chunks[0], state, theme); // Conversation on left
|
||||
right_half_body::render(frame, body_chunks[1], state, theme); // TODO/changes on right
|
||||
left_container::render(frame, body_chunks[0], state, theme); // Conversation on left
|
||||
right_container::render(frame, body_chunks[1], state, theme); // TODO/changes on right
|
||||
} else {
|
||||
left_half_body::render(frame, chunks[1], state, theme);
|
||||
left_container::render(frame, chunks[1], state, theme);
|
||||
}
|
||||
request_indicator::render(frame, chunks[2], state, theme);
|
||||
input::render(frame, chunks[3], state, theme);
|
||||
|
|
@ -94,10 +94,10 @@ pub fn render(frame: &mut Frame, state: &UiState) {
|
|||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[1]);
|
||||
left_half_body::render(frame, body_chunks[0], state, theme); // Conversation on left
|
||||
right_half_body::render(frame, body_chunks[1], state, theme); // TODO/changes on right
|
||||
left_container::render(frame, body_chunks[0], state, theme); // Conversation on left
|
||||
right_container::render(frame, body_chunks[1], state, theme); // TODO/changes on right
|
||||
} else {
|
||||
left_half_body::render(frame, chunks[1], state, theme);
|
||||
left_container::render(frame, chunks[1], state, theme);
|
||||
}
|
||||
request_indicator::render(frame, chunks[2], state, theme);
|
||||
attachment_indicator::render(frame, chunks[3], state, theme);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue