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
|
name: <agent-name> # Name of the agent, used in the UI and logs
|
||||||
description: <description> # Description of the agent, used in the UI
|
description: <description> # Description of the agent, used in the UI
|
||||||
version: 1 # Version of the agent
|
version: 1 # Version of the agent
|
||||||
# Todo System & Auto-Continuation
|
# Auto-Continue (Todo System)
|
||||||
# These settings help smaller models handle multi-step tasks more reliably.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
# See docs/TODO-SYSTEM.md for detailed documentation.
|
# 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
|
auto_continue: false # Enable automatic continuation when incomplete todos remain
|
||||||
max_auto_continues: 10 # Maximum number of automatic continuations before stopping
|
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
|
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
|
git: github,gitmcp
|
||||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
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 ----
|
# ---- Session ----
|
||||||
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
# 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
|
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
|
name: <role-name> # The name of the role
|
||||||
model: openai:gpt-4o # The model to use for this 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
|
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
|
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
|
prompt: null # A custom prompt to use for this role that will immediately query
|
||||||
# the model for output instead of using the instructions below
|
# 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.
|
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
|
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 {
|
pub fn can_spawn_agents(&self) -> bool {
|
||||||
self.config.can_spawn_agents
|
self.config.can_spawn_agents
|
||||||
}
|
}
|
||||||
@@ -439,18 +447,6 @@ impl Agent {
|
|||||||
self.config.escalation_timeout
|
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> {
|
pub fn compression_threshold(&self) -> Option<usize> {
|
||||||
self.config.compression_threshold
|
self.config.compression_threshold
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ pub struct AppConfig {
|
|||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<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 repl_prelude: Option<String>,
|
||||||
pub cmd_prelude: Option<String>,
|
pub cmd_prelude: Option<String>,
|
||||||
pub agent_session: Option<String>,
|
pub agent_session: Option<String>,
|
||||||
@@ -95,6 +100,11 @@ impl Default for AppConfig {
|
|||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
|
|
||||||
|
auto_continue: false,
|
||||||
|
max_auto_continues: 10,
|
||||||
|
inject_todo_instructions: true,
|
||||||
|
continuation_prompt: None,
|
||||||
|
|
||||||
repl_prelude: None,
|
repl_prelude: None,
|
||||||
cmd_prelude: None,
|
cmd_prelude: None,
|
||||||
agent_session: None,
|
agent_session: None,
|
||||||
@@ -152,6 +162,11 @@ impl AppConfig {
|
|||||||
mapping_mcp_servers: config.mapping_mcp_servers,
|
mapping_mcp_servers: config.mapping_mcp_servers,
|
||||||
enabled_mcp_servers: config.enabled_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,
|
repl_prelude: config.repl_prelude,
|
||||||
cmd_prelude: config.cmd_prelude,
|
cmd_prelude: config.cmd_prelude,
|
||||||
agent_session: config.agent_session,
|
agent_session: config.agent_session,
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ pub struct Config {
|
|||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<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 repl_prelude: Option<String>,
|
||||||
pub cmd_prelude: Option<String>,
|
pub cmd_prelude: Option<String>,
|
||||||
pub agent_session: Option<String>,
|
pub agent_session: Option<String>,
|
||||||
@@ -177,6 +182,11 @@ impl Default for Config {
|
|||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
|
|
||||||
|
auto_continue: false,
|
||||||
|
max_auto_continues: 10,
|
||||||
|
inject_todo_instructions: true,
|
||||||
|
continuation_prompt: None,
|
||||||
|
|
||||||
repl_prelude: None,
|
repl_prelude: None,
|
||||||
cmd_prelude: None,
|
cmd_prelude: None,
|
||||||
agent_session: None,
|
agent_session: None,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::MessageContentToolCalls;
|
|
||||||
use super::rag_cache::{RagCache, RagKey};
|
use super::rag_cache::{RagCache, RagKey};
|
||||||
use super::session::Session;
|
use super::session::Session;
|
||||||
use super::todo::TodoList;
|
use super::todo::TodoList;
|
||||||
@@ -9,6 +8,7 @@ use super::{
|
|||||||
SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME, TEMP_SESSION_NAME,
|
SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME, TEMP_SESSION_NAME,
|
||||||
WorkingMode, ensure_parent_exists, list_agents, paths,
|
WorkingMode, ensure_parent_exists, list_agents, paths,
|
||||||
};
|
};
|
||||||
|
use super::{MessageContentToolCalls, prompts};
|
||||||
use crate::client::{Model, ModelType, list_models};
|
use crate::client::{Model, ModelType, list_models};
|
||||||
use crate::function::{
|
use crate::function::{
|
||||||
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
||||||
@@ -38,6 +38,13 @@ use std::io::Write;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
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 struct RequestContext {
|
||||||
pub app: Arc<AppState>,
|
pub app: Arc<AppState>,
|
||||||
|
|
||||||
@@ -523,7 +530,7 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_role(&self, app: &AppConfig) -> Role {
|
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()
|
session.to_role()
|
||||||
} else if let Some(agent) = self.agent.as_ref() {
|
} else if let Some(agent) = self.agent.as_ref() {
|
||||||
agent.to_role()
|
agent.to_role()
|
||||||
@@ -539,6 +546,65 @@ impl RequestContext {
|
|||||||
app.enabled_mcp_servers.clone(),
|
app.enabled_mcp_servers.clone(),
|
||||||
);
|
);
|
||||||
role
|
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(),
|
app.function_calling_support.to_string(),
|
||||||
),
|
),
|
||||||
("mcp_server_support", app.mcp_server_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()),
|
("stream", app.stream.to_string()),
|
||||||
("save", app.save.to_string()),
|
("save", app.save.to_string()),
|
||||||
("keybindings", app.keybindings.clone()),
|
("keybindings", app.keybindings.clone()),
|
||||||
@@ -1402,12 +1470,24 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self, data: &str, abort_signal: AbortSignal) -> Result<()> {
|
pub async fn update(&mut self, data: &str, abort_signal: AbortSignal) -> Result<()> {
|
||||||
let parts: Vec<&str> = data.split_whitespace().collect();
|
let (key, raw_value) = match data.split_once(char::is_whitespace) {
|
||||||
if parts.len() != 2 {
|
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.");
|
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 {
|
match key {
|
||||||
"temperature" => {
|
"temperature" => {
|
||||||
let value = super::parse_value(value)?;
|
let value = super::parse_value(value)?;
|
||||||
@@ -1522,6 +1602,49 @@ impl RequestContext {
|
|||||||
let value = value.parse().with_context(|| "Invalid value")?;
|
let value = value.parse().with_context(|| "Invalid value")?;
|
||||||
self.update_app_config(|app| app.highlight = 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}'"),
|
_ => bail!("Unknown key '{key}'"),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1603,10 +1726,14 @@ impl RequestContext {
|
|||||||
},
|
},
|
||||||
".set" => {
|
".set" => {
|
||||||
let mut values = vec![
|
let mut values = vec![
|
||||||
|
"auto_continue",
|
||||||
|
"continuation_prompt",
|
||||||
"temperature",
|
"temperature",
|
||||||
"top_p",
|
"top_p",
|
||||||
"enabled_tools",
|
"enabled_tools",
|
||||||
"enabled_mcp_servers",
|
"enabled_mcp_servers",
|
||||||
|
"inject_todo_instructions",
|
||||||
|
"max_auto_continues",
|
||||||
"save_session",
|
"save_session",
|
||||||
"compression_threshold",
|
"compression_threshold",
|
||||||
"rag_reranker_model",
|
"rag_reranker_model",
|
||||||
@@ -1721,6 +1848,19 @@ impl RequestContext {
|
|||||||
.map(|v| v.id())
|
.map(|v| v.id())
|
||||||
.collect(),
|
.collect(),
|
||||||
"highlight" => super::complete_bool(app.highlight),
|
"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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
values = candidates.into_iter().map(|v| (v, None)).collect();
|
values = candidates.into_iter().map(|v| (v, None)).collect();
|
||||||
@@ -1810,6 +1950,12 @@ impl RequestContext {
|
|||||||
if self.working_mode.is_repl() {
|
if self.working_mode.is_repl() {
|
||||||
functions.append_user_interaction_functions();
|
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() {
|
if !mcp_runtime.is_empty() {
|
||||||
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
||||||
}
|
}
|
||||||
@@ -2196,7 +2342,7 @@ impl RequestContext {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| SUMMARY_CONTEXT_PROMPT.into());
|
.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!(
|
format!(
|
||||||
"[ACTIVE TODO LIST]\n{}\n\n",
|
"[ACTIVE TODO LIST]\n{}\n\n",
|
||||||
self.todo_list.render_for_model()
|
self.todo_list.render_for_model()
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ pub struct Role {
|
|||||||
enabled_tools: Option<String>,
|
enabled_tools: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enabled_mcp_servers: Option<String>,
|
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)]
|
#[serde(skip)]
|
||||||
model: Model,
|
model: Model,
|
||||||
@@ -90,6 +98,16 @@ impl Role {
|
|||||||
"enabled_mcp_servers" => {
|
"enabled_mcp_servers" => {
|
||||||
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
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() {
|
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
|
||||||
metadata.push(format!("enabled_mcp_servers: {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() {
|
if metadata.is_empty() {
|
||||||
format!("{}\n", self.prompt)
|
format!("{}\n", self.prompt)
|
||||||
} else if self.prompt.is_empty() {
|
} else if self.prompt.is_empty() {
|
||||||
@@ -225,6 +255,26 @@ impl Role {
|
|||||||
self.prompt.contains(INPUT_PLACEHOLDER)
|
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 {
|
pub fn echo_messages(&self, input: &Input) -> String {
|
||||||
let input_markdown = input.render();
|
let input_markdown = input.render();
|
||||||
if self.is_empty_prompt() {
|
if self.is_empty_prompt() {
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ pub struct Session {
|
|||||||
save_session: Option<bool>,
|
save_session: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compression_threshold: Option<usize>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
role_name: Option<String>,
|
role_name: Option<String>,
|
||||||
@@ -170,6 +178,18 @@ impl Session {
|
|||||||
if let Some(save_session) = self.save_session() {
|
if let Some(save_session) = self.save_session() {
|
||||||
data["save_session"] = save_session.into();
|
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();
|
let (tokens, percent) = self.tokens_usage();
|
||||||
data["total_tokens"] = tokens.into();
|
data["total_tokens"] = tokens.into();
|
||||||
if let Some(max_input_tokens) = self.model().max_input_tokens() {
|
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()));
|
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() {
|
if let Some(max_input_tokens) = self.model().max_input_tokens() {
|
||||||
items.push(("max_input_tokens", max_input_tokens.to_string()));
|
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 {
|
pub fn needs_compression(&self, global_compression_threshold: usize) -> bool {
|
||||||
if self.compressing {
|
if self.compressing {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -94,8 +94,14 @@ pub fn handle_todo_tool(ctx: &mut RequestContext, cmd_name: &str, args: &Value)
|
|||||||
.strip_prefix(TODO_FUNCTION_PREFIX)
|
.strip_prefix(TODO_FUNCTION_PREFIX)
|
||||||
.unwrap_or(cmd_name);
|
.unwrap_or(cmd_name);
|
||||||
|
|
||||||
if ctx.agent.is_none() {
|
if !ctx.app.config.function_calling_support {
|
||||||
bail!("No active agent");
|
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 {
|
match action {
|
||||||
|
|||||||
+47
-35
@@ -31,9 +31,19 @@ use reedline::{
|
|||||||
use reedline::{MenuBuilder, Signal};
|
use reedline::{MenuBuilder, Signal};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{env, process, sync::Arc};
|
use std::{env, process, sync::Arc};
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
const MENU_NAME: &str = "completion_menu";
|
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(|| {
|
static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| {
|
||||||
[
|
[
|
||||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||||
@@ -141,7 +151,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| {
|
|||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".clear todo",
|
".clear todo",
|
||||||
"Clear the todo list and stop auto-continuation",
|
"Clear the todo list and stop auto-continuation",
|
||||||
AssertState::True(StateFlags::AGENT),
|
AssertState::pass(),
|
||||||
),
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".rag",
|
".rag",
|
||||||
@@ -764,25 +774,18 @@ pub async fn run_repl_command(
|
|||||||
bail!("Use '.empty session' instead");
|
bail!("Use '.empty session' instead");
|
||||||
}
|
}
|
||||||
Some("todo") => {
|
Some("todo") => {
|
||||||
let cleared = match ctx.agent.as_mut() {
|
let config = ctx.auto_continue_config();
|
||||||
Some(agent) => {
|
if !config.enabled {
|
||||||
if !agent.auto_continue_enabled() {
|
bail!(
|
||||||
bail!(
|
"Auto-continue is not enabled. Set 'auto_continue: true' in your config to enable it."
|
||||||
"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() {
|
||||||
if ctx.todo_list.is_empty() {
|
println!("Todo list is already empty.");
|
||||||
println!("Todo list is already empty.");
|
} else {
|
||||||
false
|
ctx.clear_todo_list();
|
||||||
} else {
|
println!("Todo list cleared.");
|
||||||
ctx.clear_todo_list();
|
}
|
||||||
println!("Todo list cleared.");
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => bail!("No active agent"),
|
|
||||||
};
|
|
||||||
let _ = cleared;
|
|
||||||
}
|
}
|
||||||
_ => unknown_command()?,
|
_ => unknown_command()?,
|
||||||
},
|
},
|
||||||
@@ -881,19 +884,22 @@ async fn ask(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
let should_continue = agent_should_continue(ctx);
|
let do_continue = should_continue(ctx);
|
||||||
|
|
||||||
if should_continue {
|
if do_continue {
|
||||||
let full_prompt = {
|
let full_prompt = {
|
||||||
|
let config = ctx.auto_continue_config();
|
||||||
let todo_state = ctx.todo_list.render_for_model();
|
let todo_state = ctx.todo_list.render_for_model();
|
||||||
let remaining = ctx.todo_list.incomplete_count();
|
let remaining = ctx.todo_list.incomplete_count();
|
||||||
ctx.set_last_continuation_response(output.clone());
|
ctx.set_last_continuation_response(output.clone());
|
||||||
ctx.increment_auto_continue_count();
|
ctx.increment_auto_continue_count();
|
||||||
let agent = ctx.agent.as_mut().expect("agent checked above");
|
|
||||||
let count = ctx.auto_continue_count;
|
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() {
|
let color = if app.light_theme() {
|
||||||
nu_ansi_term::Color::LightGray
|
nu_ansi_term::Color::LightGray
|
||||||
@@ -934,7 +940,7 @@ async fn ask(
|
|||||||
.is_some_and(|s| s.needs_compression(app.compression_threshold));
|
.is_some_and(|s| s.needs_compression(app.compression_threshold));
|
||||||
|
|
||||||
if needs_compression {
|
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() {
|
if let Some(session) = ctx.session.as_mut() {
|
||||||
session.set_compressing(true);
|
session.set_compressing(true);
|
||||||
@@ -956,14 +962,17 @@ async fn ask(
|
|||||||
|
|
||||||
if agent_can_continue_after_compress {
|
if agent_can_continue_after_compress {
|
||||||
let full_prompt = {
|
let full_prompt = {
|
||||||
|
let config = ctx.auto_continue_config();
|
||||||
let todo_state = ctx.todo_list.render_for_model();
|
let todo_state = ctx.todo_list.render_for_model();
|
||||||
let remaining = ctx.todo_list.incomplete_count();
|
let remaining = ctx.todo_list.incomplete_count();
|
||||||
ctx.increment_auto_continue_count();
|
ctx.increment_auto_continue_count();
|
||||||
let agent = ctx.agent.as_mut().expect("agent checked above");
|
|
||||||
let count = ctx.auto_continue_count;
|
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() {
|
let color = if app.light_theme() {
|
||||||
nu_ansi_term::Color::LightGray
|
nu_ansi_term::Color::LightGray
|
||||||
@@ -989,10 +998,11 @@ async fn ask(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn agent_should_continue(ctx: &RequestContext) -> bool {
|
fn should_continue(ctx: &RequestContext) -> bool {
|
||||||
ctx.agent.as_ref().is_some_and(|agent| {
|
let config = ctx.auto_continue_config();
|
||||||
agent.auto_continue_enabled() && ctx.auto_continue_count < agent.max_auto_continues()
|
config.enabled
|
||||||
}) && ctx.todo_list.has_incomplete()
|
&& ctx.auto_continue_count < config.max_continues
|
||||||
|
&& ctx.todo_list.has_incomplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_continuation(ctx: &mut RequestContext) {
|
fn reset_continuation(ctx: &mut RequestContext) {
|
||||||
@@ -1311,13 +1321,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_commands_clear_todo_requires_agent() {
|
fn repl_commands_clear_todo_always_available() {
|
||||||
let cmd = REPL_COMMANDS
|
let cmd = REPL_COMMANDS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.name == ".clear todo")
|
.find(|c| c.name == ".clear todo")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(cmd.is_valid(StateFlags::AGENT));
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user