feat: add auto-continue support to all contexts
This commit is contained in:
@@ -17,9 +17,11 @@ agent_session: null # Set a session to use when starting the agent.
|
||||
name: <agent-name> # Name of the agent, used in the UI and logs
|
||||
description: <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
|
||||
|
||||
@@ -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
|
||||
|
||||
+14
-1
@@ -1,5 +1,9 @@
|
||||
---
|
||||
# Everything in this section is optional
|
||||
############################################
|
||||
## Everything in this section is optional ##
|
||||
############################################
|
||||
|
||||
# Role Configuration
|
||||
name: <role-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.
|
||||
|
||||
+8
-12
@@ -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<String> {
|
||||
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<usize> {
|
||||
self.config.compression_threshold
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ pub struct AppConfig {
|
||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||
pub enabled_mcp_servers: Option<String>,
|
||||
|
||||
pub auto_continue: bool,
|
||||
pub max_auto_continues: usize,
|
||||
pub inject_todo_instructions: bool,
|
||||
pub continuation_prompt: Option<String>,
|
||||
|
||||
pub repl_prelude: Option<String>,
|
||||
pub cmd_prelude: Option<String>,
|
||||
pub agent_session: Option<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -121,6 +121,11 @@ pub struct Config {
|
||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||
pub enabled_mcp_servers: Option<String>,
|
||||
|
||||
pub auto_continue: bool,
|
||||
pub max_auto_continues: usize,
|
||||
pub inject_todo_instructions: bool,
|
||||
pub continuation_prompt: Option<String>,
|
||||
|
||||
pub repl_prelude: Option<String>,
|
||||
pub cmd_prelude: Option<String>,
|
||||
pub agent_session: Option<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
pub struct RequestContext {
|
||||
pub app: Arc<AppState>,
|
||||
|
||||
@@ -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 <key> <value>. If value is null, unset key."),
|
||||
};
|
||||
|
||||
if raw_value.is_empty() {
|
||||
bail!("Usage: .set <key> <value>. 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 <key> <value>. 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<String> = 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()
|
||||
|
||||
@@ -55,6 +55,14 @@ pub struct Role {
|
||||
enabled_tools: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
enabled_mcp_servers: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
auto_continue: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_auto_continues: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
inject_todo_instructions: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
continuation_prompt: Option<String>,
|
||||
|
||||
#[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<bool> {
|
||||
self.auto_continue
|
||||
}
|
||||
|
||||
pub fn max_auto_continues(&self) -> Option<usize> {
|
||||
self.max_auto_continues
|
||||
}
|
||||
|
||||
pub fn inject_todo_instructions(&self) -> Option<bool> {
|
||||
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() {
|
||||
|
||||
@@ -32,6 +32,14 @@ pub struct Session {
|
||||
save_session: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
compression_threshold: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
auto_continue: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_auto_continues: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
inject_todo_instructions: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
continuation_prompt: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
role_name: Option<String>,
|
||||
@@ -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<bool> {
|
||||
self.auto_continue
|
||||
}
|
||||
|
||||
pub fn max_auto_continues(&self) -> Option<usize> {
|
||||
self.max_auto_continues
|
||||
}
|
||||
|
||||
pub fn set_auto_continue(&mut self, value: Option<bool>) {
|
||||
if self.auto_continue != value {
|
||||
self.auto_continue = value;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_max_auto_continues(&mut self, value: Option<usize>) {
|
||||
if self.max_auto_continues != value {
|
||||
self.max_auto_continues = value;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inject_todo_instructions(&self) -> Option<bool> {
|
||||
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<bool>) {
|
||||
if self.inject_todo_instructions != value {
|
||||
self.inject_todo_instructions = value;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_continuation_prompt(&mut self, value: Option<String>) {
|
||||
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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+47
-35
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user