aru-code 0.24.0__tar.gz → 0.24.2__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 (69) hide show
  1. {aru_code-0.24.0/aru_code.egg-info → aru_code-0.24.2}/PKG-INFO +5 -3
  2. {aru_code-0.24.0 → aru_code-0.24.2}/README.md +4 -2
  3. aru_code-0.24.2/aru/__init__.py +1 -0
  4. {aru_code-0.24.0 → aru_code-0.24.2}/aru/cli.py +3 -1
  5. {aru_code-0.24.0 → aru_code-0.24.2}/aru/display.py +28 -0
  6. {aru_code-0.24.0 → aru_code-0.24.2}/aru/runner.py +87 -6
  7. {aru_code-0.24.0 → aru_code-0.24.2}/aru/runtime.py +3 -0
  8. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/codebase.py +105 -10
  9. {aru_code-0.24.0 → aru_code-0.24.2/aru_code.egg-info}/PKG-INFO +5 -3
  10. {aru_code-0.24.0 → aru_code-0.24.2}/pyproject.toml +1 -1
  11. aru_code-0.24.0/aru/__init__.py +0 -1
  12. {aru_code-0.24.0 → aru_code-0.24.2}/LICENSE +0 -0
  13. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agent_factory.py +0 -0
  14. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/__init__.py +0 -0
  15. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/base.py +0 -0
  16. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/executor.py +0 -0
  17. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/explorer.py +0 -0
  18. {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/planner.py +0 -0
  19. {aru_code-0.24.0 → aru_code-0.24.2}/aru/cache_patch.py +0 -0
  20. {aru_code-0.24.0 → aru_code-0.24.2}/aru/checkpoints.py +0 -0
  21. {aru_code-0.24.0 → aru_code-0.24.2}/aru/commands.py +0 -0
  22. {aru_code-0.24.0 → aru_code-0.24.2}/aru/completers.py +0 -0
  23. {aru_code-0.24.0 → aru_code-0.24.2}/aru/config.py +0 -0
  24. {aru_code-0.24.0 → aru_code-0.24.2}/aru/context.py +0 -0
  25. {aru_code-0.24.0 → aru_code-0.24.2}/aru/history_blocks.py +0 -0
  26. {aru_code-0.24.0 → aru_code-0.24.2}/aru/permissions.py +0 -0
  27. {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/__init__.py +0 -0
  28. {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/custom_tools.py +0 -0
  29. {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/hooks.py +0 -0
  30. {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/manager.py +0 -0
  31. {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/tool_api.py +0 -0
  32. {aru_code-0.24.0 → aru_code-0.24.2}/aru/providers.py +0 -0
  33. {aru_code-0.24.0 → aru_code-0.24.2}/aru/session.py +0 -0
  34. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/__init__.py +0 -0
  35. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/ast_tools.py +0 -0
  36. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/gitignore.py +0 -0
  37. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/mcp_client.py +0 -0
  38. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/ranker.py +0 -0
  39. {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/tasklist.py +0 -0
  40. {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/SOURCES.txt +0 -0
  41. {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/dependency_links.txt +0 -0
  42. {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/entry_points.txt +0 -0
  43. {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/requires.txt +0 -0
  44. {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/top_level.txt +0 -0
  45. {aru_code-0.24.0 → aru_code-0.24.2}/setup.cfg +0 -0
  46. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_agents_base.py +0 -0
  47. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_checkpoints.py +0 -0
  48. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli.py +0 -0
  49. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_advanced.py +0 -0
  50. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_base.py +0 -0
  51. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_completers.py +0 -0
  52. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_new.py +0 -0
  53. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_run_cli.py +0 -0
  54. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_session.py +0 -0
  55. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_shell.py +0 -0
  56. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_codebase.py +0 -0
  57. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_confabulation_regression.py +0 -0
  58. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_config.py +0 -0
  59. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_context.py +0 -0
  60. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_executor.py +0 -0
  61. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_gitignore.py +0 -0
  62. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_guardrails_scenarios.py +0 -0
  63. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_main.py +0 -0
  64. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_mcp_client.py +0 -0
  65. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_permissions.py +0 -0
  66. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_planner.py +0 -0
  67. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_plugins.py +0 -0
  68. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_providers.py +0 -0
  69. {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.24.0
3
+ Version: 0.24.2
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,6 +513,7 @@ 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
 
@@ -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
- │ │ └── executor.py # Execution agent
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,6 +466,7 @@ 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
 
@@ -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
- │ │ └── executor.py # Execution agent
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.24.2"
@@ -199,7 +199,8 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
199
199
  # Apply tree_depth from config
200
200
  session._tree_max_depth = config.tree_depth
201
201
 
202
- # Wire file-mutation callback and atexit cleanup
202
+ # Wire session and file-mutation callback
203
+ ctx.session = session
203
204
  ctx.on_file_mutation = session.invalidate_context_cache
204
205
  atexit.register(lambda: cleanup_processes(ctx.tracked_processes))
205
206
 
@@ -745,6 +746,7 @@ async def run_oneshot(prompt: str, print_only: bool = False, skip_permissions: b
745
746
  if config.default_model:
746
747
  session.model_ref = config.default_model
747
748
 
749
+ ctx.session = session
748
750
  ctx.model_id = session.model_id
749
751
  small_ref = config.model_aliases.get("small") if config else None
750
752
  if not small_ref:
@@ -253,6 +253,34 @@ def _format_tool_label(tool_name: str, tool_args: dict | None) -> str:
253
253
  return display
254
254
 
255
255
 
256
+ def subagent_progress(label: str, tool_name: str, tool_args: dict | None,
257
+ duration: float | None = None):
258
+ """Print sub-agent tool completion into the active Live context (or console).
259
+
260
+ Only called on tool completion — shows a single ✓ line per tool call,
261
+ keeping the output compact (no duplicate start/complete lines).
262
+ """
263
+ from aru.runtime import get_ctx
264
+ try:
265
+ ctx = get_ctx()
266
+ except LookupError:
267
+ return
268
+ tool_label = _format_tool_label(tool_name, tool_args)
269
+ dur_str = f" {duration:.1f}s" if duration and duration >= 0.5 else ""
270
+ line = Text.assemble(
271
+ (" ", ""),
272
+ ("\u2713 ", "bold green"),
273
+ (f"[{label}] ", "dim"),
274
+ (tool_label, "dim"),
275
+ (dur_str, "dim cyan"),
276
+ )
277
+ target = ctx.live if ctx.live else None
278
+ if target:
279
+ target.console.print(line)
280
+ else:
281
+ ctx.console.print(line)
282
+
283
+
256
284
  class ToolTracker:
257
285
  """Tracks active tool calls with timing, displayed inside the Live area."""
258
286
 
@@ -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
- step.status = "failed"
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[red]Step {step.index} failed: {fail_msg}[/red]")
592
- if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
593
- break
594
- elif content or tasks_all_done:
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
 
@@ -122,6 +122,9 @@ class RuntimeContext:
122
122
  # -- Plugins --
123
123
  plugin_manager: Any = None # aru.plugins.manager.PluginManager (lazy to avoid circular)
124
124
 
125
+ # -- Session --
126
+ session: Any = None # aru.session.Session (set by CLI, used for sub-agent cost tracking)
127
+
125
128
  # -- Checkpoints --
126
129
  checkpoint_manager: Any = None # aru.checkpoints.CheckpointManager (lazy)
127
130
 
@@ -381,7 +381,22 @@ def edit_file(file_path: str, old_string: str, new_string: str) -> str:
381
381
 
382
382
  count = content.count(old_string)
383
383
  if count == 0:
384
- return f"Error: old_string not found in {file_path}"
384
+ # Provide context: show closest matching region to help the LLM retry
385
+ import difflib
386
+ lines = content.splitlines(keepends=True)
387
+ old_lines = old_string.splitlines(keepends=True)
388
+ matcher = difflib.SequenceMatcher(None, lines, old_lines)
389
+ best = matcher.find_longest_match(0, len(lines), 0, len(old_lines))
390
+ if best.size > 0:
391
+ # Show surrounding context around the best match
392
+ ctx_start = max(0, best.a - 2)
393
+ ctx_end = min(len(lines), best.a + best.size + 2)
394
+ snippet = "".join(f"{ctx_start + i + 1:4d} | {lines[ctx_start + i]}" for i in range(ctx_end - ctx_start))
395
+ return f"Error: old_string not found in {file_path}. Closest match region:\n{snippet}"
396
+ else:
397
+ # No match at all — show first 20 lines for context
398
+ snippet = "".join(f"{i + 1:4d} | {l}" for i, l in enumerate(lines[:20]))
399
+ return f"Error: old_string not found in {file_path}. File starts with:\n{snippet}"
385
400
  if count > 1:
386
401
  return f"Error: old_string found {count} times in {file_path}. Must be unique."
387
402
 
@@ -1193,11 +1208,15 @@ def _fetch_direct(url: str, max_chars: int) -> str:
1193
1208
  return _truncate_output(text, source_tool="web_fetch")
1194
1209
 
1195
1210
 
1211
+ _subagent_counter = 0
1212
+ _subagent_counter_lock = threading.Lock()
1213
+
1214
+
1196
1215
  def _next_subagent_id() -> int:
1197
- ctx = get_ctx()
1198
- with ctx.subagent_counter_lock:
1199
- ctx.subagent_counter += 1
1200
- return ctx.subagent_counter
1216
+ global _subagent_counter
1217
+ with _subagent_counter_lock:
1218
+ _subagent_counter += 1
1219
+ return _subagent_counter
1201
1220
 
1202
1221
 
1203
1222
  # Import new tools
@@ -1285,15 +1304,91 @@ Do not create documentation files unless explicitly asked.
1285
1304
  )
1286
1305
 
1287
1306
  label = f"Explorer-{agent_id}" if _agent_name == "explorer" else f"SubAgent-{agent_id}"
1307
+
1308
+ async def _execute_with_streaming(agent_instance) -> str:
1309
+ """Run a sub-agent with streaming events for live progress display."""
1310
+ import time as _time
1311
+ from agno.run.agent import (
1312
+ RunContentEvent,
1313
+ RunOutput,
1314
+ ToolCallCompletedEvent,
1315
+ ToolCallStartedEvent,
1316
+ )
1317
+ from aru.display import subagent_progress
1318
+
1319
+ result_content = ""
1320
+ run_output = None
1321
+ _tool_starts: dict[str, float] = {}
1322
+
1323
+ async for event in agent_instance.arun(task, stream=True, stream_events=True, yield_run_output=True):
1324
+ if isinstance(event, RunOutput):
1325
+ run_output = event
1326
+ break
1327
+ elif isinstance(event, ToolCallStartedEvent):
1328
+ if hasattr(event, "tool") and event.tool:
1329
+ t_id = getattr(event.tool, "tool_call_id", None) or (event.tool.tool_name or "tool")
1330
+ else:
1331
+ t_id = getattr(event, "tool_call_id", None) or getattr(event, "tool_name", "tool")
1332
+ _tool_starts[t_id] = _time.monotonic()
1333
+ elif isinstance(event, ToolCallCompletedEvent):
1334
+ if hasattr(event, "tool") and event.tool:
1335
+ t_id = getattr(event.tool, "tool_call_id", None) or getattr(event.tool, "tool_name", "tool")
1336
+ t_name = event.tool.tool_name or "tool"
1337
+ t_args = event.tool.tool_args
1338
+ else:
1339
+ t_id = getattr(event, "tool_call_id", None) or getattr(event, "tool_name", "tool")
1340
+ t_name = getattr(event, "tool_name", "tool")
1341
+ t_args = getattr(event, "tool_args", None)
1342
+ dur = _time.monotonic() - _tool_starts.pop(t_id, _time.monotonic())
1343
+ subagent_progress(label, t_name, t_args if isinstance(t_args, dict) else None, duration=dur)
1344
+ elif isinstance(event, RunContentEvent):
1345
+ if hasattr(event, "content") and event.content:
1346
+ result_content += event.content
1347
+
1348
+ # Track sub-agent token usage in the session so cost is not underestimated.
1349
+ if run_output and hasattr(run_output, "metrics") and run_output.metrics:
1350
+ try:
1351
+ session = get_ctx().session
1352
+ if session is not None:
1353
+ m = run_output.metrics
1354
+ session.total_input_tokens += getattr(m, "input_tokens", 0) or 0
1355
+ session.total_output_tokens += getattr(m, "output_tokens", 0) or 0
1356
+ session.total_cache_read_tokens += getattr(m, "cache_read_tokens", 0) or 0
1357
+ session.total_cache_write_tokens += getattr(m, "cache_write_tokens", 0) or 0
1358
+ session.api_calls += 1
1359
+ except (LookupError, AttributeError):
1360
+ pass
1361
+
1362
+ final_text = run_output.content if run_output and run_output.content else result_content
1363
+ if final_text:
1364
+ return _truncate_output(f"[{label}] {final_text}")
1365
+ return f"[{label}] Task completed but no output was returned."
1366
+
1288
1367
  try:
1289
1368
  from aru.permissions import permission_scope
1290
1369
  with permission_scope(agent_perm):
1291
- result = await sub.arun(task, stream=False)
1292
- if result and result.content:
1293
- return _truncate_output(f"[{label}] {result.content}")
1294
- return f"[{label}] Task completed but no output was returned."
1370
+ return await _execute_with_streaming(sub)
1295
1371
  except Exception as e:
1296
- return f"[{label}] Error: {e}"
1372
+ # Auto-retry once with a fresh agent instance
1373
+ try:
1374
+ from aru.permissions import permission_scope as _ps
1375
+ # Recreate the agent for a clean retry
1376
+ if _agent_name == "explorer":
1377
+ from aru.agents.explorer import create_explorer
1378
+ sub_retry = create_explorer(task, context)
1379
+ sub_retry.name = f"Explorer-{agent_id}"
1380
+ else:
1381
+ sub_retry = Agent(
1382
+ name=sub.name,
1383
+ model=sub.model,
1384
+ tools=sub.tools,
1385
+ instructions=sub.instructions,
1386
+ markdown=True,
1387
+ )
1388
+ with _ps(agent_perm):
1389
+ return await _execute_with_streaming(sub_retry)
1390
+ except Exception as e2:
1391
+ return f"[{label}] Error (after retry): {e2}"
1297
1392
 
1298
1393
  # Run in a separate asyncio Task so each sub-agent gets its own
1299
1394
  # contextvars snapshot — essential for parallel permission_scope isolation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.24.0
3
+ Version: 0.24.2
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,6 +513,7 @@ 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
 
@@ -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
- │ │ └── executor.py # Execution agent
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.24.0"
7
+ version = "0.24.2"
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.24.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
File without changes
File without changes