Skip to content

Commit cfece8e

Browse files
committed
add plan tool
1 parent 00b1923 commit cfece8e

13 files changed

Lines changed: 511 additions & 14 deletions

File tree

AGENTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ src/
8383
│ ├── bashexec.rs # Sandboxed bash execution
8484
│ ├── codesearch.rs # Ripgrep integration
8585
│ ├── image.rs # Image handling (local paths, URLs)
86+
│ ├── plan.rs # Task-plan validation and terminal rendering
8687
│ ├── permissions.rs # 3-tier command permission system
8788
│ ├── tool_name.rs # Type-safe tool name enum
8889
│ ├── types.rs # Tool definitions for API
@@ -130,6 +131,11 @@ src/
130131
- Trims to MAX_MESSAGES (500) to prevent token overflow
131132
- Builds system prompt with features list and custom instructions
132133

134+
**src/tools/plan.rs**
135+
- Validates `update_plan` payloads and enforces one `in_progress` step
136+
- Builds compact model-facing acknowledgements and styled terminal checklists
137+
- Safe-mode compatible because it does not read, write, execute commands, or access the network
138+
133139
**src/tools/filesystem.rs**
134140
- validate_path: Security-critical path validation
135141
- All operations check sandboxing before execution
@@ -321,7 +327,7 @@ cargo test -- --nocapture # Show println output
321327

322328
1. Add variant to `src/tools/tool_name.rs` (ToolName enum, as_str, from_str)
323329
2. Define tool schema in `src/tools/types.rs` and add to tool lists
324-
3. Implement execution in `src/tools/mod.rs` (ToolExecutor::execute match arm)
330+
3. Implement execution in `src/tools/executor.rs` (ToolExecutor::execute match arm)
325331
4. Add tests in `src/tools/tests.rs`
326332
5. Update README.md with tool description
327333

@@ -431,6 +437,7 @@ Gitignored scratchpad for helper files the user asks to be created there — typ
431437
- After each important change, but only when you have finalized the current task, update if relevant:
432438
- `README.md`
433439
- `CHANGELOG.md` under `[Unreleased]`. Stick to the standard Keep-a-Changelog categories (`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`) — do NOT add an `Internal` section. Pure refactors, dead-code removals, and test-only additions are not user-notable; leave them out. The changelog is for users, not contributors. Within each entry, describe the user-visible behaviour in plain English: no file paths, no bare `Type::method` shorthand, no Rust attribute syntax, no crate names. CLI flags, env vars, slash commands, and API wire formats are fine because the user encounters them directly.
440+
- `STRUCTURE.md`
434441
- Run:
435442
- `cargo fmt --all`
436443
- `cargo clippy --all-targets -- -D warnings`

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to Sofos are documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- **Visible task plans for multi-step work.** The assistant can now call `update_plan` to show the current plan with `pending`, `in_progress`, and `completed` statuses. Plan updates are available in safe mode too because they do not read or modify files, and the terminal renders them as a compact styled checklist while the model receives only a short acknowledgement.
10+
711
### Changed
812

913
- **File-edit tool results are now fixed-size summaries.** `edit_file`, `write_file` (when it overwrites an existing file), and `morph_edit_file` previously returned the full syntax-highlighted diff to the model as the tool result. The colored diff carried truecolor ANSI escape sequences that roughly multiplied the byte count per line, and the tool result stayed in conversation history for every subsequent turn — so a session with many edits paid that bloated cost again on each later turn, and a single large rewrite could push the response into the hundreds of thousands of tokens. The model now sees a fixed two-line summary (`Success. Updated the following files:` followed by `M <path>`) regardless of edit size, while the terminal still renders the full colored diff exactly as before. If the model needs to verify the post-edit state it can re-read a range of the file.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Sofos provides an AI assistant inside your terminal with controlled access to yo
6363
- run safe build and test commands;
6464
- fetch documentation and use provider-native web search;
6565
- review local, clipboard, or web images;
66+
- update a visible task plan during multi-step work;
6667
- save and resume conversations;
6768
- connect to external tools through Model Context Protocol servers.
6869

@@ -309,6 +310,7 @@ Provider mapping:
309310
| `delete_file` | Delete a file after confirmation. External paths require Write permission. |
310311
| `delete_directory` | Delete a directory after confirmation. External paths require Write permission. |
311312
| `execute_bash` | Run approved shell commands through the bash permission system. |
313+
| `update_plan` | Show the current multi-step task plan with `pending`, `in_progress`, and `completed` statuses. |
312314
| `web_fetch` | Fetch a URL and return readable text. |
313315
| `web_search` | Provider-native web search. |
314316

@@ -322,6 +324,7 @@ Safe mode is enabled with `--safe-mode` or `/s`. It restricts the native tool se
322324
- `read_file`;
323325
- `glob_files`;
324326
- `search_code` when ripgrep is installed;
327+
- `update_plan`;
325328
- `web_fetch`;
326329
- `web_search`.
327330

STRUCTURE.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@
4747
- [7.7 `tools/codesearch.rs`](#77-toolscodesearchrs)
4848
- [7.8 `tools/image.rs`](#78-toolsimagers)
4949
- [7.9 `tools/morph_validate.rs`](#79-toolsmorph_validaters)
50-
- [7.10 `tools/types.rs`](#710-toolstypesrs)
51-
- [7.11 `tools/tool_name.rs`](#711-toolstool_namers)
52-
- [7.12 `tools/utils.rs`](#712-toolsutilsrs)
50+
- [7.10 `tools/plan.rs`](#710-toolsplanrs)
51+
- [7.11 `tools/types.rs`](#711-toolstypesrs)
52+
- [7.12 `tools/tool_name.rs`](#712-toolstool_namers)
53+
- [7.13 `tools/utils.rs`](#713-toolsutilsrs)
5354
8. [`mcp/`](#8-mcp)
5455
- [8.1 `mcp/config.rs`](#81-mcpconfigrs)
5556
- [8.2 `mcp/protocol.rs`](#82-mcpprotocolrs)
@@ -277,6 +278,8 @@ src/
277278
│ │ # Local and web image detection, validation, encoding, and message-content conversion.
278279
│ ├── morph_validate.rs
279280
│ │ # Safety checks that reject suspicious or truncated Morph Apply output before writing files.
281+
│ ├── plan.rs
282+
│ │ # `update_plan` argument validation, model-facing acknowledgements, and terminal checklist rendering.
280283
│ ├── tool_name.rs
281284
│ │ # Type-safe native tool-name enum and string conversion logic.
282285
│ ├── types.rs
@@ -1019,7 +1022,24 @@ Rules:
10191022
- Morph output must be validated against the original file.
10201023
- Rejected Morph output leaves the original file untouched and asks the model to use `edit_file`.
10211024

1022-
### 7.10 `tools/types.rs`
1025+
### 7.10 `tools/plan.rs`
1026+
1027+
`tools/plan.rs` owns the `update_plan` payload parsing and the on-screen checklist rendering.
1028+
1029+
It contains:
1030+
1031+
- the `PlanStepStatus`, `PlanStep`, and `PlanUpdate` types;
1032+
- payload validation, including the at-most-one `in_progress` step rule;
1033+
- the compact model-facing acknowledgement string;
1034+
- the styled terminal checklist with status markers and counts.
1035+
1036+
Rules:
1037+
1038+
- The model receives only a short acknowledgement, never the full plan body, so conversation history stays bounded.
1039+
- The renderer must reuse `ui::ACCENT_RGB` so the plan checklist matches the rest of sofos' colour scheme.
1040+
- The module performs no file or network access and is safe to expose in read-only mode.
1041+
1042+
### 7.11 `tools/types.rs`
10231043

10241044
`tools/types.rs` owns native tool definitions exposed to providers.
10251045

@@ -1038,7 +1058,7 @@ Rules:
10381058
- Tool definitions should match dispatcher parameter names and accepted aliases where possible.
10391059
- Provider-specific server-side tools should be represented in shared `api/types.rs` but selected here where appropriate.
10401060

1041-
### 7.11 `tools/tool_name.rs`
1061+
### 7.12 `tools/tool_name.rs`
10421062

10431063
`tools/tool_name.rs` owns the type-safe native tool-name enum.
10441064

@@ -1053,7 +1073,7 @@ Rules:
10531073
- Native dispatch should match on `ToolName`, not raw strings.
10541074
- Adding a tool requires updating this enum, schemas, dispatch, tests, and user documentation where applicable.
10551075

1056-
### 7.12 `tools/utils.rs`
1076+
### 7.13 `tools/utils.rs`
10571077

10581078
`tools/utils.rs` owns shared tool utilities.
10591079

src/config.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ pub fn max_context_tokens_for(model: &str) -> usize {
8383
crate::api::model_info::lookup(model).effective_window() as usize
8484
}
8585

86-
/// Auto-compaction trigger for `model`. Mirrors the codex pattern of
87-
/// keeping the cost-shaping cap and the API ceiling as separate
88-
/// concepts: this is where the LLM-summary phase fires, while
89-
/// [`max_context_tokens_for`] is where the hard drop-trim kicks in.
86+
/// Auto-compaction trigger for `model`. Keeps the cost-shaping cap and
87+
/// the API ceiling as separate concepts: this is where the LLM-summary
88+
/// phase fires, while [`max_context_tokens_for`] is where the hard
89+
/// drop-trim kicks in.
9090
pub fn auto_compact_token_limit_for(model: &str) -> usize {
9191
crate::api::model_info::lookup(model).auto_compact_at() as usize
9292
}
@@ -97,7 +97,8 @@ pub fn auto_compact_token_limit_for(model: &str) -> usize {
9797
pub const SAFE_MODE_MESSAGE: &str = "[SYSTEM: Safe (read-only) mode has been enabled. \
9898
No file modifications or bash commands are allowed. \
9999
Available tools: list_directory, read_file, glob_files, \
100-
search_code (when ripgrep is installed), web_fetch, web_search.]";
100+
search_code (when ripgrep is installed), update_plan, \
101+
web_fetch, web_search.]";
101102

102103
/// Normal mode message shown when switching from safe mode
103104
pub const NORMAL_MODE_MESSAGE: &str = "[SYSTEM: Normal (unrestricted) mode has been enabled. \

src/repl/conversation/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ impl ConversationHistory {
5353
"5. Search the web for information",
5454
"6. Execute read-only bash commands (for testing code)",
5555
"7. View images (user includes image path or URL in their message)",
56+
"8. Update a visible task plan for multi-step work",
5657
];
5758

5859
if has_code_search {
59-
features.push("8. Search code using ripgrep");
60+
features.push("9. Search code using ripgrep");
6061
}
6162

6263
let edit_instruction = if has_morph {
@@ -90,6 +91,7 @@ When helping users:
9091
- Never run destructive or irreversible shell commands (e.g., rm -rf, rm, rmdir, dd, mkfs*, fdisk/parted, wipefs, chmod/chown -R on broad paths, truncate, :>, >/dev/sd*, kill -9 on system services).
9192
Prefer read-only commands and dry-runs; if a potentially destructive action seems necessary, stop and request explicit confirmation before proceeding.
9293
- Explain your reasoning when using tools
94+
- Use update_plan for complex or multi-step tasks, and keep exactly one step in_progress when work is underway
9395
9496
Outside Workspace Access (three separate scopes, each prompted independently):
9597
- Read scope: read_file and list_directory can access absolute or ~/ paths. If not pre-configured, the user is prompted to allow access and can optionally remember the decision.

src/tools/executor.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::tools::codesearch::CodeSearchTool;
88
use crate::tools::filesystem::FileSystemTool;
99
use crate::tools::morph_validate;
1010
use crate::tools::permissions::{self, PermissionManager};
11+
use crate::tools::plan;
1112
use crate::tools::resolve::ResolvedPath;
1213
use crate::tools::types::{
1314
add_code_search_tool, get_all_tools, get_all_tools_with_morph, get_read_only_tools,
@@ -1263,6 +1264,13 @@ impl ToolExecutor {
12631264
let result = self.bash_executor.execute(command)?;
12641265
Ok(result)
12651266
}
1267+
ToolName::UpdatePlan => {
1268+
let update = plan::parse_plan_update(input)?;
1269+
return Ok(ToolExecutionResult::TextWithDisplay {
1270+
text: plan::model_summary(&update),
1271+
display: plan::render_plan(&update),
1272+
});
1273+
}
12661274
ToolName::WebFetch => {
12671275
use futures::StreamExt;
12681276

src/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod filesystem;
55
pub mod image;
66
pub mod morph_validate;
77
pub mod permissions;
8+
pub mod plan;
89
pub mod resolve;
910
pub mod tool_name;
1011
pub mod types;

0 commit comments

Comments
 (0)