aru-code 0.12.0__tar.gz → 0.13.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.12.0/aru_code.egg-info → aru_code-0.13.0}/PKG-INFO +1 -1
  2. aru_code-0.13.0/aru/__init__.py +1 -0
  3. {aru_code-0.12.0 → aru_code-0.13.0}/aru/agent_factory.py +13 -2
  4. {aru_code-0.12.0 → aru_code-0.13.0}/aru/cli.py +30 -18
  5. {aru_code-0.12.0 → aru_code-0.13.0}/aru/config.py +5 -0
  6. {aru_code-0.12.0 → aru_code-0.13.0}/aru/context.py +25 -16
  7. {aru_code-0.12.0 → aru_code-0.13.0}/aru/runner.py +28 -21
  8. {aru_code-0.12.0 → aru_code-0.13.0}/aru/session.py +3 -1
  9. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/codebase.py +10 -8
  10. {aru_code-0.12.0 → aru_code-0.13.0/aru_code.egg-info}/PKG-INFO +1 -1
  11. {aru_code-0.12.0 → aru_code-0.13.0}/pyproject.toml +1 -1
  12. aru_code-0.12.0/aru/__init__.py +0 -1
  13. {aru_code-0.12.0 → aru_code-0.13.0}/LICENSE +0 -0
  14. {aru_code-0.12.0 → aru_code-0.13.0}/README.md +0 -0
  15. {aru_code-0.12.0 → aru_code-0.13.0}/aru/agents/__init__.py +0 -0
  16. {aru_code-0.12.0 → aru_code-0.13.0}/aru/agents/base.py +0 -0
  17. {aru_code-0.12.0 → aru_code-0.13.0}/aru/agents/executor.py +0 -0
  18. {aru_code-0.12.0 → aru_code-0.13.0}/aru/agents/planner.py +0 -0
  19. {aru_code-0.12.0 → aru_code-0.13.0}/aru/commands.py +0 -0
  20. {aru_code-0.12.0 → aru_code-0.13.0}/aru/completers.py +0 -0
  21. {aru_code-0.12.0 → aru_code-0.13.0}/aru/display.py +0 -0
  22. {aru_code-0.12.0 → aru_code-0.13.0}/aru/permissions.py +0 -0
  23. {aru_code-0.12.0 → aru_code-0.13.0}/aru/providers.py +0 -0
  24. {aru_code-0.12.0 → aru_code-0.13.0}/aru/runtime.py +0 -0
  25. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/__init__.py +0 -0
  26. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/ast_tools.py +0 -0
  27. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/gitignore.py +0 -0
  28. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/mcp_client.py +0 -0
  29. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/ranker.py +0 -0
  30. {aru_code-0.12.0 → aru_code-0.13.0}/aru/tools/tasklist.py +0 -0
  31. {aru_code-0.12.0 → aru_code-0.13.0}/aru_code.egg-info/SOURCES.txt +0 -0
  32. {aru_code-0.12.0 → aru_code-0.13.0}/aru_code.egg-info/dependency_links.txt +0 -0
  33. {aru_code-0.12.0 → aru_code-0.13.0}/aru_code.egg-info/entry_points.txt +0 -0
  34. {aru_code-0.12.0 → aru_code-0.13.0}/aru_code.egg-info/requires.txt +0 -0
  35. {aru_code-0.12.0 → aru_code-0.13.0}/aru_code.egg-info/top_level.txt +0 -0
  36. {aru_code-0.12.0 → aru_code-0.13.0}/setup.cfg +0 -0
  37. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_agents_base.py +0 -0
  38. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_ast_tools.py +0 -0
  39. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli.py +0 -0
  40. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_advanced.py +0 -0
  41. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_base.py +0 -0
  42. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_completers.py +0 -0
  43. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_new.py +0 -0
  44. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_run_cli.py +0 -0
  45. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_session.py +0 -0
  46. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_cli_shell.py +0 -0
  47. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_codebase.py +0 -0
  48. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_config.py +0 -0
  49. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_context.py +0 -0
  50. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_executor.py +0 -0
  51. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_gitignore.py +0 -0
  52. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_main.py +0 -0
  53. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_mcp_client.py +0 -0
  54. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_permissions.py +0 -0
  55. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_planner.py +0 -0
  56. {aru_code-0.12.0 → aru_code-0.13.0}/tests/test_providers.py +0 -0
  57. {aru_code-0.12.0 → aru_code-0.13.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.12.0
3
+ Version: 0.13.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
@@ -0,0 +1 @@
1
+ __version__ = "0.13.0"
@@ -12,8 +12,14 @@ def create_general_agent(
12
12
  session: Session,
13
13
  config: AgentConfig | None = None,
14
14
  model_override: str | None = None,
15
+ env_context: str = "",
15
16
  ):
16
- """Create the general-purpose agent."""
17
+ """Create the general-purpose agent.
18
+
19
+ Args:
20
+ env_context: Environment context (cwd, tree, git status) to include
21
+ in the system prompt. Placed in instructions so it's cacheable.
22
+ """
17
23
  from agno.agent import Agent
18
24
  from agno.compression.manager import CompressionManager
19
25
 
@@ -21,6 +27,8 @@ def create_general_agent(
21
27
  from aru.runtime import get_ctx
22
28
 
23
29
  extra = config.get_extra_instructions() if config else ""
30
+ if env_context:
31
+ extra = f"{extra}\n\n{env_context}" if extra else env_context
24
32
  model_ref = model_override or session.model_ref
25
33
 
26
34
  return Agent(
@@ -40,7 +48,8 @@ def create_general_agent(
40
48
 
41
49
 
42
50
  def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
43
- config: AgentConfig | None = None):
51
+ config: AgentConfig | None = None,
52
+ env_context: str = ""):
44
53
  """Create an Agno Agent from a CustomAgent definition."""
45
54
  from agno.agent import Agent
46
55
  from agno.compression.manager import CompressionManager
@@ -52,6 +61,8 @@ def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
52
61
  tools = resolve_tools(agent_def.tools)
53
62
 
54
63
  extra = config.get_extra_instructions() if config else ""
64
+ if env_context:
65
+ extra = f"{extra}\n\n{env_context}" if extra else env_context
55
66
  parts = [agent_def.system_prompt, BASE_INSTRUCTIONS]
56
67
  if extra:
57
68
  parts.append(extra)
@@ -71,6 +71,7 @@ from aru.runner import ( # noqa: F401
71
71
  _MUTATION_TOOLS,
72
72
  _build_file_context,
73
73
  _extract_plan_file_paths,
74
+ build_env_context,
74
75
  execute_plan_steps,
75
76
  run_agent_capture,
76
77
  )
@@ -156,6 +157,11 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
156
157
 
157
158
  extra_instructions = config.get_extra_instructions()
158
159
 
160
+ def _build_env_ctx() -> str:
161
+ """Build fresh environment context for agent system prompt."""
162
+ from aru.runner import build_env_context
163
+ return build_env_context(session)
164
+
159
165
  # Resume or create session
160
166
  if resume_id:
161
167
  if resume_id == "last":
@@ -183,6 +189,9 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
183
189
  _sync_model(session)
184
190
  _render_home(session, skip_permissions)
185
191
 
192
+ # Apply tree_depth from config
193
+ session._tree_max_depth = config.tree_depth
194
+
186
195
  # Wire file-mutation callback and atexit cleanup
187
196
  ctx.on_file_mutation = session.invalidate_context_cache
188
197
  atexit.register(lambda: cleanup_processes(ctx.tracked_processes))
@@ -192,9 +201,12 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
192
201
  paste_state = PasteState()
193
202
  prompt_session = _create_prompt_session(paste_state, config)
194
203
 
195
- # Startup: load MCP tools
196
- from aru.tools.codebase import load_mcp_tools
197
- await load_mcp_tools()
204
+ # Startup: load MCP tools in background (don't block REPL)
205
+ async def _load_mcp_background():
206
+ from aru.tools.codebase import load_mcp_tools
207
+ await load_mcp_tools()
208
+
209
+ asyncio.create_task(_load_mcp_background())
198
210
 
199
211
  while True:
200
212
  try:
@@ -359,15 +371,14 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
359
371
  continue
360
372
 
361
373
  if user_input.lower() == "/mcp":
362
- from aru.tools.codebase import ALL_TOOLS
363
- from agno.tools import Function
364
- mcp_tools = [t for t in ALL_TOOLS if isinstance(t, Function) and getattr(t, "name", "").count("__") > 0]
365
- if not mcp_tools:
374
+ from aru.tools.mcp_client import get_mcp_manager
375
+ manager = get_mcp_manager()
376
+ if not manager or not manager.catalog:
366
377
  console.print("[dim]No MCP tools loaded. Check aru.mcp.json config.[/dim]")
367
378
  else:
368
- console.print(f"[bold]Loaded MCP Tools ({len(mcp_tools)}):[/bold]\n")
369
- for t in mcp_tools:
370
- console.print(f" [bold cyan]{t.name}[/bold cyan] [dim]{t.description}[/dim]")
379
+ console.print(f"[bold]MCP Tools ({len(manager.catalog)}):[/bold]\n")
380
+ for entry in manager.catalog.values():
381
+ console.print(f" [bold cyan]{entry.name}[/bold cyan] [dim]{entry.description}[/dim]")
371
382
  continue
372
383
 
373
384
  if user_input.lower() == "/help":
@@ -436,16 +447,17 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
436
447
  prompt = render_command_template(cmd_def.template, cmd_args)
437
448
  console.print(f"[bold magenta]Running /{cmd_name}...[/bold magenta]")
438
449
 
450
+ env_ctx = _build_env_ctx()
439
451
  if cmd_def.agent and cmd_def.agent in config.custom_agents:
440
452
  agent_def = config.custom_agents[cmd_def.agent]
441
- agent = create_custom_agent_instance(agent_def, session, config)
453
+ agent = create_custom_agent_instance(agent_def, session, config, env_context=env_ctx)
442
454
  elif cmd_def.agent:
443
455
  console.print(f"[yellow]Warning: agent '{cmd_def.agent}' not found, using default[/yellow]")
444
- agent = create_general_agent(session, config, model_override=cmd_def.model)
456
+ agent = create_general_agent(session, config, model_override=cmd_def.model, env_context=env_ctx)
445
457
  elif cmd_def.model:
446
- agent = create_general_agent(session, config, model_override=cmd_def.model)
458
+ agent = create_general_agent(session, config, model_override=cmd_def.model, env_context=env_ctx)
447
459
  else:
448
- agent = create_general_agent(session, config)
460
+ agent = create_general_agent(session, config, env_context=env_ctx)
449
461
  session.add_message("user", user_input)
450
462
  run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
451
463
  if run_result.content:
@@ -458,7 +470,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
458
470
  prompt = render_skill_template(skill.content, cmd_args)
459
471
  console.print(f"[bold magenta]Running skill /{cmd_name}...[/bold magenta]")
460
472
 
461
- agent = create_general_agent(session, config)
473
+ agent = create_general_agent(session, config, env_context=_build_env_ctx())
462
474
  session.add_message("user", user_input)
463
475
  run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
464
476
  if run_result.content:
@@ -470,7 +482,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
470
482
  else:
471
483
  from aru.permissions import permission_scope
472
484
  console.print(f"[bold magenta]Running agent /{cmd_name}...[/bold magenta]")
473
- agent = create_custom_agent_instance(agent_def, session, config)
485
+ agent = create_custom_agent_instance(agent_def, session, config, env_context=_build_env_ctx())
474
486
  session.add_message("user", user_input)
475
487
  with permission_scope(agent_def.permission):
476
488
  run_result = await run_agent_capture(agent, cmd_args or user_input, session, images=attached_images or None)
@@ -498,14 +510,14 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
498
510
  agent_def = config.custom_agents[agent_name]
499
511
  from aru.permissions import permission_scope
500
512
  console.print(f"[bold magenta]Routing to @{agent_name}...[/bold magenta]")
501
- agent = create_custom_agent_instance(agent_def, session, config)
513
+ agent = create_custom_agent_instance(agent_def, session, config, env_context=_build_env_ctx())
502
514
  session.add_message("user", user_input)
503
515
  with permission_scope(agent_def.permission):
504
516
  run_result = await run_agent_capture(agent, message_text, session, images=attached_images or None)
505
517
  if run_result.content:
506
518
  session.add_message("assistant", run_result.with_tools_summary())
507
519
  else:
508
- agent = create_general_agent(session, config)
520
+ agent = create_general_agent(session, config, env_context=_build_env_ctx())
509
521
  session.add_message("user", user_input)
510
522
  run_result = await run_agent_capture(agent, user_input, session, images=attached_images or None)
511
523
  if run_result.content:
@@ -160,6 +160,7 @@ class AgentConfig:
160
160
  model_aliases: dict[str, str] = field(default_factory=dict)
161
161
  custom_agents: dict[str, CustomAgent] = field(default_factory=dict)
162
162
  plan_reviewer: bool = True
163
+ tree_depth: int = 2 # max depth for directory tree in system prompt
163
164
 
164
165
  @property
165
166
  def has_instructions(self) -> bool:
@@ -515,6 +516,10 @@ def load_config(cwd: str | None = None) -> AgentConfig:
515
516
  config.model_aliases = data["model_aliases"]
516
517
  if "plan_reviewer" in data:
517
518
  config.plan_reviewer = bool(data["plan_reviewer"])
519
+ if "tree_depth" in data:
520
+ td = data["tree_depth"]
521
+ if isinstance(td, int) and 0 <= td <= 5:
522
+ config.tree_depth = td
518
523
  # Resolve instructions (local files, globs, URLs)
519
524
  if "instructions" in data and isinstance(data["instructions"], list):
520
525
  entries = [str(e) for e in data["instructions"] if isinstance(e, str)]
@@ -10,12 +10,16 @@ from __future__ import annotations
10
10
 
11
11
  # ── Constants ──────────────────────────────────────────────────────
12
12
 
13
- # Pruning: protect the most recent N chars of assistant content from eviction
13
+ # Pruning: protect the most recent N chars of content from eviction
14
14
  PRUNE_PROTECT_CHARS = 50_000 # ~14K tokens
15
15
  # Pruning: minimum chars that must be freeable to justify a prune pass
16
16
  PRUNE_MINIMUM_CHARS = 20_000 # ~5.7K tokens
17
17
  # Placeholder that replaces evicted content
18
18
  PRUNED_PLACEHOLDER = "[previous output cleared to save context]"
19
+ # User messages larger than this threshold are truncated when outside protection window
20
+ PRUNE_USER_MSG_THRESHOLD = 2_000 # ~570 tokens — catches @file mentions
21
+ # How many chars to keep from the start of a pruned user message
22
+ PRUNE_USER_MSG_KEEP = 500 # ~140 tokens — enough to understand the request
19
23
 
20
24
  # Truncation: universal limits for any tool output
21
25
  TRUNCATE_MAX_LINES = 500
@@ -80,24 +84,24 @@ Be concise but complete. This summary replaces the full conversation history."""
80
84
  # ── Layer 1: Pruning ──────────────────────────────────────────────
81
85
 
82
86
  def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
83
- """Replace old assistant messages with a short placeholder to reduce tokens.
87
+ """Replace old messages with a short placeholder to reduce tokens.
84
88
 
85
- Walks backward through history, protecting the most recent assistant
86
- content (up to PRUNE_PROTECT_CHARS). Older assistant messages beyond
87
- that budget are replaced with a compact placeholder.
89
+ Walks backward through history, protecting the most recent content
90
+ (up to PRUNE_PROTECT_CHARS total across both roles). Older messages
91
+ beyond that budget are pruned:
92
+ - Assistant messages: replaced entirely with placeholder
93
+ - User messages over PRUNE_USER_MSG_THRESHOLD: truncated to first N chars
88
94
 
89
95
  Returns a new list (does not mutate the input).
90
96
  """
91
97
  if len(history) <= 2:
92
98
  return list(history)
93
99
 
94
- # Calculate total assistant chars
95
- total_assistant_chars = sum(
96
- len(msg["content"]) for msg in history if msg["role"] == "assistant"
97
- )
100
+ # Calculate total prunable chars (both roles)
101
+ total_chars = sum(len(msg["content"]) for msg in history)
98
102
 
99
103
  # Not enough to prune
100
- if total_assistant_chars < PRUNE_PROTECT_CHARS + PRUNE_MINIMUM_CHARS:
104
+ if total_chars < PRUNE_PROTECT_CHARS + PRUNE_MINIMUM_CHARS:
101
105
  return list(history)
102
106
 
103
107
  # Walk backward, protecting recent content
@@ -106,17 +110,22 @@ def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
106
110
 
107
111
  for i in range(len(result) - 1, -1, -1):
108
112
  msg = result[i]
109
- if msg["role"] != "assistant":
110
- continue
111
-
112
113
  msg_len = len(msg["content"])
114
+
113
115
  if protected + msg_len <= PRUNE_PROTECT_CHARS:
114
116
  # Still within protection window
115
117
  protected += msg_len
116
118
  else:
117
- # Beyond protection window — prune this message
118
- if msg["content"] != PRUNED_PLACEHOLDER:
119
- result[i] = {"role": "assistant", "content": PRUNED_PLACEHOLDER}
119
+ # Beyond protection window — prune
120
+ if msg["role"] == "assistant":
121
+ if msg["content"] != PRUNED_PLACEHOLDER:
122
+ result[i] = {"role": "assistant", "content": PRUNED_PLACEHOLDER}
123
+ elif msg["role"] == "user" and msg_len > PRUNE_USER_MSG_THRESHOLD:
124
+ # Large user messages (e.g. @file mentions with file contents)
125
+ # are truncated to keep a brief summary of the original request
126
+ truncated = msg["content"][:PRUNE_USER_MSG_KEEP] + \
127
+ f"\n\n[... {msg_len - PRUNE_USER_MSG_KEEP:,} chars pruned to save context ...]"
128
+ result[i] = {"role": "user", "content": truncated}
120
129
 
121
130
  return result
122
131
 
@@ -26,6 +26,24 @@ from aru.permissions import get_skip_permissions
26
26
  _MUTATION_TOOLS = {"write_file", "write_files", "edit_file", "edit_files", "bash", "run_command"}
27
27
 
28
28
 
29
+ def build_env_context(session, cwd: str | None = None) -> str:
30
+ """Build environment context string (cwd, git status) for system prompt.
31
+
32
+ This context goes into agent instructions (system prompt) so it's cached
33
+ by the provider between turns. Tree is omitted — the model uses
34
+ glob_search/list_directory on demand instead of paying upfront tokens.
35
+ """
36
+ cwd = cwd or os.getcwd()
37
+ parts = [f"The current working directory is: {cwd}"]
38
+
39
+ if session:
40
+ git_status = session.get_cached_git_status(cwd)
41
+ if git_status:
42
+ parts.append(f"Git status:\n{git_status}")
43
+
44
+ return "\n\n".join(parts)
45
+
46
+
29
47
  @dataclass
30
48
  class AgentRunResult:
31
49
  """Result from run_agent_capture including text output and tool call history."""
@@ -77,35 +95,24 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
77
95
  display = StreamingDisplay(status)
78
96
  tracker = display.tool_tracker
79
97
 
80
- # Build enriched message with environment context (using cache)
81
- dynamic_parts = []
82
- cwd = os.getcwd()
83
- dynamic_parts.append(f"The current working directory is: {cwd}")
98
+ # Build message environment context (tree/git/cwd) is now in the
99
+ # system prompt (agent instructions) so it's cacheable across turns.
100
+ # Only plan progress and budget warnings are added here.
101
+ msg_parts = []
84
102
 
85
103
  if session and not lightweight:
86
- env_context_parts = []
87
- tree_text = session.get_cached_tree(cwd)
88
- if tree_text:
89
- env_context_parts.append(f"Directory Tree (max depth 3):\n```text\n{tree_text}\n```")
90
-
91
- git_status = session.get_cached_git_status(cwd)
92
- if git_status:
93
- env_context_parts.append(f"Git status:\n{git_status}")
94
-
95
- if env_context_parts:
96
- dynamic_parts.append("## Environment Context\n" + "\n\n".join(env_context_parts))
97
-
98
- # Include only compact plan progress (not full plan text)
99
104
  if session.current_plan:
100
- dynamic_parts.append(f"## Active Plan\nTask: {session.plan_task}\n\n{session.render_plan_progress()}")
105
+ msg_parts.append(f"## Active Plan\nTask: {session.plan_task}\n\n{session.render_plan_progress()}")
101
106
 
102
- # Token budget warning
103
107
  warning = session.check_budget_warning()
104
108
  if warning:
105
109
  console.print(warning)
106
110
 
107
- dynamic_context = "\n\n".join(dynamic_parts)
108
- run_message = f"{dynamic_context}\n\n---\n\n## Current Task/Message\n{message}"
111
+ if msg_parts:
112
+ prefix = "\n\n".join(msg_parts)
113
+ run_message = f"{prefix}\n\n---\n\n{message}"
114
+ else:
115
+ run_message = message
109
116
 
110
117
  # Build conversation history as real messages for the LLM
111
118
  from aru.context import prune_history
@@ -145,6 +145,8 @@ class Session:
145
145
  self._cached_tree: str | None = None
146
146
  self._cached_git_status: str | None = None
147
147
  self._context_dirty: bool = True
148
+ # Tree depth for env context (configurable via aru.json "tree_depth")
149
+ self._tree_max_depth: int = 2
148
150
  # Token budget (0 = unlimited)
149
151
  self.token_budget: int = 0
150
152
 
@@ -229,7 +231,7 @@ class Session:
229
231
  """Regenerate tree and git status caches."""
230
232
  try:
231
233
  from aru.tools.codebase import get_project_tree
232
- self._cached_tree = get_project_tree(cwd, max_depth=3) or None
234
+ self._cached_tree = get_project_tree(cwd, max_depth=self._tree_max_depth) or None
233
235
  except Exception:
234
236
  self._cached_tree = None
235
237
  try:
@@ -632,8 +632,10 @@ def get_project_tree(root_dir: str, max_depth: int = 3, max_files_per_dir: int =
632
632
  lines.append(f"{file_indent}{f}")
633
633
 
634
634
  result = "\n".join(lines)
635
- if len(result) > 15000:
636
- return result[:15000] + "\n... [Tree truncated due to size]"
635
+ # Cap size — tree goes in system prompt now, keep it compact
636
+ max_chars = 5000
637
+ if len(result) > max_chars:
638
+ return result[:max_chars] + "\n... [Tree truncated due to size]"
637
639
  return result
638
640
 
639
641
 
@@ -1098,15 +1100,12 @@ ALL_TOOLS = [
1098
1100
  web_search,
1099
1101
  web_fetch,
1100
1102
  delegate_task,
1101
- code_structure,
1102
- find_dependencies,
1103
- rank_files,
1104
1103
  ]
1105
1104
 
1106
1105
  # Task list tools for executor subtask tracking
1107
1106
  from aru.tools.tasklist import create_task_list, update_task
1108
1107
 
1109
- # Executor tools — full write/execute capability, no discovery overhead
1108
+ # Executor tools — full write/execute capability
1110
1109
  EXECUTOR_TOOLS = [
1111
1110
  create_task_list,
1112
1111
  update_task,
@@ -1123,10 +1122,9 @@ EXECUTOR_TOOLS = [
1123
1122
  web_search,
1124
1123
  web_fetch,
1125
1124
  delegate_task,
1126
- code_structure,
1127
1125
  ]
1128
1126
 
1129
- # General-purpose tools — everything except niche analysis tools
1127
+ # General-purpose tools
1130
1128
  GENERAL_TOOLS = [
1131
1129
  read_file,
1132
1130
  read_file_smart,
@@ -1147,6 +1145,10 @@ GENERAL_TOOLS = [
1147
1145
  TOOL_REGISTRY: dict[str, object] = {f.__name__: f for f in ALL_TOOLS}
1148
1146
  TOOL_REGISTRY["create_task_list"] = create_task_list
1149
1147
  TOOL_REGISTRY["update_task"] = update_task
1148
+ # Kept in registry for custom agents that may reference them via resolve_tools
1149
+ TOOL_REGISTRY["code_structure"] = code_structure
1150
+ TOOL_REGISTRY["find_dependencies"] = find_dependencies
1151
+ TOOL_REGISTRY["rank_files"] = rank_files
1150
1152
 
1151
1153
 
1152
1154
  def resolve_tools(tool_spec: list[str] | dict[str, bool]) -> list:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.12.0
3
+ Version: 0.13.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.12.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes