aru-code 0.24.1__tar.gz → 0.25.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aru_code-0.24.1/aru_code.egg-info → aru_code-0.25.0}/PKG-INFO +6 -4
- {aru_code-0.24.1 → aru_code-0.25.0}/README.md +5 -3
- aru_code-0.25.0/aru/__init__.py +1 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/base.py +9 -10
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/explorer.py +16 -14
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/planner.py +7 -3
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/display.py +36 -4
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/runner.py +87 -6
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/codebase.py +659 -207
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/gitignore.py +76 -11
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/ranker.py +83 -16
- {aru_code-0.24.1 → aru_code-0.25.0/aru_code.egg-info}/PKG-INFO +6 -4
- {aru_code-0.24.1 → aru_code-0.25.0}/pyproject.toml +1 -1
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_codebase.py +68 -101
- aru_code-0.24.1/aru/__init__.py +0 -1
- {aru_code-0.24.1 → aru_code-0.25.0}/LICENSE +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agent_factory.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/executor.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/cache_patch.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/checkpoints.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/cli.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/commands.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/completers.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/config.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/context.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/history_blocks.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/permissions.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/providers.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/runtime.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/session.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/setup.cfg +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_config.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_context.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_executor.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_main.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_permissions.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_planner.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_plugins.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_providers.py +0 -0
- {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_ranker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -53,7 +53,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
53
53
|
|
|
54
54
|
## Highlights
|
|
55
55
|
|
|
56
|
-
- **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
|
|
56
|
+
- **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
|
|
57
57
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
58
58
|
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
59
59
|
- **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
|
|
@@ -513,12 +513,13 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
513
513
|
| **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
|
|
514
514
|
| **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
|
|
515
515
|
| **General** | Handles conversation and simple operations | All tools including delegation |
|
|
516
|
+
| **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
|
|
516
517
|
|
|
517
518
|
## Tools
|
|
518
519
|
|
|
519
520
|
### File Operations
|
|
520
521
|
- `read_file` — Reads files with line range support and binary detection
|
|
521
|
-
- `
|
|
522
|
+
- `read_files` — Reads multiple files in parallel (single batched call)
|
|
522
523
|
- `write_file` — Writes content to files, creating directories as needed
|
|
523
524
|
- `edit_file` — Find-and-replace edits on files
|
|
524
525
|
|
|
@@ -553,7 +554,8 @@ aru-code/
|
|
|
553
554
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
554
555
|
│ ├── agents/
|
|
555
556
|
│ │ ├── planner.py # Planning agent
|
|
556
|
-
│ │
|
|
557
|
+
│ │ ├── executor.py # Execution agent
|
|
558
|
+
│ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
|
|
557
559
|
│ └── tools/
|
|
558
560
|
│ ├── codebase.py # 11 core tools
|
|
559
561
|
│ ├── ast_tools.py # Tree-sitter code analysis
|
|
@@ -6,7 +6,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
6
6
|
|
|
7
7
|
## Highlights
|
|
8
8
|
|
|
9
|
-
- **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
|
|
9
|
+
- **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
|
|
10
10
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
11
11
|
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
12
12
|
- **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
|
|
@@ -466,12 +466,13 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
466
466
|
| **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
|
|
467
467
|
| **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
|
|
468
468
|
| **General** | Handles conversation and simple operations | All tools including delegation |
|
|
469
|
+
| **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
|
|
469
470
|
|
|
470
471
|
## Tools
|
|
471
472
|
|
|
472
473
|
### File Operations
|
|
473
474
|
- `read_file` — Reads files with line range support and binary detection
|
|
474
|
-
- `
|
|
475
|
+
- `read_files` — Reads multiple files in parallel (single batched call)
|
|
475
476
|
- `write_file` — Writes content to files, creating directories as needed
|
|
476
477
|
- `edit_file` — Find-and-replace edits on files
|
|
477
478
|
|
|
@@ -506,7 +507,8 @@ aru-code/
|
|
|
506
507
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
507
508
|
│ ├── agents/
|
|
508
509
|
│ │ ├── planner.py # Planning agent
|
|
509
|
-
│ │
|
|
510
|
+
│ │ ├── executor.py # Execution agent
|
|
511
|
+
│ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
|
|
510
512
|
│ └── tools/
|
|
511
513
|
│ ├── codebase.py # 11 core tools
|
|
512
514
|
│ ├── ast_tools.py # Tree-sitter code analysis
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.25.0"
|
|
@@ -66,8 +66,8 @@ Every tool call accumulates its result in your context window. Use the minimum n
|
|
|
66
66
|
|
|
67
67
|
1. **Find files/patterns** → `grep_search(pattern, file_glob="*.py")` or `glob_search`. \
|
|
68
68
|
Default shows 10 lines of context — use `context_lines=30` for full function bodies.
|
|
69
|
-
2. **
|
|
70
|
-
3. **Need
|
|
69
|
+
2. **Need raw content** → `read_file(file_path)` — returns first chunk + outline for large files
|
|
70
|
+
3. **Need several files at once** → `read_files(paths)` — parallel batch read
|
|
71
71
|
|
|
72
72
|
**Batch independent tool calls**: When you need answers from multiple independent sources, \
|
|
73
73
|
emit ALL those tool calls in a single response.
|
|
@@ -148,10 +148,10 @@ split into subtasks grouped by concern (e.g. "Create model files", "Create route
|
|
|
148
148
|
|
|
149
149
|
## Reading strategy — read, edit, test
|
|
150
150
|
|
|
151
|
-
1. **
|
|
152
|
-
2. **Need a specific pattern?** → `grep_search(pattern, file_glob="*.py")` — default 10 lines context. \
|
|
151
|
+
1. **Need a specific pattern?** → `grep_search(pattern, file_glob="*.py")` — default 10 lines context. \
|
|
153
152
|
Use `context_lines=30` for full function bodies.
|
|
154
|
-
|
|
153
|
+
2. **Need lines for editing?** → `read_file(file_path, start_line=N, end_line=M)` using line numbers from grep
|
|
154
|
+
3. **Need several files at once?** → `read_files(paths)` — parallel batch read
|
|
155
155
|
4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files
|
|
156
156
|
5. **Need the COMPLETE file (>60KB)?** → `read_file(file_path, max_size=0)` — reads in chunks. Use rarely.
|
|
157
157
|
|
|
@@ -194,10 +194,9 @@ Skip exploration when the task is clear and the relevant files are obvious.
|
|
|
194
194
|
|
|
195
195
|
Every tool call accumulates its result in your context window. Use the minimum needed:
|
|
196
196
|
|
|
197
|
-
1. **Don't know which file?** → `grep_search` / `glob_search` for patterns
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
3. **Need specific lines?** → `read_file(file_path, start_line=N, end_line=M)`
|
|
197
|
+
1. **Don't know which file?** → `grep_search` / `glob_search` for patterns.
|
|
198
|
+
2. **Need specific lines?** → `read_file(file_path, start_line=N, end_line=M)`
|
|
199
|
+
3. **Need several files at once?** → `read_files(paths)` — parallel batch read.
|
|
201
200
|
4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files.
|
|
202
201
|
|
|
203
202
|
**NEVER read the same file twice.** Check if you already have the content in context.
|
|
@@ -218,7 +217,7 @@ For simple, directed lookups (one known file, one specific symbol) use \
|
|
|
218
217
|
|
|
219
218
|
For **anything broader** — understanding a system, researching before implementing, \
|
|
220
219
|
analyzing multiple files, writing specs or documentation — **always use explorer agents**. \
|
|
221
|
-
Every `read_file` / `
|
|
220
|
+
Every `read_file` / `read_files` / `grep_search` result you call directly accumulates \
|
|
222
221
|
in YOUR context window and stays there forever. Explorer agents read files in their own \
|
|
223
222
|
isolated context and return only a concise summary. This is critical: \
|
|
224
223
|
**3 explorer summaries < 8 raw file reads** in context cost.
|
|
@@ -7,24 +7,26 @@ from agno.agent import Agent
|
|
|
7
7
|
from aru.providers import create_model
|
|
8
8
|
from aru.runtime import get_ctx
|
|
9
9
|
from aru.tools.codebase import (
|
|
10
|
+
_glob_search_tool,
|
|
11
|
+
_grep_search_tool,
|
|
12
|
+
_list_directory_tool,
|
|
13
|
+
_rank_files_tool,
|
|
14
|
+
_read_file_tool,
|
|
10
15
|
bash,
|
|
11
|
-
|
|
12
|
-
grep_search,
|
|
13
|
-
list_directory,
|
|
14
|
-
rank_files,
|
|
15
|
-
read_file,
|
|
16
|
-
read_file_smart,
|
|
16
|
+
read_files,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
# Read-only tools only — no write/edit/delegate (prevents recursion and mutations)
|
|
19
|
+
# Read-only tools only — no write/edit/delegate (prevents recursion and mutations).
|
|
20
|
+
# All wrappers are async so the Explorer's "multi-parallel tool calls" prompt
|
|
21
|
+
# actually matches runtime behavior — Agno can await them concurrently.
|
|
20
22
|
EXPLORER_TOOLS = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
_read_file_tool,
|
|
24
|
+
read_files,
|
|
25
|
+
_glob_search_tool,
|
|
26
|
+
_grep_search_tool,
|
|
27
|
+
_list_directory_tool,
|
|
26
28
|
bash,
|
|
27
|
-
|
|
29
|
+
_rank_files_tool,
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
EXPLORER_ROLE = """\
|
|
@@ -52,7 +54,7 @@ Guidelines:
|
|
|
52
54
|
- Use glob_search for broad file pattern matching
|
|
53
55
|
- Use grep_search for searching file contents with regex
|
|
54
56
|
- Use read_file when you know the specific file path you need to read
|
|
55
|
-
- Use
|
|
57
|
+
- Use read_files (batch) when you need to pull several files at once
|
|
56
58
|
- Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
|
57
59
|
- NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, \
|
|
58
60
|
or any file creation/modification
|
|
@@ -6,7 +6,11 @@ from agno.compression.manager import CompressionManager
|
|
|
6
6
|
from aru.agents.base import build_instructions
|
|
7
7
|
from aru.providers import create_model
|
|
8
8
|
from aru.tools.codebase import (
|
|
9
|
-
|
|
9
|
+
_glob_search_tool,
|
|
10
|
+
_grep_search_tool,
|
|
11
|
+
_list_directory_tool,
|
|
12
|
+
_read_file_tool,
|
|
13
|
+
read_files,
|
|
10
14
|
)
|
|
11
15
|
from aru.runtime import get_ctx
|
|
12
16
|
|
|
@@ -34,8 +38,8 @@ Return ONLY the markdown plan. No explanation, no preamble.\
|
|
|
34
38
|
|
|
35
39
|
# Planner uses read-only tools only — no write/edit/bash
|
|
36
40
|
PLANNER_TOOLS = [
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
_read_file_tool, read_files,
|
|
42
|
+
_glob_search_tool, _grep_search_tool, _list_directory_tool,
|
|
39
43
|
]
|
|
40
44
|
|
|
41
45
|
|
|
@@ -161,6 +161,10 @@ class StatusBar:
|
|
|
161
161
|
self._index = 0
|
|
162
162
|
self._last_switch = time.monotonic()
|
|
163
163
|
self._override: str | None = None
|
|
164
|
+
# A single persistent Spinner — Rich's Spinner tracks frames via
|
|
165
|
+
# (time - start_time), so instantiating a new one per render would
|
|
166
|
+
# reset start_time each frame and make the animation look frozen.
|
|
167
|
+
self._spinner = Spinner("dots", text="", style="cyan")
|
|
164
168
|
|
|
165
169
|
@property
|
|
166
170
|
def current_text(self) -> str:
|
|
@@ -187,8 +191,8 @@ class StatusBar:
|
|
|
187
191
|
|
|
188
192
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
189
193
|
self._maybe_rotate()
|
|
190
|
-
|
|
191
|
-
yield from
|
|
194
|
+
self._spinner.update(text=Text(self.current_text, style="dim"))
|
|
195
|
+
yield from self._spinner.__rich_console__(console, options)
|
|
192
196
|
|
|
193
197
|
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
|
|
194
198
|
return Measurement(1, options.max_width)
|
|
@@ -196,7 +200,7 @@ class StatusBar:
|
|
|
196
200
|
|
|
197
201
|
TOOL_DISPLAY_NAMES = {
|
|
198
202
|
"read_file": "Read",
|
|
199
|
-
"
|
|
203
|
+
"read_files": "ReadBatch",
|
|
200
204
|
"write_file": "Write",
|
|
201
205
|
"edit_file": "Edit",
|
|
202
206
|
"glob_search": "Glob",
|
|
@@ -209,7 +213,7 @@ TOOL_DISPLAY_NAMES = {
|
|
|
209
213
|
|
|
210
214
|
TOOL_PRIMARY_ARG = {
|
|
211
215
|
"read_file": "file_path",
|
|
212
|
-
"
|
|
216
|
+
"read_files": "paths",
|
|
213
217
|
"write_file": "file_path",
|
|
214
218
|
"edit_file": "file_path",
|
|
215
219
|
"glob_search": "pattern",
|
|
@@ -253,6 +257,34 @@ def _format_tool_label(tool_name: str, tool_args: dict | None) -> str:
|
|
|
253
257
|
return display
|
|
254
258
|
|
|
255
259
|
|
|
260
|
+
def subagent_progress(label: str, tool_name: str, tool_args: dict | None,
|
|
261
|
+
duration: float | None = None):
|
|
262
|
+
"""Print sub-agent tool completion into the active Live context (or console).
|
|
263
|
+
|
|
264
|
+
Only called on tool completion — shows a single ✓ line per tool call,
|
|
265
|
+
keeping the output compact (no duplicate start/complete lines).
|
|
266
|
+
"""
|
|
267
|
+
from aru.runtime import get_ctx
|
|
268
|
+
try:
|
|
269
|
+
ctx = get_ctx()
|
|
270
|
+
except LookupError:
|
|
271
|
+
return
|
|
272
|
+
tool_label = _format_tool_label(tool_name, tool_args)
|
|
273
|
+
dur_str = f" {duration:.1f}s" if duration and duration >= 0.5 else ""
|
|
274
|
+
line = Text.assemble(
|
|
275
|
+
(" ", ""),
|
|
276
|
+
("\u2713 ", "bold green"),
|
|
277
|
+
(f"[{label}] ", "dim"),
|
|
278
|
+
(tool_label, "dim"),
|
|
279
|
+
(dur_str, "dim cyan"),
|
|
280
|
+
)
|
|
281
|
+
target = ctx.live if ctx.live else None
|
|
282
|
+
if target:
|
|
283
|
+
target.console.print(line)
|
|
284
|
+
else:
|
|
285
|
+
ctx.console.print(line)
|
|
286
|
+
|
|
287
|
+
|
|
256
288
|
class ToolTracker:
|
|
257
289
|
"""Tracks active tool calls with timing, displayed inside the Live area."""
|
|
258
290
|
|
|
@@ -462,6 +462,24 @@ def _build_file_context(file_paths: list[str], max_total: int = 20_000) -> str:
|
|
|
462
462
|
return "## Pre-loaded file contents (do NOT re-read these files)\n\n" + "\n\n".join(parts)
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
_MODIFY_VERBS = frozenset({
|
|
466
|
+
"add", "create", "write", "edit", "modify", "update", "implement",
|
|
467
|
+
"fix", "replace", "rename", "move", "refactor", "remove", "delete",
|
|
468
|
+
})
|
|
469
|
+
_MUTATION_LABELS = frozenset({"Write", "Edit", "Bash"})
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _step_expected_mutation(description: str) -> bool:
|
|
473
|
+
"""Check if step description implies file modifications."""
|
|
474
|
+
first_word = description.strip().split()[0].lower().rstrip(":")
|
|
475
|
+
return first_word in _MODIFY_VERBS
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _has_mutation_tool(tool_calls: list[str]) -> bool:
|
|
479
|
+
"""Check if any tool call is a write/edit/bash operation."""
|
|
480
|
+
return any(tc.split("(")[0] in _MUTATION_LABELS for tc in tool_calls)
|
|
481
|
+
|
|
482
|
+
|
|
465
483
|
async def execute_plan_steps(session, executor_factory) -> str | None:
|
|
466
484
|
"""Execute plan steps one by one with live progress tracking."""
|
|
467
485
|
plan_files = _extract_plan_file_paths(session.current_plan)
|
|
@@ -586,12 +604,57 @@ async def execute_plan_steps(session, executor_factory) -> str | None:
|
|
|
586
604
|
)
|
|
587
605
|
|
|
588
606
|
if step_failed:
|
|
589
|
-
|
|
607
|
+
# Auto-retry once with error context before asking the user
|
|
590
608
|
fail_msg = content[:200] if content else f"{tasks_failed}/{tasks_total} subtasks failed"
|
|
591
|
-
console.print(f"\n[
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
609
|
+
console.print(f"\n[yellow]Step {step.index} failed, retrying automatically...[/yellow]")
|
|
610
|
+
reset_task_store()
|
|
611
|
+
retry_prompt = (
|
|
612
|
+
step_prompt
|
|
613
|
+
+ f"\n\n## Previous Attempt Failed\nError: {fail_msg}\n"
|
|
614
|
+
+ "Fix the issues from the previous attempt. Do NOT repeat the same mistake."
|
|
615
|
+
)
|
|
616
|
+
executor = executor_factory()
|
|
617
|
+
retry_result = await run_agent_capture(executor, retry_prompt, session, lightweight=True)
|
|
618
|
+
content = retry_result.content
|
|
619
|
+
run_result = retry_result
|
|
620
|
+
|
|
621
|
+
# Re-evaluate after retry
|
|
622
|
+
store = get_task_store()
|
|
623
|
+
all_tasks = store.get_all()
|
|
624
|
+
tasks_completed = sum(1 for t in all_tasks if t["status"] == "completed")
|
|
625
|
+
tasks_failed = sum(1 for t in all_tasks if t["status"] == "failed")
|
|
626
|
+
tasks_total = len(all_tasks)
|
|
627
|
+
tasks_all_done = tasks_total > 0 and (tasks_completed + tasks_failed == tasks_total)
|
|
628
|
+
|
|
629
|
+
still_failed = False
|
|
630
|
+
if tasks_all_done and tasks_failed > 0 and tasks_completed == 0:
|
|
631
|
+
still_failed = True
|
|
632
|
+
elif content:
|
|
633
|
+
still_failed = (
|
|
634
|
+
content.startswith("Error")
|
|
635
|
+
or "Error from OpenAI API" in content
|
|
636
|
+
or "Error in Agent run" in content
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if still_failed:
|
|
640
|
+
step.status = "failed"
|
|
641
|
+
fail_msg = content[:200] if content else f"{tasks_failed}/{tasks_total} subtasks failed"
|
|
642
|
+
console.print(f"\n[red]Step {step.index} failed after retry: {fail_msg}[/red]")
|
|
643
|
+
if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
|
|
644
|
+
break
|
|
645
|
+
else:
|
|
646
|
+
# Retry succeeded — fall through to success handling
|
|
647
|
+
step_failed = False
|
|
648
|
+
|
|
649
|
+
# Mutation validation warning (non-blocking)
|
|
650
|
+
if not step_failed and not run_result.stalled:
|
|
651
|
+
if _step_expected_mutation(step.description) and not _has_mutation_tool(run_result.tool_calls):
|
|
652
|
+
console.print(
|
|
653
|
+
f"[yellow]\u26a0 Step {step.index} was expected to modify files "
|
|
654
|
+
f"but no write tools were called.[/yellow]"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if not step_failed and (content or tasks_all_done):
|
|
595
658
|
step.status = "completed"
|
|
596
659
|
summary = content or f"All {tasks_completed} subtasks completed."
|
|
597
660
|
step_text = f"### Step {step.index}: {step.description}\n{summary}"
|
|
@@ -609,8 +672,26 @@ async def execute_plan_steps(session, executor_factory) -> str | None:
|
|
|
609
672
|
if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
|
|
610
673
|
break
|
|
611
674
|
except Exception as e:
|
|
675
|
+
# Auto-retry once on exception
|
|
676
|
+
console.print(f"\n[yellow]Step {step.index} error: {e}. Retrying...[/yellow]")
|
|
677
|
+
try:
|
|
678
|
+
reset_task_store()
|
|
679
|
+
retry_prompt = (
|
|
680
|
+
step_prompt
|
|
681
|
+
+ f"\n\n## Previous Attempt Error\n{e}\n"
|
|
682
|
+
+ "Fix the issues and complete this step."
|
|
683
|
+
)
|
|
684
|
+
executor = executor_factory()
|
|
685
|
+
retry_result = await run_agent_capture(executor, retry_prompt, session, lightweight=True)
|
|
686
|
+
if retry_result.content and not retry_result.content.startswith("Error"):
|
|
687
|
+
step.status = "completed"
|
|
688
|
+
all_results.append(f"### Step {step.index}: {step.description}\n{retry_result.content}")
|
|
689
|
+
completed_context += f"\n- Step {step.index} ({step.description}): Done (after retry)"
|
|
690
|
+
continue
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
612
693
|
step.status = "failed"
|
|
613
|
-
console.print(f"\n[red]Step {step.index} failed: {e}[/red]")
|
|
694
|
+
console.print(f"\n[red]Step {step.index} failed after retry: {e}[/red]")
|
|
614
695
|
if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
|
|
615
696
|
break
|
|
616
697
|
|