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.
Files changed (57) hide show
  1. {aru_code-0.14.0/aru_code.egg-info → aru_code-0.15.0}/PKG-INFO +4 -8
  2. {aru_code-0.14.0 → aru_code-0.15.0}/README.md +3 -7
  3. aru_code-0.15.0/aru/__init__.py +1 -0
  4. {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/base.py +5 -10
  5. {aru_code-0.14.0 → aru_code-0.15.0}/aru/context.py +87 -24
  6. {aru_code-0.14.0 → aru_code-0.15.0}/aru/display.py +0 -13
  7. {aru_code-0.14.0 → aru_code-0.15.0}/aru/runner.py +4 -1
  8. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/ast_tools.py +0 -124
  9. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/codebase.py +13 -27
  10. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/ranker.py +1 -1
  11. {aru_code-0.14.0 → aru_code-0.15.0/aru_code.egg-info}/PKG-INFO +4 -8
  12. {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/SOURCES.txt +0 -1
  13. {aru_code-0.14.0 → aru_code-0.15.0}/pyproject.toml +1 -1
  14. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_advanced.py +0 -10
  15. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_new.py +0 -5
  16. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_codebase.py +4 -150
  17. aru_code-0.14.0/aru/__init__.py +0 -1
  18. aru_code-0.14.0/tests/test_ast_tools.py +0 -762
  19. {aru_code-0.14.0 → aru_code-0.15.0}/LICENSE +0 -0
  20. {aru_code-0.14.0 → aru_code-0.15.0}/aru/agent_factory.py +0 -0
  21. {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/__init__.py +0 -0
  22. {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/executor.py +0 -0
  23. {aru_code-0.14.0 → aru_code-0.15.0}/aru/agents/planner.py +0 -0
  24. {aru_code-0.14.0 → aru_code-0.15.0}/aru/cli.py +0 -0
  25. {aru_code-0.14.0 → aru_code-0.15.0}/aru/commands.py +0 -0
  26. {aru_code-0.14.0 → aru_code-0.15.0}/aru/completers.py +0 -0
  27. {aru_code-0.14.0 → aru_code-0.15.0}/aru/config.py +0 -0
  28. {aru_code-0.14.0 → aru_code-0.15.0}/aru/permissions.py +0 -0
  29. {aru_code-0.14.0 → aru_code-0.15.0}/aru/providers.py +0 -0
  30. {aru_code-0.14.0 → aru_code-0.15.0}/aru/runtime.py +0 -0
  31. {aru_code-0.14.0 → aru_code-0.15.0}/aru/session.py +0 -0
  32. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/__init__.py +0 -0
  33. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/gitignore.py +0 -0
  34. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/mcp_client.py +0 -0
  35. {aru_code-0.14.0 → aru_code-0.15.0}/aru/tools/tasklist.py +0 -0
  36. {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/dependency_links.txt +0 -0
  37. {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/entry_points.txt +0 -0
  38. {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/requires.txt +0 -0
  39. {aru_code-0.14.0 → aru_code-0.15.0}/aru_code.egg-info/top_level.txt +0 -0
  40. {aru_code-0.14.0 → aru_code-0.15.0}/setup.cfg +0 -0
  41. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_agents_base.py +0 -0
  42. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli.py +0 -0
  43. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_base.py +0 -0
  44. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_completers.py +0 -0
  45. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_run_cli.py +0 -0
  46. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_session.py +0 -0
  47. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_cli_shell.py +0 -0
  48. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_config.py +0 -0
  49. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_context.py +0 -0
  50. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_executor.py +0 -0
  51. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_gitignore.py +0 -0
  52. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_main.py +0 -0
  53. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_mcp_client.py +0 -0
  54. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_permissions.py +0 -0
  55. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_planner.py +0 -0
  56. {aru_code-0.14.0 → aru_code-0.15.0}/tests/test_providers.py +0 -0
  57. {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.14.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, code_structure
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` / `write_files` — Writes single or batch files
484
- - `edit_file` / `edit_files` — Find-replace edits across multiple files
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, code_structure
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` / `write_files` — Writes single or batch files
437
- - `edit_file` / `edit_files` — Find-replace edits across multiple files
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, run_command, or any write/exec tool — they do not exist in your toolkit. \
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"). Batch independent file writes using `write_files` or `edit_files` \
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/run_command to read files.** Always use `read_file` or `grep_search`.
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/run_command to read files.** Always use `read_file` or `grep_search`.
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 = 20_000 # ~5.7K tokens
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 = 500
28
- TRUNCATE_MAX_BYTES = 20 * 1024 # 20 KB
29
- TRUNCATE_KEEP_START = 350 # lines to keep from the start
30
- TRUNCATE_KEEP_END = 100 # lines to keep from the 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 delay compaction. Returns ~10% of the model's context in chars (~3.5 chars/token).
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
- # ~3.5 chars per token, protect ~10% of context
110
- protect = int(limit * 0.10 * 3.5)
111
- # Clamp between 20K (minimum usable) and 80K (diminishing returns)
112
- return max(20_000, min(protect, 80_000))
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
- if any(marker in msg["content"] for marker in PRUNE_PROTECTED_MARKERS):
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
- _TRUNCATION_HINT = (
211
- "\n[Hint: Use grep_search to find specific content, or read_file with "
212
- "start_line/end_line for incremental reading. "
213
- "For large exploration tasks, use delegate_task to keep your context clean.]"
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
- def truncate_output(text: str) -> str:
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
- + _TRUNCATION_HINT + "\n\n"
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
- + _TRUNCATION_HINT + "\n"
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 (reactive, post-run).
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
- threshold = int(limit * COMPACTION_THRESHOLD_RATIO)
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="You summarize conversations concisely. Output ONLY the summary, no preamble.",
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", "write_files", "edit_file", "edit_files", "bash", "run_command"}
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)