From a935add2a7ec66bf48c1ab54442c818edd39d91b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 9 Feb 2026 12:49:06 -0700 Subject: [PATCH] feat: Implemented a built-in task management system to help smaller LLMs complete larger multistep tasks and minimize context drift --- README.md | 1 + config.agent.example.yaml | 7 + docs/AGENTS.md | 50 ++++++ docs/TODO-SYSTEM.md | 234 +++++++++++++++++++++++++++++ docs/function-calling/TOOLS.md | 48 ++++++ docs/images/agents/todo-system.png | Bin 0 -> 56349 bytes src/config/agent.rs | 109 +++++++++++++- src/config/mod.rs | 13 +- src/config/session.rs | 3 + src/config/todo.rs | 165 ++++++++++++++++++++ src/function/mod.rs | 22 ++- src/function/todo.rs | 160 ++++++++++++++++++++ src/repl/mod.rs | 65 +++++++- 13 files changed, 868 insertions(+), 9 deletions(-) create mode 100644 docs/TODO-SYSTEM.md create mode 100644 docs/images/agents/todo-system.png create mode 100644 src/config/todo.rs create mode 100644 src/function/todo.rs diff --git a/README.md b/README.md index dcc335b..f55a66f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g * [Sessions](/docs/SESSIONS.md): Manage and persist conversational contexts and settings across multiple interactions. * [Roles](./docs/ROLES.md): Customize model behavior for specific tasks or domains. * [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows. + * [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models. * [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables. * [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers. * [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization. diff --git a/config.agent.example.yaml b/config.agent.example.yaml index 43577ab..1accb0e 100644 --- a/config.agent.example.yaml +++ b/config.agent.example.yaml @@ -17,6 +17,13 @@ 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: 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 +continuation_prompt: null # Custom prompt used when auto-continuing (optional; uses default if null) mcp_servers: # Optional list of MCP servers that the agent utilizes - github # Corresponds to the name of an MCP server in the `/functions/mcp.json` file global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent diff --git a/docs/AGENTS.md b/docs/AGENTS.md index b53bf3f..50dd67a 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -34,6 +34,7 @@ If you're looking for more example agents, refer to the [built-in agents](../ass - [Python-Based Agent Tools](#python-based-agent-tools) - [Bash-Based Agent Tools](#bash-based-agent-tools) - [5. Conversation Starters](#5-conversation-starters) +- [6. Todo System & Auto-Continuation](#6-todo-system--auto-continuation) - [Built-In Agents](#built-in-agents) @@ -81,6 +82,11 @@ global_tools: # Optional list of additional global tools - web_search - fs - python +# Todo System & Auto-Continuation (see "Todo System & Auto-Continuation" section below) +auto_continue: false # Enable automatic continuation when incomplete todos remain +max_auto_continues: 10 # Maximum continuation attempts before stopping +inject_todo_instructions: true # Inject todo tool instructions into system prompt +continuation_prompt: null # Custom prompt for continuations (optional) ``` As mentioned previously: Agents utilize function calling to extend a model's capabilities. However, agents operate in @@ -421,6 +427,50 @@ conversation_starters: ![Example Conversation Starters](./images/agents/conversation-starters.gif) +## 6. Todo System & Auto-Continuation + +Loki includes a built-in task tracking system designed to improve the reliability of agents, especially when using +smaller language models. The Todo System helps models: + +- Break complex tasks into manageable steps +- Track progress through multi-step workflows +- Automatically continue work until all tasks are complete + +### Quick Configuration + +```yaml +# agents/my-agent/config.yaml +auto_continue: true # Enable auto-continuation +max_auto_continues: 10 # Max continuation attempts +inject_todo_instructions: true # Include the default todo instructions into prompt +``` + +### How It Works + +1. When `inject_todo_instructions` is enabled, agents receive instructions on using four built-in tools: + - `todo__init`: Initialize a todo list with a goal + - `todo__add`: Add a task to the list + - `todo__done`: Mark a task complete + - `todo__list`: View current todo state + + These instructions are a reasonable default that detail how to use Loki's To-Do System. If you wish, + you can disable the injection of the default instructions and specify your own instructions for how + to use the To-Do System into your main `instructions` for the agent. + +2. When `auto_continue` is enabled and the model stops with incomplete tasks, Loki automatically sends a + continuation prompt with the current todo state, nudging the model to continue working. + +3. This continues until all tasks are done or `max_auto_continues` is reached. + +### When to Use + +- Multistep tasks where the model might lose track +- Smaller models that need more structure +- Workflows requiring guaranteed completion of all steps + +For complete documentation including all configuration options, tool details, and best practices, see the +[Todo System Guide](./TODO-SYSTEM.md). + ## Built-In Agents Loki comes packaged with some useful built-in agents: * `coder`: An agent to assist you with all your coding tasks diff --git a/docs/TODO-SYSTEM.md b/docs/TODO-SYSTEM.md new file mode 100644 index 0000000..b5c42a4 --- /dev/null +++ b/docs/TODO-SYSTEM.md @@ -0,0 +1,234 @@ +# Todo System + +Loki's Todo System is a built-in task tracking feature designed to improve the reliability and effectiveness of LLM agents, +especially smaller models. It provides structured task management that helps models: + +- Break complex tasks into manageable steps +- Track progress through multistep workflows +- Automatically continue work until all tasks are complete +- Avoid forgetting steps or losing context + +![Todo System Example](./images/agents/todo-system.png) + +## Quick Links + +- [Why Use the Todo System?](#why-use-the-todo-system) +- [How It Works](#how-it-works) +- [Configuration Options](#configuration-options) +- [Available Tools](#available-tools) +- [Auto-Continuation](#auto-continuation) +- [Best Practices](#best-practices) +- [Example Workflow](#example-workflow) +- [Troubleshooting](#troubleshooting) + + +## Why Use the Todo System? +Smaller language models often struggle with: +- **Context drift**: Forgetting earlier steps in a multi-step task +- **Incomplete execution**: Stopping before all work is done +- **Lack of structure**: Jumping between tasks without clear organization + +The Loki Todo System addresses these issues by giving the model explicit tools to plan, track, and verify task completion. +The system automatically prompts the model to continue when incomplete tasks remain, ensuring work gets finished. + +## How It Works +1. **Planning Phase**: The model initializes a todo list with a goal and adds individual tasks +2. **Execution Phase**: The model works through tasks, marking each done immediately after completion +3. **Continuation Phase**: If incomplete tasks remain, the system automatically prompts the model to continue +4. **Completion**: When all tasks are marked done, the workflow ends naturally + +The todo state is preserved across the conversation (and any compressions), and injected into continuation prompts, +keeping the model focused on remaining work. + +## Configuration Options +The Todo System is configured per-agent in `/agents//config.yaml`: + +| Setting | Type | Default | Description | +|----------------------------|---------|-------------|---------------------------------------------------------------------------------| +| `auto_continue` | boolean | `false` | Enable the To-Do system for automatic continuation when incomplete todos remain | +| `max_auto_continues` | integer | `10` | Maximum number of automatic continuations before stopping | +| `inject_todo_instructions` | boolean | `true` | Inject the default todo tool usage instructions into the agent's system prompt | +| `continuation_prompt` | string | (see below) | Custom prompt used when auto-continuing | + +### Example Configuration +```yaml +# agents/my-agent/config.yaml +model: openai:gpt-4o +auto_continue: true # Enable auto-continuation +max_auto_continues: 15 # Allow up to 15 automatic continuations +inject_todo_instructions: true # Include todo instructions in system prompt +continuation_prompt: | # Optional: customize the continuation prompt + [CONTINUE] + You have unfinished tasks. Proceed with the next pending item. + Do not explain—just execute. +``` + +### Default Continuation Prompt +If `continuation_prompt` is not specified, the following default is used: + +``` +[SYSTEM REMINDER - TODO CONTINUATION] +You have incomplete tasks in your todo list. Continue with the next pending item. +Call tools immediately. Do not explain what you will do. +``` + +## Available Tools +When `inject_todo_instructions` is enabled (the default), agents have access to four built-in todo management tools: + +### `todo__init` +Initialize a new todo list with a goal. Clears any existing todos. + +**Parameters:** +- `goal` (string, required): The overall goal to achieve when all todos are completed + +**Example:** +```json +{"goal": "Refactor the authentication module"} +``` + +### `todo__add` +Add a new todo item to the list. + +**Parameters:** +- `task` (string, required): Description of the todo task + +**Example:** +```json +{"task": "Extract password validation into separate function"} +``` + +**Returns:** The assigned task ID + +### `todo__done` +Mark a todo item as done by its ID. + +**Parameters:** +- `id` (integer, required): The ID of the todo item to mark as done + +**Example:** +```json +{"id": 1} +``` + +### `todo__list` +Display the current todo list with status of each item. + +**Parameters:** None + +**Returns:** The full todo list with goal, progress, and item statuses + +## Auto-Continuation +When `auto_continue` is enabled, Loki automatically sends a continuation prompt if: + +1. The agent's response completes (model stops generating) +2. There are incomplete tasks in the todo list +3. The continuation count hasn't exceeded `max_auto_continues` +4. The response isn't identical to the previous continuation (prevents loops) + +### What Gets Injected +Each continuation prompt includes: +- The continuation prompt text (default or custom) +- The current todo list state showing: + - The goal + - Progress (e.g., "3/5 completed") + - Each task with status (✓ done, ○ pending) + +**Example continuation context:** +``` +[SYSTEM REMINDER - TODO CONTINUATION] +You have incomplete tasks in your todo list. Continue with the next pending item. +Call tools immediately. Do not explain what you will do. + +Goal: Refactor the authentication module +Progress: 2/4 completed + ✓ 1. Extract password validation into separate function + ✓ 2. Add unit tests for password validation + ○ 3. Update login handler to use new validation + ○ 4. Update registration handler to use new validation +``` + +### Visual Feedback +During auto-continuation, you'll see a message in your terminal: +``` +📋 Auto-continuing (3/10): 2 incomplete todo(s) remain +``` + +## Best Practices + +### For Agent Developers +1. **Enable for complex workflows**: Use `auto_continue: true` for agents that handle multistep tasks +2. **Set reasonable limits**: Adjust `max_auto_continues` based on typical task complexity +3. **Customize the prompt**: If your agent needs specific continuation behavior, set a custom `continuation_prompt` + +### For Model Behavior +The injected instructions tell models to: + +1. **Always create a todo list before starting work**: This ensures planning happens upfront +2. **Mark each task done immediately after finishing**: Don't batch completions +3. **Add all planned steps before starting**: Complete planning before execution + +### When to Use vs. Skip +**Use the Todo System when:** +- Tasks have 3+ distinct steps +- The model might lose track of progress +- You want guaranteed completion of all steps +- Working with smaller/less capable models + +**Skip the Todo System when:** +- Single-step, simple tasks +- Interactive Q&A sessions +- The overhead of task tracking isn't worth it + +## Example Workflow +Here's how a typical todo-driven workflow looks: + +**User prompt:** "Add input validation to the user registration form" + +**Model actions:** +``` +1. todo__init(goal="Add input validation to user registration form") +2. todo__add(task="Analyze current registration form fields") +3. todo__add(task="Create validation rules for email field") +4. todo__add(task="Create validation rules for password field") +5. todo__add(task="Implement client-side validation") +6. todo__add(task="Add server-side validation") +7. todo__add(task="Write tests for validation logic") +``` + +**Model executes first task, then:** +``` +8. todo__done(id=1) +9. [Proceeds with task 2...] +10. todo__done(id=2) +... +``` + +**If model stops with incomplete tasks:** +- System automatically sends continuation prompt +- Model sees remaining tasks and continues +- Repeats until all tasks are done or max continuations reached + +## Troubleshooting + +### Model Not Using Todo Tools +- Verify `inject_todo_instructions: true` in your agent config +- Check that the agent is properly loaded (not just a role) +- Some models may need explicit prompting to use the tools + +### Too Many Continuations +- Lower `max_auto_continues` to a reasonable limit +- Check if the model is creating new tasks without completing old ones +- Ensure tasks are appropriately scoped (not too granular) + +### Continuation Loop +The system detects when a model's response is identical to its previous continuation response and stops +automatically. If you're seeing loops: +- The model may be stuck; check if a task is impossible to complete +- Consider adjusting the `continuation_prompt` to be more directive + +--- + +## Additional Docs +- [Agents](./AGENTS.md) — Full agent configuration guide +- [Function Calling](./function-calling/TOOLS.md) — How tools work in Loki +- [Sessions](./SESSIONS.md) — How conversation state is managed diff --git a/docs/function-calling/TOOLS.md b/docs/function-calling/TOOLS.md index 52830d4..b555cc0 100644 --- a/docs/function-calling/TOOLS.md +++ b/docs/function-calling/TOOLS.md @@ -16,6 +16,10 @@ loki --info | grep functions_dir | awk '{print $2}' - [Enabling/Disabling Global Tools](#enablingdisabling-global-tools) - [Role Configuration](#role-configuration) - [Agent Configuration](#agent-configuration) +- [Tool Error Handling](#tool-error-handling) + - [Native/Shell Tool Errors](#nativeshell-tool-errors) + - [MCP Errors](#mcp-tool-errors) + - [Why Tool Error Handling Is Important](#why-this-matters) --- @@ -137,3 +141,47 @@ The values for `mapping_tools` are inherited from the [global configuration](#gl For more information about agents, refer to the [Agents](../AGENTS.md) documentation. For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file. + +--- + +## Tool Error Handling +When tools fail, Loki captures error information and passes it back to the model so it can diagnose issues and +potentially retry or adjust its approach. + +### Native/Shell Tool Errors +When a shell-based tool exits with a non-zero exit code, the model receives: + +```json +{ + "tool_call_error": "Tool call 'my_tool' exited with code 1", + "stderr": "Error: file not found: config.json" +} +``` + +The `stderr` field contains the actual error output from the tool, giving the model context about what went wrong. +If the tool produces no stderr output, only the `tool_call_error` field is included. + +**Note:** Tool stdout streams to your terminal in real-time so you can see progress. Only stderr is captured for +error reporting. + +### MCP Tool Errors +When an MCP (Model Context Protocol) tool invocation fails due to connection issues, timeouts, or server errors, +the model receives: + +```json +{ + "tool_call_error": "MCP tool invocation failed: connection refused" +} +``` + +This allows the model to understand that an external service failed and take appropriate action (retry, use an +alternative approach, or inform the user). + +### Why This Matters +Without proper error propagation, models would only know that "something went wrong" without understanding *what* +went wrong. By including stderr output and detailed error messages, models can: + +- Diagnose the root cause of failures +- Suggest fixes (e.g., "the file doesn't exist, should I create it?") +- Retry with corrected parameters +- Fall back to alternative approaches when appropriate diff --git a/docs/images/agents/todo-system.png b/docs/images/agents/todo-system.png new file mode 100644 index 0000000000000000000000000000000000000000..e66212a5f06badb3aba024253cc98773711868ae GIT binary patch literal 56349 zcmce;1x#G;*ENb2cZ$0W6e)u{6lj40gIjTz;tqqu;4nZb?xn@u-JK%E-3rCs3x#j! z@ArS-_kK6|a+7;=6GBc7$(%XQv!A`!T6=9Gloe&L(8&geg2Z`!B` zt8Ra#X^%GAM*=4)dURA!f-t)3m z+B0+acNf7>B3L-A}r$0s1K^3Mharhlc+S$_kV`kI2l`K4X4|>{J zJK~O*#nKshVx}+2BOCMa4oAPB9R`1TU&>7Pe(B_`sVhT0q0-j>{*{c7Of0g4rBxFE z162;hFDrkg;_+n)A|@$y4bmTAL>ZQ(xBv&ZuRVlfv$a%je&zg{&693f5Q1=t>K$ZgS z*5;;b7w>sVBxJ1+6xBTmh^S{EZNGPH`Sx!G9{rv85g z6~C4!(5OL8AM4&II!uZ$YF}%q7&Z4V3^$J7K#|G}dVxH_ySKvI6e%t`6R)j$-(vTf z3OvIOh%FnX?z&FQ92a|?*)4;z6(4UkZ!4JjQWw-A2KFA^xsM6#zmh1n~fp4mT= zfTzii5Agv2G^&aVG1HGDk|f$+`!n2kv#i}S?o__}Q1FrS;Y)j)3C4VWH>a)t0?&1= z29w(*W9xf0zcF{6y$7pA$~?NlWUzrU_E#qQY=TtlS4Z`93N@b=nttzD#Zlr<9Mt5dYIo;xEC{K+md?M zOtmymB39F@5T$lg1|~KI*R?KbOABvqG!SR^*$?gxVfj4xRjn&zg-!HSIg*`sA9T08 zE~S(p`81&?&yQQ2r$!BA#?juEC>(wGp3vwNM*jUwGfYQ$P8QScvzsa0uy@Q|FQ-^@4-pCR)5Z73s zD`!!aC+ny_y4I4tXaC?FqY<-h9vH1~6_*d(GE_WQv6ml36q_VslWxrY8-Z>rr<}PPWZhy!i#fvN1E!;l;+Z;#xG*b(I3rDhsJUvB< z750bLEgsj1p^ZHBbHO;gJSIi+wweX%+xDb*H-aTtPpx98BK?5%(mZg z`m9&wKXt}xp~*6?M)OiiM^`BJlIagK&tYQS!at*S@b+)kq0L3}xh#Pir*n*-{b{$< zBWu}oz5hB{yo`GMGJLJ(p0UP1x{BQLRTbzi67-%E#anB@18S+Hr_C?8#2ydoZv4R* zEkLrn3(|;@R1-BG%x%zlc47cG!bI}s$f<0jfs5UWUQ~q>77_=N=HBw-426BjhYFDU4J9N zBj$mImz1Y0lr4P&chT(mbpxx>KGd8FyzVIs&QS6)L&s5)R1%()l8sBN=wy}olKaoQ z(*LbQg(#p-CJj?6M1TrMYTR-999#iH-pli46ooJS%ptcijQSj6j6d>Sz#5p|Cmc=f zYpCgxvILJ>7kn?;s>#vEwyL&J)tc1z|9F(Zz9c88+e?YP^QoFjQZ8qwcfJ=r{rDnSdzpt7Y`P3`uQ^hYk z1j&$zDGd6T9j5Gg=mgfBsk|hjGjTnJBmpUhtbm^gy{$bH!^r@ZC=#~g zcPtXSRiS|~B4(Cw8$$am(j*aG4iGoAwkaDeyY5}gNieeqE;YL{7I^Byc3;+LNPk!K zlW@xD=x&#cbcSXW{zKmEMwb_U37@eBQ`K5+JC>Vh!21@DJej{aVj}>sZsZDI!V$6N zPo3HS$VIb|AABNbtd-i$vEga|81@s!8AXdssNgY=Xa^;Zy3i0*xWbH$_7=Y3)en z>|6W5Mcrd{L#7@=Y(vIQ# z_fW`?7oQsgFoJX~rnBk3_EJP_Z)L4DvGi2n*fpFmI^bbLod3!${NQhiL~=@*K!V7G zR}zqn?~DBlTm`y+!0xRF+w2PVfBO1=y^TU6f~<(Nc{bzwPt?O8Th9M?$X1fR%9=qQ zBWiXFi8g_lbm;Zys4-#Hb%h7PThmQ#n}Puy--!Mfbsc$Bpo!fNNmJSWoMaF44@!rB zKDj*Czz=#;sGvD4wS*;Sb+1D0G~eb5s&!aVns0uHFV(UQ%wLLQ;EyrMP6nB;B^&m# z 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 @@ -33,6 +46,9 @@ pub struct Agent { rag: Option>, model: Model, vault: GlobalVault, + todo_list: TodoList, + continuation_count: usize, + last_continuation_response: Option, } impl Agent { @@ -188,6 +204,10 @@ impl Agent { None }; + if agent_config.auto_continue { + functions.append_todo_functions(); + } + Ok(Self { name: name.to_string(), config: agent_config, @@ -199,6 +219,9 @@ impl Agent { rag, model, vault: Arc::clone(&config.read().vault), + todo_list: TodoList::default(), + continuation_count: 0, + last_continuation_response: None, }) } @@ -309,11 +332,16 @@ impl Agent { } pub fn interpolated_instructions(&self) -> String { - let output = self + let mut output = self .session_dynamic_instructions .clone() .or_else(|| self.shared_dynamic_instructions.clone()) .unwrap_or_else(|| self.config.instructions.clone()); + + if self.config.auto_continue && self.config.inject_todo_instructions { + output.push_str(DEFAULT_TODO_INSTRUCTIONS); + } + self.interpolate_text(&output) } @@ -376,6 +404,67 @@ impl Agent { self.session_dynamic_instructions = None; } + pub fn auto_continue_enabled(&self) -> bool { + self.config.auto_continue + } + + pub fn max_auto_continues(&self) -> usize { + self.config.max_auto_continues + } + + pub fn continuation_count(&self) -> usize { + self.continuation_count + } + + pub fn increment_continuation(&mut self) { + self.continuation_count += 1; + } + + pub fn reset_continuation(&mut self) { + self.continuation_count = 0; + self.last_continuation_response = None; + } + + pub fn is_stale_response(&self, response: &str) -> bool { + self.last_continuation_response + .as_ref() + .is_some_and(|last| last == response) + } + + pub fn set_last_continuation_response(&mut self, response: String) { + self.last_continuation_response = Some(response); + } + + pub fn todo_list(&self) -> &TodoList { + &self.todo_list + } + + pub fn init_todo_list(&mut self, goal: &str) { + self.todo_list = TodoList::new(goal); + } + + pub fn add_todo(&mut self, task: &str) -> usize { + self.todo_list.add(task) + } + + pub fn mark_todo_done(&mut self, id: usize) -> bool { + self.todo_list.mark_done(id) + } + + pub fn continuation_prompt(&self) -> String { + self.config.continuation_prompt.clone().unwrap_or_else(|| { + "[SYSTEM REMINDER - TODO CONTINUATION]\n\ + You have incomplete tasks in your todo list. \ + Continue with the next pending item. \ + Call tools immediately. Do not explain what you will do." + .to_string() + }) + } + + pub fn compression_threshold(&self) -> Option { + self.config.compression_threshold + } + pub fn is_dynamic_instructions(&self) -> bool { self.config.dynamic_instructions } @@ -498,6 +587,14 @@ pub struct AgentConfig { #[serde(skip_serializing_if = "Option::is_none")] pub agent_session: Option, #[serde(default)] + pub auto_continue: bool, + #[serde(default = "default_max_auto_continues")] + pub max_auto_continues: usize, + #[serde(default = "default_true")] + pub inject_todo_instructions: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub compression_threshold: Option, + #[serde(default)] pub description: String, #[serde(default)] pub version: String, @@ -505,6 +602,8 @@ pub struct AgentConfig { pub mcp_servers: Vec, #[serde(default)] pub global_tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub continuation_prompt: Option, #[serde(default)] pub instructions: String, #[serde(default)] @@ -517,6 +616,14 @@ pub struct AgentConfig { pub documents: Vec, } +fn default_max_auto_continues() -> usize { + 10 +} + +fn default_true() -> bool { + true +} + impl AgentConfig { pub fn load(path: &Path) -> Result { let contents = read_to_string(path) diff --git a/src/config/mod.rs b/src/config/mod.rs index 299bdbf..fb2d5ae 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,6 +3,7 @@ mod input; mod macros; mod role; mod session; +pub(crate) mod todo; pub use self::agent::{Agent, AgentVariables, complete_agent_variables, list_agents}; pub use self::input::Input; @@ -1573,8 +1574,18 @@ impl Config { .summary_context_prompt .clone() .unwrap_or_else(|| SUMMARY_CONTEXT_PROMPT.into()); + + let todo_prefix = config + .read() + .agent + .as_ref() + .map(|agent| agent.todo_list()) + .filter(|todos| !todos.is_empty()) + .map(|todos| format!("[ACTIVE TODO LIST]\n{}\n\n", todos.render_for_model())) + .unwrap_or_default(); + if let Some(session) = config.write().session.as_mut() { - session.compress(format!("{summary_context_prompt}{summary}")); + session.compress(format!("{todo_prefix}{summary_context_prompt}{summary}")); } config.write().discontinuous_last_message(); Ok(()) diff --git a/src/config/session.rs b/src/config/session.rs index cfc8e02..cb0cecf 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -299,6 +299,9 @@ impl Session { self.role_prompt = agent.interpolated_instructions(); self.agent_variables = agent.variables().clone(); self.agent_instructions = self.role_prompt.clone(); + if let Some(threshold) = agent.compression_threshold() { + self.set_compression_threshold(Some(threshold)); + } } pub fn agent_variables(&self) -> &AgentVariables { diff --git a/src/config/todo.rs b/src/config/todo.rs new file mode 100644 index 0000000..7b070fe --- /dev/null +++ b/src/config/todo.rs @@ -0,0 +1,165 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TodoStatus { + Pending, + Done, +} + +impl TodoStatus { + fn icon(&self) -> &'static str { + match self { + TodoStatus::Pending => "○", + TodoStatus::Done => "✓", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TodoItem { + pub id: usize, + #[serde(alias = "description")] + pub desc: String, + pub done: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct TodoList { + #[serde(default)] + pub goal: String, + #[serde(default)] + pub todos: Vec, +} + +impl TodoList { + pub fn new(goal: &str) -> Self { + Self { + goal: goal.to_string(), + todos: Vec::new(), + } + } + + pub fn add(&mut self, task: &str) -> usize { + let id = self.todos.iter().map(|t| t.id).max().unwrap_or(0) + 1; + self.todos.push(TodoItem { + id, + desc: task.to_string(), + done: false, + }); + id + } + + pub fn mark_done(&mut self, id: usize) -> bool { + if let Some(item) = self.todos.iter_mut().find(|t| t.id == id) { + item.done = true; + true + } else { + false + } + } + + pub fn has_incomplete(&self) -> bool { + self.todos.iter().any(|item| !item.done) + } + + pub fn is_empty(&self) -> bool { + self.todos.is_empty() + } + + pub fn render_for_model(&self) -> String { + let mut lines = Vec::new(); + if !self.goal.is_empty() { + lines.push(format!("Goal: {}", self.goal)); + } + lines.push(format!( + "Progress: {}/{} completed", + self.completed_count(), + self.todos.len() + )); + for item in &self.todos { + let status = if item.done { + TodoStatus::Done + } else { + TodoStatus::Pending + }; + lines.push(format!(" {} {}. {}", status.icon(), item.id, item.desc)); + } + lines.join("\n") + } + + pub fn incomplete_count(&self) -> usize { + self.todos.iter().filter(|item| !item.done).count() + } + + pub fn completed_count(&self) -> usize { + self.todos.iter().filter(|item| item.done).count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_and_add() { + let mut list = TodoList::new("Map Labs"); + assert_eq!(list.add("Discover"), 1); + assert_eq!(list.add("Map columns"), 2); + assert_eq!(list.todos.len(), 2); + assert!(list.has_incomplete()); + } + + #[test] + fn test_mark_done() { + let mut list = TodoList::new("Test"); + list.add("Task 1"); + list.add("Task 2"); + assert!(list.mark_done(1)); + assert!(!list.mark_done(99)); + assert_eq!(list.completed_count(), 1); + assert_eq!(list.incomplete_count(), 1); + } + + #[test] + fn test_empty_list() { + let list = TodoList::default(); + assert!(!list.has_incomplete()); + assert!(list.is_empty()); + } + + #[test] + fn test_all_done() { + let mut list = TodoList::new("Test"); + list.add("Done task"); + list.mark_done(1); + assert!(!list.has_incomplete()); + } + + #[test] + fn test_render_for_model() { + let mut list = TodoList::new("Map Labs"); + list.add("Discover"); + list.add("Map"); + list.mark_done(1); + let rendered = list.render_for_model(); + assert!(rendered.contains("Goal: Map Labs")); + assert!(rendered.contains("Progress: 1/2 completed")); + assert!(rendered.contains("✓ 1. Discover")); + assert!(rendered.contains("○ 2. Map")); + } + + #[test] + fn test_serialization_roundtrip() { + let mut list = TodoList::new("Roundtrip"); + list.add("Step 1"); + list.add("Step 2"); + list.mark_done(1); + let json = serde_json::to_string(&list).unwrap(); + let deserialized: TodoList = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.goal, "Roundtrip"); + assert_eq!(deserialized.todos.len(), 2); + assert!(deserialized.todos[0].done); + assert!(!deserialized.todos[1].done); + } +} diff --git a/src/function/mod.rs b/src/function/mod.rs index 5fb4f65..54bee4e 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod todo; + use crate::{ config::{Agent, Config, GlobalConfig}, utils::*, @@ -26,6 +28,7 @@ use std::{ process::{Command, Stdio}, }; use strum_macros::AsRefStr; +use todo::TODO_FUNCTION_PREFIX; #[derive(Embed)] #[folder = "assets/functions/"] @@ -262,6 +265,10 @@ impl Functions { self.declarations.is_empty() } + pub fn append_todo_functions(&mut self) { + self.declarations.extend(todo::todo_function_declarations()); + } + pub fn clear_mcp_meta_functions(&mut self) { self.declarations.retain(|d| { !d.name.starts_with(MCP_INVOKE_META_FUNCTION_NAME_PREFIX) @@ -850,7 +857,7 @@ impl ToolCall { _ if cmd_name.starts_with(MCP_SEARCH_META_FUNCTION_NAME_PREFIX) => { Self::search_mcp_tools(config, &cmd_name, &json_data).unwrap_or_else(|e| { let error_msg = format!("MCP search failed: {e}"); - println!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); + eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); json!({"tool_call_error": error_msg}) }) } @@ -859,7 +866,7 @@ impl ToolCall { .await .unwrap_or_else(|e| { let error_msg = format!("MCP describe failed: {e}"); - println!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); + eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); json!({"tool_call_error": error_msg}) }) } @@ -868,10 +875,17 @@ impl ToolCall { .await .unwrap_or_else(|e| { let error_msg = format!("MCP tool invocation failed: {e}"); - println!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); + eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); json!({"tool_call_error": error_msg}) }) } + _ if cmd_name.starts_with(TODO_FUNCTION_PREFIX) => { + todo::handle_todo_tool(config, &cmd_name, &json_data).unwrap_or_else(|e| { + let error_msg = format!("Todo tool failed: {e}"); + eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️"))); + json!({"tool_call_error": error_msg}) + }) + } _ => match run_llm_function(cmd_name, cmd_args, envs, agent_name) { Ok(Some(contents)) => serde_json::from_str(&contents) .ok() @@ -1052,7 +1066,7 @@ pub fn run_llm_function( eprintln!("{stderr}"); } let tool_error_message = format!("Tool call '{command_name}' exited with code {exit_code}"); - println!("{}", warning_text(&format!("⚠️ {tool_error_message} ⚠️"))); + eprintln!("{}", warning_text(&format!("⚠️ {tool_error_message} ⚠️"))); let mut error_json = json!({"tool_call_error": tool_error_message}); if !stderr.is_empty() { error_json["stderr"] = json!(stderr); diff --git a/src/function/todo.rs b/src/function/todo.rs new file mode 100644 index 0000000..e4c2738 --- /dev/null +++ b/src/function/todo.rs @@ -0,0 +1,160 @@ +use super::{FunctionDeclaration, JsonSchema}; +use crate::config::GlobalConfig; + +use anyhow::{Result, bail}; +use indexmap::IndexMap; +use serde_json::{Value, json}; + +pub const TODO_FUNCTION_PREFIX: &str = "todo__"; + +pub fn todo_function_declarations() -> Vec { + vec![ + FunctionDeclaration { + name: format!("{TODO_FUNCTION_PREFIX}init"), + description: "Initialize a new todo list with a goal. Clears any existing todos." + .to_string(), + parameters: JsonSchema { + type_value: Some("object".to_string()), + properties: Some(IndexMap::from([( + "goal".to_string(), + JsonSchema { + type_value: Some("string".to_string()), + description: Some( + "The overall goal to achieve when all todos are completed".into(), + ), + ..Default::default() + }, + )])), + required: Some(vec!["goal".to_string()]), + ..Default::default() + }, + agent: false, + }, + FunctionDeclaration { + name: format!("{TODO_FUNCTION_PREFIX}add"), + description: "Add a new todo item to the list.".to_string(), + parameters: JsonSchema { + type_value: Some("object".to_string()), + properties: Some(IndexMap::from([( + "task".to_string(), + JsonSchema { + type_value: Some("string".to_string()), + description: Some("Description of the todo task".into()), + ..Default::default() + }, + )])), + required: Some(vec!["task".to_string()]), + ..Default::default() + }, + agent: false, + }, + FunctionDeclaration { + name: format!("{TODO_FUNCTION_PREFIX}done"), + description: "Mark a todo item as done by its id.".to_string(), + parameters: JsonSchema { + type_value: Some("object".to_string()), + properties: Some(IndexMap::from([( + "id".to_string(), + JsonSchema { + type_value: Some("integer".to_string()), + description: Some("The id of the todo item to mark as done".into()), + ..Default::default() + }, + )])), + required: Some(vec!["id".to_string()]), + ..Default::default() + }, + agent: false, + }, + FunctionDeclaration { + name: format!("{TODO_FUNCTION_PREFIX}list"), + description: "Display the current todo list with status of each item.".to_string(), + parameters: JsonSchema { + type_value: Some("object".to_string()), + ..Default::default() + }, + agent: false, + }, + ] +} + +pub fn handle_todo_tool(config: &GlobalConfig, cmd_name: &str, args: &Value) -> Result { + let action = cmd_name + .strip_prefix(TODO_FUNCTION_PREFIX) + .unwrap_or(cmd_name); + + match action { + "init" => { + let goal = args.get("goal").and_then(Value::as_str).unwrap_or_default(); + let mut cfg = config.write(); + let agent = cfg.agent.as_mut(); + match agent { + Some(agent) => { + agent.init_todo_list(goal); + Ok(json!({"status": "ok", "message": "Initialized new todo list"})) + } + None => bail!("No active agent"), + } + } + "add" => { + let task = args.get("task").and_then(Value::as_str).unwrap_or_default(); + if task.is_empty() { + return Ok(json!({"error": "task description is required"})); + } + let mut cfg = config.write(); + let agent = cfg.agent.as_mut(); + match agent { + Some(agent) => { + let id = agent.add_todo(task); + Ok(json!({"status": "ok", "id": id})) + } + None => bail!("No active agent"), + } + } + "done" => { + let id = args + .get("id") + .and_then(|v| { + v.as_u64() + .or_else(|| v.as_str().and_then(|s| s.parse().ok())) + }) + .map(|v| v as usize); + match id { + Some(id) => { + let mut cfg = config.write(); + let agent = cfg.agent.as_mut(); + match agent { + Some(agent) => { + if agent.mark_todo_done(id) { + Ok( + json!({"status": "ok", "message": format!("Marked todo {id} as done")}), + ) + } else { + Ok(json!({"error": format!("Todo {id} not found")})) + } + } + None => bail!("No active agent"), + } + } + None => Ok(json!({"error": "id is required and must be a number"})), + } + } + "list" => { + let cfg = config.read(); + let agent = cfg.agent.as_ref(); + match agent { + Some(agent) => { + let list = agent.todo_list(); + if list.is_empty() { + Ok(json!({"goal": "", "todos": []})) + } else { + Ok(serde_json::to_value(list) + .unwrap_or(json!({"error": "serialization failed"}))) + } + } + None => bail!("No active agent"), + } + } + _ => bail!("Unknown todo action: {action}"), + } +} diff --git a/src/repl/mod.rs b/src/repl/mod.rs index f0cca2d..5618640 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -826,6 +826,14 @@ pub async fn run_repl_command( _ => unknown_command()?, }, None => { + if config + .read() + .agent + .as_ref() + .is_some_and(|a| a.continuation_count() > 0) + { + config.write().agent.as_mut().unwrap().reset_continuation(); + } let input = Input::from_str(config, line, None); ask(config, abort_signal.clone(), input, true).await?; } @@ -874,9 +882,60 @@ async fn ask( ) .await } else { - Config::maybe_autoname_session(config.clone()); - Config::maybe_compress_session(config.clone()); - Ok(()) + let should_continue = { + let cfg = config.read(); + if let Some(agent) = &cfg.agent { + agent.auto_continue_enabled() + && agent.continuation_count() < agent.max_auto_continues() + && !agent.is_stale_response(&output) + && agent.todo_list().has_incomplete() + } else { + false + } + }; + + if should_continue { + let full_prompt = { + let mut cfg = config.write(); + let agent = cfg.agent.as_mut().expect("agent checked above"); + agent.set_last_continuation_response(output.clone()); + agent.increment_continuation(); + let count = agent.continuation_count(); + let max = agent.max_auto_continues(); + + let todo_state = agent.todo_list().render_for_model(); + let remaining = agent.todo_list().incomplete_count(); + let prompt = agent.continuation_prompt(); + + let color = if cfg.light_theme() { + nu_ansi_term::Color::LightGray + } else { + nu_ansi_term::Color::DarkGray + }; + eprintln!( + "\n📋 {}", + color.italic().paint(format!( + "Auto-continuing ({count}/{max}): {remaining} incomplete todo(s) remain" + )) + ); + + format!("{prompt}\n\n{todo_state}") + }; + let continuation_input = Input::from_str(config, &full_prompt, None); + ask(config, abort_signal, continuation_input, false).await + } else { + if config + .read() + .agent + .as_ref() + .is_some_and(|a| a.continuation_count() > 0) + { + config.write().agent.as_mut().unwrap().reset_continuation(); + } + Config::maybe_autoname_session(config.clone()); + Config::maybe_compress_session(config.clone()); + Ok(()) + } } }