aru-code 0.14.0__tar.gz → 0.15.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.14.0/aru_code.egg-info → aru_code-0.15.0}/PKG-INFO +4 -8
- {aru_code-0.14.0 → aru_code-0.15.0}/README.md +3 -7
- aru_code-0.15.0/aru/__init__.py +1 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/base.py +5 -10
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/context.py +87 -24
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/display.py +0 -13
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/runner.py +4 -1
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/ast_tools.py +0 -124
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/codebase.py +13 -27
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/ranker.py +1 -1
- {aru_code-0.14.0 → aru_code-0.15.0/aru_code.egg-info}/PKG-INFO +4 -8
- {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/SOURCES.txt +0 -1
- {aru_code-0.14.0 → aru_code-0.15.0}/pyproject.toml +1 -1
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_advanced.py +0 -10
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_new.py +0 -5
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_codebase.py +4 -150
- aru_code-0.14.0/aru/__init__.py +0 -1
- aru_code-0.14.0/tests/test_ast_tools.py +0 -762
- {aru_code-0.14.0 → aru_code-0.15.0}/LICENSE +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/agent_factory.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/executor.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/planner.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/cli.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/commands.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/completers.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/config.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/permissions.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/providers.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/runtime.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/session.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/setup.cfg +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_config.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_context.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_executor.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_main.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_permissions.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_planner.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_providers.py +0 -0
- {aru_code-0.14.0 → aru_code-0.15.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.15.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
|
|
@@ -369,7 +369,7 @@ Custom agents are Markdown files with YAML frontmatter stored in `.agents/agents
|
|
|
369
369
|
name: Code Reviewer
|
|
370
370
|
description: Review code for quality, bugs, and best practices
|
|
371
371
|
model: anthropic/claude-sonnet-4-5
|
|
372
|
-
tools: read_file, grep_search, glob_search
|
|
372
|
+
tools: read_file, grep_search, glob_search
|
|
373
373
|
max_turns: 15
|
|
374
374
|
mode: primary
|
|
375
375
|
---
|
|
@@ -480,8 +480,8 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
480
480
|
### File Operations
|
|
481
481
|
- `read_file` — Reads files with line range support and binary detection
|
|
482
482
|
- `read_file_smart` — Smart file reading focused on relevant snippets for the query
|
|
483
|
-
- `write_file`
|
|
484
|
-
- `edit_file`
|
|
483
|
+
- `write_file` — Writes files
|
|
484
|
+
- `edit_file` — Find-replace edits
|
|
485
485
|
|
|
486
486
|
### Search & Discovery
|
|
487
487
|
- `glob_search` — Find files by pattern (respects .gitignore)
|
|
@@ -489,10 +489,6 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
489
489
|
- `list_directory` — Directory listing with gitignore filtering
|
|
490
490
|
- `rank_files` — Multi-factor file relevance ranking (name, structure, recency)
|
|
491
491
|
|
|
492
|
-
### Code Analysis
|
|
493
|
-
- `code_structure` — Extracts classes, functions, imports via tree-sitter AST
|
|
494
|
-
- `find_dependencies` — Analyzes import relationships between files
|
|
495
|
-
|
|
496
492
|
### Shell & Web
|
|
497
493
|
- `bash` — Executes shell commands with permission gates
|
|
498
494
|
- `web_search` — Web search via DuckDuckGo
|
|
@@ -322,7 +322,7 @@ Custom agents are Markdown files with YAML frontmatter stored in `.agents/agents
|
|
|
322
322
|
name: Code Reviewer
|
|
323
323
|
description: Review code for quality, bugs, and best practices
|
|
324
324
|
model: anthropic/claude-sonnet-4-5
|
|
325
|
-
tools: read_file, grep_search, glob_search
|
|
325
|
+
tools: read_file, grep_search, glob_search
|
|
326
326
|
max_turns: 15
|
|
327
327
|
mode: primary
|
|
328
328
|
---
|
|
@@ -433,8 +433,8 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
433
433
|
### File Operations
|
|
434
434
|
- `read_file` — Reads files with line range support and binary detection
|
|
435
435
|
- `read_file_smart` — Smart file reading focused on relevant snippets for the query
|
|
436
|
-
- `write_file`
|
|
437
|
-
- `edit_file`
|
|
436
|
+
- `write_file` — Writes files
|
|
437
|
+
- `edit_file` — Find-replace edits
|
|
438
438
|
|
|
439
439
|
### Search & Discovery
|
|
440
440
|
- `glob_search` — Find files by pattern (respects .gitignore)
|
|
@@ -442,10 +442,6 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
442
442
|
- `list_directory` — Directory listing with gitignore filtering
|
|
443
443
|
- `rank_files` — Multi-factor file relevance ranking (name, structure, recency)
|
|
444
444
|
|
|
445
|
-
### Code Analysis
|
|
446
|
-
- `code_structure` — Extracts classes, functions, imports via tree-sitter AST
|
|
447
|
-
- `find_dependencies` — Analyzes import relationships between files
|
|
448
|
-
|
|
449
445
|
### Shell & Web
|
|
450
446
|
- `bash` — Executes shell commands with permission gates
|
|
451
447
|
- `web_search` — Web search via DuckDuckGo
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.15.0"
|
|
@@ -35,7 +35,7 @@ PLANNER_ROLE = """\
|
|
|
35
35
|
You are a software architect agent. Your job is to analyze codebases and create concise implementation plans.
|
|
36
36
|
|
|
37
37
|
IMPORTANT: You are a READ-ONLY agent. You have NO tools to create, write, or edit files, or run shell commands. \
|
|
38
|
-
Do NOT attempt to use write_file, edit_file, bash,
|
|
38
|
+
Do NOT attempt to use write_file, edit_file, bash, or any write/exec tool — they do not exist in your toolkit. \
|
|
39
39
|
To assess test coverage, read source files and test files directly — do NOT try to run pytest or any command. \
|
|
40
40
|
Your sole output is the implementation plan. The executor agent will carry out the actual changes.
|
|
41
41
|
|
|
@@ -112,15 +112,12 @@ When all subtasks are done, STOP. Do not add extra actions beyond the task list.
|
|
|
112
112
|
## Subtask granularity — CRITICAL
|
|
113
113
|
Each subtask should touch at most **3-4 files**. If the step involves many files, \
|
|
114
114
|
split into subtasks grouped by concern (e.g. "Create model files", "Create route files", \
|
|
115
|
-
"Update config and main").
|
|
116
|
-
to minimize tool calls. Batch independent file writes using `write_files` or `edit_files` to minimize tool calls.
|
|
115
|
+
"Update config and main").
|
|
117
116
|
|
|
118
117
|
## Guidelines
|
|
119
118
|
- Read files before editing them
|
|
120
119
|
- Use edit_file for targeted changes (preferred over rewriting entire files)
|
|
121
120
|
- Use write_file only for new files or complete rewrites
|
|
122
|
-
- When creating or updating multiple independent files, use write_files to batch them
|
|
123
|
-
- When making independent edits across files, use edit_files to batch them
|
|
124
121
|
- Run existing tests after changes when applicable
|
|
125
122
|
- **When adding or modifying unit tests, ALWAYS run them to verify they pass before finishing.**
|
|
126
123
|
- Keep changes minimal and focused on the task
|
|
@@ -139,7 +136,7 @@ Use `context_lines=30` for full function bodies.
|
|
|
139
136
|
|
|
140
137
|
**NEVER read the same file twice.** If you already have the file content in context, use it.
|
|
141
138
|
|
|
142
|
-
**NEVER use bash
|
|
139
|
+
**NEVER use bash to read files.** Always use `read_file` or `grep_search`.
|
|
143
140
|
|
|
144
141
|
**Batch independent tool calls**: emit ALL independent tool calls in a single response.
|
|
145
142
|
|
|
@@ -181,7 +178,7 @@ Every tool call accumulates its result in your context window. Use the minimum n
|
|
|
181
178
|
|
|
182
179
|
**NEVER read the same file twice.** Check if you already have the content in context.
|
|
183
180
|
|
|
184
|
-
**NEVER use bash
|
|
181
|
+
**NEVER use bash to read files.** Always use `read_file` or `grep_search`.
|
|
185
182
|
|
|
186
183
|
**Batch independent tool calls**: emit ALL independent tool calls in a single response.
|
|
187
184
|
|
|
@@ -189,9 +186,7 @@ Every tool call accumulates its result in your context window. Use the minimum n
|
|
|
189
186
|
|
|
190
187
|
**When adding or modifying unit tests, ALWAYS run them to verify they pass before finishing.**
|
|
191
188
|
|
|
192
|
-
Use delegate_task to split work into independent subtasks for parallel execution
|
|
193
|
-
When creating or updating multiple independent files, use write_files to batch them.
|
|
194
|
-
When making independent edits across files, use edit_files to batch them.\
|
|
189
|
+
Use delegate_task to split work into independent subtasks for parallel execution.\
|
|
195
190
|
"""
|
|
196
191
|
|
|
197
192
|
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
# ── Constants ──────────────────────────────────────────────────────
|
|
12
12
|
|
|
13
13
|
# Pruning: minimum chars that must be freeable to justify a prune pass
|
|
14
|
-
PRUNE_MINIMUM_CHARS =
|
|
14
|
+
PRUNE_MINIMUM_CHARS = 12_000 # ~3K tokens (lower = prune sooner)
|
|
15
15
|
# Placeholder that replaces evicted content
|
|
16
16
|
PRUNED_PLACEHOLDER = "[previous output cleared to save context]"
|
|
17
17
|
# User messages larger than this threshold are truncated when outside protection window
|
|
@@ -22,18 +22,23 @@ PRUNE_USER_MSG_KEEP = 500 # ~140 tokens — enough to understand the request
|
|
|
22
22
|
PRUNE_PROTECT_TURNS = 2
|
|
23
23
|
# Tool result markers that should never be pruned (critical context)
|
|
24
24
|
PRUNE_PROTECTED_MARKERS = {"[SubAgent-", "delegate_task"}
|
|
25
|
+
# Tool names whose outputs should never be pruned (like OpenCode's PRUNE_PROTECTED_TOOLS)
|
|
26
|
+
# These are checked as substrings in message content (tool results include the tool name)
|
|
27
|
+
PRUNE_PROTECTED_TOOLS = {"delegate_task"}
|
|
25
28
|
|
|
26
29
|
# Truncation: universal limits for any tool output
|
|
27
|
-
TRUNCATE_MAX_LINES =
|
|
28
|
-
TRUNCATE_MAX_BYTES =
|
|
29
|
-
TRUNCATE_KEEP_START =
|
|
30
|
-
TRUNCATE_KEEP_END =
|
|
30
|
+
TRUNCATE_MAX_LINES = 300
|
|
31
|
+
TRUNCATE_MAX_BYTES = 15 * 1024 # 15 KB (was 20KB — tighter to prevent context bloat)
|
|
32
|
+
TRUNCATE_KEEP_START = 200 # lines to keep from the start
|
|
33
|
+
TRUNCATE_KEEP_END = 60 # lines to keep from the end
|
|
31
34
|
TRUNCATE_MAX_LINE_LENGTH = 2000 # chars per individual line (prevents minified files)
|
|
32
35
|
|
|
33
36
|
# Compaction: trigger when per-run input tokens exceed this fraction of model limit
|
|
34
|
-
COMPACTION_THRESHOLD_RATIO = 0.85
|
|
37
|
+
COMPACTION_THRESHOLD_RATIO = 0.70 # was 0.85 — compact earlier to avoid hitting limits
|
|
35
38
|
# Compaction: target post-compaction size as fraction of model context limit
|
|
36
39
|
COMPACTION_TARGET_RATIO = 0.15
|
|
40
|
+
# Compaction: reserve buffer for the compaction process itself (like OpenCode's 20K)
|
|
41
|
+
COMPACTION_BUFFER_TOKENS = 20_000
|
|
37
42
|
# Default model context limits (input tokens)
|
|
38
43
|
MODEL_CONTEXT_LIMITS: dict[str, int] = {
|
|
39
44
|
# Anthropic
|
|
@@ -103,13 +108,13 @@ def _get_prune_protect_chars(model_id: str = "default") -> int:
|
|
|
103
108
|
"""Scale protection window based on model context size.
|
|
104
109
|
|
|
105
110
|
Larger models get more protection; smaller models prune more aggressively
|
|
106
|
-
to
|
|
111
|
+
to prevent context overflow. Returns ~7% of the model's context in chars.
|
|
107
112
|
"""
|
|
108
113
|
limit = MODEL_CONTEXT_LIMITS.get(model_id, MODEL_CONTEXT_LIMITS["default"])
|
|
109
|
-
# ~
|
|
110
|
-
protect = int(limit * 0.
|
|
111
|
-
# Clamp between
|
|
112
|
-
return max(
|
|
114
|
+
# ~4 chars per token, protect ~7% of context (was 10% — tighter budget)
|
|
115
|
+
protect = int(limit * 0.07 * 4)
|
|
116
|
+
# Clamp between 15K (minimum usable) and 60K (diminishing returns)
|
|
117
|
+
return max(15_000, min(protect, 60_000))
|
|
113
118
|
|
|
114
119
|
|
|
115
120
|
def prune_history(
|
|
@@ -171,8 +176,10 @@ def prune_history(
|
|
|
171
176
|
# Still within protection window
|
|
172
177
|
protected += msg_len
|
|
173
178
|
else:
|
|
174
|
-
# Check protected markers before pruning
|
|
175
|
-
|
|
179
|
+
# Check protected markers and tool names before pruning
|
|
180
|
+
content = msg["content"]
|
|
181
|
+
if (any(marker in content for marker in PRUNE_PROTECTED_MARKERS)
|
|
182
|
+
or any(tool in content for tool in PRUNE_PROTECTED_TOOLS)):
|
|
176
183
|
protected += msg_len
|
|
177
184
|
continue
|
|
178
185
|
|
|
@@ -207,19 +214,59 @@ def _truncate_long_lines(lines: list[str]) -> list[str]:
|
|
|
207
214
|
return result
|
|
208
215
|
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
217
|
+
def _build_truncation_hint(
|
|
218
|
+
source_file: str = "",
|
|
219
|
+
source_tool: str = "",
|
|
220
|
+
lines_shown: int = 0,
|
|
221
|
+
) -> str:
|
|
222
|
+
"""Build a context-aware truncation hint that guides the LLM to save tokens.
|
|
215
223
|
|
|
224
|
+
When the source file is known, provides a direct read_file reference with
|
|
225
|
+
the next offset. Otherwise falls back to generic tool suggestions.
|
|
226
|
+
Always suggests delegate_task for large exploration work.
|
|
227
|
+
"""
|
|
228
|
+
parts = ["\n[Hint: Output was truncated."]
|
|
229
|
+
|
|
230
|
+
if source_file:
|
|
231
|
+
# File-specific: tell the LLM exactly how to access the rest
|
|
232
|
+
next_line = lines_shown + 1 if lines_shown else 1
|
|
233
|
+
parts.append(
|
|
234
|
+
f' To see more: read_file("{source_file}", start_line={next_line}).'
|
|
235
|
+
f" Use grep_search to find specific content instead of reading everything."
|
|
236
|
+
)
|
|
237
|
+
elif source_tool == "bash":
|
|
238
|
+
parts.append(
|
|
239
|
+
" Use grep_search to find specific content in project files."
|
|
240
|
+
" Do NOT re-run the command to get full output."
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
parts.append(
|
|
244
|
+
" Use grep_search to find specific content, or read_file with"
|
|
245
|
+
" start_line/end_line for incremental reading."
|
|
246
|
+
)
|
|
216
247
|
|
|
217
|
-
|
|
248
|
+
# Always suggest delegation for large outputs
|
|
249
|
+
parts.append(
|
|
250
|
+
" For large exploration tasks, use delegate_task to keep your context clean.]"
|
|
251
|
+
)
|
|
252
|
+
return "".join(parts)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def truncate_output(
|
|
256
|
+
text: str,
|
|
257
|
+
source_file: str = "",
|
|
258
|
+
source_tool: str = "",
|
|
259
|
+
) -> str:
|
|
218
260
|
"""Universal truncation for tool outputs.
|
|
219
261
|
|
|
220
262
|
Caps output at TRUNCATE_MAX_BYTES / TRUNCATE_MAX_LINES, keeping the
|
|
221
263
|
start and end with a middle marker showing what was cut.
|
|
222
264
|
Also truncates individual lines exceeding TRUNCATE_MAX_LINE_LENGTH.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
text: The output text to truncate.
|
|
268
|
+
source_file: Optional file path that produced this output (for targeted hints).
|
|
269
|
+
source_tool: Optional tool name (e.g. "bash", "grep") for hint context.
|
|
223
270
|
"""
|
|
224
271
|
if not text:
|
|
225
272
|
return text
|
|
@@ -240,10 +287,11 @@ def truncate_output(text: str) -> str:
|
|
|
240
287
|
head = lines[:TRUNCATE_KEEP_START]
|
|
241
288
|
tail = lines[-TRUNCATE_KEEP_END:]
|
|
242
289
|
omitted = line_count - TRUNCATE_KEEP_START - TRUNCATE_KEEP_END
|
|
290
|
+
hint = _build_truncation_hint(source_file, source_tool, TRUNCATE_KEEP_START)
|
|
243
291
|
return (
|
|
244
292
|
"".join(head)
|
|
245
293
|
+ f"\n\n[... {omitted:,} lines omitted ({line_count:,} total)]"
|
|
246
|
-
+
|
|
294
|
+
+ hint + "\n\n"
|
|
247
295
|
+ "".join(tail)
|
|
248
296
|
)
|
|
249
297
|
|
|
@@ -258,11 +306,12 @@ def truncate_output(text: str) -> str:
|
|
|
258
306
|
total += line_bytes
|
|
259
307
|
|
|
260
308
|
remaining = line_count - len(kept_lines)
|
|
309
|
+
hint = _build_truncation_hint(source_file, source_tool, len(kept_lines))
|
|
261
310
|
return (
|
|
262
311
|
"".join(kept_lines)
|
|
263
312
|
+ f"\n\n[... truncated at ~{TRUNCATE_MAX_BYTES // 1024}KB — "
|
|
264
313
|
f"{remaining:,} more lines]"
|
|
265
|
-
+
|
|
314
|
+
+ hint + "\n"
|
|
266
315
|
)
|
|
267
316
|
|
|
268
317
|
|
|
@@ -278,7 +327,10 @@ def should_compact(
|
|
|
278
327
|
history_or_tokens: int | list[dict[str, str]],
|
|
279
328
|
model_id: str = "default",
|
|
280
329
|
) -> bool:
|
|
281
|
-
"""Check if the conversation should be compacted
|
|
330
|
+
"""Check if the conversation should be compacted.
|
|
331
|
+
|
|
332
|
+
Uses OpenCode's approach: usable = model_limit - buffer, then
|
|
333
|
+
trigger when tokens >= usable * threshold_ratio.
|
|
282
334
|
|
|
283
335
|
Accepts either an estimated token count (int) or the history list
|
|
284
336
|
(from which tokens are estimated via char count).
|
|
@@ -288,7 +340,8 @@ def should_compact(
|
|
|
288
340
|
else:
|
|
289
341
|
tokens = history_or_tokens
|
|
290
342
|
limit = MODEL_CONTEXT_LIMITS.get(model_id, MODEL_CONTEXT_LIMITS["default"])
|
|
291
|
-
|
|
343
|
+
usable = limit - COMPACTION_BUFFER_TOKENS
|
|
344
|
+
threshold = int(usable * COMPACTION_THRESHOLD_RATIO)
|
|
292
345
|
return tokens >= threshold
|
|
293
346
|
|
|
294
347
|
|
|
@@ -340,9 +393,14 @@ def build_compaction_prompt(
|
|
|
340
393
|
if plan_task:
|
|
341
394
|
parts.append(f"**Active task:** {plan_task}\n\n")
|
|
342
395
|
|
|
396
|
+
import re as _re
|
|
397
|
+
_code_block_re = _re.compile(r"```[\s\S]*?```")
|
|
398
|
+
|
|
343
399
|
for msg in old_msgs:
|
|
344
400
|
role = msg["role"].upper()
|
|
345
401
|
content = msg["content"]
|
|
402
|
+
# Strip large code blocks — compactor only needs to know what was done, not raw code
|
|
403
|
+
content = _code_block_re.sub("[code block removed]", content)
|
|
346
404
|
# Cap individual messages in the compaction input to avoid blowing up
|
|
347
405
|
if len(content) > 2000:
|
|
348
406
|
content = content[:2000] + f"... [{len(content) - 2000} chars truncated]"
|
|
@@ -409,7 +467,12 @@ async def compact_conversation(
|
|
|
409
467
|
compactor = Agent(
|
|
410
468
|
name="Compactor",
|
|
411
469
|
model=create_model(small_ref, max_tokens=2048),
|
|
412
|
-
instructions=
|
|
470
|
+
instructions=(
|
|
471
|
+
"You summarize coding conversations concisely. Output ONLY the requested sections, no preamble. "
|
|
472
|
+
"Preserve: user goals, explicit instructions/preferences, file paths with line numbers, "
|
|
473
|
+
"function/class names that were modified, and what remains to be done. "
|
|
474
|
+
"Drop: raw code blocks, tool output details, greetings, reasoning."
|
|
475
|
+
),
|
|
413
476
|
markdown=True,
|
|
414
477
|
)
|
|
415
478
|
|
|
@@ -198,15 +198,11 @@ TOOL_DISPLAY_NAMES = {
|
|
|
198
198
|
"read_file": "Read",
|
|
199
199
|
"read_file_smart": "ReadSmart",
|
|
200
200
|
"write_file": "Write",
|
|
201
|
-
"write_files": "Write",
|
|
202
201
|
"edit_file": "Edit",
|
|
203
|
-
"edit_files": "Edit",
|
|
204
202
|
"glob_search": "Glob",
|
|
205
203
|
"grep_search": "Grep",
|
|
206
204
|
"list_directory": "List",
|
|
207
205
|
"bash": "Bash",
|
|
208
|
-
"code_structure": "Structure",
|
|
209
|
-
"find_dependencies": "Deps",
|
|
210
206
|
"rank_files": "Rank",
|
|
211
207
|
}
|
|
212
208
|
|
|
@@ -219,8 +215,6 @@ TOOL_PRIMARY_ARG = {
|
|
|
219
215
|
"grep_search": "pattern",
|
|
220
216
|
"list_directory": "directory",
|
|
221
217
|
"bash": "command",
|
|
222
|
-
"code_structure": "file_path",
|
|
223
|
-
"find_dependencies": "file_path",
|
|
224
218
|
"rank_files": "task",
|
|
225
219
|
}
|
|
226
220
|
|
|
@@ -231,13 +225,6 @@ def _format_tool_label(tool_name: str, tool_args: dict | None) -> str:
|
|
|
231
225
|
if not tool_args:
|
|
232
226
|
return display
|
|
233
227
|
|
|
234
|
-
if tool_name == "write_files":
|
|
235
|
-
files = tool_args.get("files", [])
|
|
236
|
-
return f"{display}({len(files)} files)"
|
|
237
|
-
if tool_name == "edit_files":
|
|
238
|
-
edits = tool_args.get("edits", [])
|
|
239
|
-
return f"{display}({len(edits)} edits)"
|
|
240
|
-
|
|
241
228
|
primary_key = TOOL_PRIMARY_ARG.get(tool_name)
|
|
242
229
|
if primary_key and primary_key in tool_args:
|
|
243
230
|
value = str(tool_args[primary_key])
|
|
@@ -23,7 +23,7 @@ from aru.permissions import get_skip_permissions
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
# Categories of tools that modify files (for highlighting in history)
|
|
26
|
-
_MUTATION_TOOLS = {"write_file", "
|
|
26
|
+
_MUTATION_TOOLS = {"write_file", "edit_file", "bash"}
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def build_env_context(session, cwd: str | None = None) -> str:
|
|
@@ -249,12 +249,15 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
249
249
|
run_input_tokens = getattr(run_output.metrics, "input_tokens", 0) or 0
|
|
250
250
|
if should_compact(run_input_tokens, session.model_id):
|
|
251
251
|
try:
|
|
252
|
+
# Always prune first to shrink history before compaction
|
|
253
|
+
session.history = prune_history(session.history, model_id=session.model_id)
|
|
252
254
|
session.history = await compact_conversation(
|
|
253
255
|
session.history, session.model_ref, session.plan_task,
|
|
254
256
|
model_id=session.model_id,
|
|
255
257
|
)
|
|
256
258
|
console.print("[dim]Context compacted to save tokens.[/dim]")
|
|
257
259
|
except Exception:
|
|
260
|
+
# Even if compaction fails, keep the pruned history
|
|
258
261
|
pass
|
|
259
262
|
|
|
260
263
|
final_content = accumulated or final_content
|
|
@@ -257,39 +257,6 @@ def _format_structure(structure: dict, file_path: str, total_lines: int) -> str:
|
|
|
257
257
|
return "\n".join(parts)
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def code_structure(file_path: str) -> str:
|
|
261
|
-
"""Analyze a file and return its structural overview: imports, classes, functions, and globals.
|
|
262
|
-
|
|
263
|
-
Useful for quickly understanding what a file contains without reading its full content.
|
|
264
|
-
Works best with Python files (using tree-sitter AST parsing), but falls back to
|
|
265
|
-
regex-based extraction for other languages.
|
|
266
|
-
|
|
267
|
-
Args:
|
|
268
|
-
file_path: Path to the file to analyze.
|
|
269
|
-
"""
|
|
270
|
-
try:
|
|
271
|
-
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
272
|
-
content = f.read()
|
|
273
|
-
except FileNotFoundError:
|
|
274
|
-
return f"Error: File not found: {file_path}"
|
|
275
|
-
except Exception as e:
|
|
276
|
-
return f"Error reading file: {e}"
|
|
277
|
-
|
|
278
|
-
total_lines = content.count("\n") + 1
|
|
279
|
-
_, ext = os.path.splitext(file_path)
|
|
280
|
-
|
|
281
|
-
# Try tree-sitter for supported languages
|
|
282
|
-
if ext in SUPPORTED_EXTENSIONS and _TREE_SITTER_AVAILABLE:
|
|
283
|
-
source = content.encode("utf-8")
|
|
284
|
-
tree = _parse_python_tree(source)
|
|
285
|
-
if tree:
|
|
286
|
-
structure = _extract_structure_treesitter(tree, source, file_path)
|
|
287
|
-
return _format_structure(structure, file_path, total_lines)
|
|
288
|
-
|
|
289
|
-
# Fallback to regex
|
|
290
|
-
structure = _extract_structure_regex(content)
|
|
291
|
-
return _format_structure(structure, file_path, total_lines)
|
|
292
|
-
|
|
293
260
|
|
|
294
261
|
def _resolve_import_to_file(import_text: str, project_root: str) -> str | None:
|
|
295
262
|
"""Try to resolve an import statement to a file path within the project."""
|
|
@@ -331,94 +298,3 @@ def _find_project_root(file_path: str) -> str:
|
|
|
331
298
|
current = parent
|
|
332
299
|
|
|
333
300
|
|
|
334
|
-
def find_dependencies(file_path: str, depth: int = 3) -> str:
|
|
335
|
-
"""Trace the import dependency tree of a file within the project.
|
|
336
|
-
|
|
337
|
-
Resolves local imports (within the project) and shows which files depend on which.
|
|
338
|
-
Skips stdlib and third-party packages. Useful for understanding how files are connected.
|
|
339
|
-
|
|
340
|
-
Args:
|
|
341
|
-
file_path: Path to the file to analyze.
|
|
342
|
-
depth: Maximum recursion depth for tracing imports. Defaults to 3.
|
|
343
|
-
"""
|
|
344
|
-
if not os.path.isfile(file_path):
|
|
345
|
-
return f"Error: File not found: {file_path}"
|
|
346
|
-
|
|
347
|
-
project_root = _find_project_root(file_path)
|
|
348
|
-
rel_start = os.path.relpath(file_path, project_root).replace("\\", "/")
|
|
349
|
-
|
|
350
|
-
visited: set[str] = set()
|
|
351
|
-
tree_lines: list[str] = []
|
|
352
|
-
|
|
353
|
-
def _trace(rel_path: str, current_depth: int, prefix: str = "", is_last: bool = True):
|
|
354
|
-
if rel_path in visited or current_depth > depth:
|
|
355
|
-
if rel_path in visited:
|
|
356
|
-
connector = "└── " if is_last else "├── "
|
|
357
|
-
tree_lines.append(f"{prefix}{connector}{rel_path} (circular)")
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
visited.add(rel_path)
|
|
361
|
-
connector = "└── " if is_last else "├── "
|
|
362
|
-
|
|
363
|
-
if current_depth == 0:
|
|
364
|
-
tree_lines.append(rel_path)
|
|
365
|
-
else:
|
|
366
|
-
tree_lines.append(f"{prefix}{connector}{rel_path}")
|
|
367
|
-
|
|
368
|
-
# Read file and extract imports
|
|
369
|
-
full_path = os.path.join(project_root, rel_path)
|
|
370
|
-
if not os.path.isfile(full_path):
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
375
|
-
content = f.read()
|
|
376
|
-
except OSError:
|
|
377
|
-
return
|
|
378
|
-
|
|
379
|
-
# Extract imports (tree-sitter or regex)
|
|
380
|
-
_, ext = os.path.splitext(rel_path)
|
|
381
|
-
imports = []
|
|
382
|
-
|
|
383
|
-
if ext == ".py" and _TREE_SITTER_AVAILABLE:
|
|
384
|
-
source = content.encode("utf-8")
|
|
385
|
-
tree = _parse_python_tree(source)
|
|
386
|
-
if tree:
|
|
387
|
-
for child in tree.root_node.children:
|
|
388
|
-
if child.type in ("import_statement", "import_from_statement"):
|
|
389
|
-
text = source[child.start_byte:child.end_byte].decode("utf-8", errors="ignore").strip()
|
|
390
|
-
imports.append(text)
|
|
391
|
-
else:
|
|
392
|
-
for line in content.split("\n"):
|
|
393
|
-
stripped = line.strip()
|
|
394
|
-
if stripped.startswith("import ") or stripped.startswith("from "):
|
|
395
|
-
imports.append(stripped)
|
|
396
|
-
|
|
397
|
-
# Resolve imports to local files
|
|
398
|
-
local_deps = []
|
|
399
|
-
for imp in imports:
|
|
400
|
-
resolved = _resolve_import_to_file(imp, project_root)
|
|
401
|
-
if resolved and resolved != rel_path:
|
|
402
|
-
local_deps.append(resolved)
|
|
403
|
-
|
|
404
|
-
# Remove duplicates while preserving order
|
|
405
|
-
seen = set()
|
|
406
|
-
unique_deps = []
|
|
407
|
-
for dep in local_deps:
|
|
408
|
-
normalized = dep.replace("\\", "/")
|
|
409
|
-
if normalized not in seen:
|
|
410
|
-
seen.add(normalized)
|
|
411
|
-
unique_deps.append(normalized)
|
|
412
|
-
|
|
413
|
-
# Recurse into dependencies
|
|
414
|
-
child_prefix = prefix + (" " if is_last else "│ ")
|
|
415
|
-
for i, dep in enumerate(unique_deps):
|
|
416
|
-
is_dep_last = (i == len(unique_deps) - 1)
|
|
417
|
-
_trace(dep, current_depth + 1, child_prefix if current_depth > 0 else "", is_dep_last)
|
|
418
|
-
|
|
419
|
-
_trace(rel_start, 0)
|
|
420
|
-
|
|
421
|
-
if not tree_lines:
|
|
422
|
-
return f"No dependencies found for: {file_path}"
|
|
423
|
-
|
|
424
|
-
return "\n".join(tree_lines)
|