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.
- {aru_code-0.24.0/aru_code.egg-info → aru_code-0.24.2}/PKG-INFO +5 -3
- {aru_code-0.24.0 → aru_code-0.24.2}/README.md +4 -2
- aru_code-0.24.2/aru/__init__.py +1 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/cli.py +3 -1
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/display.py +28 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/runner.py +87 -6
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/runtime.py +3 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/codebase.py +105 -10
- {aru_code-0.24.0 → aru_code-0.24.2/aru_code.egg-info}/PKG-INFO +5 -3
- {aru_code-0.24.0 → aru_code-0.24.2}/pyproject.toml +1 -1
- aru_code-0.24.0/aru/__init__.py +0 -1
- {aru_code-0.24.0 → aru_code-0.24.2}/LICENSE +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agent_factory.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/__init__.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/base.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/executor.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/explorer.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/agents/planner.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/cache_patch.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/checkpoints.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/commands.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/completers.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/config.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/context.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/history_blocks.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/permissions.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/__init__.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/hooks.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/manager.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/providers.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/session.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/__init__.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/gitignore.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/ranker.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru/tools/tasklist.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/setup.cfg +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_agents_base.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_checkpoints.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_base.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_completers.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_new.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_session.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_cli_shell.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_codebase.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_config.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_context.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_executor.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_gitignore.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_main.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_mcp_client.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_permissions.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_planner.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_plugins.py +0 -0
- {aru_code-0.24.0 → aru_code-0.24.2}/tests/test_providers.py +0 -0
- {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.
|
|
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
|
-
│ │
|
|
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
|
-
│ │
|
|
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
|
|
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
|
-
|
|
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[
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1198
|
-
with
|
|
1199
|
-
|
|
1200
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
│ │
|
|
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
|
aru_code-0.24.0/aru/__init__.py
DELETED
|
@@ -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
|
|
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
|
|
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
|
|
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
|