aru-code 0.24.1__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.1/aru_code.egg-info → aru_code-0.24.2}/PKG-INFO +5 -3
  2. {aru_code-0.24.1 → 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.1 → aru_code-0.24.2}/aru/display.py +28 -0
  5. {aru_code-0.24.1 → aru_code-0.24.2}/aru/runner.py +87 -6
  6. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/codebase.py +96 -17
  7. {aru_code-0.24.1 → aru_code-0.24.2/aru_code.egg-info}/PKG-INFO +5 -3
  8. {aru_code-0.24.1 → aru_code-0.24.2}/pyproject.toml +1 -1
  9. aru_code-0.24.1/aru/__init__.py +0 -1
  10. {aru_code-0.24.1 → aru_code-0.24.2}/LICENSE +0 -0
  11. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agent_factory.py +0 -0
  12. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agents/__init__.py +0 -0
  13. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agents/base.py +0 -0
  14. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agents/executor.py +0 -0
  15. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agents/explorer.py +0 -0
  16. {aru_code-0.24.1 → aru_code-0.24.2}/aru/agents/planner.py +0 -0
  17. {aru_code-0.24.1 → aru_code-0.24.2}/aru/cache_patch.py +0 -0
  18. {aru_code-0.24.1 → aru_code-0.24.2}/aru/checkpoints.py +0 -0
  19. {aru_code-0.24.1 → aru_code-0.24.2}/aru/cli.py +0 -0
  20. {aru_code-0.24.1 → aru_code-0.24.2}/aru/commands.py +0 -0
  21. {aru_code-0.24.1 → aru_code-0.24.2}/aru/completers.py +0 -0
  22. {aru_code-0.24.1 → aru_code-0.24.2}/aru/config.py +0 -0
  23. {aru_code-0.24.1 → aru_code-0.24.2}/aru/context.py +0 -0
  24. {aru_code-0.24.1 → aru_code-0.24.2}/aru/history_blocks.py +0 -0
  25. {aru_code-0.24.1 → aru_code-0.24.2}/aru/permissions.py +0 -0
  26. {aru_code-0.24.1 → aru_code-0.24.2}/aru/plugins/__init__.py +0 -0
  27. {aru_code-0.24.1 → aru_code-0.24.2}/aru/plugins/custom_tools.py +0 -0
  28. {aru_code-0.24.1 → aru_code-0.24.2}/aru/plugins/hooks.py +0 -0
  29. {aru_code-0.24.1 → aru_code-0.24.2}/aru/plugins/manager.py +0 -0
  30. {aru_code-0.24.1 → aru_code-0.24.2}/aru/plugins/tool_api.py +0 -0
  31. {aru_code-0.24.1 → aru_code-0.24.2}/aru/providers.py +0 -0
  32. {aru_code-0.24.1 → aru_code-0.24.2}/aru/runtime.py +0 -0
  33. {aru_code-0.24.1 → aru_code-0.24.2}/aru/session.py +0 -0
  34. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/__init__.py +0 -0
  35. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/ast_tools.py +0 -0
  36. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/gitignore.py +0 -0
  37. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/mcp_client.py +0 -0
  38. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/ranker.py +0 -0
  39. {aru_code-0.24.1 → aru_code-0.24.2}/aru/tools/tasklist.py +0 -0
  40. {aru_code-0.24.1 → aru_code-0.24.2}/aru_code.egg-info/SOURCES.txt +0 -0
  41. {aru_code-0.24.1 → aru_code-0.24.2}/aru_code.egg-info/dependency_links.txt +0 -0
  42. {aru_code-0.24.1 → aru_code-0.24.2}/aru_code.egg-info/entry_points.txt +0 -0
  43. {aru_code-0.24.1 → aru_code-0.24.2}/aru_code.egg-info/requires.txt +0 -0
  44. {aru_code-0.24.1 → aru_code-0.24.2}/aru_code.egg-info/top_level.txt +0 -0
  45. {aru_code-0.24.1 → aru_code-0.24.2}/setup.cfg +0 -0
  46. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_agents_base.py +0 -0
  47. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_checkpoints.py +0 -0
  48. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli.py +0 -0
  49. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_advanced.py +0 -0
  50. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_base.py +0 -0
  51. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_completers.py +0 -0
  52. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_new.py +0 -0
  53. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_run_cli.py +0 -0
  54. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_session.py +0 -0
  55. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_cli_shell.py +0 -0
  56. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_codebase.py +0 -0
  57. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_confabulation_regression.py +0 -0
  58. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_config.py +0 -0
  59. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_context.py +0 -0
  60. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_executor.py +0 -0
  61. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_gitignore.py +0 -0
  62. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_guardrails_scenarios.py +0 -0
  63. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_main.py +0 -0
  64. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_mcp_client.py +0 -0
  65. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_permissions.py +0 -0
  66. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_planner.py +0 -0
  67. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_plugins.py +0 -0
  68. {aru_code-0.24.1 → aru_code-0.24.2}/tests/test_providers.py +0 -0
  69. {aru_code-0.24.1 → 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.1
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"
@@ -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
 
@@ -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,19 +1304,53 @@ 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}"
1288
- try:
1289
- from aru.permissions import permission_scope
1290
- with permission_scope(agent_perm):
1291
- result = await sub.arun(task, stream=False)
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
+
1292
1348
  # Track sub-agent token usage in the session so cost is not underestimated.
1293
- # We accumulate totals directly instead of calling track_tokens() because
1294
- # track_tokens() also reads cache_patch globals (last_call_metrics) which
1295
- # may belong to a different concurrent sub-agent — we must not touch last_*.
1296
- if result and hasattr(result, "metrics") and result.metrics:
1349
+ if run_output and hasattr(run_output, "metrics") and run_output.metrics:
1297
1350
  try:
1298
1351
  session = get_ctx().session
1299
1352
  if session is not None:
1300
- m = result.metrics
1353
+ m = run_output.metrics
1301
1354
  session.total_input_tokens += getattr(m, "input_tokens", 0) or 0
1302
1355
  session.total_output_tokens += getattr(m, "output_tokens", 0) or 0
1303
1356
  session.total_cache_read_tokens += getattr(m, "cache_read_tokens", 0) or 0
@@ -1305,11 +1358,37 @@ Do not create documentation files unless explicitly asked.
1305
1358
  session.api_calls += 1
1306
1359
  except (LookupError, AttributeError):
1307
1360
  pass
1308
- if result and result.content:
1309
- return _truncate_output(f"[{label}] {result.content}")
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}")
1310
1365
  return f"[{label}] Task completed but no output was returned."
1366
+
1367
+ try:
1368
+ from aru.permissions import permission_scope
1369
+ with permission_scope(agent_perm):
1370
+ return await _execute_with_streaming(sub)
1311
1371
  except Exception as e:
1312
- 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}"
1313
1392
 
1314
1393
  # Run in a separate asyncio Task so each sub-agent gets its own
1315
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.1
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.1"
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.1"
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
File without changes
File without changes