From e3531b4dcf8e25cb0323a2f28d242949bf631476 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 8 May 2026 12:02:10 -0600 Subject: [PATCH] feat: add auto-continue support to all contexts --- config.agent.example.yaml | 8 +- config.example.yaml | 10 +++ config.role.example.md | 15 +++- src/config/agent.rs | 20 ++--- src/config/app_config.rs | 15 ++++ src/config/mod.rs | 10 +++ src/config/request_context.rs | 160 ++++++++++++++++++++++++++++++++-- src/config/role.rs | 50 +++++++++++ src/config/session.rs | 77 ++++++++++++++++ src/function/todo.rs | 10 ++- src/repl/mod.rs | 82 +++++++++-------- 11 files changed, 397 insertions(+), 60 deletions(-) diff --git a/config.agent.example.yaml b/config.agent.example.yaml index db26d3b..32bbe94 100644 --- a/config.agent.example.yaml +++ b/config.agent.example.yaml @@ -17,9 +17,11 @@ agent_session: null # Set a session to use when starting the agent. name: # Name of the agent, used in the UI and logs description: # Description of the agent, used in the UI version: 1 # Version of the agent -# Todo System & Auto-Continuation -# These settings help smaller models handle multi-step tasks more reliably. -# See docs/TODO-SYSTEM.md for detailed documentation. +# Auto-Continue (Todo System) +# The auto-continue system provides built-in task tracking for improved reliability. +# When enabled, the model can create todo lists and the system will automatically +# prompt it to continue when incomplete tasks remain. +# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information auto_continue: false # Enable automatic continuation when incomplete todos remain max_auto_continues: 10 # Maximum number of automatic continuations before stopping inject_todo_instructions: true # Inject the default todo tool usage instructions into the agent's system prompt diff --git a/config.example.yaml b/config.example.yaml index b7c1961..6899847 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -81,6 +81,16 @@ mapping_mcp_servers: # Alias for an MCP server or set of servers git: github,gitmcp enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search') +# ---- Auto-Continue (Todo System) ---- +# The auto-continue system provides built-in task tracking for improved reliability. +# When enabled, the model can create todo lists and the system will automatically +# prompt it to continue when incomplete tasks remain. +# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information +auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) +max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) +inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true) +continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default + # ---- Session ---- # See the [Session documentation](./docs/SESSIONS.md) for more information save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do diff --git a/config.role.example.md b/config.role.example.md index 3159277..25804bf 100644 --- a/config.role.example.md +++ b/config.role.example.md @@ -1,5 +1,9 @@ --- -# Everything in this section is optional +############################################ +## Everything in this section is optional ## +############################################ + +# Role Configuration name: # The name of the role model: openai:gpt-4o # The model to use for this role temperature: 0.2 # The temperature to use for this role when querying the model @@ -8,5 +12,14 @@ enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enabl enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role prompt: null # A custom prompt to use for this role that will immediately query # the model for output instead of using the instructions below +# Auto-Continue (Todo System) +# The auto-continue system provides built-in task tracking for improved reliability. +# When enabled, the model can create todo lists and the system will automatically +# prompt it to continue when incomplete tasks remain. +# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information +auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false) +max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10) +inject_todo_instructions: true # Inject default todo tool usage instructions into the system prompt (default: true) +continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default --- You are an expert at doing things. This is where you write the instructions for the role. diff --git a/src/config/agent.rs b/src/config/agent.rs index 6f0f302..fd4f69b 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -415,6 +415,14 @@ impl Agent { self.config.max_auto_continues } + pub fn inject_todo_instructions(&self) -> bool { + self.config.inject_todo_instructions + } + + pub fn continuation_prompt_value(&self) -> Option { + self.config.continuation_prompt.clone() + } + pub fn can_spawn_agents(&self) -> bool { self.config.can_spawn_agents } @@ -439,18 +447,6 @@ impl Agent { self.config.escalation_timeout } - pub fn continuation_prompt(&self) -> String { - self.config.continuation_prompt.clone().unwrap_or_else(|| { - formatdoc! {" - [SYSTEM REMINDER - TODO CONTINUATION] - You have incomplete tasks. Rules: - 1. BEFORE marking a todo done: verify the work compiles/works. No premature completion. - 2. If a todo is broad (e.g. \"implement X and implement Y\"): break it into specific subtasks FIRST using todo__add, then work on those.\n\ - 3. Each todo should be atomic and be \"single responsibility\" - completable in one focused action. - 4. Continue with the next pending item now. Call tools immediately."} - }) - } - pub fn compression_threshold(&self) -> Option { self.config.compression_threshold } diff --git a/src/config/app_config.rs b/src/config/app_config.rs index cdce823..41fb193 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -39,6 +39,11 @@ pub struct AppConfig { pub mapping_mcp_servers: IndexMap, pub enabled_mcp_servers: Option, + pub auto_continue: bool, + pub max_auto_continues: usize, + pub inject_todo_instructions: bool, + pub continuation_prompt: Option, + pub repl_prelude: Option, pub cmd_prelude: Option, pub agent_session: Option, @@ -95,6 +100,11 @@ impl Default for AppConfig { mapping_mcp_servers: Default::default(), enabled_mcp_servers: None, + auto_continue: false, + max_auto_continues: 10, + inject_todo_instructions: true, + continuation_prompt: None, + repl_prelude: None, cmd_prelude: None, agent_session: None, @@ -152,6 +162,11 @@ impl AppConfig { mapping_mcp_servers: config.mapping_mcp_servers, enabled_mcp_servers: config.enabled_mcp_servers, + auto_continue: config.auto_continue, + max_auto_continues: config.max_auto_continues, + inject_todo_instructions: config.inject_todo_instructions, + continuation_prompt: config.continuation_prompt, + repl_prelude: config.repl_prelude, cmd_prelude: config.cmd_prelude, agent_session: config.agent_session, diff --git a/src/config/mod.rs b/src/config/mod.rs index 43feb35..7e3bb9f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -121,6 +121,11 @@ pub struct Config { pub mapping_mcp_servers: IndexMap, pub enabled_mcp_servers: Option, + pub auto_continue: bool, + pub max_auto_continues: usize, + pub inject_todo_instructions: bool, + pub continuation_prompt: Option, + pub repl_prelude: Option, pub cmd_prelude: Option, pub agent_session: Option, @@ -177,6 +182,11 @@ impl Default for Config { mapping_mcp_servers: Default::default(), enabled_mcp_servers: None, + auto_continue: false, + max_auto_continues: 10, + inject_todo_instructions: true, + continuation_prompt: None, + repl_prelude: None, cmd_prelude: None, agent_session: None, diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 49c0055..27e4cc4 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -1,4 +1,3 @@ -use super::MessageContentToolCalls; use super::rag_cache::{RagCache, RagKey}; use super::session::Session; use super::todo::TodoList; @@ -9,6 +8,7 @@ use super::{ SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME, TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, paths, }; +use super::{MessageContentToolCalls, prompts}; use crate::client::{Model, ModelType, list_models}; use crate::function::{ FunctionDeclaration, Functions, ToolCallTracker, ToolResult, @@ -38,6 +38,13 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; +pub struct AutoContinueConfig { + pub enabled: bool, + pub max_continues: usize, + pub inject_instructions: bool, + pub continuation_prompt: Option, +} + pub struct RequestContext { pub app: Arc, @@ -523,7 +530,7 @@ impl RequestContext { } pub fn extract_role(&self, app: &AppConfig) -> Role { - if let Some(session) = self.session.as_ref() { + let mut role = if let Some(session) = self.session.as_ref() { session.to_role() } else if let Some(agent) = self.agent.as_ref() { agent.to_role() @@ -539,6 +546,65 @@ impl RequestContext { app.enabled_mcp_servers.clone(), ); role + }; + + if self.agent.is_none() { + let config = self.auto_continue_config(); + if config.enabled && config.inject_instructions { + role.append_to_prompt(prompts::DEFAULT_TODO_INSTRUCTIONS); + } + } + + role + } + + pub fn auto_continue_config(&self) -> AutoContinueConfig { + if let Some(agent) = &self.agent { + return AutoContinueConfig { + enabled: agent.auto_continue_enabled(), + max_continues: agent.max_auto_continues(), + inject_instructions: agent.inject_todo_instructions(), + continuation_prompt: agent.continuation_prompt_value(), + }; + } + let app = &self.app.config; + let enabled = self + .session + .as_ref() + .and_then(|s| s.auto_continue()) + .or_else(|| self.role.as_ref().and_then(|r| r.auto_continue())) + .unwrap_or(app.auto_continue); + let max = self + .session + .as_ref() + .and_then(|s| s.max_auto_continues()) + .or_else(|| self.role.as_ref().and_then(|r| r.max_auto_continues())) + .unwrap_or(app.max_auto_continues); + let inject = self + .session + .as_ref() + .and_then(|s| s.inject_todo_instructions()) + .or_else(|| { + self.role + .as_ref() + .and_then(|r| r.inject_todo_instructions()) + }) + .unwrap_or(app.inject_todo_instructions); + let prompt = self + .session + .as_ref() + .and_then(|s| s.continuation_prompt().map(|v| v.to_string())) + .or_else(|| { + self.role + .as_ref() + .and_then(|r| r.continuation_prompt().map(|v| v.to_string())) + }) + .or_else(|| app.continuation_prompt.clone()); + AutoContinueConfig { + enabled, + max_continues: max, + inject_instructions: inject, + continuation_prompt: prompt, } } @@ -747,6 +813,8 @@ impl RequestContext { app.function_calling_support.to_string(), ), ("mcp_server_support", app.mcp_server_support.to_string()), + ("auto_continue", app.auto_continue.to_string()), + ("max_auto_continues", app.max_auto_continues.to_string()), ("stream", app.stream.to_string()), ("save", app.save.to_string()), ("keybindings", app.keybindings.clone()), @@ -1402,12 +1470,24 @@ impl RequestContext { } pub async fn update(&mut self, data: &str, abort_signal: AbortSignal) -> Result<()> { - let parts: Vec<&str> = data.split_whitespace().collect(); - if parts.len() != 2 { + let (key, raw_value) = match data.split_once(char::is_whitespace) { + Some((k, v)) => (k, v.trim()), + None => bail!("Usage: .set . If value is null, unset key."), + }; + + if raw_value.is_empty() { bail!("Usage: .set . If value is null, unset key."); } - let key = parts[0]; - let value = parts[1]; + + let value = match key { + "continuation_prompt" => raw_value, + _ => { + if raw_value.contains(char::is_whitespace) { + bail!("Usage: .set . If value is null, unset key."); + } + raw_value + } + }; match key { "temperature" => { let value = super::parse_value(value)?; @@ -1522,6 +1602,49 @@ impl RequestContext { let value = value.parse().with_context(|| "Invalid value")?; self.update_app_config(|app| app.highlight = value); } + "auto_continue" => { + let value: bool = value.parse().with_context(|| "Invalid value")?; + if value && !self.app.config.function_calling_support { + bail!( + "Cannot enable auto_continue: function calling is disabled. Set 'function_calling_support: true' first." + ); + } + if let Some(session) = self.session.as_mut() { + session.set_auto_continue(Some(value)); + } else { + self.update_app_config(|app| app.auto_continue = value); + } + if value + && self.app.config.function_calling_support + && !self.tool_scope.functions.contains("todo__init") + { + self.tool_scope.functions.append_todo_functions(); + } + } + "max_auto_continues" => { + let value: usize = value.parse().with_context(|| "Invalid value")?; + if let Some(session) = self.session.as_mut() { + session.set_max_auto_continues(Some(value)); + } else { + self.update_app_config(|app| app.max_auto_continues = value); + } + } + "inject_todo_instructions" => { + let value: bool = value.parse().with_context(|| "Invalid value")?; + if let Some(session) = self.session.as_mut() { + session.set_inject_todo_instructions(Some(value)); + } else { + self.update_app_config(|app| app.inject_todo_instructions = value); + } + } + "continuation_prompt" => { + let value: Option = super::parse_value(value)?; + if let Some(session) = self.session.as_mut() { + session.set_continuation_prompt(value); + } else { + self.update_app_config(|app| app.continuation_prompt = value); + } + } _ => bail!("Unknown key '{key}'"), } Ok(()) @@ -1603,10 +1726,14 @@ impl RequestContext { }, ".set" => { let mut values = vec![ + "auto_continue", + "continuation_prompt", "temperature", "top_p", "enabled_tools", "enabled_mcp_servers", + "inject_todo_instructions", + "max_auto_continues", "save_session", "compression_threshold", "rag_reranker_model", @@ -1721,6 +1848,19 @@ impl RequestContext { .map(|v| v.id()) .collect(), "highlight" => super::complete_bool(app.highlight), + "auto_continue" => { + let config = self.auto_continue_config(); + super::complete_bool(config.enabled) + } + "max_auto_continues" => { + let config = self.auto_continue_config(); + vec![config.max_continues.to_string()] + } + "inject_todo_instructions" => { + let config = self.auto_continue_config(); + super::complete_bool(config.inject_instructions) + } + "continuation_prompt" => vec!["null".to_string()], _ => vec![], }; values = candidates.into_iter().map(|v| (v, None)).collect(); @@ -1810,6 +1950,12 @@ impl RequestContext { if self.working_mode.is_repl() { functions.append_user_interaction_functions(); } + if self.agent.is_none() + && app.function_calling_support + && self.auto_continue_config().enabled + { + functions.append_todo_functions(); + } if !mcp_runtime.is_empty() { functions.append_mcp_meta_functions(mcp_runtime.server_names()); } @@ -2196,7 +2342,7 @@ impl RequestContext { .clone() .unwrap_or_else(|| SUMMARY_CONTEXT_PROMPT.into()); - let todo_prefix = if self.agent.is_some() && !self.todo_list.is_empty() { + let todo_prefix = if self.auto_continue_config().enabled && !self.todo_list.is_empty() { format!( "[ACTIVE TODO LIST]\n{}\n\n", self.todo_list.render_for_model() diff --git a/src/config/role.rs b/src/config/role.rs index 603d90c..4f8c665 100644 --- a/src/config/role.rs +++ b/src/config/role.rs @@ -55,6 +55,14 @@ pub struct Role { enabled_tools: Option, #[serde(skip_serializing_if = "Option::is_none")] enabled_mcp_servers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + auto_continue: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_auto_continues: Option, + #[serde(skip_serializing_if = "Option::is_none")] + inject_todo_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + continuation_prompt: Option, #[serde(skip)] model: Model, @@ -90,6 +98,16 @@ impl Role { "enabled_mcp_servers" => { role.enabled_mcp_servers = value.as_str().map(|v| v.to_string()) } + "auto_continue" => role.auto_continue = value.as_bool(), + "max_auto_continues" => { + role.max_auto_continues = value.as_u64().map(|v| v as usize) + } + "inject_todo_instructions" => { + role.inject_todo_instructions = value.as_bool() + } + "continuation_prompt" => { + role.continuation_prompt = value.as_str().map(|v| v.to_string()) + } _ => (), } } @@ -131,6 +149,18 @@ impl Role { if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() { metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}")); } + if let Some(auto_continue) = self.auto_continue { + metadata.push(format!("auto_continue: {auto_continue}")); + } + if let Some(max_auto_continues) = self.max_auto_continues { + metadata.push(format!("max_auto_continues: {max_auto_continues}")); + } + if let Some(inject_todo_instructions) = self.inject_todo_instructions { + metadata.push(format!("inject_todo_instructions: {inject_todo_instructions}")); + } + if let Some(continuation_prompt) = &self.continuation_prompt { + metadata.push(format!("continuation_prompt: {continuation_prompt}")); + } if metadata.is_empty() { format!("{}\n", self.prompt) } else if self.prompt.is_empty() { @@ -225,6 +255,26 @@ impl Role { self.prompt.contains(INPUT_PLACEHOLDER) } + pub fn auto_continue(&self) -> Option { + self.auto_continue + } + + pub fn max_auto_continues(&self) -> Option { + self.max_auto_continues + } + + pub fn inject_todo_instructions(&self) -> Option { + self.inject_todo_instructions + } + + pub fn continuation_prompt(&self) -> Option<&str> { + self.continuation_prompt.as_deref() + } + + pub fn append_to_prompt(&mut self, text: &str) { + self.prompt.push_str(text); + } + pub fn echo_messages(&self, input: &Input) -> String { let input_markdown = input.render(); if self.is_empty_prompt() { diff --git a/src/config/session.rs b/src/config/session.rs index a03ba55..ad23b0f 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -32,6 +32,14 @@ pub struct Session { save_session: Option, #[serde(skip_serializing_if = "Option::is_none")] compression_threshold: Option, + #[serde(skip_serializing_if = "Option::is_none")] + auto_continue: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_auto_continues: Option, + #[serde(skip_serializing_if = "Option::is_none")] + inject_todo_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + continuation_prompt: Option, #[serde(skip_serializing_if = "Option::is_none")] role_name: Option, @@ -170,6 +178,18 @@ impl Session { if let Some(save_session) = self.save_session() { data["save_session"] = save_session.into(); } + if let Some(auto_continue) = self.auto_continue() { + data["auto_continue"] = auto_continue.into(); + } + if let Some(max_auto_continues) = self.max_auto_continues() { + data["max_auto_continues"] = max_auto_continues.into(); + } + if let Some(inject_todo_instructions) = self.inject_todo_instructions() { + data["inject_todo_instructions"] = inject_todo_instructions.into(); + } + if let Some(continuation_prompt) = self.continuation_prompt() { + data["continuation_prompt"] = continuation_prompt.into(); + } let (tokens, percent) = self.tokens_usage(); data["total_tokens"] = tokens.into(); if let Some(max_input_tokens) = self.model().max_input_tokens() { @@ -225,6 +245,19 @@ impl Session { items.push(("compression_threshold", compression_threshold.to_string())); } + if let Some(auto_continue) = self.auto_continue() { + items.push(("auto_continue", auto_continue.to_string())); + } + if let Some(max_auto_continues) = self.max_auto_continues() { + items.push(("max_auto_continues", max_auto_continues.to_string())); + } + if let Some(inject_todo_instructions) = self.inject_todo_instructions() { + items.push(("inject_todo_instructions", inject_todo_instructions.to_string())); + } + if let Some(continuation_prompt) = self.continuation_prompt() { + items.push(("continuation_prompt", continuation_prompt.to_string())); + } + if let Some(max_input_tokens) = self.model().max_input_tokens() { items.push(("max_input_tokens", max_input_tokens.to_string())); } @@ -335,6 +368,50 @@ impl Session { } } + pub fn auto_continue(&self) -> Option { + self.auto_continue + } + + pub fn max_auto_continues(&self) -> Option { + self.max_auto_continues + } + + pub fn set_auto_continue(&mut self, value: Option) { + if self.auto_continue != value { + self.auto_continue = value; + self.dirty = true; + } + } + + pub fn set_max_auto_continues(&mut self, value: Option) { + if self.max_auto_continues != value { + self.max_auto_continues = value; + self.dirty = true; + } + } + + pub fn inject_todo_instructions(&self) -> Option { + self.inject_todo_instructions + } + + pub fn continuation_prompt(&self) -> Option<&str> { + self.continuation_prompt.as_deref() + } + + pub fn set_inject_todo_instructions(&mut self, value: Option) { + if self.inject_todo_instructions != value { + self.inject_todo_instructions = value; + self.dirty = true; + } + } + + pub fn set_continuation_prompt(&mut self, value: Option) { + if self.continuation_prompt != value { + self.continuation_prompt = value; + self.dirty = true; + } + } + pub fn needs_compression(&self, global_compression_threshold: usize) -> bool { if self.compressing { return false; diff --git a/src/function/todo.rs b/src/function/todo.rs index ed8e76e..9415849 100644 --- a/src/function/todo.rs +++ b/src/function/todo.rs @@ -94,8 +94,14 @@ pub fn handle_todo_tool(ctx: &mut RequestContext, cmd_name: &str, args: &Value) .strip_prefix(TODO_FUNCTION_PREFIX) .unwrap_or(cmd_name); - if ctx.agent.is_none() { - bail!("No active agent"); + if !ctx.app.config.function_calling_support { + bail!("Cannot use todo tools: function calling is disabled."); + } + let auto_config = ctx.auto_continue_config(); + if !auto_config.enabled { + bail!( + "Auto-continue is not enabled. Set 'auto_continue: true' in your config to use todo tools." + ); } match action { diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 6f6b886..4b731dd 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -31,9 +31,19 @@ use reedline::{ use reedline::{MenuBuilder, Signal}; use std::sync::LazyLock; use std::{env, process, sync::Arc}; +use indoc::indoc; const MENU_NAME: &str = "completion_menu"; +pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {" + [SYSTEM REMINDER - TODO CONTINUATION] + You have incomplete tasks. Rules: + 1. BEFORE marking a todo done: verify the work compiles/works. No premature completion. + 2. If a todo is broad (e.g. \"implement X and implement Y\"): break it into specific subtasks FIRST using todo__add, then work on those.\n\ + 3. Each todo should be atomic and be \"single responsibility\" - completable in one focused action. + 4. Continue with the next pending item now. Call tools immediately." +}; + static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| { [ ReplCommand::new(".help", "Show this help guide", AssertState::pass()), @@ -141,7 +151,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| { ReplCommand::new( ".clear todo", "Clear the todo list and stop auto-continuation", - AssertState::True(StateFlags::AGENT), + AssertState::pass(), ), ReplCommand::new( ".rag", @@ -764,25 +774,18 @@ pub async fn run_repl_command( bail!("Use '.empty session' instead"); } Some("todo") => { - let cleared = match ctx.agent.as_mut() { - Some(agent) => { - if !agent.auto_continue_enabled() { - bail!( - "The todo system is not enabled for this agent. Set 'auto_continue: true' in the agent's config.yaml to enable it." - ); - } - if ctx.todo_list.is_empty() { - println!("Todo list is already empty."); - false - } else { - ctx.clear_todo_list(); - println!("Todo list cleared."); - true - } - } - None => bail!("No active agent"), - }; - let _ = cleared; + let config = ctx.auto_continue_config(); + if !config.enabled { + bail!( + "Auto-continue is not enabled. Set 'auto_continue: true' in your config to enable it." + ); + } + if ctx.todo_list.is_empty() { + println!("Todo list is already empty."); + } else { + ctx.clear_todo_list(); + println!("Todo list cleared."); + } } _ => unknown_command()?, }, @@ -881,19 +884,22 @@ async fn ask( ) .await } else { - let should_continue = agent_should_continue(ctx); + let do_continue = should_continue(ctx); - if should_continue { + if do_continue { let full_prompt = { + let config = ctx.auto_continue_config(); let todo_state = ctx.todo_list.render_for_model(); let remaining = ctx.todo_list.incomplete_count(); ctx.set_last_continuation_response(output.clone()); ctx.increment_auto_continue_count(); - let agent = ctx.agent.as_mut().expect("agent checked above"); let count = ctx.auto_continue_count; - let max = agent.max_auto_continues(); + let max = config.max_continues; - let prompt = agent.continuation_prompt(); + let prompt = config + .continuation_prompt + .as_deref() + .unwrap_or(DEFAULT_CONTINUATION_PROMPT); let color = if app.light_theme() { nu_ansi_term::Color::LightGray @@ -934,7 +940,7 @@ async fn ask( .is_some_and(|s| s.needs_compression(app.compression_threshold)); if needs_compression { - let agent_can_continue_after_compress = agent_should_continue(ctx); + let agent_can_continue_after_compress = should_continue(ctx); if let Some(session) = ctx.session.as_mut() { session.set_compressing(true); @@ -956,14 +962,17 @@ async fn ask( if agent_can_continue_after_compress { let full_prompt = { + let config = ctx.auto_continue_config(); let todo_state = ctx.todo_list.render_for_model(); let remaining = ctx.todo_list.incomplete_count(); ctx.increment_auto_continue_count(); - let agent = ctx.agent.as_mut().expect("agent checked above"); let count = ctx.auto_continue_count; - let max = agent.max_auto_continues(); + let max = config.max_continues; - let prompt = agent.continuation_prompt(); + let prompt = config + .continuation_prompt + .as_deref() + .unwrap_or(DEFAULT_CONTINUATION_PROMPT); let color = if app.light_theme() { nu_ansi_term::Color::LightGray @@ -989,10 +998,11 @@ async fn ask( } } -fn agent_should_continue(ctx: &RequestContext) -> bool { - ctx.agent.as_ref().is_some_and(|agent| { - agent.auto_continue_enabled() && ctx.auto_continue_count < agent.max_auto_continues() - }) && ctx.todo_list.has_incomplete() +fn should_continue(ctx: &RequestContext) -> bool { + let config = ctx.auto_continue_config(); + config.enabled + && ctx.auto_continue_count < config.max_continues + && ctx.todo_list.has_incomplete() } fn reset_continuation(ctx: &mut RequestContext) { @@ -1311,13 +1321,15 @@ mod tests { } #[test] - fn repl_commands_clear_todo_requires_agent() { + fn repl_commands_clear_todo_always_available() { let cmd = REPL_COMMANDS .iter() .find(|c| c.name == ".clear todo") .unwrap(); assert!(cmd.is_valid(StateFlags::AGENT)); - assert!(!cmd.is_valid(StateFlags::empty())); + assert!(cmd.is_valid(StateFlags::empty())); + assert!(cmd.is_valid(StateFlags::SESSION)); + assert!(cmd.is_valid(StateFlags::ROLE)); } #[test]